feat(ql3): rehearse exact local upgrade cutover

This commit is contained in:
whyour
2026-08-30 22:05:04 +08:00
parent 784e77a971
commit 93b45ca62d
16 changed files with 539 additions and 36 deletions
+1 -1
View File
@@ -240,7 +240,7 @@ function validateMilestoneRecord(record, product, sourceRevision, variant) {
: 'cluster_integration_candidate_not_public_release';
const expectedSchema =
product === 'local'
? 'qinglong/alpha-local-milestone@v4'
? 'qinglong/alpha-local-milestone@v5'
: 'qinglong/alpha-cluster-milestone@v1';
if (
!exactKeys(record, ['artifactName', 'schema', 'maturity', 'manifest']) ||
+13 -6
View File
@@ -12,7 +12,7 @@ const {
const { readReleaseIdentity } = require('./lib/ql3-release-identity.cjs');
const DEFAULT_ROOT = path.resolve(__dirname, '..');
const SCHEMA = 'qinglong/alpha-local-milestone@v4';
const SCHEMA = 'qinglong/alpha-local-milestone@v5';
const ARCHITECTURES = Object.freeze(['amd64', 'arm64']);
const FILES = Object.freeze({
readme: 'README.md',
@@ -221,6 +221,7 @@ function bundleRecord(options, architecture) {
verificationSha256: report.verificationSha256,
upgradeReadinessSha256: report.upgradeReadinessSha256,
upgradeRehearsalSha256: report.upgradeRehearsalSha256,
upgradeCutoverRehearsalSha256: report.upgradeCutoverRehearsalSha256,
});
}
@@ -236,6 +237,7 @@ function validateArtifactRecord(record, architecture, manifest) {
'verificationSha256',
'upgradeReadinessSha256',
'upgradeRehearsalSha256',
'upgradeCutoverRehearsalSha256',
]) ||
record.artifactName !==
artifactName(manifest.sourceRevision, architecture, manifest.variant) ||
@@ -251,6 +253,7 @@ function validateArtifactRecord(record, architecture, manifest) {
!SHA256_PATTERN.test(record.verificationSha256 || '') ||
!SHA256_PATTERN.test(record.upgradeReadinessSha256 || '') ||
!SHA256_PATTERN.test(record.upgradeRehearsalSha256 || '') ||
!SHA256_PATTERN.test(record.upgradeCutoverRehearsalSha256 || '') ||
record.applicationImageId === record.operatorImageId
) {
fail(`${architecture} milestone artifact record is incompatible`);
@@ -319,7 +322,7 @@ function auditLocalAlphaMilestone(options) {
'artifacts',
'readme',
]) ||
manifest.schemaVersion !== 4 ||
manifest.schemaVersion !== 5 ||
manifest.schema !== SCHEMA ||
manifest.maturity !== 'alpha_candidate_not_public_release' ||
manifest.product !== 'local' ||
@@ -366,7 +369,9 @@ function auditLocalAlphaMilestone(options) {
new Set(records.map((record) => record.upgradeReadinessSha256)).size !==
ARCHITECTURES.length ||
new Set(records.map((record) => record.upgradeRehearsalSha256)).size !==
ARCHITECTURES.length
ARCHITECTURES.length ||
new Set(records.map((record) => record.upgradeCutoverRehearsalSha256))
.size !== ARCHITECTURES.length
) {
fail('milestone architecture subjects are not distinct');
}
@@ -393,7 +398,7 @@ function auditLocalAlphaMilestone(options) {
}
return Object.freeze({
schemaVersion: 1,
schema: 'qinglong/alpha-local-milestone-audit@v4',
schema: 'qinglong/alpha-local-milestone-audit@v5',
sourceRevision: manifest.sourceRevision,
version: manifest.version,
variant: manifest.variant,
@@ -434,7 +439,9 @@ function finalizeLocalAlphaMilestone(options) {
artifacts.amd64.upgradeReadinessSha256 ===
artifacts.arm64.upgradeReadinessSha256 ||
artifacts.amd64.upgradeRehearsalSha256 ===
artifacts.arm64.upgradeRehearsalSha256
artifacts.arm64.upgradeRehearsalSha256 ||
artifacts.amd64.upgradeCutoverRehearsalSha256 ===
artifacts.arm64.upgradeCutoverRehearsalSha256
) {
fail('milestone architecture subjects must be distinct');
}
@@ -447,7 +454,7 @@ function finalizeLocalAlphaMilestone(options) {
path.join(normalized.outputRoot, FILES.readme),
);
const manifest = {
schemaVersion: 4,
schemaVersion: 5,
schema: SCHEMA,
maturity: 'alpha_candidate_not_public_release',
product: 'local',
+76 -5
View File
@@ -10,8 +10,8 @@ const { auditClusterImageSbom } = require('./ql3-cluster-image-sbom.cjs');
const { readReleaseIdentity } = require('./lib/ql3-release-identity.cjs');
const DEFAULT_ROOT = path.resolve(__dirname, '..');
const SCHEMA = 'qinglong/alpha-local-trial-kit@v7';
const VERIFICATION_SCHEMA = 'qinglong/alpha-local-trial-kit-verification@v5';
const SCHEMA = 'qinglong/alpha-local-trial-kit@v8';
const VERIFICATION_SCHEMA = 'qinglong/alpha-local-trial-kit-verification@v6';
const QUICKSTART_TEMPLATE = path.join(
DEFAULT_ROOT,
'scripts/templates/ql3-local-alpha-quickstart.sh',
@@ -24,6 +24,10 @@ const UPGRADE_REHEARSAL_TEMPLATE = path.join(
DEFAULT_ROOT,
'scripts/templates/ql3-local-alpha-upgrade-rehearsal.sh',
);
const UPGRADE_CUTOVER_REHEARSAL_TEMPLATE = path.join(
DEFAULT_ROOT,
'scripts/templates/ql3-local-alpha-upgrade-cutover-rehearsal.sh',
);
const ARCHITECTURES = Object.freeze(['amd64', 'arm64']);
const VARIANTS = Object.freeze(['headless', 'console']);
const ARCHIVE_MIN_BYTES = 1024;
@@ -32,6 +36,7 @@ const MAX_README_BYTES = 512 * 1024;
const MAX_QUICKSTART_BYTES = 256 * 1024;
const MAX_UPGRADE_READINESS_BYTES = 256 * 1024;
const MAX_UPGRADE_REHEARSAL_BYTES = 256 * 1024;
const MAX_UPGRADE_CUTOVER_REHEARSAL_BYTES = 512 * 1024;
const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/u;
const REVISION_PATTERN = /^[0-9a-f]{40}$/u;
const FILES = Object.freeze({
@@ -41,6 +46,7 @@ const FILES = Object.freeze({
quickstart: 'quickstart.sh',
upgradeReadiness: 'upgrade-readiness.sh',
upgradeRehearsal: 'upgrade-rehearsal.sh',
upgradeCutoverRehearsal: 'upgrade-cutover-rehearsal.sh',
readme: 'README.md',
manifest: 'manifest.json',
checksums: 'SHA256SUMS',
@@ -58,11 +64,13 @@ const VERIFICATION = Object.freeze({
localApiCancellation: 'passed',
legacyUpgradeReadiness: 'passed',
legacyUpgradeStage: 'passed',
legacyUpgradeCutover: 'passed',
});
function verificationGates(variant) {
return Object.freeze({
...VERIFICATION,
legacyUpgradeCutover: variant === 'headless' ? 'passed' : 'not_applicable',
consoleLiveJourney: variant === 'console' ? 'passed' : 'not_applicable',
firstAutomationJourney: variant === 'console' ? 'passed' : 'not_applicable',
});
@@ -513,6 +521,35 @@ function renderUpgradeRehearsal(identity) {
return rendered;
}
function renderUpgradeCutoverRehearsal(identity) {
const template = fs.readFileSync(
assertCanonicalFile(
UPGRADE_CUTOVER_REHEARSAL_TEMPLATE,
MAX_UPGRADE_CUTOVER_REHEARSAL_BYTES,
'upgrade cutover rehearsal template',
),
'utf8',
);
const replacements = Object.freeze({
'@@APPLICATION_IMAGE@@': identity.images.application.reference,
'@@APPLICATION_ID@@': identity.images.application.id,
'@@OPERATOR_IMAGE@@': identity.images.operator.reference,
'@@OPERATOR_ID@@': identity.images.operator.id,
'@@ARCHITECTURE@@': identity.architecture,
'@@SOURCE_REVISION@@': identity.sourceRevision,
'@@ARCHIVE@@': identity.archive.file,
'@@VARIANT@@': identity.variant,
});
let rendered = template;
for (const [token, value] of Object.entries(replacements)) {
rendered = rendered.replaceAll(token, value);
}
if (/@@[A-Z_]+@@/u.test(rendered)) {
fail('upgrade cutover rehearsal template contains an unresolved token');
}
return rendered;
}
function fileRecord(bundleRoot, name) {
const filePath = path.join(bundleRoot, name);
const stat = fs.lstatSync(filePath);
@@ -698,8 +735,13 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
renderUpgradeRehearsal(manifestIdentity),
0o700,
);
writeExclusive(
path.join(normalized.outputRoot, FILES.upgradeCutoverRehearsal),
renderUpgradeCutoverRehearsal(manifestIdentity),
0o700,
);
const manifest = {
schemaVersion: 8,
schemaVersion: 9,
schema: SCHEMA,
maturity: 'alpha_candidate_not_public_release',
product: 'local',
@@ -722,6 +764,10 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
normalized.outputRoot,
FILES.upgradeRehearsal,
),
upgradeCutoverRehearsal: fileRecord(
normalized.outputRoot,
FILES.upgradeCutoverRehearsal,
),
readme: fileRecord(normalized.outputRoot, FILES.readme),
verification: fileRecord(
normalized.outputRoot,
@@ -740,6 +786,7 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
FILES.quickstart,
FILES.upgradeReadiness,
FILES.upgradeRehearsal,
FILES.upgradeCutoverRehearsal,
FILES.readme,
FILES.manifest,
];
@@ -811,10 +858,11 @@ function auditLocalAlphaTrialKit(options) {
'quickstart',
'upgradeReadiness',
'upgradeRehearsal',
'upgradeCutoverRehearsal',
'readme',
'verification',
]) ||
manifest.schemaVersion !== 8 ||
manifest.schemaVersion !== 9 ||
manifest.schema !== SCHEMA ||
manifest.maturity !== 'alpha_candidate_not_public_release' ||
manifest.product !== 'local' ||
@@ -895,6 +943,26 @@ function auditLocalAlphaTrialKit(options) {
if (actualUpgradeRehearsal !== expectedUpgradeRehearsal) {
fail('upgrade rehearsal differs from the canonical staging journey');
}
validateFileRecord(
manifest.upgradeCutoverRehearsal,
FILES.upgradeCutoverRehearsal,
bundleRoot,
);
const expectedUpgradeCutoverRehearsal =
renderUpgradeCutoverRehearsal(manifest);
const actualUpgradeCutoverRehearsal = fs.readFileSync(
assertCanonicalFile(
path.join(bundleRoot, FILES.upgradeCutoverRehearsal),
MAX_UPGRADE_CUTOVER_REHEARSAL_BYTES,
'upgrade cutover rehearsal',
),
'utf8',
);
if (actualUpgradeCutoverRehearsal !== expectedUpgradeCutoverRehearsal) {
fail(
'upgrade cutover rehearsal differs from the canonical cutover journey',
);
}
validateFileRecord(manifest.readme, FILES.readme, bundleRoot);
validateOfflineSbom(
readBoundedJson(
@@ -933,6 +1001,7 @@ function auditLocalAlphaTrialKit(options) {
FILES.quickstart,
FILES.upgradeReadiness,
FILES.upgradeRehearsal,
FILES.upgradeCutoverRehearsal,
expectedArchive,
].sort();
const actualFiles = fs
@@ -955,6 +1024,7 @@ function auditLocalAlphaTrialKit(options) {
FILES.quickstart,
FILES.upgradeReadiness,
FILES.upgradeRehearsal,
FILES.upgradeCutoverRehearsal,
FILES.readme,
FILES.manifest,
];
@@ -968,7 +1038,7 @@ function auditLocalAlphaTrialKit(options) {
}
return Object.freeze({
schemaVersion: 1,
schema: 'qinglong/alpha-local-trial-kit-audit@v4',
schema: 'qinglong/alpha-local-trial-kit-audit@v5',
sourceRevision: manifest.sourceRevision,
version: manifest.version,
architecture: manifest.architecture,
@@ -979,6 +1049,7 @@ function auditLocalAlphaTrialKit(options) {
quickstartSha256: manifest.quickstart.sha256,
upgradeReadinessSha256: manifest.upgradeReadiness.sha256,
upgradeRehearsalSha256: manifest.upgradeRehearsal.sha256,
upgradeCutoverRehearsalSha256: manifest.upgradeCutoverRehearsal.sha256,
verificationSha256: manifest.verification.sha256,
workflowRunId: verificationEvidence.workflow.runId,
workflowRunAttempt: verificationEvidence.workflow.runAttempt,
@@ -13,6 +13,7 @@ const RUNTIME_NODE_IMAGE =
'node:24.18.0-alpine3.23@sha256:595398b0081eacda8e1c4c5b97b76cd1020e4d58a8ebcb4843b9bca1e79e7436';
const RUNTIME_OS_PATCH =
'RUN apk add --no-cache --upgrade \\\n' +
' docker-cli=29.5.2-r0 \\\n' +
' libcrypto3=3.5.8-r0 \\\n' +
' libssl3=3.5.8-r0';
const BUILD_DEPENDENCIES = Object.freeze({
@@ -194,6 +195,7 @@ function auditWorkflow(contents, findings) {
'ql3-local-operator.cdx.json',
'image-ref: qinglong3-local-operator:ci-${{ matrix.image_arch }}',
'"${OPERATOR_IMAGE}" --version',
'--entrypoint /usr/bin/docker \\\n "${OPERATOR_IMAGE}" --version',
'scripts/ql3-local-alpha-trial-kit-live-contract.cjs',
'scripts/ql3-local-alpha-trial-kit-bundle.cjs',
'--mode=record-verification',
@@ -202,6 +204,9 @@ function auditWorkflow(contents, findings) {
'sh "${BUNDLE_ROOT}/quickstart.sh" \\\n edge "${QUICKSTART_ROOT}" "${QUICKSTART_CONTAINER}"',
'docker stop --time 30 "${QUICKSTART_CONTAINER}"',
'test -s "${QUICKSTART_ROOT}/qinglong3.sqlite"',
'sh "${BUNDLE_ROOT}/upgrade-cutover-rehearsal.sh"',
'"status":"rollback_candidate"',
'docker rm "${TARGET_CONTAINER}" "${LEGACY_CONTAINER}"',
'--application-sbom="${APPLICATION_SBOM}"',
'--operator-sbom="${RUNNER_TEMP}/ql3-local-operator.cdx.json"',
'--verification-evidence="${RUNNER_TEMP}/ql3-local-alpha-verification-${{ matrix.image_arch }}.json"',
@@ -228,6 +233,7 @@ function auditWorkflow(contents, findings) {
'--mode=create',
'--mode=audit',
'/quickstart.sh"',
'/upgrade-cutover-rehearsal.sh"',
'name: Upload the tested native Local Alpha trial kit',
]) {
const index = contents.indexOf(value, cursor + 1);
@@ -0,0 +1,282 @@
#!/bin/sh
set -eu
APPLICATION_IMAGE='@@APPLICATION_IMAGE@@'
APPLICATION_ID='@@APPLICATION_ID@@'
OPERATOR_IMAGE='@@OPERATOR_IMAGE@@'
OPERATOR_ID='@@OPERATOR_ID@@'
ARCHITECTURE='@@ARCHITECTURE@@'
SOURCE_REVISION='@@SOURCE_REVISION@@'
ARCHIVE='@@ARCHIVE@@'
VARIANT='@@VARIANT@@'
fail() {
printf '%s\n' "QingLong Local Alpha cutover rehearsal failed: $*" >&2
exit 1
}
usage() {
printf '%s\n' \
'usage: sh upgrade-cutover-rehearsal.sh edge|standalone /absolute/legacy-data-root /absolute/new/rehearsal-root <reviewed-sqlite-plan-digest> <reviewed-data-directory-plan-digest> [legacy-container-name] [target-container-name]' >&2
exit 2
}
valid_digest() {
[ "${#1}" -eq 64 ] || return 1
case "$1" in *[!0-9a-f]*) return 1 ;; *) return 0 ;; esac
}
extract_digest() {
result_file=$1
field=$2
value=$(sed -n "s/^.*\"$field\":\"\([0-9a-f][0-9a-f]*\)\".*$/\1/p" "$result_file")
valid_digest "$value" || fail "$field is missing or invalid in $result_file"
printf '%s' "$value"
}
container_name() {
case "$1" in ''|[_.-]*|*[!A-Za-z0-9_.-]*) return 1 ;; *) return 0 ;; esac
}
[ "$#" -ge 5 ] && [ "$#" -le 7 ] || usage
[ "$VARIANT" = headless ] || fail 'cutover rehearsal is available only in the headless Trial Kit'
profile=$1
legacy_root=$2
rehearsal_root=$3
sqlite_plan_digest=$4
directory_plan_digest=$5
legacy_name=${6:-ql3-alpha-upgrade-legacy}
target_name=${7:-ql3-alpha-upgrade-target}
case "$profile" in
edge) memory=128m; pids=64 ;;
standalone) memory=256m; pids=256 ;;
*) usage ;;
esac
container_name "$legacy_name" || fail 'legacy container name is invalid'
container_name "$target_name" || fail 'target container name is invalid'
[ "$legacy_name" != "$target_name" ] || fail 'container names must be distinct'
valid_digest "$sqlite_plan_digest" || fail 'reviewed SQLite plan digest is invalid'
valid_digest "$directory_plan_digest" || fail 'reviewed data-directory plan digest is invalid'
[ "$(uname -s)" = Linux ] || fail 'cutover rehearsal requires a Linux Docker host'
for tool in docker sha256sum grep sed stat date; do
command -v "$tool" >/dev/null 2>&1 || fail "$tool is required"
done
[ -S /var/run/docker.sock ] || fail 'canonical Docker socket is unavailable'
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
(CDPATH= cd -- "$script_dir" && sha256sum --check SHA256SUMS)
docker info >/dev/null 2>&1 || fail 'docker daemon is unavailable'
docker load --input "$script_dir/$ARCHIVE" >/dev/null
application_identity=$(docker image inspect --format '{{.Id}}|{{.Architecture}}|{{.Config.User}}|{{index .Config.Labels "org.opencontainers.image.revision"}}' "$APPLICATION_IMAGE")
operator_identity=$(docker image inspect --format '{{.Id}}|{{.Architecture}}|{{.Config.User}}|{{index .Config.Labels "org.opencontainers.image.revision"}}|{{index .Config.Labels "io.qinglong.lifecycle"}}|{{index .Config.Labels "io.qinglong.network"}}' "$OPERATOR_IMAGE")
[ "$application_identity" = "$APPLICATION_ID|$ARCHITECTURE|65532:65532|$SOURCE_REVISION" ] || fail 'application image identity is incompatible'
[ "$operator_identity" = "$OPERATOR_ID|$ARCHITECTURE|65532:65532|$SOURCE_REVISION|short-lived|none-by-default" ] || fail 'operator image identity is incompatible'
docker run --rm --read-only --network none --cap-drop ALL \
--security-opt no-new-privileges --entrypoint /usr/bin/docker \
"$OPERATOR_IMAGE" --version >/dev/null
[ ! -e "$rehearsal_root" ] || fail 'rehearsal root must not already exist'
sh "$script_dir/upgrade-rehearsal.sh" \
"$profile" "$legacy_root" "$rehearsal_root" \
"$sqlite_plan_digest" "$directory_plan_digest"
legacy_sha256=$(sha256sum "$legacy_root/db/database.sqlite" | sed 's/ .*//')
uid=$(id -u)
gid=$(id -g)
[ "$uid" -eq 0 ] && allow_root_service=true || allow_root_service=false
socket_gid=$(stat -c %g /var/run/docker.sock)
old_umask=$(umask)
umask 077
for directory in owner-peppers owner-pepper-backup owner-delivery receipts artifacts plugin-staging plugin-activation service service/cutovers service/cutovers/alpha-upgrade-cutover data-directory/transformation; do
[ -d "$rehearsal_root/$directory" ] || mkdir -m 0700 "$rehearsal_root/$directory"
done
run_operator() {
command_name=$1
command_file=$2
result_file=$3
docker run --rm --read-only --user "$uid:$gid" --network none \
--cap-drop ALL --security-opt no-new-privileges \
--memory 128m --memory-swap 128m --cpus 0.5 --pids-limit 32 \
--tmpfs /tmp:rw,nosuid,nodev,noexec,size=8m \
--mount "type=bind,src=$legacy_root,dst=$legacy_root,readonly" \
--mount "type=bind,src=$rehearsal_root,dst=$rehearsal_root" \
"$OPERATOR_IMAGE" "$command_name" run \
--command-file "$rehearsal_root/commands/$command_file" \
>"$rehearsal_root/results/$result_file"
}
run_deploy() {
subcommand=$1
command_file=$2
result_file=$3
docker run --rm --read-only --user "$uid:$gid" \
--group-add "$socket_gid" --network none \
--cap-drop ALL --security-opt no-new-privileges \
--memory 128m --memory-swap 128m --cpus 0.5 --pids-limit 32 \
--tmpfs /tmp:rw,nosuid,nodev,noexec,size=8m \
--mount type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
--mount "type=bind,src=$legacy_root,dst=$legacy_root,readonly" \
--mount "type=bind,src=$rehearsal_root,dst=$rehearsal_root" \
"$OPERATOR_IMAGE" deploy "$subcommand" \
--command-file "$rehearsal_root/commands/$command_file" \
>"$rehearsal_root/results/$result_file"
}
run_deployment_offline() {
subcommand=$1
command_file=$2
result_file=$3
docker run --rm --read-only --user "$uid:$gid" --network none \
--cap-drop ALL --security-opt no-new-privileges \
--memory 128m --memory-swap 128m --cpus 0.5 --pids-limit 32 \
--tmpfs /tmp:rw,nosuid,nodev,noexec,size=8m \
--mount "type=bind,src=$legacy_root,dst=$legacy_root,readonly" \
--mount "type=bind,src=$rehearsal_root,dst=$rehearsal_root" \
"$OPERATOR_IMAGE" deploy "$subcommand" \
--command-file "$rehearsal_root/commands/$command_file" \
>"$rehearsal_root/results/$result_file"
}
cat >"$rehearsal_root/commands/setup.json" <<EOF
{"schemaVersion":1,"operation":"local.setup.prepare","options":{"deploymentRoot":"$rehearsal_root","databasePath":"$rehearsal_root/sqlite/qinglong3.sqlite","profile":"$profile","ownerPepperKeyringDirectory":"$rehearsal_root/owner-peppers","ownerPepperBackupDirectory":"$rehearsal_root/owner-pepper-backup","ownerPepperKeyId":"owner-v1","localSecretKeyringPath":"$rehearsal_root/local-secret-keyring.json","busyTimeoutMs":100},"request":{"registerMutationId":"019f8680-143d-4000-8000-000000000111","activateMutationId":"019f8680-143d-4000-8000-000000000112","registeredAtMs":1785254400100,"activatedAtMs":1785254400101}}
EOF
cat >"$rehearsal_root/commands/owner-provision.json" <<EOF
{"schemaVersion":1,"operation":"owner.identity.provision","options":{"deploymentRoot":"$rehearsal_root","databasePath":"$rehearsal_root/sqlite/qinglong3.sqlite","pepperPath":"$rehearsal_root/owner-peppers/b3duZXItdjE.pepper","pepperKeyId":"owner-v1","secretDeliveryDirectory":"$rehearsal_root/owner-delivery","profile":"$profile","busyTimeoutMs":100},"request":{"mutationId":"019f8680-143d-4000-8000-000000000121","requestId":"alpha-upgrade-owner-provision"}}
EOF
cat >"$rehearsal_root/commands/owner-challenge.json" <<EOF
{"schemaVersion":1,"operation":"owner.challenge.issue","options":{"deploymentRoot":"$rehearsal_root","databasePath":"$rehearsal_root/sqlite/qinglong3.sqlite","pepperPath":"$rehearsal_root/owner-peppers/b3duZXItdjE.pepper","pepperKeyId":"owner-v1","secretDeliveryDirectory":"$rehearsal_root/owner-delivery","profile":"$profile","busyTimeoutMs":100},"request":{"projectId":"default","mutationId":"019f8680-143d-4000-8000-000000000122","requestId":"alpha-upgrade-owner-challenge"}}
EOF
cat >"$rehearsal_root/commands/owner-claim.json" <<EOF
{"schemaVersion":1,"operation":"owner.claim.from-deliveries","options":{"deploymentRoot":"$rehearsal_root","databasePath":"$rehearsal_root/sqlite/qinglong3.sqlite","pepperPath":"$rehearsal_root/owner-peppers/b3duZXItdjE.pepper","pepperKeyId":"owner-v1","secretDeliveryDirectory":"$rehearsal_root/owner-delivery","profile":"$profile","busyTimeoutMs":100},"request":{"projectId":"default","mutationId":"019f8680-143d-4000-8000-000000000123","requestId":"alpha-upgrade-owner-claim","credentialMutationId":"019f8680-143d-4000-8000-000000000121","challengeMutationId":"019f8680-143d-4000-8000-000000000122"}}
EOF
cat >"$rehearsal_root/commands/owner-credential-install.json" <<EOF
{"schemaVersion":1,"operation":"owner.credential-presentation.install-from-delivery","options":{"deploymentRoot":"$rehearsal_root","databasePath":"$rehearsal_root/sqlite/qinglong3.sqlite","pepperPath":"$rehearsal_root/owner-peppers/b3duZXItdjE.pepper","pepperKeyId":"owner-v1","secretDeliveryDirectory":"$rehearsal_root/owner-delivery","profile":"$profile","busyTimeoutMs":100},"request":{"credentialMutationId":"019f8680-143d-4000-8000-000000000121","destinationFilePath":"$rehearsal_root/owner-credential.json"}}
EOF
chmod 0600 "$rehearsal_root/commands"/*.json
run_operator setup setup.json setup.result.json
run_operator owner owner-provision.json owner-provision.result.json
run_operator owner owner-challenge.json owner-challenge.result.json
run_operator owner owner-claim.json owner-claim.result.json
run_operator owner owner-credential-install.json owner-credential-install.result.json
grep -q '"role":"owner"' "$rehearsal_root/results/owner-claim.result.json" || fail 'Owner claim did not establish owner authority'
directory_manifest_digest=$(extract_digest "$rehearsal_root/stage-summary.json" manifestDigest)
activation_digest=$(extract_digest "$rehearsal_root/stage-summary.json" activationDigest)
cat >"$rehearsal_root/commands/data-directory-transform.json" <<EOF
{"schemaVersion":1,"operation":"local-data-directory.adoption.transform","options":{"deploymentRoot":"$rehearsal_root","dataRoot":"$legacy_root","stagingRoot":"$rehearsal_root/data-directory/staged","transformationRoot":"$rehearsal_root/data-directory/transformation","projectId":"default","profile":"$profile","expectedManifestDigest":"$directory_manifest_digest","sqlite":{"sourcePath":"$legacy_root/db/database.sqlite","targetPath":"$rehearsal_root/sqlite/qinglong3.sqlite","recoveryPath":"$rehearsal_root/sqlite/database.pre-ql3.sqlite","manifestPath":"$rehearsal_root/sqlite/qinglong3-adoption.json","activationPath":"$rehearsal_root/sqlite/qinglong3-activation.json","expectedActivationDigest":"$activation_digest"}}}
EOF
chmod 0600 "$rehearsal_root/commands/data-directory-transform.json"
run_operator adoption data-directory-transform.json data-directory-transform.result.json
grep -q '"assessment":"ready"' "$rehearsal_root/results/data-directory-transform.result.json" || fail 'data-directory transformation is not ready'
transformation_digest=$(extract_digest "$rehearsal_root/results/data-directory-transform.result.json" transformationDigest)
sed "s/\"operation\":\"local-data-directory.adoption.transform\"/\"operation\":\"local-data-directory.adoption.transform.verify\"/;s/\"sqlite\":{/\"expectedTransformationDigest\":\"$transformation_digest\",\"sqlite\":{/" \
"$rehearsal_root/commands/data-directory-transform.json" \
>"$rehearsal_root/commands/data-directory-transform-verify.json"
chmod 0600 "$rehearsal_root/commands/data-directory-transform-verify.json"
run_operator adoption data-directory-transform-verify.json data-directory-transform-verify.result.json
grep -q '"status":"verified"' "$rehearsal_root/results/data-directory-transform-verify.result.json" || fail 'data-directory transformation verification failed'
cleanup_required=1
cleanup() {
if [ "$cleanup_required" -eq 1 ]; then
docker rm --force "$target_name" "$legacy_name" >/dev/null 2>&1 || true
fi
}
trap cleanup EXIT
trap 'exit 130' HUP INT TERM
legacy_id=$(docker run --detach --name "$legacy_name" --restart no \
--read-only --user "$uid:$gid" --network none --cap-drop ALL \
--security-opt no-new-privileges --memory 64m --memory-swap 64m \
--pids-limit 16 --tmpfs /tmp:rw,nosuid,nodev,noexec,size=4m \
--mount "type=bind,src=$legacy_root,dst=$legacy_root,readonly" \
--entrypoint /usr/local/bin/node "$OPERATOR_IMAGE" \
-e 'setInterval(() => {}, 60000)')
[ "${#legacy_id}" -eq 64 ] || fail 'legacy container ID is invalid'
case "$legacy_id" in *[!0-9a-f]*) fail 'legacy container ID is invalid' ;; esac
now_ms=$(($(date +%s) * 1000))
cat >"$rehearsal_root/commands/legacy-stop.json" <<EOF
{"schemaVersion":1,"operation":"local.deployment.cutover.legacy-stop","options":{"deploymentRoot":"$rehearsal_root","dockerExecutable":"/usr/bin/docker","dockerSocketPath":"/var/run/docker.sock","allowRootService":$allow_root_service},"request":{"cutoverId":"alpha-upgrade-cutover","profile":"$profile","instanceId":"alpha-upgrade","activationPath":"$rehearsal_root/sqlite/qinglong3-activation.json","legacySourcePath":"$legacy_root/db/database.sqlite","targetDatabasePath":"$rehearsal_root/sqlite/qinglong3.sqlite","recoveryPath":"$rehearsal_root/sqlite/database.pre-ql3.sqlite","manifestPath":"$rehearsal_root/sqlite/qinglong3-adoption.json","expectedLegacyDatabasePath":"$legacy_root/db/database.sqlite","expectedActivationDigest":"$activation_digest","expectedLegacyContainerId":"$legacy_id","requestedAtMs":$now_ms}}
EOF
chmod 0600 "$rehearsal_root/commands/legacy-stop.json"
run_deploy cutover-legacy-stop legacy-stop.json legacy-stop.result.json
grep -q '"state":"legacy_stopped"' "$rehearsal_root/results/legacy-stop.result.json" || fail 'legacy container did not stop cleanly'
commitment_digest=$(extract_digest "$rehearsal_root/results/legacy-stop.result.json" commitmentDigest)
cat >"$rehearsal_root/commands/data-directory-apply.json" <<EOF
{"schemaVersion":1,"operation":"local-data-directory.adoption.apply","options":{"deploymentRoot":"$rehearsal_root","dataRoot":"$legacy_root","stagingRoot":"$rehearsal_root/data-directory/staged","transformationRoot":"$rehearsal_root/data-directory/transformation","projectId":"default","profile":"$profile","expectedManifestDigest":"$directory_manifest_digest","expectedTransformationDigest":"$transformation_digest","ownerPepperKeyringDirectory":"$rehearsal_root/owner-peppers","credentialFilePath":"$rehearsal_root/owner-credential.json","secretKeyringPath":"$rehearsal_root/local-secret-keyring.json","mutationId":"019f8680-143d-4000-8000-000000000131","failureAuditEventId":"019f8680-143d-4000-8000-000000000132","requestId":"alpha-upgrade-data-apply","sqlite":{"sourcePath":"$legacy_root/db/database.sqlite","targetPath":"$rehearsal_root/sqlite/qinglong3.sqlite","recoveryPath":"$rehearsal_root/sqlite/database.pre-ql3.sqlite","manifestPath":"$rehearsal_root/sqlite/qinglong3-adoption.json","activationPath":"$rehearsal_root/sqlite/qinglong3-activation.json","expectedActivationDigest":"$activation_digest"}}}
EOF
chmod 0600 "$rehearsal_root/commands/data-directory-apply.json"
run_operator adoption data-directory-apply.json data-directory-apply.result.json
grep -q '"status":"committed"' "$rehearsal_root/results/data-directory-apply.result.json" || fail 'authenticated data application did not commit'
commit_digest=$(extract_digest "$rehearsal_root/results/data-directory-apply.result.json" commitDigest)
receipt_digest=$(extract_digest "$rehearsal_root/results/data-directory-apply.result.json" receiptDigest)
sed "s/\"operation\":\"local-data-directory.adoption.apply\"/\"operation\":\"local-data-directory.adoption.apply.verify\"/;s/\"sqlite\":{/\"expectedReceiptDigest\":\"$receipt_digest\",\"sqlite\":{/" \
"$rehearsal_root/commands/data-directory-apply.json" \
>"$rehearsal_root/commands/data-directory-apply-verify.json"
chmod 0600 "$rehearsal_root/commands/data-directory-apply-verify.json"
run_operator adoption data-directory-apply-verify.json data-directory-apply-verify.result.json
grep -q '"status":"verified"' "$rehearsal_root/results/data-directory-apply-verify.result.json" || fail 'authenticated data application verification failed'
prepared_ms=$((now_ms + 1))
cat >"$rehearsal_root/commands/adopted-prepare.json" <<EOF
{"schemaVersion":1,"operation":"local.deployment.adopted.prepare","options":{"deploymentRoot":"$rehearsal_root","profile":"$profile","instanceId":"alpha-upgrade","busyTimeoutMs":100,"service":{"kind":"docker-target","targetImage":{"authority":"local-image-id","reference":"$APPLICATION_IMAGE","imageId":"$APPLICATION_ID"},"allowRootService":$allow_root_service}},"request":{"bundleId":"019f8680-143d-4000-8000-000000000141","preparedAtMs":$prepared_ms,"cutoverId":"alpha-upgrade-cutover","storage":{"sourcePath":"$legacy_root/db/database.sqlite","targetPath":"$rehearsal_root/sqlite/qinglong3.sqlite","recoveryPath":"$rehearsal_root/sqlite/database.pre-ql3.sqlite","manifestPath":"$rehearsal_root/sqlite/qinglong3-adoption.json","activationPath":"$rehearsal_root/sqlite/qinglong3-activation.json","expectedActivationDigest":"$activation_digest"},"cutover":{"commitmentPath":"$rehearsal_root/service/cutovers/alpha-upgrade-cutover/0002-legacy-stopped.json","expectedCommitmentDigest":"$commitment_digest"},"legacyDataApplication":{"commitPath":"$rehearsal_root/data-directory/transformation/commit.json","expectedCommitDigest":"$commit_digest","expectedReceiptDigest":"$receipt_digest"}}}
EOF
chmod 0600 "$rehearsal_root/commands/adopted-prepare.json"
run_deployment_offline adopted-prepare adopted-prepare.json adopted-prepare.result.json
grep -q '"status":"prepared"' "$rehearsal_root/results/adopted-prepare.result.json" || fail 'adopted bundle was not prepared'
sed 's/"operation":"local.deployment.adopted.prepare"/"operation":"local.deployment.adopted.verify"/' \
"$rehearsal_root/commands/adopted-prepare.json" \
>"$rehearsal_root/commands/adopted-verify.json"
chmod 0600 "$rehearsal_root/commands/adopted-verify.json"
run_deployment_offline adopted-verify adopted-verify.json adopted-verify.result.json
grep -q '"status":"verified"' "$rehearsal_root/results/adopted-verify.result.json" || fail 'adopted bundle verification failed'
bundle_digest=$(extract_digest "$rehearsal_root/results/adopted-verify.result.json" bundleDigest)
grep -Fq "\"reference\": \"$APPLICATION_IMAGE\"" "$rehearsal_root/service/docker-target.json" || fail 'target descriptor image reference drifted'
grep -Fq "\"imageId\": \"$APPLICATION_ID\"" "$rehearsal_root/service/docker-target.json" || fail 'target descriptor image ID drifted'
target_id=$(docker create --name "$target_name" --restart no \
--read-only --user "$uid:$gid" --network none --cap-drop ALL \
--security-opt no-new-privileges --memory "$memory" --memory-swap "$memory" \
--cpus 0.5 --pids-limit "$pids" \
--tmpfs /tmp:rw,nosuid,nodev,noexec,size=16m \
--mount "type=bind,src=$rehearsal_root,dst=$rehearsal_root" \
--mount "type=bind,src=$legacy_root,dst=$legacy_root,readonly" \
"$APPLICATION_IMAGE" --config "$rehearsal_root/local-application.json")
[ "${#target_id}" -eq 64 ] || fail 'target container ID is invalid'
case "$target_id" in *[!0-9a-f]*) fail 'target container ID is invalid' ;; esac
start_ms=$((prepared_ms + 1))
cat >"$rehearsal_root/commands/target-start.json" <<EOF
{"schemaVersion":1,"operation":"local.deployment.cutover.target-start","options":{"deploymentRoot":"$rehearsal_root","dockerExecutable":"/usr/bin/docker","dockerSocketPath":"/var/run/docker.sock","allowRootService":$allow_root_service},"request":{"cutoverId":"alpha-upgrade-cutover","profile":"$profile","instanceId":"alpha-upgrade","activationPath":"$rehearsal_root/sqlite/qinglong3-activation.json","legacySourcePath":"$legacy_root/db/database.sqlite","targetDatabasePath":"$rehearsal_root/sqlite/qinglong3.sqlite","recoveryPath":"$rehearsal_root/sqlite/database.pre-ql3.sqlite","manifestPath":"$rehearsal_root/sqlite/qinglong3-adoption.json","expectedLegacyDatabasePath":"$legacy_root/db/database.sqlite","expectedActivationDigest":"$activation_digest","expectedLegacyCommitmentDigest":"$commitment_digest","expectedLegacyContainerId":"$legacy_id","expectedTargetContainerId":"$target_id","targetImage":{"authority":"local-image-id","reference":"$APPLICATION_IMAGE","imageId":"$APPLICATION_ID"},"applicationConfigPath":"$rehearsal_root/local-application.json","expectedTargetApplicationConfigPath":"$rehearsal_root/local-application.json","expectedTargetCommitmentPath":"$rehearsal_root/service/cutovers/alpha-upgrade-cutover/0002-legacy-stopped.json","generation":1,"requestedAtMs":$start_ms}}
EOF
chmod 0600 "$rehearsal_root/commands/target-start.json"
run_deploy cutover-target-start target-start.json target-start.result.json
grep -q '"state":"target_active"' "$rehearsal_root/results/target-start.result.json" || fail 'target did not become active'
stop_ms=$((start_ms + 1))
sed "s/\"operation\":\"local.deployment.cutover.target-start\"/\"operation\":\"local.deployment.cutover.target-stop\"/;s/\"requestedAtMs\":$start_ms/\"requestedAtMs\":$stop_ms/" \
"$rehearsal_root/commands/target-start.json" \
>"$rehearsal_root/commands/target-stop.json"
chmod 0600 "$rehearsal_root/commands/target-stop.json"
run_deploy cutover-target-stop target-stop.json target-stop.result.json
grep -q '"reconciliation":"rollback_candidate"' "$rehearsal_root/results/target-stop.result.json" || fail 'target stop did not produce a clean rollback candidate'
[ "$(sha256sum "$legacy_root/db/database.sqlite" | sed 's/ .*//')" = "$legacy_sha256" ] || fail 'legacy database changed during rehearsal'
cat >"$rehearsal_root/cutover-summary.json" <<EOF
{"schemaVersion":1,"schema":"qinglong/local-alpha-upgrade-cutover-summary@v1","status":"rollback_candidate","profile":"$profile","sourceRevision":"$SOURCE_REVISION","architecture":"$ARCHITECTURE","reviewedPlans":{"sqlite":"$sqlite_plan_digest","dataDirectory":"$directory_plan_digest"},"activationDigest":"$activation_digest","transformationDigest":"$transformation_digest","application":{"commitDigest":"$commit_digest","receiptDigest":"$receipt_digest"},"cutover":{"commitmentDigest":"$commitment_digest","bundleDigest":"$bundle_digest","legacyContainerId":"$legacy_id","targetContainerId":"$target_id"},"legacySource":"unchanged","target":"stopped","rollback":"candidate_not_executed"}
EOF
chmod 0600 "$rehearsal_root/cutover-summary.json"
umask "$old_umask"
cleanup_required=0
trap - EXIT HUP INT TERM
printf '%s\n' \
"QingLong 3.0 isolated cutover rehearsal completed ($profile, $ARCHITECTURE)." \
"Summary: $rehearsal_root/cutover-summary.json" \
"Stopped legacy container: $legacy_name ($legacy_id)" \
"Stopped target container: $target_name ($target_id)" \
"Remove after review: docker rm $target_name $legacy_name" \
'The original legacy SQLite remained unchanged. Production cutover and Legacy restart were not authorized.'
@@ -99,10 +99,10 @@ for directory in commands results sqlite data-directory; do
done
cat >"$rehearsal_root/commands/sqlite-stage.json" <<EOF
{"schemaVersion":1,"operation":"local-sqlite.adoption.stage","options":{"deploymentRoot":"/var/lib/qinglong3","profile":"$profile","sourcePath":"/var/lib/qinglong2/db/database.sqlite","targetPath":"/var/lib/qinglong3/sqlite/qinglong3.sqlite","recoveryPath":"/var/lib/qinglong3/sqlite/database.pre-ql3.sqlite","manifestPath":"/var/lib/qinglong3/sqlite/qinglong3-adoption.json","expectedPlanDigest":"$sqlite_plan_digest"}}
{"schemaVersion":1,"operation":"local-sqlite.adoption.stage","options":{"deploymentRoot":"$rehearsal_root","profile":"$profile","sourcePath":"$legacy_root/db/database.sqlite","targetPath":"$rehearsal_root/sqlite/qinglong3.sqlite","recoveryPath":"$rehearsal_root/sqlite/database.pre-ql3.sqlite","manifestPath":"$rehearsal_root/sqlite/qinglong3-adoption.json","expectedPlanDigest":"$sqlite_plan_digest"}}
EOF
cat >"$rehearsal_root/commands/sqlite-verify.json" <<EOF
{"schemaVersion":1,"operation":"local-sqlite.adoption.verify","options":{"deploymentRoot":"/var/lib/qinglong3","profile":"$profile","targetPath":"/var/lib/qinglong3/sqlite/qinglong3.sqlite","recoveryPath":"/var/lib/qinglong3/sqlite/database.pre-ql3.sqlite","manifestPath":"/var/lib/qinglong3/sqlite/qinglong3-adoption.json"}}
{"schemaVersion":1,"operation":"local-sqlite.adoption.verify","options":{"deploymentRoot":"$rehearsal_root","profile":"$profile","targetPath":"$rehearsal_root/sqlite/qinglong3.sqlite","recoveryPath":"$rehearsal_root/sqlite/database.pre-ql3.sqlite","manifestPath":"$rehearsal_root/sqlite/qinglong3-adoption.json"}}
EOF
chmod 0600 "$rehearsal_root/commands/sqlite-stage.json" "$rehearsal_root/commands/sqlite-verify.json"
@@ -115,10 +115,10 @@ run_adoption() {
--cap-drop ALL --security-opt no-new-privileges \
--memory 128m --memory-swap 128m --cpus 0.5 --pids-limit 32 \
--tmpfs /tmp:rw,nosuid,nodev,noexec,size=8m \
--mount "type=bind,src=$legacy_root,dst=/var/lib/qinglong2,readonly" \
--mount "type=bind,src=$rehearsal_root,dst=/var/lib/qinglong3" \
--mount "type=bind,src=$legacy_root,dst=$legacy_root,readonly" \
--mount "type=bind,src=$rehearsal_root,dst=$rehearsal_root" \
"$OPERATOR_IMAGE" adoption run \
--command-file "/var/lib/qinglong3/commands/$command_file" \
--command-file "$rehearsal_root/commands/$command_file" \
>"$rehearsal_root/results/$result_file"
}
@@ -129,7 +129,7 @@ grep -q '"status":"verified"' "$rehearsal_root/results/sqlite-verify.result.json
sqlite_manifest_digest=$(extract_digest "$rehearsal_root/results/sqlite-verify.result.json" manifestDigest)
cat >"$rehearsal_root/commands/sqlite-activation.json" <<EOF
{"schemaVersion":1,"operation":"local-sqlite.activation.prepare","options":{"deploymentRoot":"/var/lib/qinglong3","profile":"$profile","sourcePath":"/var/lib/qinglong2/db/database.sqlite","targetPath":"/var/lib/qinglong3/sqlite/qinglong3.sqlite","recoveryPath":"/var/lib/qinglong3/sqlite/database.pre-ql3.sqlite","manifestPath":"/var/lib/qinglong3/sqlite/qinglong3-adoption.json","activationPath":"/var/lib/qinglong3/sqlite/qinglong3-activation.json","expectedManifestDigest":"$sqlite_manifest_digest"}}
{"schemaVersion":1,"operation":"local-sqlite.activation.prepare","options":{"deploymentRoot":"$rehearsal_root","profile":"$profile","sourcePath":"$legacy_root/db/database.sqlite","targetPath":"$rehearsal_root/sqlite/qinglong3.sqlite","recoveryPath":"$rehearsal_root/sqlite/database.pre-ql3.sqlite","manifestPath":"$rehearsal_root/sqlite/qinglong3-adoption.json","activationPath":"$rehearsal_root/sqlite/qinglong3-activation.json","expectedManifestDigest":"$sqlite_manifest_digest"}}
EOF
chmod 0600 "$rehearsal_root/commands/sqlite-activation.json"
run_adoption sqlite-activation.json sqlite-activation.result.json
@@ -137,7 +137,7 @@ grep -q '"status":"prepared"' "$rehearsal_root/results/sqlite-activation.result.
activation_digest=$(extract_digest "$rehearsal_root/results/sqlite-activation.result.json" activationDigest)
cat >"$rehearsal_root/commands/data-directory-stage.json" <<EOF
{"schemaVersion":1,"operation":"local-data-directory.adoption.stage","options":{"deploymentRoot":"/var/lib/qinglong3","dataRoot":"/var/lib/qinglong2","stagingRoot":"/var/lib/qinglong3/data-directory/staged","profile":"$profile","expectedPlanDigest":"$directory_plan_digest","sqlite":{"sourcePath":"/var/lib/qinglong2/db/database.sqlite","targetPath":"/var/lib/qinglong3/sqlite/qinglong3.sqlite","recoveryPath":"/var/lib/qinglong3/sqlite/database.pre-ql3.sqlite","manifestPath":"/var/lib/qinglong3/sqlite/qinglong3-adoption.json","activationPath":"/var/lib/qinglong3/sqlite/qinglong3-activation.json","expectedActivationDigest":"$activation_digest"}}}
{"schemaVersion":1,"operation":"local-data-directory.adoption.stage","options":{"deploymentRoot":"$rehearsal_root","dataRoot":"$legacy_root","stagingRoot":"$rehearsal_root/data-directory/staged","profile":"$profile","expectedPlanDigest":"$directory_plan_digest","sqlite":{"sourcePath":"$legacy_root/db/database.sqlite","targetPath":"$rehearsal_root/sqlite/qinglong3.sqlite","recoveryPath":"$rehearsal_root/sqlite/database.pre-ql3.sqlite","manifestPath":"$rehearsal_root/sqlite/qinglong3-adoption.json","activationPath":"$rehearsal_root/sqlite/qinglong3-activation.json","expectedActivationDigest":"$activation_digest"}}}
EOF
chmod 0600 "$rehearsal_root/commands/data-directory-stage.json"
run_adoption data-directory-stage.json data-directory-stage.result.json
@@ -145,7 +145,7 @@ grep -q '"status":"staged"' "$rehearsal_root/results/data-directory-stage.result
directory_manifest_digest=$(extract_digest "$rehearsal_root/results/data-directory-stage.result.json" manifestDigest)
cat >"$rehearsal_root/commands/data-directory-verify.json" <<EOF
{"schemaVersion":1,"operation":"local-data-directory.adoption.verify","options":{"deploymentRoot":"/var/lib/qinglong3","dataRoot":"/var/lib/qinglong2","stagingRoot":"/var/lib/qinglong3/data-directory/staged","profile":"$profile","expectedManifestDigest":"$directory_manifest_digest","sqlite":{"sourcePath":"/var/lib/qinglong2/db/database.sqlite","targetPath":"/var/lib/qinglong3/sqlite/qinglong3.sqlite","recoveryPath":"/var/lib/qinglong3/sqlite/database.pre-ql3.sqlite","manifestPath":"/var/lib/qinglong3/sqlite/qinglong3-adoption.json","activationPath":"/var/lib/qinglong3/sqlite/qinglong3-activation.json","expectedActivationDigest":"$activation_digest"}}}
{"schemaVersion":1,"operation":"local-data-directory.adoption.verify","options":{"deploymentRoot":"$rehearsal_root","dataRoot":"$legacy_root","stagingRoot":"$rehearsal_root/data-directory/staged","profile":"$profile","expectedManifestDigest":"$directory_manifest_digest","sqlite":{"sourcePath":"$legacy_root/db/database.sqlite","targetPath":"$rehearsal_root/sqlite/qinglong3.sqlite","recoveryPath":"$rehearsal_root/sqlite/database.pre-ql3.sqlite","manifestPath":"$rehearsal_root/sqlite/qinglong3-adoption.json","activationPath":"$rehearsal_root/sqlite/qinglong3-activation.json","expectedActivationDigest":"$activation_digest"}}}
EOF
chmod 0600 "$rehearsal_root/commands/data-directory-verify.json"
run_adoption data-directory-verify.json data-directory-verify.result.json