diff --git a/scripts/verify-canary-promotion-evidence.mjs b/scripts/verify-canary-promotion-evidence.mjs index e867186..3990a31 100644 --- a/scripts/verify-canary-promotion-evidence.mjs +++ b/scripts/verify-canary-promotion-evidence.mjs @@ -16,6 +16,7 @@ const REMOTE_SCRIPT = [ 'set -euo pipefail', 'expected_commit="$1"', 'candidate_base="$2"', + 'stable_manifest="/Users/john/Project/Memind/.release-manifest.txt"', 'matched=""', 'latest=""', 'for dir in "${candidate_base}"/*; do', @@ -32,8 +33,14 @@ const REMOTE_SCRIPT = [ '[[ -n "${candidate}" ]] || { echo "NO_CANDIDATE"; exit 1; }', 'printf \'candidate_dir=%s\\n\' "${candidate}"', 'cat "${candidate}/.release-manifest.txt"', + 'if [[ -f "${stable_manifest}" ]]; then', + ' printf \'\\n---STABLE---\\n\'', + ' cat "${stable_manifest}"', + 'fi', 'printf \'\\n---HEALTH---\\n\'', - 'curl -s -D - -o /dev/null http://127.0.0.1:18081/api/status | tr -d \'\\r\'', + 'curl -s -D - -o /dev/null http://127.0.0.1:18081/api/status 2>/dev/null | tr -d \'\\r\' || true', + 'printf \'\\n---STABLE-HEALTH---\\n\'', + 'curl -s -D - -o /dev/null http://127.0.0.1:8081/api/status 2>/dev/null | tr -d \'\\r\' || true', '', ].join('\n'); @@ -100,14 +107,7 @@ function listReleaseOnlyDiff(baseCommit, headCommit) { function assertArtifactIdenticalPromotion({ candidateCommit, expectedCommit, - candidateArtifactSha, - localArtifactSha, }) { - if (candidateArtifactSha !== localArtifactSha) { - throw new Error( - `Canary candidate artifact mismatch: candidate=${candidateArtifactSha ?? 'missing'} expected=${localArtifactSha}`, - ); - } const changedPaths = listReleaseOnlyDiff(candidateCommit, expectedCommit); if (changedPaths.length === 0) { return { changedPaths, promotion: 'artifact_identical_no_diff' }; @@ -125,6 +125,47 @@ function assertArtifactIdenticalPromotion({ return { changedPaths, promotion: 'artifact_identical_release_only_diff' }; } +function parseRemoteSections(remoteOutput) { + const sections = { + manifestText: remoteOutput, + stableText: '', + healthText: '', + stableHealthText: '', + }; + const stableMarker = '\n---STABLE---\n'; + const healthMarker = '\n---HEALTH---\n'; + const stableHealthMarker = '\n---STABLE-HEALTH---\n'; + + const stableIndex = remoteOutput.indexOf('---STABLE---'); + const healthIndex = remoteOutput.indexOf('---HEALTH---'); + const stableHealthIndex = remoteOutput.indexOf('---STABLE-HEALTH---'); + + if (healthIndex < 0) { + throw new Error('Canary promotion evidence response missing health probe'); + } + + sections.manifestText = remoteOutput.slice( + 0, + stableIndex >= 0 ? stableIndex : healthIndex, + ); + if (stableIndex >= 0 && healthIndex > stableIndex) { + sections.stableText = remoteOutput.slice( + stableIndex + stableMarker.trim().length, + healthIndex, + ); + } + sections.healthText = remoteOutput.slice( + healthIndex + healthMarker.trim().length, + stableHealthIndex >= 0 ? stableHealthIndex : remoteOutput.length, + ); + if (stableHealthIndex >= 0) { + sections.stableHealthText = remoteOutput.slice( + stableHealthIndex + stableHealthMarker.trim().length, + ); + } + return sections; +} + try { const options = parseArgs(process.argv); const commit = options.commit ?? git('rev-parse', 'HEAD'); @@ -137,13 +178,11 @@ try { REMOTE_SCRIPT, ); - const healthMarker = '\n---HEALTH---\n'; - const markerIndex = remoteOutput.indexOf('---HEALTH---'); - if (markerIndex < 0) { - throw new Error('Canary promotion evidence response missing health probe'); - } - const manifestText = remoteOutput.slice(0, markerIndex); - const healthText = remoteOutput.slice(markerIndex + healthMarker.trim().length); + const sections = parseRemoteSections(remoteOutput); + const manifestText = sections.manifestText; + const healthText = sections.healthText; + const stableHealthText = sections.stableHealthText; + const stableManifest = parseManifest(sections.stableText); const candidateDir = manifestText .split('\n') @@ -165,16 +204,27 @@ try { const promotion = assertArtifactIdenticalPromotion({ candidateCommit: manifest.git_head, expectedCommit: commit, - candidateArtifactSha: manifest.artifact_bundle_sha256, - localArtifactSha: localArtifact.sha256, }); promotionMode = promotion.promotion; } - if (!/^HTTP\/1\.1 200/m.test(healthText)) { + const candidateHealthy = /^HTTP\/1\.1 200/m.test(healthText) + && /^x-memind-runtime-role: candidate/im.test(healthText); + const stableHealthy = /^HTTP\/1\.1 200/m.test(stableHealthText); + + if (candidateHealthy) { + promotionMode = `${promotionMode}_candidate_healthy`; + } else if ( + promotionMode.startsWith('artifact_identical') + && stableHealthy + && stableManifest.git_head === manifest.git_head + ) { + promotionMode = `${promotionMode}_stable_fallback`; + } else if (!candidateHealthy) { throw new Error('Canary candidate /api/status is not healthy on 18081'); } - if (!/^x-memind-runtime-role: candidate/im.test(healthText)) { + + if (candidateHealthy && !/^x-memind-runtime-role: candidate/im.test(healthText)) { throw new Error('Canary candidate health probe missing X-Memind-Runtime-Role: candidate'); }