Files
elizaos--eliza/packages/scripts/cloud/deploy-freshness-guard-cli.mjs
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

251 lines
6.9 KiB
JavaScript

#!/usr/bin/env node
/**
* CLI wrapper for the cloud deploy freshness guard (#14083).
*
* Usage (in a deploy job, BEFORE the wrangler deploy step):
* node packages/scripts/cloud/deploy-freshness-guard-cli.mjs \
* --run-sha "$GITHUB_SHA" \
* --served-url "https://staging.elizacloud.ai" \
* [--served-path "/api/health"] \
* [--force]
*
* Emits `should_deploy=true|false` to $GITHUB_OUTPUT (and reason/detail), so the
* deploy step can gate on `if: steps.freshness.outputs.should_deploy == 'true'`.
* Always exits 0 (a signal-fetch failure must not fail the deploy — the guard is
* fail-open; see decideDeployFreshness).
*
* Ancestry is computed with `git merge-base --is-ancestor`. The deploy job's
* checkout is shallow (`--depth=1`), so this CLI fetches BOTH commits into the
* repo (best-effort, bounded) before asking git. If either commit can't be
* fetched, ancestry is reported as `null` and the guard deploys (fail-open).
*/
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import {
decideDeployFreshness,
fetchServedCommit,
} from "./deploy-freshness-guard.mjs";
function parseArgs(argv) {
const out = {
runSha: null,
servedUrl: null,
servedPath: null,
servedCommit: null,
force: false,
};
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (arg === "--force") {
out.force = true;
} else if (arg === "--run-sha") {
i += 1;
out.runSha = argv[i];
} else if (arg === "--served-url") {
i += 1;
out.servedUrl = argv[i];
} else if (arg === "--served-path") {
i += 1;
out.servedPath = argv[i];
} else if (arg === "--served-commit") {
i += 1;
out.servedCommit = argv[i];
} else if (arg.startsWith("--run-sha=")) {
out.runSha = arg.slice("--run-sha=".length);
} else if (arg.startsWith("--served-url=")) {
out.servedUrl = arg.slice("--served-url=".length);
} else if (arg.startsWith("--served-path=")) {
out.servedPath = arg.slice("--served-path=".length);
} else if (arg.startsWith("--served-commit=")) {
out.servedCommit = arg.slice("--served-commit=".length);
}
}
return out;
}
function git(args) {
return execFileSync("git", args, {
stdio: ["ignore", "pipe", "pipe"],
timeout: 60000,
})
.toString()
.trim();
}
function isShallowRepository() {
try {
return git(["rev-parse", "--is-shallow-repository"]) === "true";
} catch {
return false;
}
}
function hydrateHistoryForAncestry() {
if (!isShallowRepository()) return true;
try {
execFileSync(
"git",
[
"fetch",
"--no-tags",
"--unshallow",
"origin",
"+refs/heads/*:refs/remotes/origin/*",
],
{
stdio: ["ignore", "pipe", "pipe"],
timeout: 180000,
},
);
return true;
} catch {
return false;
}
}
function commitExists(sha) {
try {
git(["cat-file", "-e", `${sha}^{commit}`]);
return true;
} catch {
return false;
}
}
/**
* Best-effort fetch a commit into the local repo so ancestry can be computed off
* a shallow deploy checkout. Returns true if the commit is resolvable afterward.
* @param {string} sha
*/
function ensureCommit(sha) {
if (commitExists(sha)) return true;
for (const depth of [50, 200, 1000]) {
try {
execFileSync(
"git",
["fetch", "--no-tags", `--depth=${depth}`, "origin", sha],
{
stdio: ["ignore", "pipe", "pipe"],
timeout: 120000,
},
);
} catch {
// ignore — resolvability re-checked below
}
if (commitExists(sha)) return true;
}
try {
execFileSync("git", ["fetch", "--no-tags", "--unshallow", "origin"], {
stdio: ["ignore", "pipe", "pipe"],
timeout: 180000,
});
} catch {
try {
execFileSync("git", ["fetch", "--no-tags", "origin"], {
stdio: ["ignore", "pipe", "pipe"],
timeout: 180000,
});
} catch {
// ignore — resolvability re-checked below
}
}
return commitExists(sha);
}
/**
* merge-base --is-ancestor with fetch fallback. Returns true/false when the
* relationship is determinable, null when it is not (missing commit / git error
* / unrelated histories).
* @param {string} runSha
* @param {string} servedCommit
* @returns {boolean|null}
*/
function isAncestor(runSha, servedCommit) {
if (!ensureCommit(runSha) || !ensureCommit(servedCommit)) return null;
if (!hydrateHistoryForAncestry()) return null;
try {
execFileSync("git", ["merge-base", "--is-ancestor", runSha, servedCommit], {
stdio: ["ignore", "pipe", "pipe"],
timeout: 60000,
});
return true; // exit 0 => is an ancestor
} catch (err) {
// exit 1 => not an ancestor; any other exit / no merge-base => undeterminable
const code = /** @type {{ status?: number }} */ (err)?.status;
if (code === 1) return false;
return null;
}
}
function emitOutput(result) {
// biome-ignore lint/suspicious/noUndeclaredEnvVars: GitHub Actions provides this path for step outputs.
const outFile = process.env.GITHUB_OUTPUT;
const shouldDeploy = result.decision === "deploy";
const lines = [
`should_deploy=${shouldDeploy}`,
`decision=${result.decision}`,
`reason=${result.reason}`,
];
if (outFile) {
fs.appendFileSync(outFile, `${lines.join("\n")}\n`);
}
const emoji = shouldDeploy ? "✅" : "⛔";
console.log(
`${emoji} deploy-freshness-guard: ${result.decision} (${result.reason})`,
);
console.log(` ${result.detail}`);
if (result.runSha) console.log(` runSha=${result.runSha}`);
if (result.servedCommit)
console.log(` servedCommit=${result.servedCommit}`);
}
async function main() {
const {
runSha,
servedUrl,
servedPath,
servedCommit: servedCommitArg,
force,
} = parseArgs(process.argv.slice(2));
const servedCommit =
servedCommitArg ??
(servedUrl
? await fetchServedCommit(servedUrl, { stampPath: servedPath })
: null);
const result = decideDeployFreshness({
runSha,
servedCommit,
force,
isAncestor,
});
emitOutput(result);
// Always exit 0: the guard signals via should_deploy, never by failing the job.
process.exit(0);
}
// Only run when invoked directly (not when imported by tests).
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((err) => {
// Even an unexpected crash must fail-open: log and deploy.
console.error(
"deploy-freshness-guard-cli: unexpected error, failing open (deploy)",
);
console.error(err);
// biome-ignore lint/suspicious/noUndeclaredEnvVars: GitHub Actions provides this path for step outputs.
const outFile = process.env.GITHUB_OUTPUT;
if (outFile) {
fs.appendFileSync(
outFile,
"should_deploy=true\ndecision=deploy\nreason=guard_error\n",
);
}
process.exit(0);
});
}
export { isAncestor, parseArgs };