fix(release): allow stable fallback for release-only artifact promotions
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user