@mutmutco/codex-plugin 4.5.129 → 4.5.131
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +1 -1
- package/cli/dist/main.cjs +67 -25
- package/cli/package.json +1 -1
- package/package.json +1 -1
- package/skills/bootstrap/seeds/gate.template.yml +39 -5
- package/skills/bootstrap/seeds/runbook-unaffected.json +6 -0
- package/skills/bootstrap/seeds/suite-gate.template.yml +36 -2
package/cli/dist/main.cjs
CHANGED
|
@@ -8162,6 +8162,11 @@ var DECLARED_ENTRIES = [
|
|
|
8162
8162
|
// archived stage (including a stage whose containers are down) into scratch Postgres and compare
|
|
8163
8163
|
// row counts, and read-verify every storage archive against its listing; per-stage PASS/FAIL.
|
|
8164
8164
|
{ name: "mmi-backup-restore-check (mm-central, mm-work)", cadence: "0 3 1 * *", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mmi-backup-restore-check \u2192 /opt/mmi-control/nightly-backup-restore-check.sh; monthly restore proof for every archived stage (#7148; source scripts/nightly-backup-restore-check.sh); owner-approved mmi-oguz rebuild as CI-only mmi-runner2 with full erasure and no backups (#7859); zuber (46.225.58.31) is gone: deleted 2026-09-18 by the owner-approved ZuberShade decommission (MMC-ZuberShade#1838, snapshot kept); Hetzner has since reassigned that IP to another machine (#7351)" },
|
|
8165
|
+
// #8176: continuous WAL archiving of the mm-work dev Postgres (owner targets RPO <= 5 min, RTO <= 1 h). Postgres copies each
|
|
8166
|
+
// finished WAL file into a host spool (MM-Work#4170); this timer ships it encrypted to the Storage Box with WAL-G.
|
|
8167
|
+
{ name: "mmi-walg-ship@mm-work-dev (mm-work)", cadence: "every 30s (systemd OnUnitInactiveSec=30s)", executor: "box systemd", llm: "no", resolved: "declared", source: "/etc/systemd/system/mmi-walg-ship@.timer \u2192 /opt/mmi-control/walg-ship.sh mm-work-dev; spool /srv/mm-work/walspool/mm-work-dev \u2192 Storage Box backups/walg/mmi-tools/mm-work-dev, libsodium-encrypted (#8176; source scripts/walg/; status /var/lib/mmi-backup-status/mm-work-dev.ship.json)" },
|
|
8168
|
+
{ name: "mmi-walg-base (mm-work)", cadence: "30 3 * * 0", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mmi-walg-base \u2192 /opt/mmi-control/walg-base.sh mm-work-dev; weekly WAL-G base backup + age-based retention (keep the base at or before now-7d and everything after, #8193) (#8176; source scripts/walg/; log /var/log/mmi-walg.log; status /var/lib/mmi-backup-status/mm-work-dev.base.json)" },
|
|
8169
|
+
{ name: "mmi-walg-drill (mm-work)", cadence: "0 4 2 * *", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mmi-walg-drill \u2192 /opt/mmi-control/walg-drill.sh mm-work-dev; monthly restore drill measuring RPO/RTO against the owner targets (300s/3600s) into a throwaway --network none container (#8177; source scripts/walg/; status /var/lib/mmi-backup-status/mm-work-dev.drill.json; alarm: backup-health.yml)" },
|
|
8165
8170
|
// #7662 (MM-Work#1520): Vercel Cron's 15-minute central mail call, moved to the box with MM-Work. The app
|
|
8166
8171
|
// calls its own endpoint with its own CRON_SECRET via docker exec, so the secret stays in the container.
|
|
8167
8172
|
{ name: "mm-work-mail-tick (mm-work)", cadence: "*/15 * * * *", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mm-work-mail-tick \u2192 /opt/mmi-control/mm-work-mail-tick.sh; GET /api/internal/mail per stage listed in MM_WORK_MAIL_STAGES (source scripts/mm-work-mail-tick.sh; log /var/log/mm-work-mail-tick.log)" },
|
|
@@ -15380,6 +15385,17 @@ function missingPlaceholders(rendered) {
|
|
|
15380
15385
|
function isPropagatableSeed(seed) {
|
|
15381
15386
|
return seed.ownership === "org" && seed.source === "self" || seed.managedBlock != null;
|
|
15382
15387
|
}
|
|
15388
|
+
function seedSourcePath(seed) {
|
|
15389
|
+
const source = seed.managedBlock?.source ?? seed.source;
|
|
15390
|
+
return source.startsWith("seed:") ? `skills/bootstrap/seeds/${source.slice(5)}` : seed.target;
|
|
15391
|
+
}
|
|
15392
|
+
var RUNBOOK_UNAFFECTED_PATH = "skills/bootstrap/seeds/runbook-unaffected.json";
|
|
15393
|
+
function runbookUnaffectedReason(declarations, target, sourceBlob) {
|
|
15394
|
+
const entry = declarations?.[target];
|
|
15395
|
+
if (!entry || entry.blob !== sourceBlob || typeof entry.reason !== "string") return void 0;
|
|
15396
|
+
const reason = entry.reason.trim();
|
|
15397
|
+
return reason && !/[\r\n]/.test(reason) ? reason : void 0;
|
|
15398
|
+
}
|
|
15383
15399
|
function sourceLines(content) {
|
|
15384
15400
|
const lines2 = [];
|
|
15385
15401
|
const newline = /\r\n|\n|\r/g;
|
|
@@ -16670,6 +16686,7 @@ function gateExtraStepsYaml(steps) {
|
|
|
16670
16686
|
return steps.map((step) => [
|
|
16671
16687
|
`
|
|
16672
16688
|
- name: ${yamlScalar(step.name)}`,
|
|
16689
|
+
"\n if: ${{ steps.development-gate.outputs.reuse != 'true' }}",
|
|
16673
16690
|
step.env && Object.keys(step.env).length ? `
|
|
16674
16691
|
env:${envYaml(step.env, " ")}` : "",
|
|
16675
16692
|
"\n uses: mutmutco/MMI-Hub/.github/actions/run-with-budget@{{GATE_BUDGET_SHA}}",
|
|
@@ -17037,9 +17054,12 @@ function planSeedDelivery(branchRules, slug, baseBranch, branchPrefix = "bootstr
|
|
|
17037
17054
|
}
|
|
17038
17055
|
return { mode: "direct", ref: baseBranch, reason: decision.reason };
|
|
17039
17056
|
}
|
|
17040
|
-
function contentPutBody(path2, content, branch, sha) {
|
|
17057
|
+
function contentPutBody(path2, content, branch, sha, runbookUnaffected) {
|
|
17041
17058
|
const body = {
|
|
17042
|
-
|
|
17059
|
+
// #8169: a reviewed seed revision may declare it leaves runbooks correct; the tenant guard reads the trailer.
|
|
17060
|
+
message: `bootstrap: seed ${path2}${runbookUnaffected ? `
|
|
17061
|
+
|
|
17062
|
+
Runbook-Unaffected: ${runbookUnaffected}` : ""}`,
|
|
17043
17063
|
content: encodeGitHubContents(content),
|
|
17044
17064
|
branch
|
|
17045
17065
|
};
|
|
@@ -17226,23 +17246,33 @@ async function installAppFoldDeps(deps) {
|
|
|
17226
17246
|
}
|
|
17227
17247
|
var SHARED_INSTALLER_PACKAGES = ["@mutmutco/installer-face", "@mutmutco/installer-launcher", "@mutmutco/installer-gate"];
|
|
17228
17248
|
async function refreshSharedInstallerDeps(deps) {
|
|
17229
|
-
let
|
|
17249
|
+
let lockPaths;
|
|
17230
17250
|
try {
|
|
17231
|
-
|
|
17232
|
-
const present = Object.keys(lock.packages ?? {});
|
|
17233
|
-
declared = SHARED_INSTALLER_PACKAGES.filter((name) => present.some((key) => key === `node_modules/${name}` || key.endsWith(`/node_modules/${name}`)));
|
|
17234
|
-
} catch {
|
|
17235
|
-
return null;
|
|
17236
|
-
}
|
|
17237
|
-
if (declared.length === 0) return null;
|
|
17238
|
-
try {
|
|
17239
|
-
await deps.run("npm", ["update", ...declared, "--package-lock-only", "--no-audit", "--no-fund"]);
|
|
17251
|
+
lockPaths = (await deps.run("git", ["ls-tree", "-r", "--name-only", "HEAD"])).split("\n").filter((path2) => path2 === "package-lock.json" || path2.endsWith("/package-lock.json"));
|
|
17240
17252
|
} catch (e) {
|
|
17241
|
-
|
|
17242
|
-
|
|
17243
|
-
|
|
17253
|
+
throw trainReadFailure(
|
|
17254
|
+
"shared installer refresh: `git ls-tree -r --name-only HEAD`",
|
|
17255
|
+
e,
|
|
17256
|
+
"rerun in a readable checkout \u2014 lockfile roots must be discovered, never guessed"
|
|
17257
|
+
);
|
|
17244
17258
|
}
|
|
17245
|
-
|
|
17259
|
+
const notes = [];
|
|
17260
|
+
for (const lockPath of lockPaths) {
|
|
17261
|
+
const lock = JSON.parse(await deps.run("git", ["show", `HEAD:${lockPath}`]));
|
|
17262
|
+
const present = Object.keys(lock.packages ?? {});
|
|
17263
|
+
const declared = SHARED_INSTALLER_PACKAGES.filter((name) => present.some((key) => key === `node_modules/${name}` || key.endsWith(`/node_modules/${name}`)));
|
|
17264
|
+
if (declared.length === 0) continue;
|
|
17265
|
+
const prefix = lockPath.includes("/") ? lockPath.slice(0, lockPath.lastIndexOf("/")) : null;
|
|
17266
|
+
try {
|
|
17267
|
+
await deps.run("npm", [...prefix ? ["--prefix", prefix] : [], "update", ...declared, "--package-lock-only", "--no-audit", "--no-fund"]);
|
|
17268
|
+
notes.push(`shared installer deps refreshed in ${prefix ?? "."} (${declared.join(", ")})`);
|
|
17269
|
+
} catch (e) {
|
|
17270
|
+
const why = e instanceof Error ? e.message.split("\n")[0] : String(e);
|
|
17271
|
+
deps.warn?.(`shared installer refresh skipped for ${prefix ?? "."} (${why}) \u2014 shipping the locked versions`);
|
|
17272
|
+
notes.push(`shared installer refresh skipped for ${prefix ?? "."} (${why}); the locked ${declared.join(", ")} still satisfy their ranges`);
|
|
17273
|
+
}
|
|
17274
|
+
}
|
|
17275
|
+
return notes.length > 0 ? notes.join("; ") : null;
|
|
17246
17276
|
}
|
|
17247
17277
|
async function restoreMainNpmPackIdentities(deps, version) {
|
|
17248
17278
|
const bomPath = "distribution-bom.json";
|
|
@@ -18244,10 +18274,10 @@ var rollout_plan_default = {
|
|
|
18244
18274
|
note: "The v4.0.0 stamp happens at cut time (D6e #4463); until then the candidate is the origin/development head artifacts (built cli/dist + npm pack), identity proven by dist content hash (D6a)."
|
|
18245
18275
|
},
|
|
18246
18276
|
baseline: {
|
|
18247
|
-
version: "4.5.
|
|
18248
|
-
tag: "v4.5.
|
|
18249
|
-
commit: "
|
|
18250
|
-
npm: "@mutmutco/cli@4.5.
|
|
18277
|
+
version: "4.5.131",
|
|
18278
|
+
tag: "v4.5.131",
|
|
18279
|
+
commit: "1070f097f90f",
|
|
18280
|
+
npm: "@mutmutco/cli@4.5.131"
|
|
18251
18281
|
},
|
|
18252
18282
|
exitCriterion: "fleet-n-of-n",
|
|
18253
18283
|
hubOnlyShortcut: "forbidden",
|
|
@@ -18264,14 +18294,14 @@ var rollout_plan_default = {
|
|
|
18264
18294
|
repo: "mutmutco/mmi-hub",
|
|
18265
18295
|
role: "canary",
|
|
18266
18296
|
schedule: "train",
|
|
18267
|
-
v3Target: "v4.5.
|
|
18297
|
+
v3Target: "v4.5.131"
|
|
18268
18298
|
}
|
|
18269
18299
|
],
|
|
18270
18300
|
rollbackTrigger: "Any red inside the post-contract soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a pre-v4 client admitted instead of receiving actionable HTTP 426, or npm consumer install/doctor failure on the v4-only dist.",
|
|
18271
18301
|
rollback: {
|
|
18272
18302
|
independent: true,
|
|
18273
|
-
mechanism: "npm dist-tag latest -> 4.5.
|
|
18274
|
-
v3Target: "v4.5.
|
|
18303
|
+
mechanism: "npm dist-tag latest -> 4.5.131 and redeploy the Hub Lambda from tag v4.5.131 (1070f097f90f); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
18304
|
+
v3Target: "v4.5.131 (@mutmutco/cli@4.5.131, tag commit 1070f097f90f \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
18275
18305
|
}
|
|
18276
18306
|
},
|
|
18277
18307
|
{
|
|
@@ -28689,6 +28719,7 @@ var UNBLOCKING_GATE_JOBS_LABEL = "every gate job can block a merge (#7012)";
|
|
|
28689
28719
|
var TAG_ADDRESSABLE_CONTEXTS_LABEL = "rc/main required contexts are tag-addressable (#3880)";
|
|
28690
28720
|
var RELEASE_BRANCH_REACHABLE_LABEL = "rc/main required contexts are reachable from a release-base head (#5193)";
|
|
28691
28721
|
var DEV_STAGE_FRESH_LABEL = "tenant dev stage runs the development tip (#7536)";
|
|
28722
|
+
var NO_CANCEL_WAIVER = /ci-audit:\s*allow-no-cancel-in-progress\b[^\n]*#\d+/i;
|
|
28692
28723
|
var DEV_STAGE_STALE_TOLERANCE_MS = 30 * 60 * 1e3;
|
|
28693
28724
|
function devStageFreshnessCheck(input) {
|
|
28694
28725
|
const { repo, deployedTag, tipSha, tipCommittedAt, nowMs } = input;
|
|
@@ -29372,7 +29403,6 @@ async function auditRepoCi(repo, callerDeps) {
|
|
|
29372
29403
|
detail: `${policy} (${reason})`
|
|
29373
29404
|
});
|
|
29374
29405
|
}
|
|
29375
|
-
const NO_CANCEL_WAIVER = /ci-audit:\s*allow-no-cancel-in-progress\b[^\n]*#\d+/i;
|
|
29376
29406
|
if (deployableGated || repoClass === "hub") {
|
|
29377
29407
|
const allPaths = await listWorkflowPaths(deps, repo, baseBranch) ?? [];
|
|
29378
29408
|
const prPaths = await filterPullRequestTriggered(deps, repo, baseBranch, allPaths);
|
|
@@ -29503,10 +29533,17 @@ ${r.repo} (${r.class}, policy=${r.mergePolicy}) ${verdict}`);
|
|
|
29503
29533
|
}
|
|
29504
29534
|
return lines2.join("\n");
|
|
29505
29535
|
}
|
|
29536
|
+
function unauditedReconcileError(reason) {
|
|
29537
|
+
return `cannot audit \u2014 ${reason}; no reconcile change made (unaudited, not canonical)`;
|
|
29538
|
+
}
|
|
29506
29539
|
async function applyCiReconcileMergeSettingsFromReport(repo, deps, report) {
|
|
29507
29540
|
const applied = [];
|
|
29508
29541
|
const skipped = [];
|
|
29509
29542
|
const errors = [];
|
|
29543
|
+
if (report.unauditedReason) {
|
|
29544
|
+
errors.push(unauditedReconcileError(report.unauditedReason));
|
|
29545
|
+
return { repo, applied, skipped, errors };
|
|
29546
|
+
}
|
|
29510
29547
|
const mergeChecks = report.checks.filter((c) => c.label.startsWith("allow_") || c.label.startsWith("delete_branch") || c.label.startsWith("has_wiki"));
|
|
29511
29548
|
const needsPatch = mergeChecks.some((c) => !c.ok);
|
|
29512
29549
|
if (!needsPatch) {
|
|
@@ -29810,6 +29847,9 @@ async function applyCiReconcileRepo(repo, deps, options = {}) {
|
|
|
29810
29847
|
return { repo, applied: [], skipped: [], errors: ["product-ruleset requires registry META requiredChecks"] };
|
|
29811
29848
|
}
|
|
29812
29849
|
const report = await auditRepoCi(repo, deps);
|
|
29850
|
+
if (report.unauditedReason) {
|
|
29851
|
+
return { repo, applied: [], skipped: [], errors: [unauditedReconcileError(report.unauditedReason)] };
|
|
29852
|
+
}
|
|
29813
29853
|
const finish = (result2, pendingReason) => options.productRulesetOnly ? Promise.resolve(result2) : finalizeCiReconcile(repo, deps, result2, report, pendingReason);
|
|
29814
29854
|
const baseBranch = "development";
|
|
29815
29855
|
let raw = report.class === "deployable" && !report.explicitNoCi ? await fetchRulesetSeedBody(deps, repo) : null;
|
|
@@ -36404,6 +36444,8 @@ LIVE apply to ${repo}:
|
|
|
36404
36444
|
if (o.execute) {
|
|
36405
36445
|
if (plan.refusedNoCanary) return fail("bootstrap propagate --execute: no canary declared for this target \u2014 refusing to write (set seedCanary:true on exactly one registry repo)");
|
|
36406
36446
|
const headSha = seedSource.sha;
|
|
36447
|
+
const sourceBlob = (await execGitForSeedSource(["rev-parse", `${headSha}:${seedSourcePath(seed)}`])).trim();
|
|
36448
|
+
const runbookUnaffected = (0, import_node_fs30.existsSync)(RUNBOOK_UNAFFECTED_PATH) ? runbookUnaffectedReason(JSON.parse((0, import_node_fs30.readFileSync)(RUNBOOK_UNAFFECTED_PATH, "utf8")), seed.target, sourceBlob) : void 0;
|
|
36407
36449
|
for (const rec of plan.records) {
|
|
36408
36450
|
if (rec.action !== "open-pr") continue;
|
|
36409
36451
|
const baseBranch = classOf(rec.repo) === "content" ? "main" : "development";
|
|
@@ -36428,7 +36470,7 @@ LIVE apply to ${repo}:
|
|
|
36428
36470
|
const tmp = (0, import_node_path29.join)((0, import_node_os18.tmpdir)(), `mmi-propagate-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
|
|
36429
36471
|
const desiredContent = desiredByRepo.get(rec.repo);
|
|
36430
36472
|
if (desiredContent == null) return fail(`bootstrap propagate: no resolved content for ${rec.repo} ${seed.target} \u2014 refusing to write`);
|
|
36431
|
-
(0, import_node_fs30.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, desiredContent, branch, existingSha)), "utf8");
|
|
36473
|
+
(0, import_node_fs30.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, desiredContent, branch, existingSha, runbookUnaffected)), "utf8");
|
|
36432
36474
|
try {
|
|
36433
36475
|
await gh2(contentPutInputArgs(rec.repo, seed.target, tmp));
|
|
36434
36476
|
} finally {
|
package/cli/package.json
CHANGED
package/package.json
CHANGED
|
@@ -50,6 +50,7 @@ concurrency:
|
|
|
50
50
|
cancel-in-progress: true
|
|
51
51
|
|
|
52
52
|
permissions:
|
|
53
|
+
actions: read # #8206: exact-SHA development workflow verdict
|
|
53
54
|
contents: read
|
|
54
55
|
# MMI-Hub#7542: the product image build below reads org private packages with the workflow's
|
|
55
56
|
# own ephemeral token (NODE_AUTH_TOKEN) — the same reader the central deploy uses.
|
|
@@ -65,12 +66,40 @@ jobs:
|
|
|
65
66
|
defaults:
|
|
66
67
|
run: { working-directory: {{GATE_WORKDIR}} }{{GATE_SERVICES_YAML}}
|
|
67
68
|
steps:
|
|
68
|
-
|
|
69
|
+
# #8206: keep a fresh successful tag gate; promote the exact development verdict, not a ref.
|
|
70
|
+
- name: Reuse development gate for rc tag
|
|
71
|
+
id: development-gate
|
|
72
|
+
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
|
73
|
+
with:
|
|
74
|
+
script: |
|
|
75
|
+
core.setOutput('reuse', 'false');
|
|
76
|
+
if (context.eventName !== 'push' || !/^refs\/tags\/v.+-rc\.\d+$/.test(context.ref) || !/^[0-9a-f]{40}$/.test(context.sha)) return;
|
|
77
|
+
try {
|
|
78
|
+
const { data: current } = await github.rest.actions.getWorkflowRun({ ...context.repo, run_id: context.runId });
|
|
79
|
+
const { data } = await github.rest.actions.listWorkflowRuns({
|
|
80
|
+
...context.repo, workflow_id: current.workflow_id, branch: 'development',
|
|
81
|
+
event: 'push', head_sha: context.sha, per_page: 100
|
|
82
|
+
});
|
|
83
|
+
// API returns newest first: a newer red/pending rerun must not reuse an older green.
|
|
84
|
+
const run = data.workflow_runs[0];
|
|
85
|
+
if (!run || run.head_sha !== context.sha || run.head_branch !== 'development' ||
|
|
86
|
+
run.event !== 'push' || run.status !== 'completed' || run.conclusion !== 'success') return;
|
|
87
|
+
const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, {
|
|
88
|
+
...context.repo, run_id: run.id, filter: 'latest', per_page: 100
|
|
89
|
+
});
|
|
90
|
+
if (!jobs.some(job => job.name === 'gate' && job.status === 'completed' && job.conclusion === 'success')) return;
|
|
91
|
+
core.setOutput('reuse', 'true');
|
|
92
|
+
core.info('Reusing successful development gate run ' + run.id + ' for exact SHA ' + context.sha);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
core.warning('Development gate proof unavailable; running normal gate: ' + error.message);
|
|
95
|
+
}
|
|
96
|
+
- if: ${{ steps.development-gate.outputs.reuse != 'true' }}
|
|
97
|
+
uses: mutmutco/MMI-Hub/.github/actions/runner-checkout@{{GATE_RUNNER_SHA}}
|
|
69
98
|
with:
|
|
70
99
|
lockfile-path: {{GATE_CACHE_DEP_PATH}}
|
|
71
100
|
fetch-depth: 0 # #8129: runbook freshness needs the PR merge-base history.
|
|
72
101
|
- name: Runbook guard
|
|
73
|
-
if: ${{ github.event_name == 'pull_request' }}
|
|
102
|
+
if: ${{ steps.development-gate.outputs.reuse != 'true' && (github.event_name == 'pull_request') }}
|
|
74
103
|
uses: mutmutco/MMI-Hub/.github/actions/runbook-guard@{{GATE_RUNBOOK_SHA}}
|
|
75
104
|
with:
|
|
76
105
|
base-ref: origin/${{ github.base_ref }}
|
|
@@ -81,11 +110,13 @@ jobs:
|
|
|
81
110
|
# `npm config get cache` for any explicit cache restore). Job-level `env:` cannot use the
|
|
82
111
|
# `runner` context — write via GITHUB_ENV from the shell `$RUNNER_TEMP` instead.
|
|
83
112
|
- name: Isolate npm cache (per-job)
|
|
113
|
+
if: ${{ steps.development-gate.outputs.reuse != 'true' }}
|
|
84
114
|
shell: bash
|
|
85
115
|
run: echo "NPM_CONFIG_CACHE=$RUNNER_TEMP/npm-cache" >> "$GITHUB_ENV"
|
|
86
116
|
# MMI-Hub#5111: Windows CRLF in tracked shell scripts breaks Linux runners before the real
|
|
87
117
|
# check can explain it. Inspect only tracked files before install; .gitattributes is the fix.
|
|
88
118
|
- name: Line-ending guard
|
|
119
|
+
if: ${{ steps.development-gate.outputs.reuse != 'true' }}
|
|
89
120
|
shell: bash
|
|
90
121
|
run: |
|
|
91
122
|
offenders=$(mktemp)
|
|
@@ -116,6 +147,7 @@ jobs:
|
|
|
116
147
|
# files (Dockerfile + docker-compose.yml) skip in a second — no new job, no new required
|
|
117
148
|
# context, no ruleset or registry change (#7012 needs nothing to cover).
|
|
118
149
|
- name: Product image build (tenant-container only)
|
|
150
|
+
if: ${{ steps.development-gate.outputs.reuse != 'true' }}
|
|
119
151
|
env:
|
|
120
152
|
# The documented build-secret opt-in (compose `build.secrets` + registry
|
|
121
153
|
# requiredBuildSecrets `NODE_AUTH_TOKEN=@github-packages-token`): the workflow's own
|
|
@@ -146,25 +178,27 @@ jobs:
|
|
|
146
178
|
# MMI-Hub#5871: declare the npm MAJOR rather than inheriting whatever the runner's node bundles.
|
|
147
179
|
# A bare major, never an exact patch (it goes stale and then prescribes downgrading operator
|
|
148
180
|
# hosts toward it, #5862) and never `latest` (it takes the next major silently, #4782).
|
|
149
|
-
- if: ${{ '{{GATE_RUNTIME}}' == 'node' }}
|
|
181
|
+
- if: ${{ steps.development-gate.outputs.reuse != 'true' && ('{{GATE_RUNTIME}}' == 'node') }}
|
|
150
182
|
uses: mutmutco/MMI-Hub/.github/actions/runner-node-toolchain@{{GATE_RUNNER_SHA}}
|
|
151
183
|
with:
|
|
152
184
|
node-version: 24
|
|
153
185
|
npm-version: '12'
|
|
154
|
-
- if: ${{ '{{GATE_RUNTIME}}' == 'python' }}
|
|
186
|
+
- if: ${{ steps.development-gate.outputs.reuse != 'true' && ('{{GATE_RUNTIME}}' == 'python') }}
|
|
155
187
|
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
156
188
|
with: { python-version: '{{GATE_PY_VERSION}}' }
|
|
157
189
|
# MMI-Hub#3446: assert the runtime toolchain is on PATH BEFORE the install step. Node uses the
|
|
158
190
|
# runner-node-toolchain composite above (setup-node + preflight in one action, #5610). Python
|
|
159
191
|
# keeps the inline preflight below.
|
|
160
192
|
- name: Toolchain preflight
|
|
161
|
-
if: ${{ '{{GATE_RUNTIME}}' == 'python' }}
|
|
193
|
+
if: ${{ steps.development-gate.outputs.reuse != 'true' && ('{{GATE_RUNTIME}}' == 'python') }}
|
|
162
194
|
run: |
|
|
163
195
|
python3 --version || { echo "::error title=CI runner toolchain::RUNNER NOT PROVISIONED — python3 not on PATH after setup-python. This is the runner, not your diff; no install or test ran. Re-run the job; if it persists the mmi-runner python toolchain needs attention (docs/Guides/gh-runner-runbook.md in MMI-Hub)."; exit 1; }
|
|
164
196
|
- run: {{GATE_INSTALL_CMD}}
|
|
197
|
+
if: ${{ steps.development-gate.outputs.reuse != 'true' }}
|
|
165
198
|
# #3178: the check runs under the org wall-clock budget — a hung suite dies at the ceiling instead
|
|
166
199
|
# of squatting a runner lane for GitHub's 6h default. ci-audit + the release train enforce this step.
|
|
167
200
|
- uses: mutmutco/MMI-Hub/.github/actions/run-with-budget@{{GATE_BUDGET_SHA}}
|
|
201
|
+
if: ${{ steps.development-gate.outputs.reuse != 'true' }}
|
|
168
202
|
with:
|
|
169
203
|
command: {{GATE_CMD}}
|
|
170
204
|
max-seconds: {{GATE_MAX_SECONDS}}
|
|
@@ -8,6 +8,7 @@ concurrency:
|
|
|
8
8
|
group: gate-${{ github.event.pull_request.number || github.ref }}
|
|
9
9
|
cancel-in-progress: true
|
|
10
10
|
permissions:
|
|
11
|
+
actions: read # #8206: exact-SHA development workflow verdict
|
|
11
12
|
contents: read
|
|
12
13
|
packages: read
|
|
13
14
|
jobs:
|
|
@@ -15,26 +16,59 @@ jobs:
|
|
|
15
16
|
runs-on: [self-hosted, linux, x64, mmi-live]
|
|
16
17
|
timeout-minutes: 10
|
|
17
18
|
steps:
|
|
18
|
-
|
|
19
|
+
# #8206: keep a fresh successful tag gate; promote the exact development verdict, not a ref.
|
|
20
|
+
- name: Reuse development gate for rc tag
|
|
21
|
+
id: development-gate
|
|
22
|
+
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
|
23
|
+
with:
|
|
24
|
+
script: |
|
|
25
|
+
core.setOutput('reuse', 'false');
|
|
26
|
+
if (context.eventName !== 'push' || !/^refs\/tags\/v.+-rc\.\d+$/.test(context.ref) || !/^[0-9a-f]{40}$/.test(context.sha)) return;
|
|
27
|
+
try {
|
|
28
|
+
const { data: current } = await github.rest.actions.getWorkflowRun({ ...context.repo, run_id: context.runId });
|
|
29
|
+
const { data } = await github.rest.actions.listWorkflowRuns({
|
|
30
|
+
...context.repo, workflow_id: current.workflow_id, branch: 'development',
|
|
31
|
+
event: 'push', head_sha: context.sha, per_page: 100
|
|
32
|
+
});
|
|
33
|
+
// API returns newest first: a newer red/pending rerun must not reuse an older green.
|
|
34
|
+
const run = data.workflow_runs[0];
|
|
35
|
+
if (!run || run.head_sha !== context.sha || run.head_branch !== 'development' ||
|
|
36
|
+
run.event !== 'push' || run.status !== 'completed' || run.conclusion !== 'success') return;
|
|
37
|
+
const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, {
|
|
38
|
+
...context.repo, run_id: run.id, filter: 'latest', per_page: 100
|
|
39
|
+
});
|
|
40
|
+
if (!jobs.some(job => job.name === 'gate' && job.status === 'completed' && job.conclusion === 'success')) return;
|
|
41
|
+
core.setOutput('reuse', 'true');
|
|
42
|
+
core.info('Reusing successful development gate run ' + run.id + ' for exact SHA ' + context.sha);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
core.warning('Development gate proof unavailable; running normal gate: ' + error.message);
|
|
45
|
+
}
|
|
46
|
+
- if: ${{ steps.development-gate.outputs.reuse != 'true' }}
|
|
47
|
+
uses: mutmutco/MMI-Hub/.github/actions/runner-checkout@{{GATE_RUNNER_SHA}}
|
|
19
48
|
with:
|
|
20
49
|
lockfile-path: package-lock.json
|
|
21
50
|
fetch-depth: 0 # #8129: runbook freshness needs the PR merge-base history.
|
|
22
51
|
- name: Runbook guard
|
|
23
|
-
if: ${{ github.event_name == 'pull_request' }}
|
|
52
|
+
if: ${{ steps.development-gate.outputs.reuse != 'true' && (github.event_name == 'pull_request') }}
|
|
24
53
|
uses: mutmutco/MMI-Hub/.github/actions/runbook-guard@{{GATE_RUNBOOK_SHA}}
|
|
25
54
|
with:
|
|
26
55
|
base-ref: origin/${{ github.base_ref }}
|
|
27
56
|
- name: Isolate npm cache
|
|
57
|
+
if: ${{ steps.development-gate.outputs.reuse != 'true' }}
|
|
28
58
|
run: echo "NPM_CONFIG_CACHE=$RUNNER_TEMP/npm-cache" >> "$GITHUB_ENV"
|
|
29
59
|
- uses: mutmutco/MMI-Hub/.github/actions/runner-node-toolchain@{{GATE_RUNNER_SHA}}
|
|
60
|
+
if: ${{ steps.development-gate.outputs.reuse != 'true' }}
|
|
30
61
|
with:
|
|
31
62
|
node-version: 24
|
|
32
63
|
npm-version: '12'
|
|
33
64
|
- run: npm ci --no-audit --no-fund
|
|
65
|
+
if: ${{ steps.development-gate.outputs.reuse != 'true' }}
|
|
34
66
|
- run: npm ci --prefix module --no-audit --no-fund
|
|
67
|
+
if: ${{ steps.development-gate.outputs.reuse != 'true' }}
|
|
35
68
|
env:
|
|
36
69
|
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
37
70
|
- uses: mutmutco/MMI-Hub/.github/actions/run-with-budget@{{GATE_BUDGET_SHA}}
|
|
71
|
+
if: ${{ steps.development-gate.outputs.reuse != 'true' }}
|
|
38
72
|
with:
|
|
39
73
|
command: npm run typecheck && npm run build && npm --prefix module run check
|
|
40
74
|
max-seconds: {{GATE_MAX_SECONDS}}
|