fix(staging): make compose guard host-portable
This commit is contained in:
+4
-4
@@ -23,10 +23,10 @@
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "tsx src/db/migrate.ts",
|
||||
"db:studio": "drizzle-kit studio",
|
||||
"compose:up": "node scripts/staging-compose.mjs up",
|
||||
"compose:down": "node scripts/staging-compose.mjs down",
|
||||
"staging:preflight": "node scripts/staging-compose.mjs preflight",
|
||||
"staging:verify": "node scripts/staging-compose.mjs verify",
|
||||
"compose:up": "scripts/staging-compose.sh up",
|
||||
"compose:down": "scripts/staging-compose.sh down",
|
||||
"staging:preflight": "scripts/staging-compose.sh preflight",
|
||||
"staging:verify": "scripts/staging-compose.sh verify",
|
||||
"docker:opencode:build": "docker build --build-arg GROWQR_IMAGE_VERSION=${OPENCODE_IMAGE_VERSION:-dev} --build-arg GROWQR_PROMPT_VERSION=${PROMPT_VERSION:-4} --build-arg GROWQR_MIGRATION_VERSION=${MIGRATION_VERSION:-1} -f docker/opencode/Dockerfile -t growqr/opencode:dev ."
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -73,17 +73,17 @@ for (const obsoleteName of [
|
||||
}
|
||||
|
||||
|
||||
const stagingCompose = readFileSync(fileURLToPath(new URL("./staging-compose.mjs", import.meta.url)), "utf8");
|
||||
assert.match(stagingCompose, /const PROJECT = "growqr-backend-staging"/, "staging wrapper must pin the compose project name");
|
||||
const stagingCompose = readFileSync(fileURLToPath(new URL("./staging-compose.sh", import.meta.url)), "utf8");
|
||||
assert.match(stagingCompose, /PROJECT=growqr-backend-staging/, "staging wrapper must pin the compose project name");
|
||||
for (const [, name] of canonicalServiceNetworks) {
|
||||
assert.match(stagingCompose, new RegExp(`"${name.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")}"`), `staging wrapper must verify ${name}`);
|
||||
assert.match(stagingCompose, new RegExp(name.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")), `staging wrapper must verify ${name}`);
|
||||
}
|
||||
|
||||
const packageJson = JSON.parse(readFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf8")) as {
|
||||
scripts?: Record<string, string>;
|
||||
};
|
||||
assert.equal(packageJson.scripts?.["compose:up"], "node scripts/staging-compose.mjs up");
|
||||
assert.equal(packageJson.scripts?.["compose:down"], "node scripts/staging-compose.mjs down");
|
||||
assert.equal(packageJson.scripts?.["staging:preflight"], "node scripts/staging-compose.mjs preflight");
|
||||
assert.equal(packageJson.scripts?.["staging:verify"], "node scripts/staging-compose.mjs verify");
|
||||
assert.equal(packageJson.scripts?.["compose:up"], "scripts/staging-compose.sh up");
|
||||
assert.equal(packageJson.scripts?.["compose:down"], "scripts/staging-compose.sh down");
|
||||
assert.equal(packageJson.scripts?.["staging:preflight"], "scripts/staging-compose.sh preflight");
|
||||
assert.equal(packageJson.scripts?.["staging:verify"], "scripts/staging-compose.sh verify");
|
||||
console.log("onboarding-rev10-read: fixture serialization and staging network assertions passed");
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
const PROJECT = "growqr-backend-staging";
|
||||
const FILES = ["-f", "docker-compose.yml", "-f", "docker-compose.override.yml"];
|
||||
const REQUIRED_NETWORKS = [
|
||||
"growqr-backend-staging_default",
|
||||
"interview-service_default",
|
||||
"roleplay-service_default",
|
||||
"resume-builder-staging_default",
|
||||
"course-service_default",
|
||||
"qscore-service-staging_default",
|
||||
"matchmaking-v2_default",
|
||||
"user-service-staging_default",
|
||||
];
|
||||
const SERVICE_CHECKS = [
|
||||
["user-service", "http://growqr_user_service:8003/health", [200]],
|
||||
["qscore", "http://qscore-service-staging-api-1:8000/health", [200]],
|
||||
["course", "http://growqr-course-service:8010/health", [200]],
|
||||
["resume", "http://growqr_resume_api:8000/health", [200]],
|
||||
["interview", "http://interview-service-api-1:8000/health", [200]],
|
||||
["roleplay", "http://roleplay-service-api-1:8000/health", [200]],
|
||||
];
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
const result = spawnSync(command, args, { encoding: "utf8", stdio: options.capture ? "pipe" : "inherit" });
|
||||
if (result.status !== 0) {
|
||||
const detail = options.capture ? `${result.stdout ?? ""}${result.stderr ?? ""}`.trim() : "";
|
||||
throw new Error(`${command} ${args.join(" ")} failed${detail ? `: ${detail}` : ""}`);
|
||||
}
|
||||
return options.capture ? (result.stdout ?? "").trim() : "";
|
||||
}
|
||||
|
||||
function compose(args) {
|
||||
return run("docker", ["compose", "-p", PROJECT, ...FILES, ...args]);
|
||||
}
|
||||
|
||||
function preflight() {
|
||||
for (const network of REQUIRED_NETWORKS.slice(1)) {
|
||||
run("docker", ["network", "inspect", network], { capture: true });
|
||||
}
|
||||
compose(["config", "--quiet"]);
|
||||
}
|
||||
|
||||
function verify() {
|
||||
const networks = JSON.parse(run("docker", ["inspect", "growqr-backend", "--format", "{{json .NetworkSettings.Networks}}"], { capture: true }));
|
||||
const actual = new Set(Object.keys(networks));
|
||||
const missing = REQUIRED_NETWORKS.filter((network) => !actual.has(network));
|
||||
const unexpected = [...actual].filter((network) => !REQUIRED_NETWORKS.includes(network));
|
||||
if (missing.length || unexpected.length) {
|
||||
throw new Error(`backend network mismatch; missing=[${missing.join(", ")}], unexpected=[${unexpected.join(", ")}]`);
|
||||
}
|
||||
|
||||
const script = `const checks=${JSON.stringify(SERVICE_CHECKS)};let failed=false;for(const [name,url,ok] of checks){try{const response=await fetch(url,{signal:AbortSignal.timeout(3000)});console.log(name,response.status);if(!ok.includes(response.status))failed=true}catch(error){console.error(name,"ERROR",error.message);failed=true}}if(failed)process.exit(1);`;
|
||||
run("docker", ["exec", "growqr-backend", "node", "--input-type=module", "-e", script]);
|
||||
run("docker", ["exec", "growqr-backend", "node", "--input-type=module", "-e", "const r=await fetch('http://127.0.0.1:4000/healthz',{signal:AbortSignal.timeout(3000)});console.log('backend',r.status);if(r.status!==200)process.exit(1)"]);
|
||||
}
|
||||
|
||||
const [action, ...args] = process.argv.slice(2);
|
||||
try {
|
||||
if (action === "preflight") preflight();
|
||||
else if (action === "verify") verify();
|
||||
else if (action === "up") {
|
||||
preflight();
|
||||
compose(["up", "-d", ...args]);
|
||||
verify();
|
||||
} else if (action === "down") compose(["down", ...args]);
|
||||
else {
|
||||
console.error("Usage: node scripts/staging-compose.mjs <preflight|verify|up|down> [compose args]");
|
||||
process.exit(2);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
PROJECT=growqr-backend-staging
|
||||
COMPOSE="docker compose -p $PROJECT -f docker-compose.yml -f docker-compose.override.yml"
|
||||
REQUIRED_NETWORKS="growqr-backend-staging_default interview-service_default roleplay-service_default resume-builder-staging_default course-service_default qscore-service-staging_default matchmaking-v2_default user-service-staging_default"
|
||||
EXTERNAL_NETWORKS="interview-service_default roleplay-service_default resume-builder-staging_default course-service_default qscore-service-staging_default matchmaking-v2_default user-service-staging_default"
|
||||
|
||||
preflight() {
|
||||
for network in $EXTERNAL_NETWORKS; do
|
||||
docker network inspect "$network" >/dev/null
|
||||
done
|
||||
$COMPOSE config --quiet
|
||||
}
|
||||
|
||||
verify() {
|
||||
actual=$(docker inspect growqr-backend --format '{{range $name, $_ := .NetworkSettings.Networks}}{{$name}} {{end}}')
|
||||
for network in $REQUIRED_NETWORKS; do
|
||||
case " $actual " in
|
||||
*" $network "*) ;;
|
||||
*) echo "missing backend network: $network" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
for network in $actual; do
|
||||
case " $REQUIRED_NETWORKS " in
|
||||
*" $network "*) ;;
|
||||
*) echo "unexpected backend network: $network" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
docker exec growqr-backend node --input-type=module -e '
|
||||
const checks = [
|
||||
["user-service", "http://growqr_user_service:8003/health"],
|
||||
["qscore", "http://qscore-service-staging-api-1:8000/health"],
|
||||
["course", "http://growqr-course-service:8010/health"],
|
||||
["resume", "http://growqr_resume_api:8000/health"],
|
||||
["interview", "http://interview-service-api-1:8000/health"],
|
||||
["roleplay", "http://roleplay-service-api-1:8000/health"],
|
||||
];
|
||||
let failed = false;
|
||||
for (const [name, url] of checks) {
|
||||
try {
|
||||
const response = await fetch(url, { signal: AbortSignal.timeout(3000) });
|
||||
console.log(name, response.status);
|
||||
if (response.status !== 200) failed = true;
|
||||
} catch (error) {
|
||||
console.error(name, "ERROR", error.message);
|
||||
failed = true;
|
||||
}
|
||||
}
|
||||
if (failed) process.exit(1);
|
||||
'
|
||||
docker exec growqr-backend node --input-type=module -e "const response=await fetch('http://127.0.0.1:4000/healthz',{signal:AbortSignal.timeout(3000)});console.log('backend',response.status);if(response.status!==200)process.exit(1)"
|
||||
}
|
||||
|
||||
action=${1:-}
|
||||
[ "$#" -gt 0 ] && shift
|
||||
case "$action" in
|
||||
preflight) preflight ;;
|
||||
verify) verify ;;
|
||||
up) preflight; $COMPOSE up -d "$@"; verify ;;
|
||||
down) $COMPOSE down "$@" ;;
|
||||
*) echo "Usage: scripts/staging-compose.sh <preflight|verify|up|down> [compose args]" >&2; exit 2 ;;
|
||||
esac
|
||||
Reference in New Issue
Block a user