41bf775c4c
Memind CI / Test, build, and release guards (pull_request) Successful in 13m26s
Verify matching 103 canary evidence before 8081 promotion, fix agent-run drain JSON parsing, tolerate macOS full-backup tar races, and extend goosed remount health waits for non-interactive SSH releases. Co-authored-by: Cursor <cursoragent@cursor.com>
130 lines
4.2 KiB
JavaScript
130 lines
4.2 KiB
JavaScript
#!/usr/bin/env node
|
|
import { spawnSync } from 'node:child_process';
|
|
import path from 'node:path';
|
|
|
|
const ROOT = path.resolve(new URL('..', import.meta.url).pathname);
|
|
const REMOTE_ROOT = process.env.STUDIO_REMOTE_ROOT ?? '/Users/john/Project';
|
|
const CANDIDATE_BASE = `${REMOTE_ROOT}/Memind-candidates`;
|
|
|
|
const REMOTE_SCRIPT = [
|
|
'set -euo pipefail',
|
|
'expected_commit="$1"',
|
|
'candidate_base="$2"',
|
|
'matched=""',
|
|
'latest=""',
|
|
'for dir in "${candidate_base}"/*; do',
|
|
' [[ -d "${dir}" ]] || continue',
|
|
' [[ -f "${dir}/.release-manifest.txt" ]] || continue',
|
|
' if [[ -z "${latest}" || "$(basename "${dir}")" > "$(basename "${latest}")" ]]; then',
|
|
' latest="${dir}"',
|
|
' fi',
|
|
' if grep -q "^git_head=${expected_commit}$" "${dir}/.release-manifest.txt"; then',
|
|
' matched="${dir}"',
|
|
' fi',
|
|
'done',
|
|
'if [[ -n "${matched}" ]]; then candidate="${matched}"; else candidate="${latest}"; fi',
|
|
'[[ -n "${candidate}" ]] || { echo "NO_CANDIDATE"; exit 1; }',
|
|
'printf \'candidate_dir=%s\\n\' "${candidate}"',
|
|
'cat "${candidate}/.release-manifest.txt"',
|
|
'printf \'\\n---HEALTH---\\n\'',
|
|
'curl -s -D - -o /dev/null http://127.0.0.1:18081/api/status | tr -d \'\\r\'',
|
|
'',
|
|
].join('\n');
|
|
|
|
function parseArgs(argv) {
|
|
const options = {
|
|
host: process.env.STUDIO_HOST ?? '58.38.22.103',
|
|
commit: null,
|
|
};
|
|
for (let index = 2; index < argv.length; index += 1) {
|
|
const arg = argv[index];
|
|
if (arg === '--host') options.host = argv[++index] ?? '';
|
|
else if (arg === '--commit') options.commit = argv[++index] ?? '';
|
|
else throw new Error(`Unknown argument: ${arg}`);
|
|
}
|
|
return options;
|
|
}
|
|
|
|
function git(...args) {
|
|
const result = spawnSync('git', args, { cwd: ROOT, encoding: 'utf8' });
|
|
if (result.status !== 0) {
|
|
throw new Error(`git ${args.join(' ')} failed: ${result.stderr || result.stdout}`);
|
|
}
|
|
return result.stdout.trim();
|
|
}
|
|
|
|
function ssh(host, args, input = '') {
|
|
const result = spawnSync('ssh', ['-o', 'BatchMode=yes', '-o', 'ConnectTimeout=15', host, ...args], {
|
|
encoding: 'utf8',
|
|
input,
|
|
});
|
|
if (result.status !== 0) {
|
|
throw new Error(`SSH to ${host} failed:\n${result.stderr || result.stdout}`);
|
|
}
|
|
return result.stdout.trim();
|
|
}
|
|
|
|
function parseManifest(content) {
|
|
const fields = {};
|
|
for (const line of content.split('\n')) {
|
|
const separator = line.indexOf('=');
|
|
if (separator <= 0) continue;
|
|
fields[line.slice(0, separator)] = line.slice(separator + 1);
|
|
}
|
|
return fields;
|
|
}
|
|
|
|
try {
|
|
const options = parseArgs(process.argv);
|
|
const commit = options.commit ?? git('rev-parse', 'HEAD');
|
|
const remoteUserHost = options.host.includes('@') ? options.host : `john@${options.host}`;
|
|
|
|
const remoteOutput = ssh(
|
|
remoteUserHost,
|
|
['/bin/bash', '-s', '--', commit, CANDIDATE_BASE],
|
|
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 candidateDir = manifestText
|
|
.split('\n')
|
|
.find((line) => line.startsWith('candidate_dir='))
|
|
?.slice('candidate_dir='.length);
|
|
const manifest = parseManifest(
|
|
manifestText
|
|
.split('\n')
|
|
.filter((line) => !line.startsWith('candidate_dir='))
|
|
.join('\n'),
|
|
);
|
|
|
|
if (!candidateDir) {
|
|
throw new Error('103 has no canary candidate directory');
|
|
}
|
|
if (manifest.git_head !== commit) {
|
|
throw new Error(
|
|
`Canary candidate commit mismatch: candidate=${manifest.git_head ?? 'missing'} expected=${commit}`,
|
|
);
|
|
}
|
|
if (!/^HTTP\/1\.1 200/m.test(healthText)) {
|
|
throw new Error('Canary candidate /api/status is not healthy on 18081');
|
|
}
|
|
if (!/^x-memind-runtime-role: candidate/im.test(healthText)) {
|
|
throw new Error('Canary candidate health probe missing X-Memind-Runtime-Role: candidate');
|
|
}
|
|
|
|
console.log('Canary promotion evidence verified');
|
|
console.log(`candidate_dir=${candidateDir}`);
|
|
console.log(`release_id=${manifest.release_id ?? 'unknown'}`);
|
|
console.log(`git_head=${manifest.git_head}`);
|
|
} catch (error) {
|
|
console.error(error.message);
|
|
process.exit(1);
|
|
}
|