import fs from 'node:fs'; import path from 'node:path'; import { readPrivateLocalCommandFile } from '@qinglong/local-command-file'; import { currentIdentity, LocalDeploymentConfigurationError, } from '../foundation/contract'; import { runLocalDeploymentDockerCommand, validateLocalDeploymentDockerSocket, type LocalDeploymentDockerRunner, } from '../foundation/docker'; import { preflightPublishedFile, publishExactFile, validatePrivateDirectory, } from '../foundation/files'; import { advanceLocalCutoverInstanceHead, localCutoverInstanceDirectory, readLocalCutoverInstanceHead, type LocalCutoverInstanceHead, } from './instanceLineage'; import { EMPTY_ROLLBACK_PREPARATION_DIGEST, legacyRollbackTargetRunCommand, normalizeLocalDeploymentLegacyRollbackCommand, type LocalDeploymentLegacyRollbackCommand, type LocalDeploymentLegacyRollbackResult, } from './legacyRollbackContract'; import { readTargetDataReconciliationEvidence, type TargetDataReconciliationEvidence, } from './targetDataEvidence'; import { cutoverDigest, legacyCommitmentPath, parseActiveLegacyEvidence, parseStoppedLegacyEvidence, parseTargetContainerEvidence, readLegacySilenceEvidence, readTargetApplicationBinding, type LegacySilenceEvidence, type TargetApplicationBinding, } from './targetEvidence'; import { legacyRollbackPhasePath, legacyRollbackSequence, publishTargetRunJournalRecord, readTargetRunJournalRecord, targetRunJournalRecord, targetRunManualEvidence, targetRunPhasePath, targetRunSequence, targetStopPhasePath, targetStopSequence, verifyTargetRunManualEvidence, type TargetRunJournalContext, type TargetRunJournalRecord, } from './target-run/targetRunJournal'; import { verifyTargetActiveEvidence, verifyTargetRequestEvidence, } from './target-run/targetRunRecordEvidence'; import { verifyTargetStoppedEvidence, verifyTargetStopRequestEvidence, type TargetStopActiveEvidence, } from './targetStopRecordEvidence'; import type { LocalDeploymentTargetRunCommand } from './target-run/targetRunContract'; const PREPARATION_SCHEMA = 'qinglong3-local-legacy-rollback-preparation'; const DIGEST_PATTERN = /^[0-9a-f]{64}$/; const MAX_PREPARATIONS_PER_INSTANCE = 15; export interface LocalDeploymentLegacyRollbackDependencies { readonly runDocker?: LocalDeploymentDockerRunner; readonly validateSocket?: (socketPath: string, uid: number) => void; readonly afterBarrier?: () => void; readonly afterStart?: () => void; } interface RollbackContext { readonly rollbackCommand: Readonly; readonly command: Readonly; readonly sourceCommand: Readonly; readonly journalCommand: Readonly; readonly journal: string; readonly uid: number; readonly commitment: Readonly; readonly application: Readonly; } interface RollbackSource { readonly active: Readonly; readonly stoppedRecord: Readonly; readonly reconciliation: Readonly; } interface RollbackPreparation { readonly schema: typeof PREPARATION_SCHEMA; readonly schemaVersion: 1; readonly state: 'rollback_prepared'; readonly cutoverId: string; readonly profile: 'edge' | 'standalone'; readonly instanceId: string; readonly activationDigest: string; readonly generation: number; readonly expectedInstanceHeadDigest: string; readonly stoppedRecordDigest: string; readonly reconciliationEvidenceDigest: string; readonly legacyContainerIdentityDigest: string; readonly legacySourceBindingDigest: string; readonly targetContainerIdentityDigest: string; readonly targetApplicationBindingDigest: string; readonly rollbackRequestedAtMs: number; readonly preparationDigest: string; } function configurationError(message: string, cause?: unknown): never { throw new LocalDeploymentConfigurationError(message, { cause }); } function object(value: unknown, label: string): Record { if ( !value || typeof value !== 'object' || Array.isArray(value) || (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) ) { configurationError(`${label} must be an object`); } return value as Record; } function exact( value: Record, keys: readonly string[], label: string, ): void { const actual = Object.keys(value).sort(); const expected = [...keys].sort(); if ( actual.length !== expected.length || actual.some((key, index) => key !== expected[index]) ) { configurationError(`${label} shape is invalid`); } } function journalCommand( source: Readonly, requestedAtMs: number, ): Readonly { return Object.freeze({ ...source, request: Object.freeze({ ...source.request, requestedAtMs }), }); } function targetContext( context: Readonly, ): Readonly { return Object.freeze({ command: context.sourceCommand, uid: context.uid }); } function rollbackContext( context: Readonly, ): Readonly { return Object.freeze({ command: context.journalCommand, uid: context.uid }); } function readRollbackSource( context: Readonly, ): Readonly { const generation = context.sourceCommand.request.generation; const request = readTargetRunJournalRecord( targetRunPhasePath(context.journal, generation, 'request'), targetContext(context), { sequence: targetRunSequence(generation, 'request'), generation, states: [ generation === 1 ? 'target_start_requested' : 'target_restart_requested', ], }, ); const requestEvidence = verifyTargetRequestEvidence(context, request); const activeRecord = readTargetRunJournalRecord( targetRunPhasePath(context.journal, generation, 'outcome'), targetContext(context), { sequence: targetRunSequence(generation, 'outcome'), generation, states: ['target_active'], previousRecordDigest: request.recordDigest, }, ); const active = Object.freeze({ activeRecordDigest: activeRecord.recordDigest, targetContainerIdentityDigest: requestEvidence.targetContainerIdentityDigest, targetApplicationBindingDigest: requestEvidence.targetApplicationBindingDigest, startupReceiptDigest: verifyTargetActiveEvidence( context, activeRecord, requestEvidence, ), }); const stopRequest = readTargetRunJournalRecord( targetStopPhasePath(context.journal, generation, 'request'), targetContext(context), { sequence: targetStopSequence(generation, 'request'), generation, states: ['target_stop_requested'], previousRecordDigest: activeRecord.recordDigest, requestedAtMs: context.sourceCommand.request.requestedAtMs, }, ); verifyTargetStopRequestEvidence(stopRequest, active); const stoppedRecord = readTargetRunJournalRecord( targetStopPhasePath(context.journal, generation, 'outcome'), targetContext(context), { sequence: targetStopSequence(generation, 'outcome'), generation, states: ['target_stopped'], previousRecordDigest: stopRequest.recordDigest, requestedAtMs: context.sourceCommand.request.requestedAtMs, }, ); const reconciliation = verifyTargetStoppedEvidence(stoppedRecord, active); if ( stoppedRecord.recordDigest !== context.rollbackCommand.request.expectedStoppedRecordDigest || reconciliation.disposition !== 'rollback_candidate' ) { configurationError('legacy rollback requires the exact rollback candidate'); } return Object.freeze({ active, stoppedRecord, reconciliation }); } function preparationPath( command: Readonly, ): string { return path.join( localCutoverInstanceDirectory( command.options.deploymentRoot, command.request.instanceId, ), `rollback-${command.request.cutoverId}-${String( command.request.generation, ).padStart(2, '0')}.json`, ); } function preparationRecord( context: Readonly, source: Readonly, ): Readonly { const payload = Object.freeze({ schema: PREPARATION_SCHEMA, schemaVersion: 1 as const, state: 'rollback_prepared' as const, cutoverId: context.sourceCommand.request.cutoverId, profile: context.sourceCommand.request.profile, instanceId: context.sourceCommand.request.instanceId, activationDigest: context.sourceCommand.request.expectedActivationDigest, generation: context.sourceCommand.request.generation, expectedInstanceHeadDigest: context.rollbackCommand.request.expectedInstanceHeadDigest, stoppedRecordDigest: source.stoppedRecord.recordDigest, reconciliationEvidenceDigest: source.reconciliation.evidenceDigest, legacyContainerIdentityDigest: context.commitment.legacyContainerIdentityDigest, legacySourceBindingDigest: context.commitment.legacySourceBindingDigest, targetContainerIdentityDigest: source.active.targetContainerIdentityDigest, targetApplicationBindingDigest: source.active.targetApplicationBindingDigest, rollbackRequestedAtMs: context.rollbackCommand.request.rollbackRequestedAtMs, }); return Object.freeze({ ...payload, preparationDigest: cutoverDigest(payload), }); } function parsePreparation( value: unknown, context: Readonly, source: Readonly, ): Readonly { const record = object(value, 'legacy rollback preparation'); exact( record, [ 'activationDigest', 'cutoverId', 'expectedInstanceHeadDigest', 'generation', 'instanceId', 'legacyContainerIdentityDigest', 'legacySourceBindingDigest', 'preparationDigest', 'profile', 'reconciliationEvidenceDigest', 'rollbackRequestedAtMs', 'schema', 'schemaVersion', 'state', 'stoppedRecordDigest', 'targetApplicationBindingDigest', 'targetContainerIdentityDigest', ], 'legacy rollback preparation', ); const { preparationDigest, ...payload } = record; if ( record.schema !== PREPARATION_SCHEMA || record.schemaVersion !== 1 || record.state !== 'rollback_prepared' || record.cutoverId !== context.sourceCommand.request.cutoverId || record.profile !== context.sourceCommand.request.profile || record.instanceId !== context.sourceCommand.request.instanceId || record.activationDigest !== context.sourceCommand.request.expectedActivationDigest || record.generation !== context.sourceCommand.request.generation || record.expectedInstanceHeadDigest !== context.rollbackCommand.request.expectedInstanceHeadDigest || record.stoppedRecordDigest !== source.stoppedRecord.recordDigest || record.reconciliationEvidenceDigest !== source.reconciliation.evidenceDigest || record.legacyContainerIdentityDigest !== context.commitment.legacyContainerIdentityDigest || record.legacySourceBindingDigest !== context.commitment.legacySourceBindingDigest || record.targetContainerIdentityDigest !== source.active.targetContainerIdentityDigest || record.targetApplicationBindingDigest !== source.active.targetApplicationBindingDigest || record.rollbackRequestedAtMs !== context.rollbackCommand.request.rollbackRequestedAtMs || typeof preparationDigest !== 'string' || !DIGEST_PATTERN.test(preparationDigest) || cutoverDigest(payload) !== preparationDigest ) { configurationError('legacy rollback preparation drifted'); } return record as unknown as Readonly; } function docker( context: Readonly, runDocker: LocalDeploymentDockerRunner, args: readonly string[], timeoutMs: number, ): string { return runDocker({ executable: context.sourceCommand.options.dockerExecutable, socketPath: context.sourceCommand.options.dockerSocketPath, args, timeoutMs, }); } function stoppedObservations( context: Readonly, source: Readonly, runDocker: LocalDeploymentDockerRunner, ): void { const legacy = parseStoppedLegacyEvidence( docker( context, runDocker, [ 'container', 'inspect', context.sourceCommand.request.expectedLegacyContainerId, ], 30_000, ), context.sourceCommand, ); const target = parseTargetContainerEvidence( docker( context, runDocker, [ 'container', 'inspect', context.sourceCommand.request.expectedTargetContainerId, ], 30_000, ), context.sourceCommand, context.application, 'stopped', ); const reconciliation = readTargetDataReconciliationEvidence( context.sourceCommand, context.uid, ); if ( legacy.identityDigest !== context.commitment.legacyContainerIdentityDigest || legacy.sourceBindingDigest !== context.commitment.legacySourceBindingDigest || target.identityDigest !== source.active.targetContainerIdentityDigest || target.applicationBindingDigest !== source.active.targetApplicationBindingDigest || reconciliation.disposition !== 'rollback_candidate' || reconciliation.evidenceDigest !== source.reconciliation.evidenceDigest ) { configurationError('legacy rollback stopped evidence drifted'); } } function result( context: Readonly, status: 'prepared' | 'existing', state: LocalDeploymentLegacyRollbackResult['state'], preparationDigest: string, recordDigest: string, ): Readonly { const head = advanceLocalCutoverInstanceHead( context.sourceCommand, context.uid, state, context.sourceCommand.request.generation, state === 'rollback_prepared' ? preparationDigest : recordDigest, ); return Object.freeze({ schemaVersion: 1 as const, operation: context.rollbackCommand.operation, status, state, cutoverId: context.sourceCommand.request.cutoverId, generation: context.sourceCommand.request.generation, preparationDigest, recordDigest, instanceHeadDigest: head.headDigest, }); } function requestEvidence( preparation: Readonly, ): Readonly> { return Object.freeze({ preparationDigest: preparation.preparationDigest, stoppedRecordDigest: preparation.stoppedRecordDigest, reconciliationEvidenceDigest: preparation.reconciliationEvidenceDigest, legacyContainerIdentityDigest: preparation.legacyContainerIdentityDigest, legacySourceBindingDigest: preparation.legacySourceBindingDigest, targetContainerIdentityDigest: preparation.targetContainerIdentityDigest, targetApplicationBindingDigest: preparation.targetApplicationBindingDigest, }); } function verifyRequestEvidence( record: Readonly, preparation: Readonly, ): void { const evidence = object(record.evidence, 'legacy rollback request evidence'); const expected = requestEvidence(preparation); exact(evidence, Object.keys(expected), 'legacy rollback request evidence'); if ( Object.entries(expected).some(([key, value]) => evidence[key] !== value) ) { configurationError('legacy rollback request evidence drifted'); } } function outcomeEvidence( request: Readonly, preparation: Readonly, ): Readonly> { return Object.freeze({ preparationDigest: preparation.preparationDigest, requestRecordDigest: request.recordDigest, legacyContainerIdentityDigest: preparation.legacyContainerIdentityDigest, legacySourceBindingDigest: preparation.legacySourceBindingDigest, targetContainerIdentityDigest: preparation.targetContainerIdentityDigest, targetApplicationBindingDigest: preparation.targetApplicationBindingDigest, }); } function verifyOutcomeEvidence( record: Readonly, request: Readonly, preparation: Readonly, ): void { const evidence = object(record.evidence, 'legacy rollback outcome evidence'); const expected = outcomeEvidence(request, preparation); exact(evidence, Object.keys(expected), 'legacy rollback outcome evidence'); if ( Object.entries(expected).some(([key, value]) => evidence[key] !== value) ) { configurationError('legacy rollback outcome evidence drifted'); } } function publishManual( context: Readonly, filePath: string, sequence: number, previousRecordDigest: string, reason: | 'legacy_restart_preflight_unproved' | 'legacy_restart_result_unproved', preparationDigest: string, ): Readonly { const record = targetRunJournalRecord( context.journalCommand, sequence, 'manual_required', previousRecordDigest, targetRunManualEvidence(reason), ); const status = publishTargetRunJournalRecord( rollbackContext(context), filePath, record, 'legacy rollback manual resolution', ); return result( context, status, 'manual_required', preparationDigest, record.recordDigest, ); } function replayCommit( context: Readonly, preparation: Readonly, ): Readonly | undefined { const generation = context.sourceCommand.request.generation; const requestPath = legacyRollbackPhasePath( context.journal, generation, 'request', ); if (!fs.existsSync(requestPath)) return undefined; const request = readTargetRunJournalRecord( requestPath, rollbackContext(context), { sequence: legacyRollbackSequence(generation, 'request'), generation, states: ['legacy_restart_requested', 'manual_required'], previousRecordDigest: preparation.preparationDigest, requestedAtMs: context.rollbackCommand.request.rollbackRequestedAtMs, }, ); if (request.state === 'manual_required') { verifyTargetRunManualEvidence(request); return result( context, 'existing', 'manual_required', preparation.preparationDigest, request.recordDigest, ); } verifyRequestEvidence(request, preparation); const outcomePath = legacyRollbackPhasePath( context.journal, generation, 'outcome', ); if (!fs.existsSync(outcomePath)) return undefined; const outcome = readTargetRunJournalRecord( outcomePath, rollbackContext(context), { sequence: legacyRollbackSequence(generation, 'outcome'), generation, states: ['legacy_running', 'manual_required'], previousRecordDigest: request.recordDigest, requestedAtMs: context.rollbackCommand.request.rollbackRequestedAtMs, }, ); if (outcome.state === 'manual_required') { verifyTargetRunManualEvidence(outcome); return result( context, 'existing', 'manual_required', preparation.preparationDigest, outcome.recordDigest, ); } verifyOutcomeEvidence(outcome, request, preparation); return result( context, 'existing', 'legacy_running', preparation.preparationDigest, outcome.recordDigest, ); } function prepare( context: Readonly, source: Readonly, head: Readonly, dependencies: LocalDeploymentLegacyRollbackDependencies, ): Readonly { const filePath = preparationPath(context.rollbackCommand); if (head.state === 'rollback_prepared') { const preparation = parsePreparation( readPrivateLocalCommandFile(filePath), context, source, ); if ( head.previousHeadDigest !== context.rollbackCommand.request.expectedInstanceHeadDigest || head.sourceRecordDigest !== preparation.preparationDigest ) { configurationError('legacy rollback preparation lost the instance head'); } return result( context, 'existing', 'rollback_prepared', preparation.preparationDigest, source.stoppedRecord.recordDigest, ); } if ( head.state !== 'target_stopped' || head.headDigest !== context.rollbackCommand.request.expectedInstanceHeadDigest || head.sourceRecordDigest !== source.stoppedRecord.recordDigest || head.generation !== context.sourceCommand.request.generation ) { configurationError( 'legacy rollback prepare is not bound to target stopped', ); } const validateSocket = dependencies.validateSocket ?? validateLocalDeploymentDockerSocket; validateSocket(context.sourceCommand.options.dockerSocketPath, context.uid); const runDocker = dependencies.runDocker ?? runLocalDeploymentDockerCommand; stoppedObservations(context, source, runDocker); const preparation = preparationRecord(context, source); const serialized = `${JSON.stringify(preparation, null, 2)}\n`; const directory = path.dirname(filePath); const preparationEntries = fs .readdirSync(directory, { withFileTypes: true }) .filter((entry) => entry.name.startsWith('rollback-')); if (preparationEntries.some((entry) => !entry.isFile())) { configurationError( 'legacy rollback preparation directory contains an unsafe entry', ); } if ( preparationEntries.length >= MAX_PREPARATIONS_PER_INSTANCE && !fs.existsSync(filePath) ) { configurationError( 'legacy rollback preparation retention limit is reached', ); } preflightPublishedFile( filePath, serialized, 0o600, context.uid, 'legacy rollback preparation', ); const status = publishExactFile( filePath, serialized, 0o600, context.uid, 'legacy rollback preparation', ); return result( context, status, 'rollback_prepared', preparation.preparationDigest, source.stoppedRecord.recordDigest, ); } function commit( context: Readonly, source: Readonly, head: Readonly, dependencies: LocalDeploymentLegacyRollbackDependencies, ): Readonly { const preparation = parsePreparation( readPrivateLocalCommandFile(preparationPath(context.rollbackCommand)), context, source, ); if ( preparation.preparationDigest !== context.rollbackCommand.request.expectedPreparationDigest ) { configurationError('legacy rollback commit preparation is invalid'); } const replay = replayCommit(context, preparation); if (replay !== undefined) return replay; if ( head.state !== 'rollback_prepared' && head.state !== 'legacy_restart_requested' ) { configurationError('legacy rollback commit is not bound to preparation'); } if ( head.state === 'rollback_prepared' && (head.sourceRecordDigest !== preparation.preparationDigest || head.previousHeadDigest !== context.rollbackCommand.request.expectedInstanceHeadDigest) ) { configurationError('legacy rollback commit lost the instance head'); } const validateSocket = dependencies.validateSocket ?? validateLocalDeploymentDockerSocket; validateSocket(context.sourceCommand.options.dockerSocketPath, context.uid); const runDocker = dependencies.runDocker ?? runLocalDeploymentDockerCommand; const generation = context.sourceCommand.request.generation; const requestPath = legacyRollbackPhasePath( context.journal, generation, 'request', ); let request: Readonly; let shouldStart = false; if (fs.existsSync(requestPath)) { request = readTargetRunJournalRecord( requestPath, rollbackContext(context), { sequence: legacyRollbackSequence(generation, 'request'), generation, states: ['legacy_restart_requested'], previousRecordDigest: preparation.preparationDigest, requestedAtMs: context.rollbackCommand.request.rollbackRequestedAtMs, }, ); verifyRequestEvidence(request, preparation); advanceLocalCutoverInstanceHead( context.sourceCommand, context.uid, 'legacy_restart_requested', generation, request.recordDigest, ); } else { try { stoppedObservations(context, source, runDocker); } catch { return publishManual( context, requestPath, legacyRollbackSequence(generation, 'request'), preparation.preparationDigest, 'legacy_restart_preflight_unproved', preparation.preparationDigest, ); } request = targetRunJournalRecord( context.journalCommand, legacyRollbackSequence(generation, 'request'), 'legacy_restart_requested', preparation.preparationDigest, requestEvidence(preparation), ); publishTargetRunJournalRecord( rollbackContext(context), requestPath, request, 'legacy rollback start barrier', ); advanceLocalCutoverInstanceHead( context.sourceCommand, context.uid, 'legacy_restart_requested', generation, request.recordDigest, ); shouldStart = true; dependencies.afterBarrier?.(); } if (shouldStart) { try { stoppedObservations(context, source, runDocker); } catch { return publishManual( context, legacyRollbackPhasePath(context.journal, generation, 'outcome'), legacyRollbackSequence(generation, 'outcome'), request.recordDigest, 'legacy_restart_result_unproved', preparation.preparationDigest, ); } try { docker( context, runDocker, [ 'container', 'start', context.sourceCommand.request.expectedLegacyContainerId, ], 45_000, ); } catch { // The exact running inspection below resolves a lost start response. } dependencies.afterStart?.(); } const outcomePath = legacyRollbackPhasePath( context.journal, generation, 'outcome', ); try { const legacy = parseActiveLegacyEvidence( docker( context, runDocker, [ 'container', 'inspect', context.sourceCommand.request.expectedLegacyContainerId, ], 30_000, ), context.sourceCommand, ); const target = parseTargetContainerEvidence( docker( context, runDocker, [ 'container', 'inspect', context.sourceCommand.request.expectedTargetContainerId, ], 30_000, ), context.sourceCommand, context.application, 'stopped', ); if ( legacy.identityDigest !== context.commitment.legacyContainerIdentityDigest || legacy.sourceBindingDigest !== context.commitment.legacySourceBindingDigest || target.identityDigest !== source.active.targetContainerIdentityDigest || target.applicationBindingDigest !== source.active.targetApplicationBindingDigest ) { configurationError('legacy rollback outcome identity drifted'); } } catch { return publishManual( context, outcomePath, legacyRollbackSequence(generation, 'outcome'), request.recordDigest, 'legacy_restart_result_unproved', preparation.preparationDigest, ); } const outcome = targetRunJournalRecord( context.journalCommand, legacyRollbackSequence(generation, 'outcome'), 'legacy_running', request.recordDigest, outcomeEvidence(request, preparation), ); const status = publishTargetRunJournalRecord( rollbackContext(context), outcomePath, outcome, 'legacy rollback running commitment', ); return result( context, status, 'legacy_running', preparation.preparationDigest, outcome.recordDigest, ); } export function runLocalDeploymentLegacyRollback( input: unknown, dependencies: LocalDeploymentLegacyRollbackDependencies = {}, ): Readonly { const rollbackCommand = normalizeLocalDeploymentLegacyRollbackCommand(input); const sourceCommand = legacyRollbackTargetRunCommand(rollbackCommand); const identity = currentIdentity(); const journal = path.dirname(legacyCommitmentPath(sourceCommand)); validatePrivateDirectory( sourceCommand.options.deploymentRoot, identity.uid, 'deploymentRoot', ); validatePrivateDirectory( path.join(sourceCommand.options.deploymentRoot, 'service'), identity.uid, 'serviceDescriptorRoot', ); validatePrivateDirectory(journal, identity.uid, 'cutoverJournal'); validatePrivateDirectory( localCutoverInstanceDirectory( sourceCommand.options.deploymentRoot, sourceCommand.request.instanceId, ), identity.uid, 'cutoverInstanceDirectory', ); const context = Object.freeze({ rollbackCommand, command: sourceCommand, sourceCommand, journalCommand: journalCommand( sourceCommand, rollbackCommand.request.rollbackRequestedAtMs, ), journal, uid: identity.uid, commitment: readLegacySilenceEvidence(sourceCommand), application: readTargetApplicationBinding(sourceCommand), }); const source = readRollbackSource(context); const head = readLocalCutoverInstanceHead( sourceCommand.options.deploymentRoot, sourceCommand.request.instanceId, identity.uid, ); if ( head.profile !== sourceCommand.request.profile || head.cutoverId !== sourceCommand.request.cutoverId || head.activationDigest !== sourceCommand.request.expectedActivationDigest || head.generation !== sourceCommand.request.generation ) { configurationError('legacy rollback is not bound to the instance lineage'); } return rollbackCommand.operation === 'local.deployment.cutover.legacy-rollback-prepare' ? prepare(context, source, head, dependencies) : commit(context, source, head, dependencies); } export function runLocalDeploymentLegacyRollbackCommandFile( filePath: string, expectedOperation?: LocalDeploymentLegacyRollbackCommand['operation'], ): Readonly { const input = readPrivateLocalCommandFile(filePath); if ( expectedOperation !== undefined && (!input || typeof input !== 'object' || Array.isArray(input) || (input as Record).operation !== expectedOperation) ) { configurationError( 'legacy rollback command does not match the CLI operation', ); } return runLocalDeploymentLegacyRollback(input); } export { EMPTY_ROLLBACK_PREPARATION_DIGEST };