#!/usr/bin/env node 'use strict'; const crypto = require('node:crypto'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const { spawnSync } = require('node:child_process'); const { DatabaseSync } = require('node:sqlite'); const { writeSyntheticLocalReleaseSelection, } = require('./lib/ql3-local-release-selection-test-fixture.cjs'); const IMAGE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}@sha256:[0-9a-f]{64}$/; const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/; const MAX_RELEASE_SELECTION_BYTES = 1024 * 1024; function fail(message) { throw new Error(`QingLong local Compose rollout failed: ${message}`); } function argumentsValue(argv) { const values = Object.fromEntries( argv.map((argument) => { const separator = argument.indexOf('='); if (separator < 3 || !argument.startsWith('--')) { fail('arguments must use --name=value'); } return [argument.slice(2, separator), argument.slice(separator + 1)]; }), ); const keys = Object.keys(values).sort(); const baseKeys = [ 'docker-executable', 'docker-socket', 'image', 'profile', ].sort(); const catalogKeys = [ ...baseKeys, 'expected-selection-digest', 'release-selection', ].sort(); const syntheticInput = JSON.stringify(keys) === JSON.stringify(baseKeys); const catalogInput = JSON.stringify(keys) === JSON.stringify(catalogKeys); if ( Object.keys(values).length !== argv.length || (!syntheticInput && !catalogInput) || !IMAGE_PATTERN.test(values.image ?? '') || (values.profile !== 'edge' && values.profile !== 'standalone') || typeof values['docker-executable'] !== 'string' || typeof values['docker-socket'] !== 'string' ) { fail( 'usage: --image=repository@sha256:digest --docker-executable=/absolute/docker --docker-socket=/absolute/docker.sock --profile=edge|standalone [--release-selection=/canonical/selection.json --expected-selection-digest=sha256:digest]', ); } const dockerExecutable = fs.realpathSync(values['docker-executable']); const dockerSocket = fs.realpathSync(values['docker-socket']); if ( dockerExecutable !== values['docker-executable'] || dockerSocket !== values['docker-socket'] ) { fail('Docker executable and socket must be canonical paths'); } let releaseSelection; if (catalogInput) { if ( !path.isAbsolute(values['release-selection']) || !DIGEST_PATTERN.test(values['expected-selection-digest']) ) { fail('catalog selection path and digest are invalid'); } const selectionPath = fs.realpathSync(values['release-selection']); if (selectionPath !== values['release-selection']) { fail('catalog selection path must be canonical'); } releaseSelection = Object.freeze({ path: selectionPath, expectedSelectionDigest: values['expected-selection-digest'], }); } return Object.freeze({ image: values.image, profile: values.profile, dockerExecutable, dockerSocket, ...(releaseSelection ? { releaseSelection } : {}), }); } function sha256(value) { return `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`; } function resolveReleaseSelection(input, temporaryRoot, uid) { if (!input.releaseSelection) { const releaseSelection = writeSyntheticLocalReleaseSelection({ directory: temporaryRoot, image: input.image, allowRootService: uid === 0, sourceRevision: process.env.GITHUB_SHA, }); return Object.freeze({ releaseSelection, authority: Object.freeze({ mode: 'synthetic_live_fixture' }), }); } const selectionPath = input.releaseSelection.path; const stat = fs.lstatSync(selectionPath); const contents = fs.readFileSync(selectionPath, 'utf8'); let selection; try { selection = JSON.parse(contents); } catch { fail('catalog selection must be canonical JSON'); } const unsigned = { ...selection }; delete unsigned.selectionDigest; const computedDigest = sha256(JSON.stringify(unsigned)); if ( !stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.uid !== uid || (stat.mode & 0o777) !== 0o600 || stat.size < 2 || stat.size > MAX_RELEASE_SELECTION_BYTES || fs.realpathSync(selectionPath) !== selectionPath || fs.realpathSync(path.dirname(selectionPath)) !== path.dirname(selectionPath) || contents !== `${JSON.stringify(selection)}\n` || computedDigest !== input.releaseSelection.expectedSelectionDigest || selection.selectionDigest !== computedDigest || selection.schemaVersion !== 1 || selection.schema !== 'qinglong/local-compose-release-image@v2' || !/^3\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$/u.test( selection.release?.version ?? '', ) || !/^[a-f0-9]{40}$/u.test(selection.release?.sourceRevision ?? '') || selection.release?.sourceRef !== `refs/tags/v${selection.release?.version}` || !['local', 'all'].includes(selection.release?.scope) || !DIGEST_PATTERN.test(selection.releaseSetDigest ?? '') || selection.deploymentFamily !== 'local' || selection.service?.kind !== 'compose' || selection.service?.image !== input.image || selection.service?.allowRootService !== (uid === 0) || selection.catalog?.schema !== 'qinglong/release-catalog-consumption-ceremony@v1' || !DIGEST_PATTERN.test(selection.catalog?.manifestDigest ?? '') || !DIGEST_PATTERN.test(selection.catalog?.consumptionReportDigest ?? '') || selection.catalog?.releaseSetDigest !== selection.releaseSetDigest || selection.catalog?.discoveryTagAuthority !== 'none' || selection.verification?.catalogConsumption !== 'offline_reconstructed' || selection.verification?.externalToolResultsReplayed !== false || selection.verification?.networkAccess !== false || selection.verification?.deploymentMutation !== false ) { fail('catalog-bound release selection is incompatible'); } return Object.freeze({ releaseSelection: input.releaseSelection, authority: Object.freeze({ mode: 'verified_release_catalog', sourceRevision: selection.release.sourceRevision, sourceRef: selection.release.sourceRef, scope: selection.release.scope, releaseSetDigest: selection.releaseSetDigest, catalogManifestDigest: selection.catalog.manifestDigest, catalogConsumptionDigest: selection.catalog.consumptionReportDigest, selectionDigest: selection.selectionDigest, }), }); } function docker(input, configRoot, args, timeout = 45_000) { return spawnSync( input.dockerExecutable, ['--host', `unix://${input.dockerSocket}`, '--config', configRoot, ...args], { encoding: 'utf8', maxBuffer: 256 * 1024, timeout, killSignal: 'SIGKILL', env: { PATH: `${path.dirname( input.dockerExecutable, )}:/usr/local/bin:/usr/bin:/bin`, HOME: configRoot, DOCKER_CONFIG: configRoot, NO_PROXY: '*', no_proxy: '*', }, }, ); } function composeArguments(deploymentRoot, args) { const serviceRoot = path.join(deploymentRoot, 'service'); return [ 'compose', '--project-directory', serviceRoot, '-f', path.join(serviceRoot, 'compose.yaml'), '-f', path.join(serviceRoot, 'compose.image.yaml'), ...args, ]; } function cleanup(input, configRoot, deploymentRoot, requireGracefulStop) { if (!fs.existsSync(path.join(deploymentRoot, 'service', 'compose.yaml'))) { return; } let cleanupFailure; if (requireGracefulStop) { const stopped = docker( input, configRoot, composeArguments(deploymentRoot, [ 'stop', '--timeout', '30', 'qinglong3', ]), 45_000, ); const logs = docker( input, configRoot, composeArguments(deploymentRoot, [ 'logs', '--no-color', '--tail', '256', 'qinglong3', ]), ); if ( stopped.error || stopped.status !== 0 || logs.error || logs.status !== 0 || !logs.stdout.includes('"event":"stopped"') || !logs.stdout.includes('"stopResult":"stopped"') ) { cleanupFailure = 'container did not publish graceful stop evidence'; } } const removed = docker( input, configRoot, composeArguments(deploymentRoot, [ 'down', '--timeout', '30', '--remove-orphans', ]), 45_000, ); if (removed.error || removed.status !== 0) { cleanupFailure ??= 'Compose resources could not be removed'; } if (cleanupFailure) fail(cleanupFailure); } async function main() { if (process.versions.node.split('.')[0] !== '24') { fail('Node 24 is required'); } if ( typeof process.getuid !== 'function' || typeof process.getgid !== 'function' ) { fail('a POSIX identity is required'); } const input = argumentsValue(process.argv.slice(2)); const root = path.resolve(__dirname, '..'); const { applyLocalDeploymentCompose, collectLocalDeploymentComposeEvidence, prepareLocalDeployment, switchLocalDeploymentComposeRevision, } = require(path.join( root, 'packages/ql3-local-owner-cli/dist/deployment/localDeployment.js', )); const { inspectLocalSqliteRolloutBackup, LOCAL_SQLITE_WRITE_CONTRACT_VERSION, } = require(path.join( root, 'packages/ql3-local-sqlite/dist/readiness/rolloutSafety.js', )); const { LOCAL_SQLITE_CONTRACT_VERSION } = require(path.join( root, 'packages/ql3-local-sqlite/dist/readiness/readinessInspection.js', )); const temporaryRoot = fs.realpathSync( fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-compose-rollout-')), ); fs.chmodSync(temporaryRoot, 0o700); const dockerConfigRoot = path.join(temporaryRoot, 'docker-config'); fs.mkdirSync(dockerConfigRoot, { mode: 0o700 }); const deploymentRoot = path.join(temporaryRoot, 'deployment'); const commandPath = path.join(temporaryRoot, 'rollout.json'); const uid = process.getuid(); const selection = resolveReleaseSelection(input, temporaryRoot, uid); const releaseSelection = selection.releaseSelection; let composeResourcesPresent = false; try { const setup = await prepareLocalDeployment({ schemaVersion: 1, operation: 'local.deployment.prepare', options: { deploymentRoot, profile: input.profile, instanceId: `compose-rollout-${input.profile}`, busyTimeoutMs: 100, service: { kind: 'compose', releaseSelection, allowRootService: uid === 0, }, }, request: { ownerPepperKeyId: 'owner-v1', registerMutationId: '019f8680-143d-4000-8000-000000000201', activateMutationId: '019f8680-143d-4000-8000-000000000202', registeredAtMs: 1_785_254_600_000, activatedAtMs: 1_785_254_600_001, }, }); if (setup.status !== 'prepared' || setup.profile !== input.profile) { fail('fresh deployment did not prepare'); } const firstCommand = { schemaVersion: 1, operation: 'local.deployment.compose.apply', options: { deploymentRoot, dockerExecutable: input.dockerExecutable, dockerSocketPath: input.dockerSocket, allowRootService: uid === 0, }, request: { expectedGeneration: 1, rolloutId: '019f8680-143d-4000-8000-000000000203', startedAtMs: 1_785_254_600_002, failureRollbackMutationId: '019f8680-143d-4000-8000-000000000204', failureRollbackChangedAtMs: 1_785_254_600_003, }, }; fs.writeFileSync(commandPath, `${JSON.stringify(firstCommand)}\n`, { mode: 0o600, }); const cli = path.join( root, 'packages/ql3-local-owner-cli/dist/deployment/localDeploymentCli.js', ); const invoked = spawnSync( process.execPath, [cli, 'compose-apply', '--command-file', commandPath], { encoding: 'utf8', maxBuffer: 64 * 1024, timeout: input.profile === 'edge' ? 75_000 : 105_000, killSignal: 'SIGKILL', }, ); if ( invoked.error || invoked.status !== 0 || invoked.stderr !== '' || typeof invoked.stdout !== 'string' || invoked.stdout.includes(deploymentRoot) || /sha256|mutation|rollout|socket|executable|container/i.test( invoked.stdout, ) ) { fail('private CLI did not return a low-sensitive active result'); } const firstReport = JSON.parse(invoked.stdout); if ( firstReport.schemaVersion !== 1 || firstReport.operation !== 'local.deployment.compose.apply' || firstReport.status !== 'active' || firstReport.attemptedGeneration !== 1 || firstReport.activeGeneration !== 1 || firstReport.profile !== input.profile || firstReport.health?.event !== 'active' || firstReport.service?.kind !== 'compose' ) { fail('first rollout report is incompatible'); } composeResourcesPresent = true; const revision = await switchLocalDeploymentComposeRevision({ schemaVersion: 1, operation: 'local.deployment.compose.upgrade', options: { deploymentRoot, allowRootService: uid === 0, }, request: { expectedGeneration: 1, releaseSelection, mutationId: '019f8680-143d-4000-8000-000000000205', changedAtMs: 1_785_254_600_004, }, }); if ( revision.status !== 'prepared' || revision.operation !== 'local.deployment.compose.upgrade' || revision.generation !== 2 ) { fail('second generation did not publish'); } const secondCommand = { ...firstCommand, request: { expectedGeneration: 2, rolloutId: '019f8680-143d-4000-8000-000000000206', startedAtMs: 1_785_254_600_005, failureRollbackMutationId: '019f8680-143d-4000-8000-000000000207', failureRollbackChangedAtMs: 1_785_254_600_006, }, }; fs.writeFileSync(commandPath, `${JSON.stringify(secondCommand)}\n`, { mode: 0o600, }); const upgraded = spawnSync( process.execPath, [cli, 'compose-apply', '--command-file', commandPath], { encoding: 'utf8', maxBuffer: 64 * 1024, timeout: input.profile === 'edge' ? 75_000 : 105_000, killSignal: 'SIGKILL', }, ); if ( upgraded.error || upgraded.status !== 0 || upgraded.stderr !== '' || typeof upgraded.stdout !== 'string' || upgraded.stdout.includes(deploymentRoot) || /sha256|mutation|rollout|socket|executable|container/i.test( upgraded.stdout, ) ) { fail('second private CLI did not return a low-sensitive active result'); } const report = JSON.parse(upgraded.stdout); if ( report.schemaVersion !== 1 || report.operation !== 'local.deployment.compose.apply' || report.status !== 'active' || report.attemptedGeneration !== 2 || report.activeGeneration !== 2 || report.profile !== input.profile || report.health?.event !== 'active' || report.service?.kind !== 'compose' ) { fail('second rollout report is incompatible'); } const serviceRoot = path.join(deploymentRoot, 'service'); const rolloutRoot = path.join(serviceRoot, 'rollouts'); const receipts = fs.readdirSync(rolloutRoot).sort(); const backupPath = path.join( serviceRoot, 'rollout-backups', `${secondCommand.request.rolloutId}.sqlite`, ); const receipt = JSON.parse( fs.readFileSync( path.join(rolloutRoot, `${secondCommand.request.rolloutId}.json`), 'utf8', ), ); const backup = await inspectLocalSqliteRolloutBackup({ databasePath: path.join(deploymentRoot, 'qinglong3.sqlite'), backupPath, profile: input.profile, }); if ( receipts.length !== 2 || receipts.some( (name) => (fs.statSync(path.join(rolloutRoot, name)).mode & 0o777) !== 0o600, ) || (fs.statSync(backupPath).mode & 0o777) !== 0o600 || receipt.sqlite?.contractVersion !== LOCAL_SQLITE_CONTRACT_VERSION || receipt.sqlite?.writeContractVersion !== LOCAL_SQLITE_WRITE_CONTRACT_VERSION || (receipt.sqlite?.writeObservation !== 'unchanged' && receipt.sqlite?.writeObservation !== 'changed') || receipt.sqlite?.backup?.sha256 !== backup.sha256 || receipt.sqlite?.backup?.bytes !== backup.bytes || receipt.sqlite?.backup?.pageCount !== backup.pageCount || receipt.sqlite?.backup?.pageSize !== backup.pageSize || fs.existsSync( path.join(deploymentRoot, 'service', '.compose-rollout.lock'), ) ) { fail('durable rollout backup or receipt is incomplete'); } const databasePath = path.join(deploymentRoot, 'qinglong3.sqlite'); const beforeRestore = new DatabaseSync(databasePath, { readonly: true }); const sourceUserVersion = beforeRestore .prepare('PRAGMA user_version') .get().user_version; beforeRestore.close(); const mutatedUserVersion = sourceUserVersion === 987 ? 988 : 987; const thirdRevision = await switchLocalDeploymentComposeRevision({ schemaVersion: 1, operation: 'local.deployment.compose.upgrade', options: { deploymentRoot, allowRootService: uid === 0, }, request: { expectedGeneration: 2, releaseSelection, mutationId: '019f8680-143d-4000-8000-000000000208', changedAtMs: 1_785_254_600_007, }, }); if (thirdRevision.status !== 'prepared' || thirdRevision.generation !== 3) { fail('restore exercise generation did not publish'); } const restoreRolloutCommand = { ...firstCommand, request: { expectedGeneration: 3, rolloutId: '019f8680-143d-4000-8000-000000000209', startedAtMs: 1_785_254_600_008, failureRollbackMutationId: '019f8680-143d-4000-8000-00000000020a', failureRollbackChangedAtMs: 1_785_254_600_009, }, }; let injectedWrite = false; let faultNow = 0; const failedHealthRunner = ({ args, timeoutMs }) => { const executed = docker( input, dockerConfigRoot, args, timeoutMs ?? 45_000, ); if (executed.error || executed.status !== 0) { throw new Error( executed.stderr || 'Docker restore exercise command failed', ); } const selection = fs.readFileSync( path.join(serviceRoot, 'compose.image.yaml'), 'utf8', ); const generation = Number( /^ generation: ([0-9]+)$/m.exec(selection)?.[1], ); if ( args[0] === 'compose' && args.includes('up') && generation === 3 && !injectedWrite ) { const writer = new DatabaseSync(databasePath); writer.exec(`PRAGMA user_version = ${mutatedUserVersion}`); writer.close(); injectedWrite = true; } if ( args[0] === 'container' && args[1] === 'logs' && (generation === 3 || generation === 4) ) { return ''; } return executed.stdout; }; let failedHealthError; try { await applyLocalDeploymentCompose(restoreRolloutCommand, { runDocker: failedHealthRunner, now() { faultNow += 61_000; return faultNow; }, async wait() {}, }); } catch (error) { failedHealthError = error; } const restoreRolloutLock = path.join(serviceRoot, '.compose-rollout.lock'); const restoreRolloutReceipt = path.join( rolloutRoot, `${restoreRolloutCommand.request.rolloutId}.json`, ); const restoreSourcePath = path.join( serviceRoot, 'rollout-backups', `${restoreRolloutCommand.request.rolloutId}.sqlite`, ); const failedSelection = fs.readFileSync( path.join(serviceRoot, 'compose.image.yaml'), 'utf8', ); const failedGeneration = Number( /^ generation: ([0-9]+)$/m.exec(failedSelection)?.[1], ); const changedDatabase = new DatabaseSync(databasePath, { readonly: true, }); const changedUserVersion = changedDatabase .prepare('PRAGMA user_version') .get().user_version; changedDatabase.close(); if ( !(failedHealthError instanceof Error) || !injectedWrite || changedUserVersion !== mutatedUserVersion || !/^ generation: 4$/m.test(failedSelection) || !fs.existsSync(restoreRolloutLock) || fs.existsSync(restoreRolloutReceipt) || !fs.existsSync(restoreSourcePath) ) { fail( `failed rollout did not preserve a restorable fenced state ${JSON.stringify( { failedHealthError: failedHealthError instanceof Error, injectedWrite, changedDatabase: changedUserVersion === mutatedUserVersion, failedGeneration, rolloutLock: fs.existsSync(restoreRolloutLock), rolloutReceipt: fs.existsSync(restoreRolloutReceipt), sourceSnapshot: fs.existsSync(restoreSourcePath), }, )}`, ); } const restoreId = '019f8680-143d-4000-8000-00000000020b'; const prepareRestoreCommand = { schemaVersion: 1, operation: 'local.deployment.compose.restore.prepare', options: restoreRolloutCommand.options, request: { expectedGeneration: 4, restoreId, sourceRolloutId: restoreRolloutCommand.request.rolloutId, preparedAtMs: 1_785_254_600_010, }, }; const commitRestoreCommand = { schemaVersion: 1, operation: 'local.deployment.compose.restore.commit', options: restoreRolloutCommand.options, request: { expectedGeneration: 4, restoreId, committedAtMs: 1_785_254_600_011, }, }; const invokeRestoreCli = (verb, command) => { fs.writeFileSync(commandPath, `${JSON.stringify(command)}\n`, { mode: 0o600, }); const invokedRestore = spawnSync( process.execPath, [cli, verb, '--command-file', commandPath], { encoding: 'utf8', maxBuffer: 64 * 1024, timeout: 75_000, killSignal: 'SIGKILL', }, ); if ( invokedRestore.error || invokedRestore.status !== 0 || invokedRestore.stderr !== '' || invokedRestore.stdout.includes(deploymentRoot) || /sha256|mutation|rollout|socket|executable|container/i.test( invokedRestore.stdout, ) ) { fail( `${verb} did not return a low-sensitive result ${JSON.stringify({ status: invokedRestore.status, hasProcessError: invokedRestore.error !== undefined, stderr: invokedRestore.stderr.trim(), containsDeploymentRoot: invokedRestore.stdout.includes(deploymentRoot), containsForbiddenOutput: /sha256|mutation|rollout|socket|executable|container/i.test( invokedRestore.stdout, ), })}`, ); } return JSON.parse(invokedRestore.stdout); }; const preparedRestore = invokeRestoreCli( 'compose-restore-prepare', prepareRestoreCommand, ); const safeguardPath = path.join( serviceRoot, 'restore-safeguards', `${restoreId}.sqlite`, ); if ( preparedRestore.status !== 'prepared' || preparedRestore.generation !== 4 || preparedRestore.profile !== input.profile || preparedRestore.service?.state !== 'stopped' || !fs.existsSync(safeguardPath) ) { fail('restore prepare evidence is incomplete'); } const committedRestore = invokeRestoreCli( 'compose-restore-commit', commitRestoreCommand, ); const restoredDatabase = new DatabaseSync(databasePath, { readonly: true, }); const restoredUserVersion = restoredDatabase .prepare('PRAGMA user_version') .get().user_version; restoredDatabase.close(); if ( committedRestore.status !== 'restored' || committedRestore.service?.state !== 'stopped' || restoredUserVersion !== sourceUserVersion || fs.existsSync(path.join(serviceRoot, '.compose-restore.lock')) || !fs.existsSync(restoreRolloutLock) ) { fail('restore commit did not restore the exact source snapshot'); } fs.writeFileSync( commandPath, `${JSON.stringify(restoreRolloutCommand)}\n`, { mode: 0o600 }, ); const resumedRollout = spawnSync( process.execPath, [cli, 'compose-apply', '--command-file', commandPath], { encoding: 'utf8', maxBuffer: 64 * 1024, timeout: input.profile === 'edge' ? 75_000 : 105_000, killSignal: 'SIGKILL', }, ); if ( resumedRollout.error || resumedRollout.status !== 2 || resumedRollout.stderr !== '' || resumedRollout.stdout.includes(deploymentRoot) || /sha256|mutation|rollout|socket|executable|container/i.test( resumedRollout.stdout, ) ) { fail('restored rollout did not return a low-sensitive recovery result'); } const recoveredReport = JSON.parse(resumedRollout.stdout); if ( recoveredReport.status !== 'rolled_back' || recoveredReport.attemptedGeneration !== 3 || recoveredReport.activeGeneration !== 4 || recoveredReport.health?.event !== 'active' || fs.existsSync(restoreRolloutLock) || !fs.existsSync(restoreRolloutReceipt) ) { fail('restored rollout did not recover the rollback generation'); } const commitReplay = invokeRestoreCli( 'compose-restore-commit', commitRestoreCommand, ); const runningAfterReplay = docker( input, dockerConfigRoot, composeArguments(deploymentRoot, [ 'ps', '--status', 'running', '--quiet', 'qinglong3', ]), ); if ( commitReplay.status !== 'existing' || commitReplay.service?.state !== 'unchanged' || runningAfterReplay.error || runningAfterReplay.status !== 0 || runningAfterReplay.stdout.trim() === '' ) { fail('commit replay changed the recovered service'); } const extraTransactions = input.profile === 'edge' ? [ { revisionId: '019f8680-143d-4000-8000-00000000020c', rolloutId: '019f8680-143d-4000-8000-00000000020d', rollbackId: '019f8680-143d-4000-8000-00000000020e', }, ] : [ { revisionId: '019f8680-143d-4000-8000-00000000020c', rolloutId: '019f8680-143d-4000-8000-00000000020d', rollbackId: '019f8680-143d-4000-8000-00000000020e', }, { revisionId: '019f8680-143d-4000-8000-00000000020f', rolloutId: '019f8680-143d-4000-8000-000000000210', rollbackId: '019f8680-143d-4000-8000-000000000211', }, { revisionId: '019f8680-143d-4000-8000-000000000212', rolloutId: '019f8680-143d-4000-8000-000000000213', rollbackId: '019f8680-143d-4000-8000-000000000214', }, ]; let evidenceGeneration = 4; for (const [index, transaction] of extraTransactions.entries()) { const revisionResult = await switchLocalDeploymentComposeRevision({ schemaVersion: 1, operation: 'local.deployment.compose.upgrade', options: { deploymentRoot, allowRootService: uid === 0, }, request: { expectedGeneration: evidenceGeneration, releaseSelection, mutationId: transaction.revisionId, changedAtMs: 1_785_254_600_012 + index * 3, }, }); evidenceGeneration += 1; if ( revisionResult.status !== 'prepared' || revisionResult.generation !== evidenceGeneration ) { fail('evidence collection setup revision did not publish'); } const extraRollout = await applyLocalDeploymentCompose({ ...firstCommand, request: { expectedGeneration: evidenceGeneration, rolloutId: transaction.rolloutId, startedAtMs: 1_785_254_600_013 + index * 3, failureRollbackMutationId: transaction.rollbackId, failureRollbackChangedAtMs: 1_785_254_600_014 + index * 3, }, }); if ( extraRollout.status !== 'active' || extraRollout.activeGeneration !== evidenceGeneration ) { fail('evidence collection setup rollout did not become active'); } } const collectionId = '019f8680-143d-4000-8000-000000000215'; const collectionOptions = { deploymentRoot, allowRootService: uid === 0, }; const collectionPrepare = await collectLocalDeploymentComposeEvidence({ schemaVersion: 1, operation: 'local.deployment.compose.evidence-collection.prepare', options: collectionOptions, request: { expectedGeneration: evidenceGeneration, collectionId, rolloutIds: [secondCommand.request.rolloutId], restoreIds: [], preparedAtMs: 1_785_254_600_100, }, }); const runningBeforeCollectionReplay = docker( input, dockerConfigRoot, composeArguments(deploymentRoot, [ 'ps', '--status', 'running', '--quiet', 'qinglong3', ]), ); const collectionCommitCommand = { schemaVersion: 1, operation: 'local.deployment.compose.evidence-collection.commit', options: collectionOptions, request: { expectedGeneration: evidenceGeneration, collectionId, committedAtMs: 1_785_254_600_101, }, }; const collectionCommit = await collectLocalDeploymentComposeEvidence( collectionCommitCommand, ); const historicalReplay = await applyLocalDeploymentCompose(secondCommand); const collectionReplay = await collectLocalDeploymentComposeEvidence( collectionCommitCommand, ); const runningAfterCollectionReplay = docker( input, dockerConfigRoot, composeArguments(deploymentRoot, [ 'ps', '--status', 'running', '--quiet', 'qinglong3', ]), ); const collectedTombstone = path.join( serviceRoot, 'collected-evidence', 'rollout-backups', `${secondCommand.request.rolloutId}.json`, ); if ( collectionPrepare.status !== 'prepared' || collectionCommit.status !== 'collected' || collectionReplay.status !== 'existing' || collectionCommit.collected?.rolloutBackups !== 1 || collectionCommit.collected?.bytes !== backup.bytes || historicalReplay.status !== 'active' || fs.existsSync(backupPath) || !fs.existsSync(collectedTombstone) || runningBeforeCollectionReplay.error || runningBeforeCollectionReplay.status !== 0 || runningBeforeCollectionReplay.stdout.trim() === '' || runningAfterCollectionReplay.error || runningAfterCollectionReplay.status !== 0 || runningAfterCollectionReplay.stdout.trim() !== runningBeforeCollectionReplay.stdout.trim() ) { fail('explicit evidence collection or historical replay drifted'); } cleanup(input, dockerConfigRoot, deploymentRoot, true); composeResourcesPresent = false; process.stdout.write( `${JSON.stringify({ schemaVersion: 1, profile: recoveredReport.profile, generation: evidenceGeneration, exactRepoDigest: true, composeMerge: true, rolloutActive: true, durableReceipt: true, sqliteWriteContract: LOCAL_SQLITE_WRITE_CONTRACT_VERSION, sqliteBackup: true, sqliteWriteObservation: receipt.sqlite.writeObservation, sqliteRestorePrepared: true, sqliteRestoreCommitted: true, sqliteRestoreRolloutRecovered: true, sqliteRestoreReplayUnchanged: true, sqliteEvidenceCollected: true, sqliteCollectedRolloutReplayUnchanged: true, gracefulCleanup: true, releaseAuthority: selection.authority, compatible: true, })}\n`, ); } finally { if (composeResourcesPresent) { try { cleanup(input, dockerConfigRoot, deploymentRoot, false); } catch { // The original failure remains authoritative. } } fs.rmSync(temporaryRoot, { recursive: true, force: true }); } } if (require.main === module) { main().catch((error) => { process.stderr.write( `${error instanceof Error ? error.message : String(error)}\n`, ); process.exitCode = 1; }); } module.exports = Object.freeze({ argumentsValue, resolveReleaseSelection, });