mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(local): apply signed automation decisions atomically
This commit is contained in:
@@ -48,6 +48,20 @@ export interface PublishReviewedLegacyCrontabAdoptionOptions {
|
|||||||
) => void | Promise<void>;
|
) => void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PublishVerifiedLegacyCrontabAdoptionOptions = Pick<
|
||||||
|
PublishReviewedLegacyCrontabAdoptionOptions,
|
||||||
|
| 'sourceClient'
|
||||||
|
| 'targetPath'
|
||||||
|
| 'profile'
|
||||||
|
| 'timezone'
|
||||||
|
| 'projectId'
|
||||||
|
| 'mutationId'
|
||||||
|
| 'requestId'
|
||||||
|
| 'observedAtMs'
|
||||||
|
| 'confirmSourceIdentity'
|
||||||
|
| 'confirmReviewerAuthority'
|
||||||
|
>;
|
||||||
|
|
||||||
export class LegacyCrontabPublicationAuthorizationError extends Error {
|
export class LegacyCrontabPublicationAuthorizationError extends Error {
|
||||||
readonly code = 'LEGACY_CRONTAB_PUBLICATION_NOT_AUTHORIZED';
|
readonly code = 'LEGACY_CRONTAB_PUBLICATION_NOT_AUTHORIZED';
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -65,7 +79,7 @@ export class LegacyCrontabPublicationUnavailableError extends Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function auditRecord(
|
function auditRecord(
|
||||||
options: PublishReviewedLegacyCrontabAdoptionOptions,
|
options: PublishVerifiedLegacyCrontabAdoptionOptions,
|
||||||
scope: VerifiedLegacyCrontabDecisionAuthorizationFileScope,
|
scope: VerifiedLegacyCrontabDecisionAuthorizationFileScope,
|
||||||
decision: Readonly<SecurityPolicyDecision> | null,
|
decision: Readonly<SecurityPolicyDecision> | null,
|
||||||
outcome: SecurityAuditRecord['outcome'],
|
outcome: SecurityAuditRecord['outcome'],
|
||||||
@@ -157,7 +171,27 @@ export async function publishReviewedLegacyCrontabAdoption(
|
|||||||
options.observedAtMs,
|
options.observedAtMs,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
async (scope) => {
|
(scope) => publishVerifiedLegacyCrontabAdoption(options, scope),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function publishVerifiedLegacyCrontabAdoption(
|
||||||
|
options: PublishVerifiedLegacyCrontabAdoptionOptions,
|
||||||
|
scope: VerifiedLegacyCrontabDecisionAuthorizationFileScope,
|
||||||
|
): Promise<PublishLocalLegacyAdoptionResult> {
|
||||||
|
if (
|
||||||
|
!options ||
|
||||||
|
typeof options !== 'object' ||
|
||||||
|
Array.isArray(options) ||
|
||||||
|
typeof options.confirmSourceIdentity !== 'function' ||
|
||||||
|
!scope ||
|
||||||
|
typeof scope !== 'object' ||
|
||||||
|
typeof scope.confirmIdentity !== 'function'
|
||||||
|
) {
|
||||||
|
throw new LegacyCrontabPublicationUnavailableError(
|
||||||
|
'Verified Legacy publication scope is invalid',
|
||||||
|
);
|
||||||
|
}
|
||||||
options.confirmSourceIdentity();
|
options.confirmSourceIdentity();
|
||||||
await options.confirmReviewerAuthority?.(scope.result.receipt.reviewer);
|
await options.confirmReviewerAuthority?.(scope.result.receipt.reviewer);
|
||||||
const target = await openLocalSqliteAdoptionDatabase({
|
const target = await openLocalSqliteAdoptionDatabase({
|
||||||
@@ -247,6 +281,4 @@ export async function publishReviewedLegacyCrontabAdoption(
|
|||||||
} finally {
|
} finally {
|
||||||
await target.close();
|
await target.close();
|
||||||
}
|
}
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
import type { DatabaseSync } from 'node:sqlite';
|
import type { DatabaseSync } from 'node:sqlite';
|
||||||
import type { LocalSecretKeyProvider } from '@qinglong/runtime-core/local-secret';
|
import type { LocalSecretKeyProvider } from '@qinglong/runtime-core/local-secret';
|
||||||
import type { SecurityPrincipal } from '@qinglong/runtime-core/security';
|
import type { SecurityPrincipal } from '@qinglong/runtime-core/security';
|
||||||
|
import {
|
||||||
|
publishVerifiedLegacyCrontabAdoption,
|
||||||
|
type PublishReviewedLegacyCrontabAdoptionResult,
|
||||||
|
type PublishVerifiedLegacyCrontabAdoptionOptions,
|
||||||
|
} from './legacyCrontabPublisher';
|
||||||
import {
|
import {
|
||||||
publishLegacyCrontabDecisionAuthorizationFile,
|
publishLegacyCrontabDecisionAuthorizationFile,
|
||||||
withVerifiedLegacyCrontabDecisionAuthorizationFile,
|
withVerifiedLegacyCrontabDecisionAuthorizationFile,
|
||||||
@@ -35,7 +40,7 @@ export interface ReconciliationAutomationDecisionIdentity {
|
|||||||
readonly inventoryDigest: string;
|
readonly inventoryDigest: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReconciliationAutomationDecisionVerificationOptions
|
export interface ReconciliationAutomationDecisionVerificationOptions
|
||||||
extends ReconciliationAutomationDecisionIdentity {
|
extends ReconciliationAutomationDecisionIdentity {
|
||||||
readonly authorizationPath: string;
|
readonly authorizationPath: string;
|
||||||
readonly sourceClient: DatabaseSync;
|
readonly sourceClient: DatabaseSync;
|
||||||
@@ -47,6 +52,13 @@ interface ReconciliationAutomationDecisionVerificationOptions
|
|||||||
readonly allowedParentModes?: readonly (0o500 | 0o700)[];
|
readonly allowedParentModes?: readonly (0o500 | 0o700)[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApplyReconciliationAutomationDecisionOptions
|
||||||
|
extends ReconciliationAutomationDecisionVerificationOptions,
|
||||||
|
Omit<
|
||||||
|
PublishVerifiedLegacyCrontabAdoptionOptions,
|
||||||
|
'sourceClient' | 'profile' | 'timezone'
|
||||||
|
> {}
|
||||||
|
|
||||||
export interface IssueReconciliationAutomationDecisionOptions
|
export interface IssueReconciliationAutomationDecisionOptions
|
||||||
extends ReconciliationAutomationDecisionVerificationOptions {
|
extends ReconciliationAutomationDecisionVerificationOptions {
|
||||||
readonly reviewFilePath: string;
|
readonly reviewFilePath: string;
|
||||||
@@ -409,3 +421,29 @@ export async function verifyReconciliationAutomationDecision(
|
|||||||
return scope.result;
|
return scope.result;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function applyReconciliationAutomationDecision(
|
||||||
|
options: ApplyReconciliationAutomationDecisionOptions,
|
||||||
|
): Promise<PublishReviewedLegacyCrontabAdoptionResult> {
|
||||||
|
return withVerifiedReconciliationAutomationDecision(options, (scope) =>
|
||||||
|
publishVerifiedLegacyCrontabAdoption(
|
||||||
|
{
|
||||||
|
sourceClient: options.sourceClient,
|
||||||
|
targetPath: options.targetPath,
|
||||||
|
profile: options.profile,
|
||||||
|
timezone: options.timezone,
|
||||||
|
projectId: options.projectId,
|
||||||
|
mutationId: options.mutationId,
|
||||||
|
requestId: options.requestId,
|
||||||
|
observedAtMs: options.observedAtMs,
|
||||||
|
confirmSourceIdentity: options.confirmSourceIdentity,
|
||||||
|
...(options.confirmReviewerAuthority === undefined
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
confirmReviewerAuthority: options.confirmReviewerAuthority,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
scope,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ const { migrateLocalSqlitePath } = require('@qinglong/local-sqlite/migration');
|
|||||||
const {
|
const {
|
||||||
visitLegacyCrontabAdoptionInspections,
|
visitLegacyCrontabAdoptionInspections,
|
||||||
} = require('../dist/legacy-adoption/legacyCrontabAdoption');
|
} = require('../dist/legacy-adoption/legacyCrontabAdoption');
|
||||||
|
const {
|
||||||
|
applyReconciliationAutomationDecision,
|
||||||
|
} = require('../dist/legacy-adoption/reconciliationAutomationDecision');
|
||||||
|
|
||||||
const REVIEWED_AT_MS = 1_760_000_000_000;
|
const REVIEWED_AT_MS = 1_760_000_000_000;
|
||||||
const REVIEWER = Object.freeze({
|
const REVIEWER = Object.freeze({
|
||||||
@@ -442,6 +445,7 @@ test('publishes one reviewed legacy task set into the target atomically', async
|
|||||||
authorizationDirectory,
|
authorizationDirectory,
|
||||||
`${decisionId}.ndjson`,
|
`${decisionId}.ndjson`,
|
||||||
);
|
);
|
||||||
|
const authorization =
|
||||||
await publishReviewedLegacyCrontabAdoptionDecisionAuthorizationFile({
|
await publishReviewedLegacyCrontabAdoptionDecisionAuthorizationFile({
|
||||||
sourcePath: value.sourcePath,
|
sourcePath: value.sourcePath,
|
||||||
profile: 'edge',
|
profile: 'edge',
|
||||||
@@ -492,13 +496,42 @@ test('publishes one reviewed legacy task set into the target atomically', async
|
|||||||
reviewerAuthorityChecks += 1;
|
reviewerAuthorityChecks += 1;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const inserted = await publishReviewedLegacyCrontabAdoption(
|
const publicationSource = new DatabaseSync(value.sourcePath, {
|
||||||
publicationOptions,
|
readOnly: true,
|
||||||
);
|
});
|
||||||
|
let sourceIdentityChecks = 0;
|
||||||
|
const inserted = await applyReconciliationAutomationDecision({
|
||||||
|
authorizationPath,
|
||||||
|
decisionId,
|
||||||
|
profile: 'edge',
|
||||||
|
automationPlanDigest: plan.planDigest,
|
||||||
|
inventoryDigest: authorization.receipt.inventoryDigest,
|
||||||
|
sourceClient: publicationSource,
|
||||||
|
timezone: 'UTC',
|
||||||
|
keyProvider: authorizationKeyProvider(),
|
||||||
|
observedAtMs: REVIEWED_AT_MS + 1,
|
||||||
|
openRequirements: () =>
|
||||||
|
page.diagnostics.map((diagnostic) => ({
|
||||||
|
rowOrdinal: diagnostic.rowOrdinal,
|
||||||
|
sourceDigest: diagnostic.sourceDigest,
|
||||||
|
classification: diagnostic.classification,
|
||||||
|
requirement: 'review_adopt',
|
||||||
|
})),
|
||||||
|
targetPath: value.targetPath,
|
||||||
|
projectId: 'default',
|
||||||
|
mutationId,
|
||||||
|
requestId: 'reconciliation-automation-publication-test',
|
||||||
|
confirmSourceIdentity() {
|
||||||
|
sourceIdentityChecks += 1;
|
||||||
|
},
|
||||||
|
confirmReviewerAuthority: publicationOptions.confirmReviewerAuthority,
|
||||||
|
});
|
||||||
|
publicationSource.close();
|
||||||
assert.equal(inserted.status, 'inserted');
|
assert.equal(inserted.status, 'inserted');
|
||||||
assert.equal(inserted.adoption.adoptedTaskCount, 1);
|
assert.equal(inserted.adoption.adoptedTaskCount, 1);
|
||||||
assert.equal(inserted.adoption.adoptedTriggerCount, 1);
|
assert.equal(inserted.adoption.adoptedTriggerCount, 1);
|
||||||
assert.equal(reviewerAuthorityChecks, 2);
|
assert.equal(reviewerAuthorityChecks, 2);
|
||||||
|
assert.equal(sourceIdentityChecks, 2);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
(await publishReviewedLegacyCrontabAdoption(publicationOptions)).status,
|
(await publishReviewedLegacyCrontabAdoption(publicationOptions)).status,
|
||||||
'existing',
|
'existing',
|
||||||
|
|||||||
Reference in New Issue
Block a user