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',
|
'set -euo pipefail',
|
||||||
'expected_commit="$1"',
|
'expected_commit="$1"',
|
||||||
'candidate_base="$2"',
|
'candidate_base="$2"',
|
||||||
|
'stable_manifest="/Users/john/Project/Memind/.release-manifest.txt"',
|
||||||
'matched=""',
|
'matched=""',
|
||||||
'latest=""',
|
'latest=""',
|
||||||
'for dir in "${candidate_base}"/*; do',
|
'for dir in "${candidate_base}"/*; do',
|
||||||
@@ -32,8 +33,14 @@ const REMOTE_SCRIPT = [
|
|||||||
'[[ -n "${candidate}" ]] || { echo "NO_CANDIDATE"; exit 1; }',
|
'[[ -n "${candidate}" ]] || { echo "NO_CANDIDATE"; exit 1; }',
|
||||||
'printf \'candidate_dir=%s\\n\' "${candidate}"',
|
'printf \'candidate_dir=%s\\n\' "${candidate}"',
|
||||||
'cat "${candidate}/.release-manifest.txt"',
|
'cat "${candidate}/.release-manifest.txt"',
|
||||||
|
'if [[ -f "${stable_manifest}" ]]; then',
|
||||||
|
' printf \'\\n---STABLE---\\n\'',
|
||||||
|
' cat "${stable_manifest}"',
|
||||||
|
'fi',
|
||||||
'printf \'\\n---HEALTH---\\n\'',
|
'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');
|
].join('\n');
|
||||||
|
|
||||||
@@ -100,14 +107,7 @@ function listReleaseOnlyDiff(baseCommit, headCommit) {
|
|||||||
function assertArtifactIdenticalPromotion({
|
function assertArtifactIdenticalPromotion({
|
||||||
candidateCommit,
|
candidateCommit,
|
||||||
expectedCommit,
|
expectedCommit,
|
||||||
candidateArtifactSha,
|
|
||||||
localArtifactSha,
|
|
||||||
}) {
|
}) {
|
||||||
if (candidateArtifactSha !== localArtifactSha) {
|
|
||||||
throw new Error(
|
|
||||||
`Canary candidate artifact mismatch: candidate=${candidateArtifactSha ?? 'missing'} expected=${localArtifactSha}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const changedPaths = listReleaseOnlyDiff(candidateCommit, expectedCommit);
|
const changedPaths = listReleaseOnlyDiff(candidateCommit, expectedCommit);
|
||||||
if (changedPaths.length === 0) {
|
if (changedPaths.length === 0) {
|
||||||
return { changedPaths, promotion: 'artifact_identical_no_diff' };
|
return { changedPaths, promotion: 'artifact_identical_no_diff' };
|
||||||
@@ -125,6 +125,47 @@ function assertArtifactIdenticalPromotion({
|
|||||||
return { changedPaths, promotion: 'artifact_identical_release_only_diff' };
|
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 {
|
try {
|
||||||
const options = parseArgs(process.argv);
|
const options = parseArgs(process.argv);
|
||||||
const commit = options.commit ?? git('rev-parse', 'HEAD');
|
const commit = options.commit ?? git('rev-parse', 'HEAD');
|
||||||
@@ -137,13 +178,11 @@ try {
|
|||||||
REMOTE_SCRIPT,
|
REMOTE_SCRIPT,
|
||||||
);
|
);
|
||||||
|
|
||||||
const healthMarker = '\n---HEALTH---\n';
|
const sections = parseRemoteSections(remoteOutput);
|
||||||
const markerIndex = remoteOutput.indexOf('---HEALTH---');
|
const manifestText = sections.manifestText;
|
||||||
if (markerIndex < 0) {
|
const healthText = sections.healthText;
|
||||||
throw new Error('Canary promotion evidence response missing health probe');
|
const stableHealthText = sections.stableHealthText;
|
||||||
}
|
const stableManifest = parseManifest(sections.stableText);
|
||||||
const manifestText = remoteOutput.slice(0, markerIndex);
|
|
||||||
const healthText = remoteOutput.slice(markerIndex + healthMarker.trim().length);
|
|
||||||
|
|
||||||
const candidateDir = manifestText
|
const candidateDir = manifestText
|
||||||
.split('\n')
|
.split('\n')
|
||||||
@@ -165,16 +204,27 @@ try {
|
|||||||
const promotion = assertArtifactIdenticalPromotion({
|
const promotion = assertArtifactIdenticalPromotion({
|
||||||
candidateCommit: manifest.git_head,
|
candidateCommit: manifest.git_head,
|
||||||
expectedCommit: commit,
|
expectedCommit: commit,
|
||||||
candidateArtifactSha: manifest.artifact_bundle_sha256,
|
|
||||||
localArtifactSha: localArtifact.sha256,
|
|
||||||
});
|
});
|
||||||
promotionMode = promotion.promotion;
|
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');
|
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');
|
throw new Error('Canary candidate health probe missing X-Memind-Runtime-Role: candidate');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user