mirror of
https://github.com/multica-ai/multica.git
synced 2026-09-28 13:23:48 +08:00
* ci: scope validation and remove duplicate lifecycle runs
Co-authored-by: multica-agent <github@multica.ai>
* fix(migrations): renumber the Triage validate migration to 489
#8449 and #8436 merged minutes apart and both took 485, so
TestMigrationNumericPrefixesAreUnique fails on main and every PR branched from
it. #8436 owns 485-488, so the single validate file moves to 489.
Renaming an applied migration makes databases that already recorded
485_issue_triage_state_validate run it once more under the new version string.
That is safe here: VALIDATE CONSTRAINT on an already-validated constraint is a
no-op, verified against PostgreSQL 17.
Co-authored-by: multica-agent <github@multica.ai>
(cherry picked from commit 09140a04ae)
* fix(ci): gate installers and reuse frontend quality setup
Co-authored-by: multica-agent <github@multica.ai>
* docs(ci): remove redundant CI overview
Co-authored-by: multica-agent <github@multica.ai>
* refactor(ci): remove unused output and duplicate test fixtures
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: J <bohan@devv.ai>
71 lines
2.7 KiB
JavaScript
71 lines
2.7 KiB
JavaScript
#!/usr/bin/env node
|
|
// Path filtering and aggregate gates share this fail-closed decision contract.
|
|
import { appendFileSync, readFileSync } from "node:fs";
|
|
import { pathToFileURL } from "node:url";
|
|
|
|
export const filters = JSON.parse(
|
|
readFileSync(new URL("../.github/ci-paths.json", import.meta.url), "utf8"),
|
|
);
|
|
|
|
export function decideScopes(event, filtered) {
|
|
const full = event === "schedule" || event === "workflow_dispatch";
|
|
if (!full && event !== "push" && event !== "pull_request") {
|
|
throw new Error(`Unsupported CI event: ${event}`);
|
|
}
|
|
const outputs = { full: String(full) };
|
|
for (const scope of Object.keys(filters)) {
|
|
if (!full && filtered[scope] !== "true" && filtered[scope] !== "false") {
|
|
throw new Error(`Missing or invalid path-filter result: ${scope}`);
|
|
}
|
|
outputs[scope] = full ? "true" : filtered[scope];
|
|
}
|
|
// Product builds already install dependencies and run the shared checks.
|
|
// Only allocate a separate runner when quality is the sole frontend work.
|
|
outputs.quality_only = String(outputs.quality === "true" && outputs.frontend === "false");
|
|
return outputs;
|
|
}
|
|
|
|
export function checkGate(needs, jobScopes) {
|
|
if (needs.changes?.result !== "success") {
|
|
throw new Error("Path filtering did not succeed");
|
|
}
|
|
for (const [job, scope] of Object.entries(jobScopes)) {
|
|
const selected = needs.changes.outputs?.[scope];
|
|
if (selected !== "true" && selected !== "false") {
|
|
throw new Error(`Missing or invalid scope: ${scope}`);
|
|
}
|
|
const expected = selected === "true" ? "success" : "skipped";
|
|
if (needs[job]?.result !== expected) {
|
|
throw new Error(`${job}: expected ${expected}, got ${needs[job]?.result ?? "missing"}`);
|
|
}
|
|
}
|
|
// A newly added dependency must have a declared scope before the gate can pass.
|
|
for (const job of Object.keys(needs)) {
|
|
if (job !== "changes" && !Object.hasOwn(jobScopes, job)) {
|
|
throw new Error(`Unchecked dependency: ${job}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
try {
|
|
switch (process.argv[2]) {
|
|
case "decide": {
|
|
const outputs = decideScopes(process.env.EVENT_NAME, JSON.parse(process.env.FILTER_RESULTS));
|
|
appendFileSync(process.env.GITHUB_OUTPUT,
|
|
Object.entries(outputs).map(([key, value]) => `${key}=${value}\n`).join(""));
|
|
break;
|
|
}
|
|
case "gate":
|
|
checkGate(JSON.parse(process.env.NEEDS_JSON), JSON.parse(process.env.JOB_SCOPES));
|
|
console.log("All selected CI jobs succeeded; only unselected jobs were skipped.");
|
|
break;
|
|
default:
|
|
throw new Error("Usage: node scripts/ci-scope.mjs decide|gate");
|
|
}
|
|
} catch (error) {
|
|
console.error(error.message);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|