dsh 0.2.0-rc.2: in-container update (experimental channel), path-agnostic compose, upstream patches adapted to 0.1.x/0.2.x

This commit is contained in:
hermes committed 2026-10-01 19:21:19 +00:00
commit e6dcef9bf5
302 files changed
+75178

No files matched your search

@@ -0,0 +1,143 @@
import { createHash } from 'node:crypto'
import { canonicalJson } from '../../../../platform/lib/canonical-json.mjs'
import { TrustError } from '../../../../platform/lib/validation.mjs'
function deploymentIdentity(deployment) {
return {
dsh: deployment.dsh,
environment: deployment.environment,
runtime: deployment.runtime,
receiptTokens: [...deployment.receiptTokens].sort(),
}
}
function recordIdentity(record) {
return deploymentIdentity({
dsh: record.dshVersion,
environment: record.environmentVersion,
runtime: record.id,
receiptTokens: record.receiptTokens,
})
}
function recordMatchesDeployment(record, deployment) {
return record.dshVersion === deployment.dsh
&& record.environmentVersion === deployment.environment
&& [...record.receiptTokens].sort().join('\0') === [...deployment.receiptTokens].sort().join('\0')
}
function sameRecordContent(left, right) {
return left.authority === right.authority
&& left.targetSequence === right.targetSequence
&& left.dshVersion === right.dshVersion
&& left.environmentVersion === right.environmentVersion
&& left.environment.sha256 === right.environment.sha256
&& left.pristine.sha256 === right.pristine.sha256
&& left.runtime.sha256 === right.runtime.sha256
&& left.systemPlugins.sha256 === right.systemPlugins.sha256
}
function bindPlan(value) {
return Object.freeze({
...value,
planId: createHash('sha256').update(canonicalJson(value)).digest('hex'),
})
}
export class CompleteStateRecovery {
constructor({ journal, snapshots, activator }) {
this.journal = journal
this.snapshots = snapshots
this.activator = activator
}
async plan() {
const transaction = await this.journal.read()
if (transaction?.phase === 'committed' && transaction.snapshotId !== null) {
const current = await this.activator.currentDeployment()
if (
current.dsh === transaction.to.dsh
&& current.environment === transaction.to.environment
&& current.runtime === transaction.to.runtime
) {
let previous = deploymentIdentity(transaction.from)
if (this.activator.rollbackDeployments !== undefined) {
const slots = await this.activator.rollbackDeployments()
if (
slots.current?.id !== current.runtime
|| slots.previous === null
|| !recordMatchesDeployment(slots.previous, transaction.from)
) return null
previous = recordIdentity(slots.previous)
}
const snapshot = await this.snapshots.inspect(transaction.snapshotId)
if (
snapshot.runtimeId !== transaction.from.runtime
|| snapshot.environmentVersion !== transaction.from.environment
|| snapshot.dshVersion !== transaction.from.dsh
) throw new TrustError('rollback snapshot does not describe the previous deployment')
return bindPlan({
transactionId: transaction.transactionId,
mode: transaction.mode,
current: deploymentIdentity(current),
previous,
snapshot: {
id: snapshot.id,
createdAt: snapshot.createdAt,
sha256: snapshot.archiveSha256,
size: snapshot.archiveSize,
},
})
}
}
if (this.activator.rollbackDeployments === undefined) return null
const { current, previous } = await this.activator.rollbackDeployments()
if (current === null || previous === null) return null
if (sameRecordContent(current, previous)) return null
return bindPlan({
transactionId: null,
mode: 'stable',
current: recordIdentity(current),
previous: recordIdentity(previous),
snapshot: null,
})
}
async restore(planId, {
confirmDataLoss = false,
requireConfirmation = false,
onProgress = async () => {},
} = {}) {
if (requireConfirmation && confirmDataLoss !== true) {
throw new TrustError('return to Stable requires explicit data-loss confirmation')
}
const plan = await this.plan()
if (plan === null || plan.planId !== planId) throw new TrustError('rollback plan is stale or unavailable')
if (requireConfirmation && plan.snapshot === null) {
throw new TrustError('return to Stable requires a verified data snapshot')
}
if (plan.snapshot === null) {
await onProgress('switching', 35, {})
await this.activator.rollback(plan.previous.runtime)
await onProgress('verifying', 90, {})
return Object.freeze({ status: 'rolled-back', transactionId: null })
}
let transaction = await this.journal.transition('restoring-data', { error: 'manual complete-state rollback' })
await onProgress('stopping', 15, {})
await this.activator.suspendDsh()
try {
await onProgress('switching', 35, {})
await this.activator.restoreDeployment({ ...transaction.from, runtime: plan.previous.runtime }, { resume: false })
await onProgress('restoring-data', 65, {})
await this.snapshots.restore(transaction.snapshotId, {
onProgress: metrics => onProgress('restoring-data', 65, metrics),
})
await onProgress('verifying', 85, {})
await this.activator.resumeDsh()
transaction = await this.journal.transition('rolled-back', { error: 'manual complete-state rollback completed' })
return Object.freeze({ status: 'rolled-back', transactionId: transaction.transactionId })
} catch (error) {
throw error
}
}
}