@mutmutco/claude-plugin 4.6.17 → 4.6.19
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/.claude-plugin/plugin.json +1 -1
- package/cli/dist/main.cjs +447 -317
- package/cli/package.json +1 -1
- package/package.json +1 -1
package/cli/dist/main.cjs
CHANGED
|
@@ -2998,9 +2998,9 @@ ${indent}`) + "'";
|
|
|
2998
2998
|
return quotedString(value, ctx);
|
|
2999
2999
|
}
|
|
3000
3000
|
const indent = ctx.indent || (ctx.forceBlockIndent || containsDocumentMarker(value) ? " " : "");
|
|
3001
|
-
const
|
|
3001
|
+
const literal = blockQuote === "literal" ? true : blockQuote === "folded" || type === Scalar.Scalar.BLOCK_FOLDED ? false : type === Scalar.Scalar.BLOCK_LITERAL ? true : !lineLengthOverLimit(value, lineWidth, indent.length);
|
|
3002
3002
|
if (!value)
|
|
3003
|
-
return
|
|
3003
|
+
return literal ? "|\n" : ">\n";
|
|
3004
3004
|
let chomp;
|
|
3005
3005
|
let endStart;
|
|
3006
3006
|
for (endStart = value.length; endStart > 0; --endStart) {
|
|
@@ -3049,7 +3049,7 @@ ${indent}`) + "'";
|
|
|
3049
3049
|
if (onComment)
|
|
3050
3050
|
onComment();
|
|
3051
3051
|
}
|
|
3052
|
-
if (!
|
|
3052
|
+
if (!literal) {
|
|
3053
3053
|
const foldedValue = value.replace(/\n+/g, "\n$&").replace(/(?:^|\n)([\t ].*)(?:([\n\t ]*)\n(?![\n\t ]))?/g, "$1$2").replace(/\n+/g, `$&${indent}`);
|
|
3054
3054
|
let literalFallback = false;
|
|
3055
3055
|
const foldOptions = getFoldOptions(ctx, true);
|
|
@@ -13637,13 +13637,16 @@ function isExemptPorcelainLine(status, path2) {
|
|
|
13637
13637
|
return isLiveSessionPath(path2);
|
|
13638
13638
|
}
|
|
13639
13639
|
function porcelainBlockingPaths(porcelain) {
|
|
13640
|
-
|
|
13640
|
+
return porcelainBlockingEntries(porcelain).map((e) => e.path);
|
|
13641
|
+
}
|
|
13642
|
+
function porcelainBlockingEntries(porcelain) {
|
|
13643
|
+
const entries = [];
|
|
13641
13644
|
for (const line of porcelain.split("\n")) {
|
|
13642
13645
|
if (!line.trim()) continue;
|
|
13643
13646
|
const { status, path: path2 } = splitPorcelainLine(line);
|
|
13644
|
-
if (path2 !== "" && !isExemptPorcelainLine(status, path2))
|
|
13647
|
+
if (path2 !== "" && !isExemptPorcelainLine(status, path2)) entries.push({ status, path: path2 });
|
|
13645
13648
|
}
|
|
13646
|
-
return
|
|
13649
|
+
return entries;
|
|
13647
13650
|
}
|
|
13648
13651
|
function porcelainHasBlockingChanges(porcelain) {
|
|
13649
13652
|
return porcelainBlockingPaths(porcelain).length > 0;
|
|
@@ -17607,6 +17610,279 @@ var REGISTRY_FETCH_TIMEOUT_MS = 8e3;
|
|
|
17607
17610
|
init_fetch_retry();
|
|
17608
17611
|
init_client_version();
|
|
17609
17612
|
|
|
17613
|
+
// src/hub-release-ci-authority.ts
|
|
17614
|
+
var HUB_RELEASE_REPO = "mutmutco/MMI-Hub";
|
|
17615
|
+
var HUB_RELEASE_WORKFLOW = ".github/workflows/hub-release.yml";
|
|
17616
|
+
var HUB_RELEASE_REF = "refs/heads/development";
|
|
17617
|
+
var WORKFLOW_REF = `${HUB_RELEASE_REPO}/${HUB_RELEASE_WORKFLOW}@${HUB_RELEASE_REF}`;
|
|
17618
|
+
var ID = /^[1-9][0-9]*$/;
|
|
17619
|
+
function hubReleaseCiRun(env) {
|
|
17620
|
+
if (env.MMI_HUB_RELEASE_CI !== "1" || env.GITHUB_ACTIONS !== "true") return null;
|
|
17621
|
+
if (env.GITHUB_REPOSITORY !== HUB_RELEASE_REPO || env.GITHUB_EVENT_NAME !== "workflow_dispatch") return null;
|
|
17622
|
+
if (env.GITHUB_WORKFLOW_REF !== WORKFLOW_REF || env.GITHUB_REF !== HUB_RELEASE_REF) return null;
|
|
17623
|
+
const runId = env.GITHUB_RUN_ID ?? "";
|
|
17624
|
+
const runAttempt2 = env.GITHUB_RUN_ATTEMPT ?? "";
|
|
17625
|
+
if (!ID.test(runId) || !ID.test(runAttempt2)) return null;
|
|
17626
|
+
return { runId: Number(runId), runAttempt: Number(runAttempt2) };
|
|
17627
|
+
}
|
|
17628
|
+
|
|
17629
|
+
// ../shared/runner-fallback.ts
|
|
17630
|
+
var HOSTED_RUNNER_LABEL_RE = /\b(?:ubuntu|windows|macos)-(?:latest|\d[\w.-]*)\b/i;
|
|
17631
|
+
var STANDARD_HOSTED_LABEL_RE = /^(?:ubuntu-(?:latest|24\.04|22\.04|24\.04-arm|22\.04-arm)|windows-(?:latest|2025|2022|11-arm)|macos-(?:latest|\d+|\d+-intel))$/i;
|
|
17632
|
+
var BLESSED_RUNNER_CHOICE_SHA = "cf35db6cdf053746bc5b2cd75f241a7d93cf7f48";
|
|
17633
|
+
function classifyRunnerCost(selector) {
|
|
17634
|
+
const raw = selector.replace(/\s+#.*$/gm, "").trim();
|
|
17635
|
+
if (STANDARD_HOSTED_LABEL_RE.test(raw.replace(/^(['"])(.*)\1$/, "$2"))) return "standard-hosted";
|
|
17636
|
+
if (HOSTED_RUNNER_LABEL_RE.test(raw)) return "hosted";
|
|
17637
|
+
const json = /^\$\{\{\s*fromJSON\('([^']+)'\)\s*\}\}$/.exec(raw);
|
|
17638
|
+
if (json) {
|
|
17639
|
+
try {
|
|
17640
|
+
const labels2 = JSON.parse(json[1]);
|
|
17641
|
+
return Array.isArray(labels2) && labels2.length > 0 && labels2.every((l) => typeof l === "string" && /^[\w-]+$/.test(l)) && labels2.some((l) => l.toLowerCase() === "self-hosted") ? "self-hosted" : "unknown";
|
|
17642
|
+
} catch {
|
|
17643
|
+
return "unknown";
|
|
17644
|
+
}
|
|
17645
|
+
}
|
|
17646
|
+
if (raw.includes("${{")) return "unknown";
|
|
17647
|
+
const labels = /^\[[^\[\]]+\]$/.test(raw) ? raw.slice(1, -1).split(",").map((s) => s.trim().replace(/^(['"])(.*)\1$/, "$2")) : /^(?:-\s*[^\n]+\s*)+$/.test(raw) ? raw.split("\n").map((s) => s.trim().replace(/^-\s*/, "").replace(/^(['"])(.*)\1$/, "$2")) : [raw.replace(/^(['"])(.*)\1$/, "$2")];
|
|
17648
|
+
return labels.every((l) => /^[\w-]+$/.test(l)) && labels.some((l) => l.toLowerCase() === "self-hosted") ? "self-hosted" : "unknown";
|
|
17649
|
+
}
|
|
17650
|
+
var STANDARD_HOSTED_ALLOWLIST = [
|
|
17651
|
+
["mutmutco/MMI-Hub", ".github/workflows/launcher-build.yml", "build", "windows-latest"],
|
|
17652
|
+
["mutmutco/Jerv-JervCode", ".github/workflows/release-archives.yml", "release-platform-arm64", "windows-11-arm"]
|
|
17653
|
+
];
|
|
17654
|
+
var HOSTED_SPILL_RE = /^\$\{\{\s*(?:matrix\.[\w-]+\s*==\s*'[\w-]+'\s*&&\s*(?:'(?<leg>[^']+)'|fromJSON\(needs\.(?<legDecider>[\w-]+)\.outputs\.(?<legOutput>\w+_runs_on)\s*\|\|\s*(?<legOwned>'[^']+')\))\s*\|\|\s*)?fromJSON\(needs\.(?<decider>[\w-]+)\.outputs\.(?<output>\w+_runs_on)\s*\|\|\s*(?<owned>'[^']+')\)\s*\}\}$/;
|
|
17655
|
+
var RUNNER_CHOICE_ACTION = "mutmutco/MMI-Hub/.github/actions/runner-choice";
|
|
17656
|
+
function runnerChoiceProduces(job, decider, output, source) {
|
|
17657
|
+
const needs = job.needs;
|
|
17658
|
+
if (!(needs === decider || Array.isArray(needs) && needs.includes(decider))) return false;
|
|
17659
|
+
const producer = source.jobs.get(decider);
|
|
17660
|
+
if (!record(producer) || !record(producer.outputs) || !Array.isArray(producer.steps)) return false;
|
|
17661
|
+
const step = /^\$\{\{\s*steps\.([\w-]+)\.outputs\.runs-on\s*\}\}$/.exec(String(producer.outputs[output] ?? ""))?.[1];
|
|
17662
|
+
const steps = producer.steps.filter((s) => record(s) && s.id === step);
|
|
17663
|
+
if (steps.length !== 1 || !record(steps[0])) return false;
|
|
17664
|
+
const { uses } = steps[0], inputs = steps[0].with ?? {};
|
|
17665
|
+
const pinned = /^[0-9a-f]{40}$/.test(source.blessedSha) && uses === `${RUNNER_CHOICE_ACTION}@${source.blessedSha}`;
|
|
17666
|
+
if (!pinned && !(source.repo === "mutmutco/MMI-Hub" && uses === "./.github/actions/runner-choice")) return false;
|
|
17667
|
+
const hosted = record(inputs) ? inputs.hosted ?? "ubuntu-latest" : void 0;
|
|
17668
|
+
return typeof hosted === "string" && STANDARD_HOSTED_LABEL_RE.test(hosted);
|
|
17669
|
+
}
|
|
17670
|
+
function standardHostedLabelFor(repo, path2, jobId) {
|
|
17671
|
+
if (repo === "mutmutco/Jerv-Hub") return ["ubuntu-latest", "windows-latest", "macos-14"];
|
|
17672
|
+
return STANDARD_HOSTED_ALLOWLIST.find(([r, p, j]) => r === repo && p === path2 && j === jobId)?.[3];
|
|
17673
|
+
}
|
|
17674
|
+
var record = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
17675
|
+
function classifyJobRunnerCost(job, allowance2, source) {
|
|
17676
|
+
if (!record(job) || !Object.hasOwn(job, "runs-on")) return "unknown";
|
|
17677
|
+
const selector = job["runs-on"];
|
|
17678
|
+
const raw = typeof selector === "string" ? selector.trim() : Array.isArray(selector) && selector.length && selector.every((label) => typeof label === "string") ? JSON.stringify(selector) : "";
|
|
17679
|
+
if (!raw) return "unknown";
|
|
17680
|
+
const approved = (value) => Array.isArray(allowance2) && allowance2.includes(value);
|
|
17681
|
+
if (approved(raw) || allowance2 === "windows-11-arm" && raw === allowance2) return "standard-hosted";
|
|
17682
|
+
const fallback = /^\$\{\{\s*vars\.MMI_RUNNER_FALLBACK\s*==\s*'hosted'\s*&&\s*'([^']+)'\s*\|\|\s*(fromJSON\('[^']+'\))\s*\}\}$/.exec(raw);
|
|
17683
|
+
if (fallback && approved(fallback[1]) && classifyRunnerCost("${{ " + fallback[2] + " }}") === "self-hosted") return "standard-hosted";
|
|
17684
|
+
const layerZero = /^\$\{\{\s*vars\.[\w-]+\s*==\s*'off'\s*&&\s*(fromJSON\('[^']+'\))\s*\|\|\s*'([^']+)'\s*\}\}$/.exec(raw);
|
|
17685
|
+
if (layerZero && classifyRunnerCost("${{ " + layerZero[1] + " }}") === "self-hosted" && STANDARD_HOSTED_LABEL_RE.test(layerZero[2])) return "standard-hosted";
|
|
17686
|
+
const spill = HOSTED_SPILL_RE.exec(raw)?.groups;
|
|
17687
|
+
const legOk = (g) => g.legDecider ? classifyRunnerCost(`\${{ fromJSON(${g.legOwned}) }}`) === "self-hosted" && Boolean(source) && runnerChoiceProduces(job, g.legDecider, g.legOutput, source) : !g.leg || STANDARD_HOSTED_LABEL_RE.test(g.leg);
|
|
17688
|
+
if (spill && source && legOk(spill) && classifyRunnerCost(`\${{ fromJSON(${spill.owned}) }}`) === "self-hosted" && runnerChoiceProduces(job, spill.decider, spill.output, source)) return "standard-hosted";
|
|
17689
|
+
const direct = classifyRunnerCost(raw);
|
|
17690
|
+
const matrix = /^\$\{\{\s*matrix\.([\w-]+)(?:\.([\w-]+))?\s*\}\}$/.exec(raw);
|
|
17691
|
+
if (!matrix || direct === "hosted") return direct;
|
|
17692
|
+
if (!Object.hasOwn(job, "strategy") || !record(job.strategy) || !Object.hasOwn(job.strategy, "matrix") || !record(job.strategy.matrix)) return "unknown";
|
|
17693
|
+
const axes = job.strategy.matrix;
|
|
17694
|
+
if (Object.hasOwn(axes, "exclude")) return "unknown";
|
|
17695
|
+
let alternatives;
|
|
17696
|
+
if (Object.hasOwn(axes, "include")) {
|
|
17697
|
+
if (Object.keys(axes).length !== 1 || !Array.isArray(axes.include) || !axes.include.length) return "unknown";
|
|
17698
|
+
alternatives = axes.include.map((row) => record(row) && Object.hasOwn(row, matrix[1]) ? row[matrix[1]] : void 0);
|
|
17699
|
+
} else {
|
|
17700
|
+
const axis = axes[matrix[1]];
|
|
17701
|
+
if (!Object.hasOwn(axes, matrix[1]) || !Array.isArray(axis) || !axis.length) return "unknown";
|
|
17702
|
+
alternatives = axis;
|
|
17703
|
+
}
|
|
17704
|
+
if (matrix[2]) alternatives = alternatives.map((row) => record(row) && Object.hasOwn(row, matrix[2]) ? row[matrix[2]] : void 0);
|
|
17705
|
+
const costs = alternatives.map((value) => {
|
|
17706
|
+
if (typeof value === "string") {
|
|
17707
|
+
if (value.includes("${{")) return "unknown";
|
|
17708
|
+
if (approved(value) || !matrix[2] && allowance2 === "windows-latest" && value === allowance2) return "standard-hosted";
|
|
17709
|
+
return classifyRunnerCost(value);
|
|
17710
|
+
}
|
|
17711
|
+
if (!Array.isArray(value) || !value.length || !value.every((label) => typeof label === "string" && /^[\w-]+$/.test(label))) return "unknown";
|
|
17712
|
+
return classifyRunnerCost(JSON.stringify(value));
|
|
17713
|
+
});
|
|
17714
|
+
if (costs.includes("hosted")) return "hosted";
|
|
17715
|
+
if (costs.includes("unknown")) return "unknown";
|
|
17716
|
+
return costs.includes("standard-hosted") ? "standard-hosted" : "self-hosted";
|
|
17717
|
+
}
|
|
17718
|
+
|
|
17719
|
+
// ../shared/github-contents.ts
|
|
17720
|
+
function decodeGitHubContents(content) {
|
|
17721
|
+
return Buffer.from(content, "base64").toString("utf8");
|
|
17722
|
+
}
|
|
17723
|
+
function encodeGitHubContents(content) {
|
|
17724
|
+
return Buffer.from(content, "utf8").toString("base64");
|
|
17725
|
+
}
|
|
17726
|
+
|
|
17727
|
+
// ../shared/workflow-runner-source.ts
|
|
17728
|
+
var WORKFLOW_PATH = /^\.github\/workflows\/[\w.-]+\.ya?ml$/;
|
|
17729
|
+
var REPO = /^[A-Za-z0-9][A-Za-z0-9-]*\/[A-Za-z0-9_][\w.-]*$/;
|
|
17730
|
+
var REF = /^[\w./-]+$/;
|
|
17731
|
+
var MAX_FILES = 64;
|
|
17732
|
+
var MAX_DEPTH = 8;
|
|
17733
|
+
var WorkflowSourceError = class extends Error {
|
|
17734
|
+
/** Value-free category (safeReadCategory vocabulary) — routes/logs carry it, never a location or body. */
|
|
17735
|
+
category;
|
|
17736
|
+
constructor(location, category) {
|
|
17737
|
+
super(`${location}: ${category}`);
|
|
17738
|
+
this.category = category;
|
|
17739
|
+
}
|
|
17740
|
+
};
|
|
17741
|
+
function safeReadCategory(error) {
|
|
17742
|
+
const e = error;
|
|
17743
|
+
if (e?.rateLimited === true) return "rate-limited";
|
|
17744
|
+
if (typeof e?.status === "number" && Number.isInteger(e.status) && e.status >= 400 && e.status <= 599) return `HTTP ${e.status}`;
|
|
17745
|
+
if (e?.name === "TimeoutError" || e?.name === "AbortError" || e?.code === "ETIMEDOUT") return "read-timeout";
|
|
17746
|
+
if (e?.code === "ENOENT") return "reader-unavailable";
|
|
17747
|
+
if (e?.code === "ENOBUFS" || e?.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") return "reader-output-limit";
|
|
17748
|
+
if (error instanceof SyntaxError) return "invalid-json";
|
|
17749
|
+
return "read-failed";
|
|
17750
|
+
}
|
|
17751
|
+
function locationOf(file) {
|
|
17752
|
+
return `${file.repo}/${file.path}${file.ref ? `@${file.ref}` : ""}`;
|
|
17753
|
+
}
|
|
17754
|
+
function contentsPath(file) {
|
|
17755
|
+
if (!REPO.test(file.repo) || !(WORKFLOW_PATH.test(file.path) || file.path === ".github/workflows") || file.ref !== void 0 && !REF.test(file.ref)) {
|
|
17756
|
+
throw new WorkflowSourceError("workflow source", "invalid-location");
|
|
17757
|
+
}
|
|
17758
|
+
return `repos/${file.repo}/contents/${file.path}${file.ref ? `?ref=${encodeURIComponent(file.ref)}` : ""}`;
|
|
17759
|
+
}
|
|
17760
|
+
async function readSource(read, file) {
|
|
17761
|
+
const endpoint = contentsPath(file);
|
|
17762
|
+
try {
|
|
17763
|
+
return await read(endpoint);
|
|
17764
|
+
} catch (e) {
|
|
17765
|
+
throw new WorkflowSourceError(locationOf(file), safeReadCategory(e));
|
|
17766
|
+
}
|
|
17767
|
+
}
|
|
17768
|
+
async function readBody(read, file) {
|
|
17769
|
+
const value = await readSource(read, file);
|
|
17770
|
+
if (!value || value.encoding !== "base64" || typeof value.content !== "string" || !value.content.trim() || value.content.length > 15e5 || !/^[A-Za-z0-9+/=\r\n]+$/.test(value.content)) {
|
|
17771
|
+
throw new WorkflowSourceError(locationOf(file), "invalid-content");
|
|
17772
|
+
}
|
|
17773
|
+
return { ...file, body: decodeGitHubContents(value.content) };
|
|
17774
|
+
}
|
|
17775
|
+
function resolveCallee(caller, uses) {
|
|
17776
|
+
if (uses.startsWith("./") && WORKFLOW_PATH.test(uses.slice(2))) return { repo: caller.repo, path: uses.slice(2), ref: caller.ref };
|
|
17777
|
+
const remote = /^([\w.-]+\/[\w.-]+)\/(\.github\/workflows\/[\w.-]+\.ya?ml)@([\w./-]+)$/.exec(uses);
|
|
17778
|
+
return remote ? { repo: remote[1], path: remote[2], ref: remote[3] } : void 0;
|
|
17779
|
+
}
|
|
17780
|
+
async function readRunnerWorkflowClosure(parse2, read, repo, ref, roots, bounds) {
|
|
17781
|
+
let totalBodyBytes = 0;
|
|
17782
|
+
const readBounded = async (file) => {
|
|
17783
|
+
const child2 = await readBody(read, file);
|
|
17784
|
+
totalBodyBytes += Buffer.byteLength(child2.body, "utf8");
|
|
17785
|
+
if (bounds?.maxTotalBodyBytes !== void 0 && totalBodyBytes > bounds.maxTotalBodyBytes) {
|
|
17786
|
+
throw new WorkflowSourceError(locationOf(child2), "source-read-limit");
|
|
17787
|
+
}
|
|
17788
|
+
return child2;
|
|
17789
|
+
};
|
|
17790
|
+
let files;
|
|
17791
|
+
if (roots) {
|
|
17792
|
+
files = roots.map((file) => ({ ...file, repo, ref }));
|
|
17793
|
+
} else {
|
|
17794
|
+
const listing = await readSource(read, { repo, ref, path: ".github/workflows" });
|
|
17795
|
+
if (!Array.isArray(listing)) throw new WorkflowSourceError(`${repo}/.github/workflows`, "invalid-listing");
|
|
17796
|
+
const paths = listing.filter((f) => f?.type === "file" && /\.ya?ml$/i.test(f?.path ?? "")).map((f) => f.path);
|
|
17797
|
+
if (!paths.length || paths.length > MAX_FILES) throw new WorkflowSourceError(`${repo}/.github/workflows`, "empty-or-oversized-listing");
|
|
17798
|
+
files = [];
|
|
17799
|
+
for (const path2 of paths) files.push(await readBounded({ repo, ref, path: path2 }));
|
|
17800
|
+
}
|
|
17801
|
+
if (!files.length || files.length > MAX_FILES) throw new WorkflowSourceError("workflow source", "empty-or-oversized-listing");
|
|
17802
|
+
for (const file of files) contentsPath(file);
|
|
17803
|
+
const byLocation = new Map(files.map((file) => [locationOf(file), file]));
|
|
17804
|
+
const active = /* @__PURE__ */ new Set();
|
|
17805
|
+
const completed = /* @__PURE__ */ new Set();
|
|
17806
|
+
async function visit2(file, depth) {
|
|
17807
|
+
const key = locationOf(file);
|
|
17808
|
+
if (active.has(key)) throw new WorkflowSourceError(key, "reusable-cycle");
|
|
17809
|
+
if (depth > MAX_DEPTH) throw new WorkflowSourceError(key, "reusable-depth-limit");
|
|
17810
|
+
if (completed.has(key)) return;
|
|
17811
|
+
active.add(key);
|
|
17812
|
+
const jobs = parse2(file.body);
|
|
17813
|
+
if (!jobs) throw new WorkflowSourceError(key, "invalid-workflow-yaml");
|
|
17814
|
+
for (const job of jobs.values()) {
|
|
17815
|
+
if (!Object.hasOwn(job, "uses")) continue;
|
|
17816
|
+
const uses = job.uses;
|
|
17817
|
+
if (typeof uses !== "string" || Object.hasOwn(job, "runs-on")) throw new WorkflowSourceError(key, "unresolved-reusable-ref");
|
|
17818
|
+
const target = resolveCallee(file, uses);
|
|
17819
|
+
if (!target) throw new WorkflowSourceError(key, "unresolved-reusable-ref");
|
|
17820
|
+
if (bounds?.calleeAllowed && !bounds.calleeAllowed(target)) throw new WorkflowSourceError(key, "unauthorized-callee");
|
|
17821
|
+
const targetKey = locationOf(target);
|
|
17822
|
+
let child2 = byLocation.get(targetKey);
|
|
17823
|
+
if (!child2) {
|
|
17824
|
+
if (byLocation.size >= MAX_FILES) throw new WorkflowSourceError(key, "workflow-read-limit");
|
|
17825
|
+
child2 = await readBounded(target);
|
|
17826
|
+
byLocation.set(targetKey, child2);
|
|
17827
|
+
}
|
|
17828
|
+
await visit2(child2, depth + 1);
|
|
17829
|
+
}
|
|
17830
|
+
active.delete(key);
|
|
17831
|
+
completed.add(key);
|
|
17832
|
+
}
|
|
17833
|
+
for (const file of files) await visit2(file, 0);
|
|
17834
|
+
return [...byLocation.values()];
|
|
17835
|
+
}
|
|
17836
|
+
function classifyRunnerWorkflows(parse2, files) {
|
|
17837
|
+
const known = new Map(files.map((file) => [locationOf(file), file]));
|
|
17838
|
+
const parsed = new Map(files.map((file) => [locationOf(file), parse2(file.body)]));
|
|
17839
|
+
const resolvedCache = /* @__PURE__ */ new Map();
|
|
17840
|
+
function resolved(file, active) {
|
|
17841
|
+
const key = locationOf(file);
|
|
17842
|
+
if (active.has(key)) return false;
|
|
17843
|
+
if (resolvedCache.has(key)) return resolvedCache.get(key);
|
|
17844
|
+
const next = new Set(active).add(key);
|
|
17845
|
+
const jobs = parsed.get(key);
|
|
17846
|
+
const result = Boolean(jobs && [...jobs.values()].every((job) => {
|
|
17847
|
+
if (!Object.hasOwn(job, "uses")) return true;
|
|
17848
|
+
const uses = job.uses;
|
|
17849
|
+
if (typeof uses !== "string" || Object.hasOwn(job, "runs-on")) return false;
|
|
17850
|
+
const target = resolveCallee(file, uses);
|
|
17851
|
+
const child2 = target && known.get(locationOf(target));
|
|
17852
|
+
return Boolean(child2 && resolved(child2, next));
|
|
17853
|
+
}));
|
|
17854
|
+
resolvedCache.set(key, result);
|
|
17855
|
+
return result;
|
|
17856
|
+
}
|
|
17857
|
+
return files.flatMap((file) => {
|
|
17858
|
+
const jobs = parsed.get(locationOf(file));
|
|
17859
|
+
if (!jobs) return [{ repo: file.repo, path: file.path, job: "(invalid YAML)", cost: "unknown", callee: false }];
|
|
17860
|
+
return [...jobs].flatMap(([id, job]) => {
|
|
17861
|
+
const hasCallee = Object.hasOwn(job, "uses");
|
|
17862
|
+
const uses = hasCallee && typeof job.uses === "string" && !Object.hasOwn(job, "runs-on") ? job.uses : void 0;
|
|
17863
|
+
const target = uses && resolveCallee(file, uses);
|
|
17864
|
+
if (target && known.has(locationOf(target)) && resolved(file, /* @__PURE__ */ new Set())) return [];
|
|
17865
|
+
const cost = hasCallee ? "unknown" : classifyJobRunnerCost(job, standardHostedLabelFor(file.repo, file.path, id), { repo: file.repo, jobs, blessedSha: BLESSED_RUNNER_CHOICE_SHA });
|
|
17866
|
+
return [{ repo: file.repo, path: file.path, job: id, cost, callee: hasCallee }];
|
|
17867
|
+
});
|
|
17868
|
+
});
|
|
17869
|
+
}
|
|
17870
|
+
function runnerWorkflowFromContents(value) {
|
|
17871
|
+
const file = value;
|
|
17872
|
+
if (!file || typeof file.repo !== "string" || typeof file.path !== "string" || file.ref !== void 0 && typeof file.ref !== "string") {
|
|
17873
|
+
throw new WorkflowSourceError("workflow source", "invalid-content");
|
|
17874
|
+
}
|
|
17875
|
+
if (!REPO.test(file.repo) || !WORKFLOW_PATH.test(file.path) || file.ref !== void 0 && !REF.test(file.ref)) {
|
|
17876
|
+
throw new WorkflowSourceError("workflow source", "invalid-content");
|
|
17877
|
+
}
|
|
17878
|
+
if (file.encoding !== "base64" || typeof file.content !== "string" || !file.content.trim() || file.content.length > 15e5 || !/^[A-Za-z0-9+/=\r\n]+$/.test(file.content)) {
|
|
17879
|
+
throw new WorkflowSourceError("workflow source", "invalid-content");
|
|
17880
|
+
}
|
|
17881
|
+
const body = decodeGitHubContents(file.content);
|
|
17882
|
+
if (!body.trim()) throw new WorkflowSourceError("workflow source", "invalid-content");
|
|
17883
|
+
return { repo: file.repo, path: file.path, ref: file.ref, body };
|
|
17884
|
+
}
|
|
17885
|
+
|
|
17610
17886
|
// ../shared/schedule-inventory.ts
|
|
17611
17887
|
function classifyLlmValue(raw) {
|
|
17612
17888
|
const value = raw.trim().toLowerCase();
|
|
@@ -18669,6 +18945,23 @@ async function tenantDeploy(payload, deps) {
|
|
|
18669
18945
|
async function releaseAnnouncement(payload, deps) {
|
|
18670
18946
|
return postJson("/release-announcement", payload, deps, "POST", { noRetry: true, timeoutMs: 3e4 });
|
|
18671
18947
|
}
|
|
18948
|
+
async function trainWorkflowSourceClosure(repo, deps) {
|
|
18949
|
+
const verdict = await fetchTrainAuthority(repo, deps);
|
|
18950
|
+
if (!verdict.ok) throw new WorkflowSourceError("workflow source", "authority-unverified");
|
|
18951
|
+
const delegatedTrain = verdict.authority.train === true && verdict.authority.authenticatedDelegation === true;
|
|
18952
|
+
if (!delegatedTrain || repo.toLowerCase() === HUB_RELEASE_REPO.toLowerCase()) return void 0;
|
|
18953
|
+
const SAFE_CHECK = /^[A-Za-z0-9 -]{1,64}$/;
|
|
18954
|
+
const res = await postJson("/train-workflow-source", { repo }, deps, "POST", { noRetry: true, timeoutMs: 3e4 });
|
|
18955
|
+
if (res.status === 422) {
|
|
18956
|
+
const body2 = res.body;
|
|
18957
|
+
const check = typeof body2?.check === "string" && SAFE_CHECK.test(body2.check) ? body2.check : "read-failed";
|
|
18958
|
+
throw new WorkflowSourceError("workflow source", check);
|
|
18959
|
+
}
|
|
18960
|
+
if (!res.ok || res.status !== 200) throw new WorkflowSourceError("workflow source", "source-endpoint-unavailable");
|
|
18961
|
+
const body = res.body;
|
|
18962
|
+
if (!body || !Array.isArray(body.files)) throw new WorkflowSourceError("workflow source", "invalid-response");
|
|
18963
|
+
return body.files.map(runnerWorkflowFromContents);
|
|
18964
|
+
}
|
|
18672
18965
|
async function actionsCanary(payload, deps) {
|
|
18673
18966
|
return postJson("/actions-canary", payload, deps, "POST", { noRetry: true });
|
|
18674
18967
|
}
|
|
@@ -24389,14 +24682,6 @@ async function archiveClosedDoneBoardItems(options, deps = {}) {
|
|
|
24389
24682
|
return result;
|
|
24390
24683
|
}
|
|
24391
24684
|
|
|
24392
|
-
// src/github-contents.ts
|
|
24393
|
-
function decodeGitHubContents(content) {
|
|
24394
|
-
return Buffer.from(content, "base64").toString("utf8");
|
|
24395
|
-
}
|
|
24396
|
-
function encodeGitHubContents(content) {
|
|
24397
|
-
return Buffer.from(content, "utf8").toString("base64");
|
|
24398
|
-
}
|
|
24399
|
-
|
|
24400
24685
|
// src/bootstrap-ruleset.ts
|
|
24401
24686
|
var PRODUCT_RULESET_NAME = "mmi-product-required-checks";
|
|
24402
24687
|
var PRODUCT_PENDING_RULESET_PREFIX = "mmi-product-required-checks-pending-";
|
|
@@ -25538,7 +25823,6 @@ var BLESSED_RUNNER_CHECK_POLICY_SHA = "67bb2012710a96d21be825c29a5d5c2b4b8001e2"
|
|
|
25538
25823
|
var BLESSED_KIT_AGENT_FILES_SHA = "e976df208d891b215844386c1cac0f161946b6fa";
|
|
25539
25824
|
var BLESSED_JEV_FAILURES_SHA = "d5278c6133318b33727483ddfdfddc8f841e3ef6";
|
|
25540
25825
|
var BLESSED_VERDICT_RECEIPT_SHA = "";
|
|
25541
|
-
var BLESSED_RUNNER_CHOICE_SHA = "cf35db6cdf053746bc5b2cd75f241a7d93cf7f48";
|
|
25542
25826
|
var BLESSED_RUNBOOK_GUARD_SHA = "e661e0aedc0e58b73c738bf6cdfeac65e5914584";
|
|
25543
25827
|
var REMOTE_USE = /^mutmutco\/MMI-Hub\/\.github\/actions\/run-with-budget@(\S+)$/;
|
|
25544
25828
|
var LOCAL_USE = "./.github/actions/run-with-budget";
|
|
@@ -25705,9 +25989,9 @@ function checkGateBudget(files, opts = {}) {
|
|
|
25705
25989
|
if (!rawMs) {
|
|
25706
25990
|
failures.push(`${path2}: run-with-budget step has no max-seconds input \u2014 the budget step guards nothing`);
|
|
25707
25991
|
} else {
|
|
25708
|
-
const
|
|
25709
|
-
const seconds =
|
|
25710
|
-
if (!
|
|
25992
|
+
const literal = /^['"]?(\d+)['"]?$/.exec(rawMs);
|
|
25993
|
+
const seconds = literal ? Number(literal[1]) : NaN;
|
|
25994
|
+
if (!literal || seconds < 1) {
|
|
25711
25995
|
failures.push(`${path2}: max-seconds '${rawMs}' is not a literal positive integer`);
|
|
25712
25996
|
} else if (seconds > MAX_SECONDS_CEILING) {
|
|
25713
25997
|
failures.push(`${path2}: max-seconds ${seconds} exceeds the org ceiling ${MAX_SECONDS_CEILING} \u2014 a budget that generous guards nothing; measure the gate and set a real ceiling`);
|
|
@@ -27805,10 +28089,10 @@ var rollout_plan_default = {
|
|
|
27805
28089
|
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)."
|
|
27806
28090
|
},
|
|
27807
28091
|
baseline: {
|
|
27808
|
-
version: "4.6.
|
|
27809
|
-
tag: "v4.6.
|
|
27810
|
-
commit: "
|
|
27811
|
-
npm: "@mutmutco/cli@4.6.
|
|
28092
|
+
version: "4.6.19",
|
|
28093
|
+
tag: "v4.6.19",
|
|
28094
|
+
commit: "4dce08ee7823",
|
|
28095
|
+
npm: "@mutmutco/cli@4.6.19"
|
|
27812
28096
|
},
|
|
27813
28097
|
exitCriterion: "fleet-n-of-n",
|
|
27814
28098
|
hubOnlyShortcut: "forbidden",
|
|
@@ -27825,14 +28109,14 @@ var rollout_plan_default = {
|
|
|
27825
28109
|
repo: "mutmutco/mmi-hub",
|
|
27826
28110
|
role: "canary",
|
|
27827
28111
|
schedule: "train",
|
|
27828
|
-
v3Target: "v4.6.
|
|
28112
|
+
v3Target: "v4.6.19"
|
|
27829
28113
|
}
|
|
27830
28114
|
],
|
|
27831
28115
|
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.",
|
|
27832
28116
|
rollback: {
|
|
27833
28117
|
independent: true,
|
|
27834
|
-
mechanism: "npm dist-tag latest -> 4.6.
|
|
27835
|
-
v3Target: "v4.6.
|
|
28118
|
+
mechanism: "npm dist-tag latest -> 4.6.19 and redeploy the Hub Lambda from tag v4.6.19 (4dce08ee7823); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
28119
|
+
v3Target: "v4.6.19 (@mutmutco/cli@4.6.19, tag commit 4dce08ee7823 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
27836
28120
|
}
|
|
27837
28121
|
},
|
|
27838
28122
|
{
|
|
@@ -32387,290 +32671,100 @@ var import_node_fs31 = require("node:fs");
|
|
|
32387
32671
|
var import_promises4 = require("node:fs/promises");
|
|
32388
32672
|
var import_node_path28 = require("node:path");
|
|
32389
32673
|
|
|
32390
|
-
// ../shared/runner-fallback.ts
|
|
32391
|
-
var HOSTED_RUNNER_LABEL_RE = /\b(?:ubuntu|windows|macos)-(?:latest|\d[\w.-]*)\b/i;
|
|
32392
|
-
var STANDARD_HOSTED_LABEL_RE = /^(?:ubuntu-(?:latest|24\.04|22\.04|24\.04-arm|22\.04-arm)|windows-(?:latest|2025|2022|11-arm)|macos-(?:latest|\d+|\d+-intel))$/i;
|
|
32393
|
-
function classifyRunnerCost(selector) {
|
|
32394
|
-
const raw = selector.replace(/\s+#.*$/gm, "").trim();
|
|
32395
|
-
if (STANDARD_HOSTED_LABEL_RE.test(raw.replace(/^(['"])(.*)\1$/, "$2"))) return "standard-hosted";
|
|
32396
|
-
if (HOSTED_RUNNER_LABEL_RE.test(raw)) return "hosted";
|
|
32397
|
-
const json = /^\$\{\{\s*fromJSON\('([^']+)'\)\s*\}\}$/.exec(raw);
|
|
32398
|
-
if (json) {
|
|
32399
|
-
try {
|
|
32400
|
-
const labels2 = JSON.parse(json[1]);
|
|
32401
|
-
return Array.isArray(labels2) && labels2.length > 0 && labels2.every((l) => typeof l === "string" && /^[\w-]+$/.test(l)) && labels2.some((l) => l.toLowerCase() === "self-hosted") ? "self-hosted" : "unknown";
|
|
32402
|
-
} catch {
|
|
32403
|
-
return "unknown";
|
|
32404
|
-
}
|
|
32405
|
-
}
|
|
32406
|
-
if (raw.includes("${{")) return "unknown";
|
|
32407
|
-
const labels = /^\[[^\[\]]+\]$/.test(raw) ? raw.slice(1, -1).split(",").map((s) => s.trim().replace(/^(['"])(.*)\1$/, "$2")) : /^(?:-\s*[^\n]+\s*)+$/.test(raw) ? raw.split("\n").map((s) => s.trim().replace(/^-\s*/, "").replace(/^(['"])(.*)\1$/, "$2")) : [raw.replace(/^(['"])(.*)\1$/, "$2")];
|
|
32408
|
-
return labels.every((l) => /^[\w-]+$/.test(l)) && labels.some((l) => l.toLowerCase() === "self-hosted") ? "self-hosted" : "unknown";
|
|
32409
|
-
}
|
|
32410
|
-
var STANDARD_HOSTED_ALLOWLIST = [
|
|
32411
|
-
["mutmutco/MMI-Hub", ".github/workflows/launcher-build.yml", "build", "windows-latest"],
|
|
32412
|
-
["mutmutco/Jerv-JervCode", ".github/workflows/release-archives.yml", "release-platform-arm64", "windows-11-arm"]
|
|
32413
|
-
];
|
|
32414
|
-
var HOSTED_SPILL_RE = /^\$\{\{\s*(?:matrix\.[\w-]+\s*==\s*'[\w-]+'\s*&&\s*(?:'(?<leg>[^']+)'|fromJSON\(needs\.(?<legDecider>[\w-]+)\.outputs\.(?<legOutput>\w+_runs_on)\s*\|\|\s*(?<legOwned>'[^']+')\))\s*\|\|\s*)?fromJSON\(needs\.(?<decider>[\w-]+)\.outputs\.(?<output>\w+_runs_on)\s*\|\|\s*(?<owned>'[^']+')\)\s*\}\}$/;
|
|
32415
|
-
var RUNNER_CHOICE_ACTION = "mutmutco/MMI-Hub/.github/actions/runner-choice";
|
|
32416
|
-
function runnerChoiceProduces(job, decider, output, source) {
|
|
32417
|
-
const needs = job.needs;
|
|
32418
|
-
if (!(needs === decider || Array.isArray(needs) && needs.includes(decider))) return false;
|
|
32419
|
-
const producer = source.jobs.get(decider);
|
|
32420
|
-
if (!record(producer) || !record(producer.outputs) || !Array.isArray(producer.steps)) return false;
|
|
32421
|
-
const step = /^\$\{\{\s*steps\.([\w-]+)\.outputs\.runs-on\s*\}\}$/.exec(String(producer.outputs[output] ?? ""))?.[1];
|
|
32422
|
-
const steps = producer.steps.filter((s) => record(s) && s.id === step);
|
|
32423
|
-
if (steps.length !== 1 || !record(steps[0])) return false;
|
|
32424
|
-
const { uses } = steps[0], inputs = steps[0].with ?? {};
|
|
32425
|
-
const pinned = /^[0-9a-f]{40}$/.test(source.blessedSha) && uses === `${RUNNER_CHOICE_ACTION}@${source.blessedSha}`;
|
|
32426
|
-
if (!pinned && !(source.repo === "mutmutco/MMI-Hub" && uses === "./.github/actions/runner-choice")) return false;
|
|
32427
|
-
const hosted = record(inputs) ? inputs.hosted ?? "ubuntu-latest" : void 0;
|
|
32428
|
-
return typeof hosted === "string" && STANDARD_HOSTED_LABEL_RE.test(hosted);
|
|
32429
|
-
}
|
|
32430
|
-
function standardHostedLabelFor(repo, path2, jobId) {
|
|
32431
|
-
if (repo === "mutmutco/Jerv-Hub") return ["ubuntu-latest", "windows-latest", "macos-14"];
|
|
32432
|
-
return STANDARD_HOSTED_ALLOWLIST.find(([r, p, j]) => r === repo && p === path2 && j === jobId)?.[3];
|
|
32433
|
-
}
|
|
32434
|
-
var record = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
32435
|
-
function classifyJobRunnerCost(job, allowance2, source) {
|
|
32436
|
-
if (!record(job) || !Object.hasOwn(job, "runs-on")) return "unknown";
|
|
32437
|
-
const selector = job["runs-on"];
|
|
32438
|
-
const raw = typeof selector === "string" ? selector.trim() : Array.isArray(selector) && selector.length && selector.every((label) => typeof label === "string") ? JSON.stringify(selector) : "";
|
|
32439
|
-
if (!raw) return "unknown";
|
|
32440
|
-
const approved = (value) => Array.isArray(allowance2) && allowance2.includes(value);
|
|
32441
|
-
if (approved(raw) || allowance2 === "windows-11-arm" && raw === allowance2) return "standard-hosted";
|
|
32442
|
-
const fallback = /^\$\{\{\s*vars\.MMI_RUNNER_FALLBACK\s*==\s*'hosted'\s*&&\s*'([^']+)'\s*\|\|\s*(fromJSON\('[^']+'\))\s*\}\}$/.exec(raw);
|
|
32443
|
-
if (fallback && approved(fallback[1]) && classifyRunnerCost("${{ " + fallback[2] + " }}") === "self-hosted") return "standard-hosted";
|
|
32444
|
-
const layerZero = /^\$\{\{\s*vars\.[\w-]+\s*==\s*'off'\s*&&\s*(fromJSON\('[^']+'\))\s*\|\|\s*'([^']+)'\s*\}\}$/.exec(raw);
|
|
32445
|
-
if (layerZero && classifyRunnerCost("${{ " + layerZero[1] + " }}") === "self-hosted" && STANDARD_HOSTED_LABEL_RE.test(layerZero[2])) return "standard-hosted";
|
|
32446
|
-
const spill = HOSTED_SPILL_RE.exec(raw)?.groups;
|
|
32447
|
-
const legOk = (g) => g.legDecider ? classifyRunnerCost(`\${{ fromJSON(${g.legOwned}) }}`) === "self-hosted" && Boolean(source) && runnerChoiceProduces(job, g.legDecider, g.legOutput, source) : !g.leg || STANDARD_HOSTED_LABEL_RE.test(g.leg);
|
|
32448
|
-
if (spill && source && legOk(spill) && classifyRunnerCost(`\${{ fromJSON(${spill.owned}) }}`) === "self-hosted" && runnerChoiceProduces(job, spill.decider, spill.output, source)) return "standard-hosted";
|
|
32449
|
-
const direct = classifyRunnerCost(raw);
|
|
32450
|
-
const matrix = /^\$\{\{\s*matrix\.([\w-]+)(?:\.([\w-]+))?\s*\}\}$/.exec(raw);
|
|
32451
|
-
if (!matrix || direct === "hosted") return direct;
|
|
32452
|
-
if (!Object.hasOwn(job, "strategy") || !record(job.strategy) || !Object.hasOwn(job.strategy, "matrix") || !record(job.strategy.matrix)) return "unknown";
|
|
32453
|
-
const axes = job.strategy.matrix;
|
|
32454
|
-
if (Object.hasOwn(axes, "exclude")) return "unknown";
|
|
32455
|
-
let alternatives;
|
|
32456
|
-
if (Object.hasOwn(axes, "include")) {
|
|
32457
|
-
if (Object.keys(axes).length !== 1 || !Array.isArray(axes.include) || !axes.include.length) return "unknown";
|
|
32458
|
-
alternatives = axes.include.map((row) => record(row) && Object.hasOwn(row, matrix[1]) ? row[matrix[1]] : void 0);
|
|
32459
|
-
} else {
|
|
32460
|
-
const axis = axes[matrix[1]];
|
|
32461
|
-
if (!Object.hasOwn(axes, matrix[1]) || !Array.isArray(axis) || !axis.length) return "unknown";
|
|
32462
|
-
alternatives = axis;
|
|
32463
|
-
}
|
|
32464
|
-
if (matrix[2]) alternatives = alternatives.map((row) => record(row) && Object.hasOwn(row, matrix[2]) ? row[matrix[2]] : void 0);
|
|
32465
|
-
const costs = alternatives.map((value) => {
|
|
32466
|
-
if (typeof value === "string") {
|
|
32467
|
-
if (value.includes("${{")) return "unknown";
|
|
32468
|
-
if (approved(value) || !matrix[2] && allowance2 === "windows-latest" && value === allowance2) return "standard-hosted";
|
|
32469
|
-
return classifyRunnerCost(value);
|
|
32470
|
-
}
|
|
32471
|
-
if (!Array.isArray(value) || !value.length || !value.every((label) => typeof label === "string" && /^[\w-]+$/.test(label))) return "unknown";
|
|
32472
|
-
return classifyRunnerCost(JSON.stringify(value));
|
|
32473
|
-
});
|
|
32474
|
-
if (costs.includes("hosted")) return "hosted";
|
|
32475
|
-
if (costs.includes("unknown")) return "unknown";
|
|
32476
|
-
return costs.includes("standard-hosted") ? "standard-hosted" : "self-hosted";
|
|
32477
|
-
}
|
|
32478
|
-
|
|
32479
32674
|
// src/workflow-runner-yaml.ts
|
|
32480
32675
|
var import_yaml = __toESM(require_dist(), 1);
|
|
32481
|
-
|
|
32482
|
-
|
|
32483
|
-
function
|
|
32484
|
-
|
|
32485
|
-
|
|
32486
|
-
|
|
32487
|
-
if (
|
|
32488
|
-
|
|
32489
|
-
|
|
32490
|
-
|
|
32491
|
-
|
|
32492
|
-
|
|
32493
|
-
|
|
32494
|
-
|
|
32495
|
-
|
|
32496
|
-
|
|
32497
|
-
const jobs = doc.contents.get("jobs", true);
|
|
32498
|
-
if (!literalMap(jobs) || !jobs.items.length) return void 0;
|
|
32499
|
-
const result = /* @__PURE__ */ new Map();
|
|
32500
|
-
for (const { key, value: job } of jobs.items) {
|
|
32501
|
-
if (!(0, import_yaml.isScalar)(key) || typeof key.value !== "string" || !literalMap(job)) return void 0;
|
|
32502
|
-
const projected = /* @__PURE__ */ Object.create(null);
|
|
32503
|
-
for (const field of ["runs-on", "uses", "strategy", "needs", "outputs", "steps"]) {
|
|
32504
|
-
if (!job.has(field)) continue;
|
|
32505
|
-
const optional = field === "needs" || field === "outputs" || field === "steps";
|
|
32506
|
-
const value = job.get(field, true);
|
|
32507
|
-
if (value !== null && !(0, import_yaml.isNode)(value)) {
|
|
32508
|
-
if (optional) continue;
|
|
32509
|
-
return void 0;
|
|
32676
|
+
|
|
32677
|
+
// ../shared/workflow-runner-yaml-core.ts
|
|
32678
|
+
function createParseRunnerJobs(ops) {
|
|
32679
|
+
const literal = (node) => !ops.isAlias(node) && !(ops.isNode(node) && (node.tag || "anchor" in node && node.anchor));
|
|
32680
|
+
const literalMap = (node) => ops.isMap(node) && literal(node) && node.items.every(({ key }) => ops.isScalar(key) && literal(key) && typeof key.value === "string" && key.value !== "<<");
|
|
32681
|
+
return (source) => {
|
|
32682
|
+
if (!source || source.length > 15e5) return void 0;
|
|
32683
|
+
try {
|
|
32684
|
+
const doc = ops.parseDocument(source, { uniqueKeys: true, stringKeys: true, merge: false, prettyErrors: false });
|
|
32685
|
+
if (doc.errors.length || doc.warnings.length) return void 0;
|
|
32686
|
+
let invalid = false;
|
|
32687
|
+
let nodes = 0;
|
|
32688
|
+
ops.visit(doc, (_key, _node, path2) => {
|
|
32689
|
+
if (++nodes > 1e5 || path2.length > 64) {
|
|
32690
|
+
invalid = true;
|
|
32691
|
+
return ops.BREAK;
|
|
32510
32692
|
}
|
|
32511
|
-
|
|
32512
|
-
|
|
32513
|
-
|
|
32514
|
-
|
|
32693
|
+
});
|
|
32694
|
+
if (invalid || !literalMap(doc.contents)) return void 0;
|
|
32695
|
+
const jobs = doc.contents.get("jobs", true);
|
|
32696
|
+
if (!literalMap(jobs) || !jobs.items.length) return void 0;
|
|
32697
|
+
const result = /* @__PURE__ */ new Map();
|
|
32698
|
+
for (const { key, value: job } of jobs.items) {
|
|
32699
|
+
if (!ops.isScalar(key) || typeof key.value !== "string" || !literalMap(job)) return void 0;
|
|
32700
|
+
const projected = /* @__PURE__ */ Object.create(null);
|
|
32701
|
+
for (const field of ["runs-on", "uses", "strategy", "needs", "outputs", "steps"]) {
|
|
32702
|
+
if (!job.has(field)) continue;
|
|
32703
|
+
const optional = field === "needs" || field === "outputs" || field === "steps";
|
|
32704
|
+
const value = job.get(field, true);
|
|
32705
|
+
if (value !== null && !ops.isNode(value)) {
|
|
32706
|
+
if (optional) continue;
|
|
32707
|
+
return void 0;
|
|
32515
32708
|
}
|
|
32516
|
-
|
|
32517
|
-
|
|
32518
|
-
|
|
32519
|
-
|
|
32709
|
+
ops.visit(value, (_key, node) => {
|
|
32710
|
+
if (!literal(node) || ops.isPair(node) && ops.isScalar(node.key) && node.key.value === "<<") {
|
|
32711
|
+
invalid = true;
|
|
32712
|
+
return ops.BREAK;
|
|
32713
|
+
}
|
|
32714
|
+
});
|
|
32715
|
+
if (invalid && optional) {
|
|
32716
|
+
invalid = false;
|
|
32717
|
+
continue;
|
|
32718
|
+
}
|
|
32719
|
+
if (invalid) return void 0;
|
|
32720
|
+
projected[field] = value === null ? null : value.toJS(doc, { maxAliasCount: 0 });
|
|
32520
32721
|
}
|
|
32521
|
-
|
|
32522
|
-
projected[field] = value === null ? null : value.toJS(doc, { maxAliasCount: 0 });
|
|
32722
|
+
result.set(key.value, projected);
|
|
32523
32723
|
}
|
|
32524
|
-
result
|
|
32724
|
+
return result;
|
|
32725
|
+
} catch {
|
|
32726
|
+
return void 0;
|
|
32525
32727
|
}
|
|
32526
|
-
|
|
32527
|
-
} catch {
|
|
32528
|
-
return void 0;
|
|
32529
|
-
}
|
|
32728
|
+
};
|
|
32530
32729
|
}
|
|
32531
32730
|
|
|
32731
|
+
// src/workflow-runner-yaml.ts
|
|
32732
|
+
var parseRunnerJobs = createParseRunnerJobs({ isAlias: import_yaml.isAlias, isMap: import_yaml.isMap, isNode: import_yaml.isNode, isPair: import_yaml.isPair, isScalar: import_yaml.isScalar, parseDocument: import_yaml.parseDocument, visit: import_yaml.visit, BREAK: import_yaml.visit.BREAK });
|
|
32733
|
+
|
|
32532
32734
|
// src/workflow-runner-source.ts
|
|
32533
|
-
|
|
32534
|
-
|
|
32535
|
-
var REF = /^[\w./-]+$/;
|
|
32536
|
-
var MAX_FILES = 64;
|
|
32537
|
-
var MAX_DEPTH = 8;
|
|
32538
|
-
var WorkflowSourceError = class extends Error {
|
|
32539
|
-
constructor(location, category) {
|
|
32540
|
-
super(`${location}: ${category}`);
|
|
32541
|
-
}
|
|
32542
|
-
};
|
|
32543
|
-
function safeReadCategory(error) {
|
|
32544
|
-
const e = error;
|
|
32545
|
-
if (e?.rateLimited === true) return "rate-limited";
|
|
32546
|
-
if (typeof e?.status === "number" && Number.isInteger(e.status) && e.status >= 400 && e.status <= 599) return `HTTP ${e.status}`;
|
|
32547
|
-
if (e?.name === "TimeoutError" || e?.name === "AbortError" || e?.code === "ETIMEDOUT") return "read-timeout";
|
|
32548
|
-
if (e?.code === "ENOENT") return "reader-unavailable";
|
|
32549
|
-
if (e?.code === "ENOBUFS" || e?.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") return "reader-output-limit";
|
|
32550
|
-
if (error instanceof SyntaxError) return "invalid-json";
|
|
32551
|
-
return "read-failed";
|
|
32552
|
-
}
|
|
32553
|
-
function locationOf(file) {
|
|
32554
|
-
return `${file.repo}/${file.path}${file.ref ? `@${file.ref}` : ""}`;
|
|
32555
|
-
}
|
|
32556
|
-
function contentsPath(file) {
|
|
32557
|
-
if (!REPO.test(file.repo) || !(WORKFLOW_PATH.test(file.path) || file.path === ".github/workflows") || file.ref !== void 0 && !REF.test(file.ref)) {
|
|
32558
|
-
throw new WorkflowSourceError("workflow source", "invalid-location");
|
|
32559
|
-
}
|
|
32560
|
-
return `repos/${file.repo}/contents/${file.path}${file.ref ? `?ref=${encodeURIComponent(file.ref)}` : ""}`;
|
|
32561
|
-
}
|
|
32562
|
-
async function readSource(read, file) {
|
|
32563
|
-
const endpoint = contentsPath(file);
|
|
32564
|
-
try {
|
|
32565
|
-
return await read(endpoint);
|
|
32566
|
-
} catch (e) {
|
|
32567
|
-
throw new WorkflowSourceError(locationOf(file), safeReadCategory(e));
|
|
32568
|
-
}
|
|
32735
|
+
function readRunnerWorkflowClosure2(read, repo, ref, roots, bounds) {
|
|
32736
|
+
return readRunnerWorkflowClosure(parseRunnerJobs, read, repo, ref, roots, bounds);
|
|
32569
32737
|
}
|
|
32570
|
-
|
|
32571
|
-
|
|
32572
|
-
if (!value || value.encoding !== "base64" || typeof value.content !== "string" || !value.content.trim() || value.content.length > 15e5 || !/^[A-Za-z0-9+/=\r\n]+$/.test(value.content)) {
|
|
32573
|
-
throw new WorkflowSourceError(locationOf(file), "invalid-content");
|
|
32574
|
-
}
|
|
32575
|
-
return { ...file, body: decodeGitHubContents(value.content) };
|
|
32576
|
-
}
|
|
32577
|
-
function resolveCallee(caller, uses) {
|
|
32578
|
-
if (uses.startsWith("./") && WORKFLOW_PATH.test(uses.slice(2))) return { repo: caller.repo, path: uses.slice(2), ref: caller.ref };
|
|
32579
|
-
const remote = /^([\w.-]+\/[\w.-]+)\/(\.github\/workflows\/[\w.-]+\.ya?ml)@([\w./-]+)$/.exec(uses);
|
|
32580
|
-
return remote ? { repo: remote[1], path: remote[2], ref: remote[3] } : void 0;
|
|
32581
|
-
}
|
|
32582
|
-
async function readRunnerWorkflowClosure(read, repo, ref, roots) {
|
|
32583
|
-
let files;
|
|
32584
|
-
if (roots) {
|
|
32585
|
-
files = roots.map((file) => ({ ...file, repo, ref }));
|
|
32586
|
-
} else {
|
|
32587
|
-
const listing = await readSource(read, { repo, ref, path: ".github/workflows" });
|
|
32588
|
-
if (!Array.isArray(listing)) throw new WorkflowSourceError(`${repo}/.github/workflows`, "invalid-listing");
|
|
32589
|
-
const paths = listing.filter((f) => f?.type === "file" && /\.ya?ml$/i.test(f?.path ?? "")).map((f) => f.path);
|
|
32590
|
-
if (!paths.length || paths.length > MAX_FILES) throw new WorkflowSourceError(`${repo}/.github/workflows`, "empty-or-oversized-listing");
|
|
32591
|
-
files = [];
|
|
32592
|
-
for (const path2 of paths) files.push(await readBody(read, { repo, ref, path: path2 }));
|
|
32593
|
-
}
|
|
32594
|
-
if (!files.length || files.length > MAX_FILES) throw new WorkflowSourceError("workflow source", "empty-or-oversized-listing");
|
|
32595
|
-
for (const file of files) contentsPath(file);
|
|
32596
|
-
const byLocation = new Map(files.map((file) => [locationOf(file), file]));
|
|
32597
|
-
const active = /* @__PURE__ */ new Set();
|
|
32598
|
-
const completed = /* @__PURE__ */ new Set();
|
|
32599
|
-
async function visit2(file, depth) {
|
|
32600
|
-
const key = locationOf(file);
|
|
32601
|
-
if (active.has(key)) throw new WorkflowSourceError(key, "reusable-cycle");
|
|
32602
|
-
if (depth > MAX_DEPTH) throw new WorkflowSourceError(key, "reusable-depth-limit");
|
|
32603
|
-
if (completed.has(key)) return;
|
|
32604
|
-
active.add(key);
|
|
32605
|
-
const jobs = parseRunnerJobs(file.body);
|
|
32606
|
-
if (!jobs) throw new WorkflowSourceError(key, "invalid-workflow-yaml");
|
|
32607
|
-
for (const job of jobs.values()) {
|
|
32608
|
-
if (!Object.hasOwn(job, "uses")) continue;
|
|
32609
|
-
const uses = job.uses;
|
|
32610
|
-
if (typeof uses !== "string" || Object.hasOwn(job, "runs-on")) throw new WorkflowSourceError(key, "unresolved-reusable-ref");
|
|
32611
|
-
const target = resolveCallee(file, uses);
|
|
32612
|
-
if (!target) throw new WorkflowSourceError(key, "unresolved-reusable-ref");
|
|
32613
|
-
const targetKey = locationOf(target);
|
|
32614
|
-
let child2 = byLocation.get(targetKey);
|
|
32615
|
-
if (!child2) {
|
|
32616
|
-
if (byLocation.size >= MAX_FILES) throw new WorkflowSourceError(key, "workflow-read-limit");
|
|
32617
|
-
child2 = await readBody(read, target);
|
|
32618
|
-
byLocation.set(targetKey, child2);
|
|
32619
|
-
}
|
|
32620
|
-
await visit2(child2, depth + 1);
|
|
32621
|
-
}
|
|
32622
|
-
active.delete(key);
|
|
32623
|
-
completed.add(key);
|
|
32624
|
-
}
|
|
32625
|
-
for (const file of files) await visit2(file, 0);
|
|
32626
|
-
return [...byLocation.values()];
|
|
32627
|
-
}
|
|
32628
|
-
function classifyRunnerWorkflows(files) {
|
|
32629
|
-
const known = new Map(files.map((file) => [locationOf(file), file]));
|
|
32630
|
-
const parsed = new Map(files.map((file) => [locationOf(file), parseRunnerJobs(file.body)]));
|
|
32631
|
-
const resolvedCache = /* @__PURE__ */ new Map();
|
|
32632
|
-
function resolved(file, active) {
|
|
32633
|
-
const key = locationOf(file);
|
|
32634
|
-
if (active.has(key)) return false;
|
|
32635
|
-
if (resolvedCache.has(key)) return resolvedCache.get(key);
|
|
32636
|
-
const next = new Set(active).add(key);
|
|
32637
|
-
const jobs = parsed.get(key);
|
|
32638
|
-
const result = Boolean(jobs && [...jobs.values()].every((job) => {
|
|
32639
|
-
if (!Object.hasOwn(job, "uses")) return true;
|
|
32640
|
-
const uses = job.uses;
|
|
32641
|
-
if (typeof uses !== "string" || Object.hasOwn(job, "runs-on")) return false;
|
|
32642
|
-
const target = resolveCallee(file, uses);
|
|
32643
|
-
const child2 = target && known.get(locationOf(target));
|
|
32644
|
-
return Boolean(child2 && resolved(child2, next));
|
|
32645
|
-
}));
|
|
32646
|
-
resolvedCache.set(key, result);
|
|
32647
|
-
return result;
|
|
32648
|
-
}
|
|
32649
|
-
return files.flatMap((file) => {
|
|
32650
|
-
const jobs = parsed.get(locationOf(file));
|
|
32651
|
-
if (!jobs) return [{ repo: file.repo, path: file.path, job: "(invalid YAML)", cost: "unknown", callee: false }];
|
|
32652
|
-
return [...jobs].flatMap(([id, job]) => {
|
|
32653
|
-
const hasCallee = Object.hasOwn(job, "uses");
|
|
32654
|
-
const uses = hasCallee && typeof job.uses === "string" && !Object.hasOwn(job, "runs-on") ? job.uses : void 0;
|
|
32655
|
-
const target = uses && resolveCallee(file, uses);
|
|
32656
|
-
if (target && known.has(locationOf(target)) && resolved(file, /* @__PURE__ */ new Set())) return [];
|
|
32657
|
-
const cost = hasCallee ? "unknown" : classifyJobRunnerCost(job, standardHostedLabelFor(file.repo, file.path, id), { repo: file.repo, jobs, blessedSha: BLESSED_RUNNER_CHOICE_SHA });
|
|
32658
|
-
return [{ repo: file.repo, path: file.path, job: id, cost, callee: hasCallee }];
|
|
32659
|
-
});
|
|
32660
|
-
});
|
|
32738
|
+
function classifyRunnerWorkflows2(files) {
|
|
32739
|
+
return classifyRunnerWorkflows(parseRunnerJobs, files);
|
|
32661
32740
|
}
|
|
32662
32741
|
|
|
32663
32742
|
// src/actions-billing-preflight.ts
|
|
32664
32743
|
async function assertActionsJobsCanStart(deps, targetRepo2) {
|
|
32665
|
-
const read = deps.readWorkflowContents ?? (async (path2) => JSON.parse(await deps.run("gh", ["api", path2])));
|
|
32666
32744
|
let files;
|
|
32667
32745
|
try {
|
|
32668
|
-
|
|
32746
|
+
const served = await deps.readTrainWorkflowClosure?.(targetRepo2);
|
|
32747
|
+
if (served && !served.some((f) => f.repo.toLowerCase() === targetRepo2.toLowerCase())) {
|
|
32748
|
+
throw new WorkflowSourceError("workflow source", "invalid-response");
|
|
32749
|
+
}
|
|
32750
|
+
if (served) {
|
|
32751
|
+
files = served;
|
|
32752
|
+
} else {
|
|
32753
|
+
const read = deps.readWorkflowContents ?? (async (path2) => JSON.parse(await deps.run("gh", ["api", path2])));
|
|
32754
|
+
try {
|
|
32755
|
+
files = await readRunnerWorkflowClosure2(read, targetRepo2);
|
|
32756
|
+
} catch (e) {
|
|
32757
|
+
const detail = e instanceof WorkflowSourceError ? e.message : "workflow source: read-failed";
|
|
32758
|
+
throw new Error("release refused (#8911): workflow source inventory unreadable; " + detail + ". Resolve this source read before tagging.");
|
|
32759
|
+
}
|
|
32760
|
+
}
|
|
32669
32761
|
} catch (e) {
|
|
32670
|
-
|
|
32671
|
-
|
|
32762
|
+
if (e instanceof WorkflowSourceError) {
|
|
32763
|
+
throw new Error("release refused (#8911): workflow source inventory unreadable; " + e.message + ". Resolve this source read before tagging.");
|
|
32764
|
+
}
|
|
32765
|
+
throw e;
|
|
32672
32766
|
}
|
|
32673
|
-
const unresolved =
|
|
32767
|
+
const unresolved = classifyRunnerWorkflows2(files).filter((f) => f.cost !== "self-hosted" && f.cost !== "standard-hosted");
|
|
32674
32768
|
if (unresolved.length) throw new Error("release refused (#8911): paid or unresolved runner path(s): " + unresolved.map((f) => f.repo + "/" + f.path + ":" + f.job).join(", ") + ". Standard included GitHub-hosted labels are allowed (#8977, Jerv-Hub#2459); only larger/paid runners and unresolved selectors/callees are refused. Resolve them before tagging; no paid capacity probe is permitted.");
|
|
32675
32769
|
process.stderr.write("actions preflight: " + targetRepo2 + " source selectors are self-hosted or standard included GitHub-hosted (#8977); no canary dispatch; the org spending limit guards over-allowance spend (unprovable from source); access/isolation/execution unproven (#8958)\n");
|
|
32676
32770
|
}
|
|
@@ -35904,18 +35998,23 @@ async function runTrainDoctor(input) {
|
|
|
35904
35998
|
}
|
|
35905
35999
|
add(healedBlock ? { code: "scratch-untracked", severity: "healed", source: "local", title: `wrote the org-managed .gitignore block for ${liveScratch.join(", ")} (#5409)`, remedy: "nothing \u2014 the live-session dirs are ignored from now on" } : { code: "scratch-untracked", severity: "warning", source: "local", title: `untracked live-session scratch: ${liveScratch.join(", ")}`, remedy: "run `mmi-cli devops train doctor --heal` (or `mmi-cli doctor --apply`) to write the org-managed .gitignore block (#5409); the train gate already exempts these (#5397)" });
|
|
35906
36000
|
}
|
|
35907
|
-
const
|
|
35908
|
-
const modifiedOnly = new Set(
|
|
36001
|
+
const blockingEntries = porcelainBlockingEntries(porcelain);
|
|
36002
|
+
const modifiedOnly = new Set(blockingEntries.filter((e) => e.status === " M").map((e) => e.path));
|
|
35909
36003
|
const churn = [];
|
|
35910
|
-
for (const path2 of
|
|
36004
|
+
for (const path2 of blockingEntries.map((e) => e.path).filter((p) => modifiedOnly.has(p))) {
|
|
35911
36005
|
try {
|
|
35912
36006
|
if (!clean2(await train.run("git", ["diff", "HEAD", "--numstat", "--", path2]))) churn.push(path2);
|
|
35913
36007
|
} catch {
|
|
35914
36008
|
}
|
|
35915
36009
|
}
|
|
35916
|
-
const dirty =
|
|
35917
|
-
|
|
35918
|
-
|
|
36010
|
+
const dirty = blockingEntries.filter((e) => !churn.includes(e.path));
|
|
36011
|
+
const dirtyTracked = dirty.filter((e) => e.status !== "??");
|
|
36012
|
+
const dirtyUntracked = dirty.filter((e) => e.status === "??");
|
|
36013
|
+
if (dirtyTracked.length) {
|
|
36014
|
+
add({ code: "tree-dirty-work", severity: "blocker", source: "local", title: `working tree carries tracked changes \u2014 blocking: ${dirtyTracked.map((e) => e.path).join(", ")}`, remedy: "commit or stash the work, then rerun; the train refuses a dirty tree before any mutation" });
|
|
36015
|
+
}
|
|
36016
|
+
if (dirtyUntracked.length) {
|
|
36017
|
+
add({ code: "tree-dirty-work", severity: "blocker", source: "local", title: `working tree carries untracked content \u2014 blocking: ${dirtyUntracked.map((e) => e.path).join(", ")}`, remedy: "inspect ownership first \u2014 another session's content stays untouched; commit intended new source through its owning lane; gitignore or move only verified own scratch to the gitignored repo-root tmp/, then rerun" });
|
|
35919
36018
|
}
|
|
35920
36019
|
if (churn.length) {
|
|
35921
36020
|
const restored = [];
|
|
@@ -39318,7 +39417,7 @@ var SHARED_ACTION_PINS = [
|
|
|
39318
39417
|
{ path: "npm-publish", floor: NPM_PUBLISH_SAFE_FLOOR_SHA }
|
|
39319
39418
|
];
|
|
39320
39419
|
function auditRunnerFallbacks(repo, files, closure) {
|
|
39321
|
-
return
|
|
39420
|
+
return classifyRunnerWorkflows2(closure ?? files.map((file) => ({ ...file, repo }))).map((f) => ({
|
|
39322
39421
|
ok: f.cost === "self-hosted" || f.cost === "standard-hosted",
|
|
39323
39422
|
label: f.cost === "standard-hosted" ? `runner uses approved standard-hosted source (#8912/#8958/#8977): ${f.repo}/${f.path}:${f.job}` : `runner is self-hosted only (#8911): ${f.repo}/${f.path}:${f.job}`,
|
|
39324
39423
|
detail: f.callee ? "unknown callee runner policy; source must be read at its referenced revision" : f.cost === "hosted" ? "larger/paid GitHub-hosted runner is forbidden; only standard included GitHub-hosted labels are allowed (#8977, Jerv-Hub#2459)" : f.cost === "unknown" ? "unknown runner selector; source unresolved" : f.cost === "standard-hosted" ? "standard GitHub-hosted labels within included Enterprise minutes only (#8977, Jerv-Hub#2459); the org spending limit guards over-allowance spend (unprovable from source); runner access and isolation not established" : "self-hosted selection only; runner access and isolation not established",
|
|
@@ -40381,7 +40480,7 @@ async function auditRepoCi(repo, callerDeps) {
|
|
|
40381
40480
|
const workflows = await getBranchWorkflows();
|
|
40382
40481
|
if (workflows) {
|
|
40383
40482
|
try {
|
|
40384
|
-
const closure = workflows.length ? await
|
|
40483
|
+
const closure = workflows.length ? await readRunnerWorkflowClosure2((path2) => callerDeps.client.rest("GET", path2), repo, baseBranch, workflows) : [];
|
|
40385
40484
|
checks.push(...auditRunnerFallbacks(repo, workflows, closure));
|
|
40386
40485
|
} catch (e) {
|
|
40387
40486
|
checks.push({ ok: false, label: "runner workflow source is readable (#8958)", detail: e instanceof WorkflowSourceError ? e.message : "workflow source: read-failed" });
|
|
@@ -54222,22 +54321,6 @@ async function deleteMergedRemoteBranch(options) {
|
|
|
54222
54321
|
// src/command-register-train-operations.ts
|
|
54223
54322
|
init_github_client();
|
|
54224
54323
|
|
|
54225
|
-
// src/hub-release-ci-authority.ts
|
|
54226
|
-
var HUB_RELEASE_REPO = "mutmutco/MMI-Hub";
|
|
54227
|
-
var HUB_RELEASE_WORKFLOW = ".github/workflows/hub-release.yml";
|
|
54228
|
-
var HUB_RELEASE_REF = "refs/heads/development";
|
|
54229
|
-
var WORKFLOW_REF = `${HUB_RELEASE_REPO}/${HUB_RELEASE_WORKFLOW}@${HUB_RELEASE_REF}`;
|
|
54230
|
-
var ID = /^[1-9][0-9]*$/;
|
|
54231
|
-
function hubReleaseCiRun(env) {
|
|
54232
|
-
if (env.MMI_HUB_RELEASE_CI !== "1" || env.GITHUB_ACTIONS !== "true") return null;
|
|
54233
|
-
if (env.GITHUB_REPOSITORY !== HUB_RELEASE_REPO || env.GITHUB_EVENT_NAME !== "workflow_dispatch") return null;
|
|
54234
|
-
if (env.GITHUB_WORKFLOW_REF !== WORKFLOW_REF || env.GITHUB_REF !== HUB_RELEASE_REF) return null;
|
|
54235
|
-
const runId = env.GITHUB_RUN_ID ?? "";
|
|
54236
|
-
const runAttempt2 = env.GITHUB_RUN_ATTEMPT ?? "";
|
|
54237
|
-
if (!ID.test(runId) || !ID.test(runAttempt2)) return null;
|
|
54238
|
-
return { runId: Number(runId), runAttempt: Number(runAttempt2) };
|
|
54239
|
-
}
|
|
54240
|
-
|
|
54241
54324
|
// src/release-custody.ts
|
|
54242
54325
|
var import_node_crypto18 = require("node:crypto");
|
|
54243
54326
|
var import_node_fs41 = require("node:fs");
|
|
@@ -56721,6 +56804,32 @@ var COMPARE_RE = new RegExp(`^repos/${NAME}/${NAME}/compare/${FREE}\\.\\.\\.${FR
|
|
|
56721
56804
|
var CONTENTS_BASE_RE = new RegExp(`^repos/${NAME}/${NAME}/contents/${FREE}$`);
|
|
56722
56805
|
var CONTENTS_QUERY_RE = /^ref=[^&\s]+$/;
|
|
56723
56806
|
var ACTIONS_RUNS_RE = new RegExp(`^repos/(${NAME})/(${NAME})/actions/runs$`);
|
|
56807
|
+
var ACTIONS_WORKFLOW_RUNS_RE = new RegExp(`^repos/(${NAME})/(${NAME})/actions/workflows/([A-Za-z0-9][A-Za-z0-9_.-]*\\.ya?ml)/runs$`);
|
|
56808
|
+
var WORKFLOW_RUNS_CANONICAL_DIR = ".github/workflows/";
|
|
56809
|
+
function workflowRunsAliasRequest(owner, repo, file, query) {
|
|
56810
|
+
let page = "1";
|
|
56811
|
+
let perPage = 30;
|
|
56812
|
+
const filters = [];
|
|
56813
|
+
for (const pair of query.split("&")) {
|
|
56814
|
+
if (!pair) continue;
|
|
56815
|
+
const eq = pair.indexOf("=");
|
|
56816
|
+
if (pair.slice(0, eq) === "page") {
|
|
56817
|
+
page = pair.slice(eq + 1);
|
|
56818
|
+
continue;
|
|
56819
|
+
}
|
|
56820
|
+
if (pair.slice(0, eq) === "per_page") {
|
|
56821
|
+
perPage = Number(pair.slice(eq + 1));
|
|
56822
|
+
continue;
|
|
56823
|
+
}
|
|
56824
|
+
filters.push(pair);
|
|
56825
|
+
}
|
|
56826
|
+
return {
|
|
56827
|
+
kind: "actions-workflow-runs",
|
|
56828
|
+
path: `repos/${owner}/${repo}/actions/runs?per_page=100&page=${page}${filters.map((pair) => `&${pair}`).join("")}`,
|
|
56829
|
+
paginate: false,
|
|
56830
|
+
workflowRuns: { file, page: Number(page), perPage }
|
|
56831
|
+
};
|
|
56832
|
+
}
|
|
56724
56833
|
var RUNS_QUERY_VALUE = {
|
|
56725
56834
|
per_page: /^(?:[1-9]\d?|100)$/,
|
|
56726
56835
|
page: /^[1-9]\d{0,5}$/,
|
|
@@ -56757,7 +56866,7 @@ var RELEASE_LIST_QUERY_VALUE = { per_page: RUNS_QUERY_VALUE.per_page, page: RUNS
|
|
|
56757
56866
|
var RELEASE_LATEST_RE = new RegExp(`^repos/${NAME}/${NAME}/releases/latest$`);
|
|
56758
56867
|
var RELEASE_TAG_RE = new RegExp(`^repos/${NAME}/${NAME}/releases/tags/(${NAME})$`);
|
|
56759
56868
|
var RELEASE_ASSET_RE = new RegExp(`^repos/${NAME}/${NAME}/releases/assets/\\d+$`);
|
|
56760
|
-
var API_GET_ALLOWED_SHAPES = "repos/{owner}/{repo}/issues/comments/{id}, repos/{owner}/{repo}/issues/{n}/comments[?per_page=1-100&page=<n>, each key at most once], repos/{owner}/{repo}/pulls/{n}/files, repos/{owner}/{repo}/compare/{a}...{b}, repos/{owner}/{repo}/contents/{path}[?ref=<branch>], repos/{owner}/{repo}/actions/runs[?per_page=1-100&page=<n>&status=&branch=&event=&head_sha=<40-hex>&created=<date-range>&actor=, each key at most once], repos/{owner}/{repo}/actions/runs/{id}, repos/{owner}/{repo}/actions/runs/{id}/jobs[?per_page=1-100&page=<n>, each key at most once], repos/{owner}/{repo}/actions/runs/{id}/attempts/{n}/jobs[?per_page=1-100&page=<n>, each key at most once], repos/{owner}/{repo}/actions/runs/{id}/artifacts[?per_page=1-100&page=<n>, each key at most once], repos/{owner}/{repo}/actions/artifacts/{id}/zip (needs --out <file>), repos/{owner}/{repo}/actions/jobs/{id}/logs (needs --out <file>), repos/{owner}/{repo}/releases[?per_page=1-100&page=<n>, each key at most once], repos/{owner}/{repo}/releases/latest, repos/{owner}/{repo}/releases/tags/{tag}, repos/{owner}/{repo}/releases/assets/{id} (needs --out <file>)";
|
|
56869
|
+
var API_GET_ALLOWED_SHAPES = "repos/{owner}/{repo}/issues/comments/{id}, repos/{owner}/{repo}/issues/{n}/comments[?per_page=1-100&page=<n>, each key at most once], repos/{owner}/{repo}/pulls/{n}/files, repos/{owner}/{repo}/compare/{a}...{b}, repos/{owner}/{repo}/contents/{path}[?ref=<branch>], repos/{owner}/{repo}/actions/runs[?per_page=1-100&page=<n>&status=&branch=&event=&head_sha=<40-hex>&created=<date-range>&actor=, each key at most once], repos/{owner}/{repo}/actions/workflows/<file>.yml|.yaml/runs[?same keys, each at most once \u2014 #9762 alias: ONE repository page (per_page=100 upstream) with workflow_runs sliced to exact .github/workflows/<file> path matches], repos/{owner}/{repo}/actions/runs/{id}, repos/{owner}/{repo}/actions/runs/{id}/jobs[?per_page=1-100&page=<n>, each key at most once], repos/{owner}/{repo}/actions/runs/{id}/attempts/{n}/jobs[?per_page=1-100&page=<n>, each key at most once], repos/{owner}/{repo}/actions/runs/{id}/artifacts[?per_page=1-100&page=<n>, each key at most once], repos/{owner}/{repo}/actions/artifacts/{id}/zip (needs --out <file>), repos/{owner}/{repo}/actions/jobs/{id}/logs (needs --out <file>), repos/{owner}/{repo}/releases[?per_page=1-100&page=<n>, each key at most once], repos/{owner}/{repo}/releases/latest, repos/{owner}/{repo}/releases/tags/{tag}, repos/{owner}/{repo}/releases/assets/{id} (needs --out <file>)";
|
|
56761
56870
|
var API_GET_RECOVERY = " No other GET read exists \u2014 do not retry path variants; use a dedicated mmi-cli subcommand or ask a human token holder. This refusal exits 1 on stderr with nothing on stdout: check the exit code and never merge stderr into stdout (2>&1) before JSON-parsing. Piping empty stdout into JSON.parse hides the stderr refusal; check the exit code before parsing.";
|
|
56762
56871
|
var TYPED_READ_ROUTE_RE = new RegExp(`^repos/(${NAME})/(${NAME})/(issues|pulls)(?:/(comments/\\d+|\\d+)(?:/([a-z_]+))?)?$`);
|
|
56763
56872
|
function typedReadHint(path2) {
|
|
@@ -56829,7 +56938,7 @@ function workflowRunsReadHint(path2) {
|
|
|
56829
56938
|
if (!match) return "";
|
|
56830
56939
|
const [, owner, repo, workflow] = match;
|
|
56831
56940
|
const slug = `${owner}/${repo}`;
|
|
56832
|
-
return ` Workflow routes are outside the App allowlist \u2014 use the typed workflow runs read \`MMI_ACTOR=app mmi-cli devops read run list --repo ${slug} --workflow ${workflow ?? "<workflow.yml>"} --branch <branch> --limit <n> --json databaseId,workflowName,event,status,conclusion\`, or the allowlisted REST runs list \`MMI_ACTOR=app mmi-cli devops api get "repos/${slug}/actions/runs?branch=<branch>&per_page=<n>"
|
|
56941
|
+
return ` Workflow routes are outside the App allowlist \u2014 use the typed workflow runs read \`MMI_ACTOR=app mmi-cli devops read run list --repo ${slug} --workflow ${workflow ?? "<workflow.yml>"} --branch <branch> --limit <n> --json databaseId,workflowName,event,status,conclusion\`, or the allowlisted REST runs list \`MMI_ACTOR=app mmi-cli devops api get "repos/${slug}/actions/runs?branch=<branch>&per_page=<n>"\`. A canonical workflow FILENAME at \`repos/${slug}/actions/workflows/<file>.yml|.yaml/runs\` IS the allowlisted alias (#9762: one repository page, runs sliced to exact .github/workflows/<file> path matches); numeric ids and display names are not.`;
|
|
56833
56942
|
}
|
|
56834
56943
|
function normalizeApiGetPath(raw) {
|
|
56835
56944
|
const trimmed = raw.trim();
|
|
@@ -56852,6 +56961,8 @@ function parseApiGetPath(rawPath) {
|
|
|
56852
56961
|
if (PULL_FILES_RE.test(path2)) return { kind: "pull-files", path: path2, paginate: true };
|
|
56853
56962
|
if (COMPARE_RE.test(path2)) return { kind: "compare", path: path2, paginate: false };
|
|
56854
56963
|
if (ACTIONS_RUNS_RE.test(base) && (q === -1 || validRunsQuery(query))) return { kind: "actions-runs", path: path2, paginate: false };
|
|
56964
|
+
const wfRuns = ACTIONS_WORKFLOW_RUNS_RE.exec(base);
|
|
56965
|
+
if (wfRuns && (q === -1 || validRunsQuery(query))) return workflowRunsAliasRequest(wfRuns[1], wfRuns[2], wfRuns[3], query);
|
|
56855
56966
|
if (ACTIONS_RUN_RE.test(path2)) return { kind: "actions-run", path: path2, paginate: false };
|
|
56856
56967
|
if (ACTIONS_JOBS_RE.test(base) && (q === -1 || validRunsQuery(query, RELEASE_LIST_QUERY_VALUE))) return { kind: "actions-jobs", path: path2, paginate: q === -1 };
|
|
56857
56968
|
if (ACTIONS_ARTIFACTS_RE.test(base) && (q === -1 || validRunsQuery(query, RELEASE_LIST_QUERY_VALUE))) return { kind: "actions-artifacts", path: path2, paginate: q === -1 };
|
|
@@ -56867,6 +56978,20 @@ function parseApiGetPath(rawPath) {
|
|
|
56867
56978
|
}
|
|
56868
56979
|
throw refuse();
|
|
56869
56980
|
}
|
|
56981
|
+
function sliceWorkflowRunsPage(body, selector) {
|
|
56982
|
+
const parsed = JSON.parse(body);
|
|
56983
|
+
if (!Array.isArray(parsed.workflow_runs)) throw new Error("api get: upstream repository runs page has no workflow_runs array");
|
|
56984
|
+
const matched = parsed.workflow_runs.filter((run) => run?.path === `${WORKFLOW_RUNS_CANONICAL_DIR}${selector.file}`).slice(0, selector.perPage);
|
|
56985
|
+
return JSON.stringify({
|
|
56986
|
+
workflow_runs: matched,
|
|
56987
|
+
matched_count: matched.length,
|
|
56988
|
+
repository_total_count: parsed.total_count,
|
|
56989
|
+
scope: "repository-page",
|
|
56990
|
+
incomplete_results: true,
|
|
56991
|
+
repository_page: selector.page,
|
|
56992
|
+
repository_per_page: 100
|
|
56993
|
+
});
|
|
56994
|
+
}
|
|
56870
56995
|
var ZIP_TIMEOUT_MS = 12e4;
|
|
56871
56996
|
var ZIP_MAX_BUFFER = 512 * 1024 * 1024;
|
|
56872
56997
|
function buildApiGetArgs(path2, accept) {
|
|
@@ -56887,7 +57012,8 @@ async function readAppApi(path2, runner = apiReadRunner) {
|
|
|
56887
57012
|
requireBrokerApp("read run log");
|
|
56888
57013
|
const log = req.kind === "actions-job-logs";
|
|
56889
57014
|
const body = await runApiGet((args, timeout) => runner(req.kind === "actions-jobs" ? [...args, "--slurp"] : args, log ? ZIP_TIMEOUT_MS : timeout, brokerChildEnv(), log || void 0), path2);
|
|
56890
|
-
|
|
57015
|
+
const out = req.workflowRuns ? sliceWorkflowRunsPage(body, req.workflowRuns) : body;
|
|
57016
|
+
return log ? Buffer.from(body, "latin1").toString("utf8") : out;
|
|
56891
57017
|
}
|
|
56892
57018
|
function registerApiReadCommands(program3, overrides) {
|
|
56893
57019
|
const spawn7 = overrides?.runner ?? apiReadRunner;
|
|
@@ -56913,6 +57039,7 @@ function registerApiReadCommands(program3, overrides) {
|
|
|
56913
57039
|
req.path,
|
|
56914
57040
|
o.accept
|
|
56915
57041
|
);
|
|
57042
|
+
if (req.workflowRuns) body = sliceWorkflowRunsPage(body, req.workflowRuns);
|
|
56916
57043
|
if (o.jq !== void 0) body = await jqRun(o.jq, body, localJqEnv(), ...bindings.length ? [bindings] : []);
|
|
56917
57044
|
if (zip) {
|
|
56918
57045
|
(0, import_node_fs46.writeFileSync)(o.out, body, "latin1");
|
|
@@ -62670,6 +62797,9 @@ function trainApplyDeps() {
|
|
|
62670
62797
|
return withHubReleaseCustody({
|
|
62671
62798
|
// #8958: same authenticated bounded read boundary as devops api; no raw gh contents subprocess.
|
|
62672
62799
|
readWorkflowContents: (path2) => defaultGitHubClient().rest("GET", path2, { timeoutMs: GH_TRAIN_TIMEOUT_MS }),
|
|
62800
|
+
// #9755: delegated Jerv trains require the Hub's sourceSha-pinned closure and fail closed on errors.
|
|
62801
|
+
// Humans and the Hub binding retain the caller-side read above; no producer token is widened.
|
|
62802
|
+
readTrainWorkflowClosure: async (repo) => trainWorkflowSourceClosure(repo, registryClientDeps(await loadConfig())),
|
|
62673
62803
|
// #7027: every mutating train verb holds this checkout's train writer lock for its whole run.
|
|
62674
62804
|
trainWriter: (verb, cwd) => acquireTrainWriterLock(cwd, `mmi-cli devops ${verb}`),
|
|
62675
62805
|
run: async (file, args) => {
|
package/cli/package.json
CHANGED