mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 10:32:40 +08:00
feat(ql3): atomically withdraw quarantined automation
This commit is contained in:
+66
-44
@@ -44,7 +44,10 @@ function integer(row: Row, key: string): number {
|
||||
return value as number;
|
||||
}
|
||||
|
||||
function targetIdentity(projectId: unknown, packageName: unknown): {
|
||||
function targetIdentity(
|
||||
projectId: unknown,
|
||||
packageName: unknown,
|
||||
): {
|
||||
readonly projectId: string;
|
||||
readonly packageName: string;
|
||||
} {
|
||||
@@ -119,19 +122,18 @@ export class LocalSqlitePluginPackageAutomationPublicationRepository
|
||||
#parse(row: Row): Readonly<PluginPackageAutomationPublication> {
|
||||
try {
|
||||
const publication = normalizePluginPackageAutomationPublication(
|
||||
JSON.parse(text(row, 'publicationJson')) as
|
||||
PluginPackageAutomationPublication,
|
||||
JSON.parse(
|
||||
text(row, 'publicationJson'),
|
||||
) as PluginPackageAutomationPublication,
|
||||
);
|
||||
if (
|
||||
publication.publicationDigest !==
|
||||
text(row, 'publicationDigest') ||
|
||||
publication.publicationDigest !== text(row, 'publicationDigest') ||
|
||||
publication.target.projectId !== text(row, 'projectId') ||
|
||||
publication.target.packageName !== text(row, 'packageName') ||
|
||||
publication.target.installationId !== text(row, 'installationId') ||
|
||||
publication.target.lockDigest !== text(row, 'lockDigest') ||
|
||||
publication.target.generation !== integer(row, 'generation') ||
|
||||
publication.target.generationDigest !==
|
||||
text(row, 'generationDigest') ||
|
||||
publication.target.generationDigest !== text(row, 'generationDigest') ||
|
||||
publication.target.materializedRevisionDigest !==
|
||||
text(row, 'materializedRevisionDigest') ||
|
||||
publication.state !== text(row, 'state') ||
|
||||
@@ -142,9 +144,7 @@ export class LocalSqlitePluginPackageAutomationPublicationRepository
|
||||
}
|
||||
return publication;
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof PluginPackageAutomationPublicationUnavailableError
|
||||
) {
|
||||
if (error instanceof PluginPackageAutomationPublicationUnavailableError) {
|
||||
throw error;
|
||||
}
|
||||
throw new PluginPackageAutomationPublicationUnavailableError();
|
||||
@@ -389,8 +389,9 @@ export class LocalSqlitePluginPackageAutomationPublicationRepository
|
||||
return this.#findCurrent(projectId, packageName);
|
||||
}
|
||||
|
||||
publishInTransaction(
|
||||
#publishInTransaction(
|
||||
value: Readonly<PluginPackageAutomationPublication>,
|
||||
securityWithdrawal: boolean,
|
||||
): Readonly<{
|
||||
status: 'created' | 'existing';
|
||||
publication: Readonly<PluginPackageAutomationPublication>;
|
||||
@@ -430,10 +431,7 @@ export class LocalSqlitePluginPackageAutomationPublicationRepository
|
||||
);
|
||||
}
|
||||
try {
|
||||
assertPluginPackageAutomationPublicationSuccessor(
|
||||
current,
|
||||
publication,
|
||||
);
|
||||
assertPluginPackageAutomationPublicationSuccessor(current, publication);
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidPluginPackageAutomationPublicationError) {
|
||||
throw new PluginPackageAutomationPublicationConflictError(
|
||||
@@ -467,33 +465,35 @@ export class LocalSqlitePluginPackageAutomationPublicationRepository
|
||||
'materialized revision fence does not match publication target',
|
||||
);
|
||||
}
|
||||
const securityFence = client
|
||||
.prepare(
|
||||
`SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM "QingLong3PluginPackageQuarantineEvents" AS quarantine
|
||||
WHERE quarantine.project_id = ?
|
||||
AND quarantine.package_name = ?
|
||||
AND quarantine.installation_id = ?
|
||||
AND quarantine.lock_digest = ?
|
||||
) AS "blocked"`,
|
||||
)
|
||||
.get(
|
||||
publication.target.projectId,
|
||||
publication.target.packageName,
|
||||
publication.target.installationId,
|
||||
publication.target.lockDigest,
|
||||
) as Row | undefined;
|
||||
if (
|
||||
!securityFence ||
|
||||
(securityFence.blocked !== 0 && securityFence.blocked !== 1)
|
||||
) {
|
||||
throw new PluginPackageAutomationPublicationUnavailableError();
|
||||
}
|
||||
if (securityFence.blocked === 1) {
|
||||
throw new PluginPackageAutomationPublicationConflictError(
|
||||
'quarantined Package generation cannot publish automation',
|
||||
);
|
||||
if (!securityWithdrawal) {
|
||||
const securityFence = client
|
||||
.prepare(
|
||||
`SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM "QingLong3PluginPackageQuarantineEvents" AS quarantine
|
||||
WHERE quarantine.project_id = ?
|
||||
AND quarantine.package_name = ?
|
||||
AND quarantine.installation_id = ?
|
||||
AND quarantine.lock_digest = ?
|
||||
) AS "blocked"`,
|
||||
)
|
||||
.get(
|
||||
publication.target.projectId,
|
||||
publication.target.packageName,
|
||||
publication.target.installationId,
|
||||
publication.target.lockDigest,
|
||||
) as Row | undefined;
|
||||
if (
|
||||
!securityFence ||
|
||||
(securityFence.blocked !== 0 && securityFence.blocked !== 1)
|
||||
) {
|
||||
throw new PluginPackageAutomationPublicationUnavailableError();
|
||||
}
|
||||
if (securityFence.blocked === 1) {
|
||||
throw new PluginPackageAutomationPublicationConflictError(
|
||||
'quarantined Package generation cannot publish automation',
|
||||
);
|
||||
}
|
||||
}
|
||||
client
|
||||
.prepare(
|
||||
@@ -570,9 +570,31 @@ export class LocalSqlitePluginPackageAutomationPublicationRepository
|
||||
});
|
||||
}
|
||||
|
||||
publish(
|
||||
publishInTransaction(
|
||||
value: Readonly<PluginPackageAutomationPublication>,
|
||||
): Promise<
|
||||
): Readonly<{
|
||||
status: 'created' | 'existing';
|
||||
publication: Readonly<PluginPackageAutomationPublication>;
|
||||
}> {
|
||||
return this.#publishInTransaction(value, false);
|
||||
}
|
||||
|
||||
publishSecurityWithdrawalInTransaction(
|
||||
value: Readonly<PluginPackageAutomationPublication>,
|
||||
): Readonly<{
|
||||
status: 'created' | 'existing';
|
||||
publication: Readonly<PluginPackageAutomationPublication>;
|
||||
}> {
|
||||
const publication = normalizePluginPackageAutomationPublication(value);
|
||||
if (publication.state !== 'withdrawn') {
|
||||
throw new PluginPackageAutomationPublicationConflictError(
|
||||
'security withdrawal must narrow automation state',
|
||||
);
|
||||
}
|
||||
return this.#publishInTransaction(publication, true);
|
||||
}
|
||||
|
||||
publish(value: Readonly<PluginPackageAutomationPublication>): Promise<
|
||||
Readonly<{
|
||||
status: 'created' | 'existing';
|
||||
publication: Readonly<PluginPackageAutomationPublication>;
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
normalizePluginPackageInstallRecord,
|
||||
type PluginPackageInstallRecord,
|
||||
} from '@qinglong/runtime-core/plugin-package-install';
|
||||
import { createPluginPackageAutomationLifecyclePublication } from '@qinglong/runtime-core/plugin-package-automation-publication';
|
||||
import {
|
||||
createProjectToolDefinitionSnapshot,
|
||||
normalizeProjectToolDefinitionSnapshot,
|
||||
@@ -40,6 +41,7 @@ import {
|
||||
} from '@qinglong/runtime-core/task-spec-semantic';
|
||||
|
||||
import { LocalSqliteOperationAuthority } from '../authority/operationAuthority';
|
||||
import { LocalSqlitePluginPackageAutomationPublicationRepository } from './pluginPackageAutomationPublicationRepository';
|
||||
|
||||
type Row = Record<string, unknown>;
|
||||
|
||||
@@ -314,6 +316,30 @@ export class LocalSqlitePluginPackageQuarantineRepository
|
||||
if (error instanceof PluginPackageQuarantineUnavailableError) throw error;
|
||||
throw new PluginPackageQuarantineUnavailableError();
|
||||
}
|
||||
const automation = this.#authority.client
|
||||
.prepare(
|
||||
`SELECT state,
|
||||
lifecycle_event_digest AS "lifecycleEventDigest"
|
||||
FROM "QingLong3PluginPackageAutomationPublications"
|
||||
WHERE project_id = ? AND package_name = ?
|
||||
AND installation_id = ? AND lock_digest = ?
|
||||
ORDER BY version DESC
|
||||
LIMIT 1`,
|
||||
)
|
||||
.get(
|
||||
receipt.target.projectId,
|
||||
receipt.target.packageName,
|
||||
receipt.target.installationId,
|
||||
receipt.target.lockDigest,
|
||||
) as Row | undefined;
|
||||
if (
|
||||
automation &&
|
||||
(text(automation, 'state') === 'active' ||
|
||||
(automation.lifecycleEventDigest === receipt.eventDigest &&
|
||||
text(automation, 'state') !== 'withdrawn'))
|
||||
) {
|
||||
throw new PluginPackageQuarantineUnavailableError();
|
||||
}
|
||||
}
|
||||
|
||||
#findStored(
|
||||
@@ -433,6 +459,40 @@ export class LocalSqlitePluginPackageQuarantineRepository
|
||||
return record;
|
||||
}
|
||||
|
||||
#withdrawAutomation(
|
||||
event: Readonly<PluginPackageQuarantineEvent>,
|
||||
record: Readonly<PluginPackageInstallRecord>,
|
||||
committedAtMs: number,
|
||||
): void {
|
||||
const publications =
|
||||
new LocalSqlitePluginPackageAutomationPublicationRepository(
|
||||
this.#authority,
|
||||
);
|
||||
const current = publications.findCurrentInTransaction(
|
||||
event.target.projectId,
|
||||
event.target.packageName,
|
||||
);
|
||||
if (!current) return;
|
||||
if (
|
||||
current.target.installationId !== event.target.installationId ||
|
||||
current.target.lockDigest !== event.target.lockDigest ||
|
||||
current.target.generation !== record.targetGeneration
|
||||
) {
|
||||
throw new PluginPackageQuarantineConflictError(
|
||||
'Workflow/Prompt publication does not match the quarantined Package generation',
|
||||
);
|
||||
}
|
||||
if (current.state === 'absent' || current.state === 'withdrawn') return;
|
||||
publications.publishSecurityWithdrawalInTransaction(
|
||||
createPluginPackageAutomationLifecyclePublication({
|
||||
previous: current,
|
||||
state: 'withdrawn',
|
||||
lifecycleEventDigest: event.eventDigest,
|
||||
publishedAtMs: committedAtMs,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#activeContributions(
|
||||
projectId: string,
|
||||
): readonly Readonly<ProjectToolDefinitionSnapshotContribution>[] {
|
||||
@@ -792,7 +852,7 @@ export class LocalSqlitePluginPackageQuarantineRepository
|
||||
'target lock is already quarantined by another event',
|
||||
);
|
||||
}
|
||||
this.#install(event);
|
||||
const install = this.#install(event);
|
||||
const clock = client
|
||||
.prepare(
|
||||
`SELECT CAST(unixepoch('subsec') * 1000 AS INTEGER) AS "nowMs"`,
|
||||
@@ -857,6 +917,7 @@ export class LocalSqlitePluginPackageQuarantineRepository
|
||||
),
|
||||
);
|
||||
this.#insertEvent(event);
|
||||
this.#withdrawAutomation(event, install, committedAtMs);
|
||||
this.#publishSnapshot(snapshot, committedAtMs);
|
||||
const receipt = createPluginPackageWithdrawalReceipt({
|
||||
eventDigest: event.eventDigest,
|
||||
|
||||
Reference in New Issue
Block a user