@mutmutco/cli 4.5.24 → 4.5.25
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/dist/main.cjs +331 -50
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -791,6 +791,8 @@ var init_house_map = __esm({
|
|
|
791
791
|
// register/run/park schedule rows under the one lane contract
|
|
792
792
|
// --- devops — shipping --------------------------------------------------------------------------
|
|
793
793
|
pr: "devops",
|
|
794
|
+
api: "devops",
|
|
795
|
+
// allowlisted GitHub REST GET reads for automation (pull files, compare, contents)
|
|
794
796
|
ci: "devops",
|
|
795
797
|
// the CI/gate audit
|
|
796
798
|
rcand: "devops",
|
|
@@ -6537,6 +6539,10 @@ async function filterDependencyBlockedClaimables(items, client, opts = {}) {
|
|
|
6537
6539
|
}
|
|
6538
6540
|
let body = item.details?.body;
|
|
6539
6541
|
if (body === void 0) {
|
|
6542
|
+
if (item.detailsUnavailable) {
|
|
6543
|
+
results[index] = { bucket: "claimable", item, warnings: [] };
|
|
6544
|
+
continue;
|
|
6545
|
+
}
|
|
6540
6546
|
if (opts.requireBundledBody) {
|
|
6541
6547
|
results[index] = { bucket: "claimable", item, warnings: [] };
|
|
6542
6548
|
continue;
|
|
@@ -6544,7 +6550,7 @@ async function filterDependencyBlockedClaimables(items, client, opts = {}) {
|
|
|
6544
6550
|
try {
|
|
6545
6551
|
const data = await client.rest("GET", `repos/${item.repository}/issues/${item.number}`);
|
|
6546
6552
|
body = data?.body ?? "";
|
|
6547
|
-
item.details = { body, comments: item.details?.comments ?? [] };
|
|
6553
|
+
item.details = { body, comments: item.details?.comments ?? [], ...item.details?.comments === void 0 ? { commentsUnread: true } : {} };
|
|
6548
6554
|
} catch (e) {
|
|
6549
6555
|
results[index] = {
|
|
6550
6556
|
bucket: "claimable",
|
|
@@ -8026,7 +8032,7 @@ async function probeHttpBounded(url, timeoutMs = 5e3) {
|
|
|
8026
8032
|
}
|
|
8027
8033
|
async function probePublicHealth(publicUrl) {
|
|
8028
8034
|
const root = await probeHttpBounded(publicUrl);
|
|
8029
|
-
if (root.status !== 401 && root.status !== 403) return root;
|
|
8035
|
+
if (root.status !== 401 && root.status !== 403 && root.status !== 404) return root;
|
|
8030
8036
|
return probeHttpBounded(`${publicUrl.replace(/\/$/, "")}/health`);
|
|
8031
8037
|
}
|
|
8032
8038
|
var TENANT_DEPLOY_RUN_SCAN_LIMIT = 100;
|
|
@@ -8048,7 +8054,7 @@ function tenantRuntimeHints(stage, fact, probe, edgeCertificate) {
|
|
|
8048
8054
|
if (!fact.sshHostPresent && fact.substrate === "hetzner-ssh") hints.push(`DEPLOY#${stage} has hetzner-ssh substrate but no sshHost presence; tenant-deploy cannot reach the box.`);
|
|
8049
8055
|
if (!fact.domain) hints.push(`DEPLOY#${stage} has no edgeVhost.domain; Cloudflare/Caddy public URL cannot be derived.`);
|
|
8050
8056
|
if (typeof fact.port !== "number") hints.push(`DEPLOY#${stage} has no edgeVhost.port; Caddy upstream/healthUrl hints cannot be checked.`);
|
|
8051
|
-
if (probe?.url?.endsWith("/health") && probe.ok === false) hints.push(`Root
|
|
8057
|
+
if (probe?.url?.endsWith("/health") && probe.ok === false) hints.push(`Root answered 401/403/404 and ${probe.url} answered ${probe.status ?? probe.error}; the app is up and the edge is fine \u2014 expose a 2xx /health route to make this probe green.`);
|
|
8052
8058
|
else if (edgeCertificate?.missing && edgeCertificate.hint) hints.push(edgeCertificate.hint);
|
|
8053
8059
|
else if (probe?.ok === false || probe?.status != null && probe.status >= 500) hints.push("Public URL probe failed; for Cloudflare 525 check Caddy TLS/origin certificate and Cloudflare SSL mode before changing app code.");
|
|
8054
8060
|
if (stage === "rc") hints.push("rc runtime is expected to be ephemeral: present between /rcand and /release, then retired after release.");
|
|
@@ -12447,6 +12453,7 @@ function renderBoardSource() {
|
|
|
12447
12453
|
return "source: live";
|
|
12448
12454
|
}
|
|
12449
12455
|
async function collectBoardPreferringSnapshot(cfg, options, deps) {
|
|
12456
|
+
if (options.snapshotOnly && !deps.snapshot) throw new Error("--snapshot-only needs the Hub snapshot transport (not with --direct)");
|
|
12450
12457
|
const attempt = deps.snapshot ? await fetchHubBoardSnapshot(
|
|
12451
12458
|
{
|
|
12452
12459
|
owner: cfg.projectOwner,
|
|
@@ -12486,6 +12493,9 @@ async function collectBoardPreferringSnapshot(cfg, options, deps) {
|
|
|
12486
12493
|
snapshot
|
|
12487
12494
|
};
|
|
12488
12495
|
}
|
|
12496
|
+
if (options.snapshotOnly && attempt?.state === "unavailable") {
|
|
12497
|
+
throw new Error(`Hub board snapshot unavailable (${attempt.reason}) \u2014 --snapshot-only refuses the direct user-auth fallback`);
|
|
12498
|
+
}
|
|
12489
12499
|
const collected = await collectBoardItems(cfg, { repo: options.repo, allowPartial: options.allowPartial, activeOnly: true }, deps);
|
|
12490
12500
|
if (attempt?.state === "unavailable") {
|
|
12491
12501
|
collected.warnings.push(`Hub board snapshot unavailable (${attempt.reason}) \u2014 served by the direct user-auth read (emergency fallback)`);
|
|
@@ -12768,7 +12778,13 @@ function nodeToItem(node, cfg) {
|
|
|
12768
12778
|
priorityOptionId: fields.priority?.optionId,
|
|
12769
12779
|
assignees,
|
|
12770
12780
|
labels,
|
|
12771
|
-
type: labels.find((label) => TYPE_LABELS.includes(label))
|
|
12781
|
+
type: labels.find((label) => TYPE_LABELS.includes(label)),
|
|
12782
|
+
...content.__typename === "Issue" && content.claimEvidenceUnavailable ? { detailsUnavailable: "App-token claim evidence unread" } : {},
|
|
12783
|
+
...content.__typename === "Issue" && typeof content.body === "string" && Array.isArray(content.comments?.nodes) && typeof content.comments.totalCount === "number" && Number.isInteger(content.comments.totalCount) && content.comments.totalCount >= content.comments.nodes.length ? { details: {
|
|
12784
|
+
body: content.body,
|
|
12785
|
+
comments: content.comments.nodes.map((comment) => ({ author: comment.author?.login ?? "", body: comment.body ?? "" })),
|
|
12786
|
+
...content.comments.totalCount > content.comments.nodes.length ? { omittedComments: content.comments.totalCount - content.comments.nodes.length } : {}
|
|
12787
|
+
} } : {}
|
|
12772
12788
|
})
|
|
12773
12789
|
};
|
|
12774
12790
|
}
|
|
@@ -12783,7 +12799,8 @@ async function attachBundleDetails(report, client, allowPartial, options = {}) {
|
|
|
12783
12799
|
while (next < candidates.length) {
|
|
12784
12800
|
const item = candidates[next++];
|
|
12785
12801
|
try {
|
|
12786
|
-
item.details = await fetchIssueDetails(client, item.repository, item.number);
|
|
12802
|
+
if (!item.details && !item.detailsUnavailable) item.details = await fetchIssueDetails(client, item.repository, item.number);
|
|
12803
|
+
if (!item.details) continue;
|
|
12787
12804
|
const claim = boardItemClaim(item);
|
|
12788
12805
|
if (claim) item.claim = claim;
|
|
12789
12806
|
} catch (e) {
|
|
@@ -13017,7 +13034,8 @@ async function demoteLiveClaimedClaimables(scope, client, actor = describeSessio
|
|
|
13017
13034
|
const warnings = [];
|
|
13018
13035
|
let partial = false;
|
|
13019
13036
|
const fetchComments = deps.fetchComments ?? (async (item) => {
|
|
13020
|
-
if (item.
|
|
13037
|
+
if (item.detailsUnavailable) return void 0;
|
|
13038
|
+
if (item.details && !item.details.commentsUnread && !item.details.omittedComments) return item.details.comments;
|
|
13021
13039
|
const comments = await client.restPaginate(`repos/${item.repository}/issues/${item.number}/comments`);
|
|
13022
13040
|
return comments.map((comment) => ({ body: comment.body ?? "" }));
|
|
13023
13041
|
});
|
|
@@ -13600,11 +13618,6 @@ async function prepareClaimContext(options, selectors, deps, collected, snapshot
|
|
|
13600
13618
|
warnings: collected.warnings,
|
|
13601
13619
|
partial: collected.partial
|
|
13602
13620
|
};
|
|
13603
|
-
for (const scope of ["primary", "secondary"]) {
|
|
13604
|
-
const filtered = await filterDependencyBlockedClaimables(report[scope].claimable, client);
|
|
13605
|
-
report[scope].claimable = filtered.claimable;
|
|
13606
|
-
report.warnings.push(...filtered.warnings);
|
|
13607
|
-
}
|
|
13608
13621
|
return {
|
|
13609
13622
|
cfg,
|
|
13610
13623
|
client,
|
|
@@ -14061,7 +14074,7 @@ init_github_client();
|
|
|
14061
14074
|
var PRODUCT_RULESET_NAME = "mmi-product-required-checks";
|
|
14062
14075
|
var PRODUCT_GATE_CONTEXT = "gate";
|
|
14063
14076
|
var PRODUCT_RULESET_PATH = ".github/rulesets/mmi-product-required-checks.json";
|
|
14064
|
-
function reseedProductRulesetStrictness(current, seed) {
|
|
14077
|
+
function reseedProductRulesetStrictness(current, seed, declared = {}) {
|
|
14065
14078
|
const parse = (raw) => {
|
|
14066
14079
|
const policy2 = JSON.parse(raw);
|
|
14067
14080
|
if (!policy2 || typeof policy2 !== "object" || Array.isArray(policy2) || policy2.name !== PRODUCT_RULESET_NAME || policy2.target !== "branch" || !Array.isArray(policy2.rules) || policy2.rules.some((r) => !r || typeof r !== "object" || Array.isArray(r) || typeof r.type !== "string")) {
|
|
@@ -14074,14 +14087,28 @@ function reseedProductRulesetStrictness(current, seed) {
|
|
|
14074
14087
|
}
|
|
14075
14088
|
return { policy: policy2, parameters: parameters2 };
|
|
14076
14089
|
};
|
|
14077
|
-
const
|
|
14090
|
+
const canonical = parse(seed);
|
|
14091
|
+
const strict = canonical.parameters.strict_required_status_checks_policy;
|
|
14078
14092
|
if (typeof strict !== "boolean") throw new Error("canonical product ruleset must declare boolean strictness");
|
|
14079
14093
|
if (current === null) return seed;
|
|
14080
14094
|
const { policy, parameters } = parse(current);
|
|
14081
|
-
|
|
14095
|
+
let changed = parameters.strict_required_status_checks_policy !== strict;
|
|
14082
14096
|
parameters.strict_required_status_checks_policy = strict;
|
|
14083
|
-
|
|
14084
|
-
|
|
14097
|
+
if (declared.requiredChecks) {
|
|
14098
|
+
const checks = canonical.parameters.required_status_checks.map((check) => parameters.required_status_checks.find((old) => old.context === check.context) ?? check);
|
|
14099
|
+
if (JSON.stringify(parameters.required_status_checks) !== JSON.stringify(checks)) changed = true;
|
|
14100
|
+
parameters.required_status_checks = checks;
|
|
14101
|
+
}
|
|
14102
|
+
if (declared.requiredCheckBranches) {
|
|
14103
|
+
const refs = canonical.policy.conditions?.ref_name?.include;
|
|
14104
|
+
if (!Array.isArray(refs) || !refs.length) throw new Error("canonical product ruleset must declare branch includes");
|
|
14105
|
+
const conditions = policy.conditions;
|
|
14106
|
+
const refName = conditions?.ref_name;
|
|
14107
|
+
if (JSON.stringify(refName?.include) !== JSON.stringify(refs)) changed = true;
|
|
14108
|
+
policy.conditions = { ...conditions, ref_name: { ...refName, include: refs } };
|
|
14109
|
+
}
|
|
14110
|
+
return changed ? `${JSON.stringify(policy, null, 2)}
|
|
14111
|
+
` : current;
|
|
14085
14112
|
}
|
|
14086
14113
|
function stripRulesetComment(raw) {
|
|
14087
14114
|
const parsed = JSON.parse(raw);
|
|
@@ -15398,6 +15425,20 @@ function labelsToPrune(orgLabelNames) {
|
|
|
15398
15425
|
const org = new Set(orgLabelNames);
|
|
15399
15426
|
return GITHUB_DEFAULT_LABELS.filter((name) => !org.has(name));
|
|
15400
15427
|
}
|
|
15428
|
+
function planBootstrapLabels(labels, live) {
|
|
15429
|
+
const existing = new Set(live.map((label) => label.name));
|
|
15430
|
+
return [
|
|
15431
|
+
...labels.map((label) => ({ name: label.name, action: existing.has(label.name) ? "UPDATE" : "CREATE" })),
|
|
15432
|
+
...labelsToPrune(labels.map((label) => label.name)).filter((name) => existing.has(name)).map((name) => ({ name, action: "DELETE" }))
|
|
15433
|
+
];
|
|
15434
|
+
}
|
|
15435
|
+
function renderBootstrapAncillaryPlan(labels, environments) {
|
|
15436
|
+
return [
|
|
15437
|
+
"bootstrap apply \u2014 labels and environments (dry-run; no mutations):",
|
|
15438
|
+
...labels.map(({ action, name }) => ` ${action} label ${name}`),
|
|
15439
|
+
...environments.map(({ action, environment, reason }) => ` ${action} environment ${environment} (${reason})`)
|
|
15440
|
+
].join("\n");
|
|
15441
|
+
}
|
|
15401
15442
|
function resolveSeedContent(seed, vars, readFile9) {
|
|
15402
15443
|
if (seed.source === "self") return readFile9(seed.target);
|
|
15403
15444
|
if (seed.source.startsWith("seed:")) {
|
|
@@ -15418,6 +15459,11 @@ function resolveSeedWriteContent(seed, vars, readFile9, remoteContent) {
|
|
|
15418
15459
|
const result = upsertManagedSeedBlock(base, desired, seed.managedBlock.begin, seed.managedBlock.end);
|
|
15419
15460
|
return result.ok ? { ok: true, content: result.content, managed: true } : { ok: false, reason: result.reason, managed: true };
|
|
15420
15461
|
}
|
|
15462
|
+
function edgeCertificateStagesForBootstrap(track, domains) {
|
|
15463
|
+
if (!domains || typeof domains !== "object" || Array.isArray(domains)) return [];
|
|
15464
|
+
const stages = branchesForTrack(track);
|
|
15465
|
+
return ["dev", "rc"].filter((stage) => stages.includes(stage === "dev" ? "development" : stage) && Boolean(domains[stage]));
|
|
15466
|
+
}
|
|
15421
15467
|
function buildRegisterPayload(repo, cls, vars, options = {}) {
|
|
15422
15468
|
const parsedRepo = parseOwnerRepo(repo);
|
|
15423
15469
|
const slug = parsedRepo.slug;
|
|
@@ -15442,6 +15488,12 @@ function buildRegisterPayload(repo, cls, vars, options = {}) {
|
|
|
15442
15488
|
);
|
|
15443
15489
|
}
|
|
15444
15490
|
const deployModel = inferDeployModelForWrite({ ...shape, projectType }, parsedRepo.fullName);
|
|
15491
|
+
const releaseTrack = (isReleaseTrack(options.releaseTrack) ? options.releaseTrack : void 0) ?? (isReleaseTrack(options.existing?.releaseTrack) ? options.existing.releaseTrack : void 0) ?? resolveBootstrapReleaseTrack(cls, void 0);
|
|
15492
|
+
const inferredDomains = inferEdgeDomainsForWrite(slug);
|
|
15493
|
+
const derivedDomains = Object.fromEntries(branchesForTrack(releaseTrack).map((branch) => {
|
|
15494
|
+
const stage = branch === "development" ? "dev" : branch;
|
|
15495
|
+
return [stage, inferredDomains[stage]];
|
|
15496
|
+
}));
|
|
15445
15497
|
const num = (v) => {
|
|
15446
15498
|
if (v == null || v === "") return void 0;
|
|
15447
15499
|
const n = Number(v);
|
|
@@ -15471,10 +15523,9 @@ function buildRegisterPayload(repo, cls, vars, options = {}) {
|
|
|
15471
15523
|
deployModel,
|
|
15472
15524
|
// Persist the canonical stage hosts at the same write-time boundary as deployModel. Read paths never
|
|
15473
15525
|
// infer from an absent edgeDomains field.
|
|
15474
|
-
edgeDomains: options.existing?.edgeDomains ??
|
|
15526
|
+
edgeDomains: options.existing?.edgeDomains ?? derivedDomains,
|
|
15475
15527
|
// #1359: always persist an explicit track so release tooling never guesses from absence alone.
|
|
15476
|
-
|
|
15477
|
-
releaseTrack: options.releaseTrack ?? (isReleaseTrack(options.existing?.releaseTrack) ? options.existing.releaseTrack : void 0) ?? resolveBootstrapReleaseTrack(cls, void 0),
|
|
15528
|
+
releaseTrack,
|
|
15478
15529
|
// Board coords (from GraphQL at bootstrap, passed as --var by the skill).
|
|
15479
15530
|
projectOwner: vars.PROJECT_OWNER || void 0,
|
|
15480
15531
|
projectNumber: num(vars.PROJECT_NUMBER),
|
|
@@ -16753,10 +16804,10 @@ var rollout_plan_default = {
|
|
|
16753
16804
|
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)."
|
|
16754
16805
|
},
|
|
16755
16806
|
baseline: {
|
|
16756
|
-
version: "4.5.
|
|
16757
|
-
tag: "v4.5.
|
|
16758
|
-
commit: "
|
|
16759
|
-
npm: "@mutmutco/cli@4.5.
|
|
16807
|
+
version: "4.5.25",
|
|
16808
|
+
tag: "v4.5.25",
|
|
16809
|
+
commit: "d7c572c38021",
|
|
16810
|
+
npm: "@mutmutco/cli@4.5.25"
|
|
16760
16811
|
},
|
|
16761
16812
|
exitCriterion: "fleet-n-of-n",
|
|
16762
16813
|
hubOnlyShortcut: "forbidden",
|
|
@@ -16773,14 +16824,14 @@ var rollout_plan_default = {
|
|
|
16773
16824
|
repo: "mutmutco/mmi-hub",
|
|
16774
16825
|
role: "canary",
|
|
16775
16826
|
schedule: "train",
|
|
16776
|
-
v3Target: "v4.5.
|
|
16827
|
+
v3Target: "v4.5.25"
|
|
16777
16828
|
}
|
|
16778
16829
|
],
|
|
16779
16830
|
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.",
|
|
16780
16831
|
rollback: {
|
|
16781
16832
|
independent: true,
|
|
16782
|
-
mechanism: "npm dist-tag latest -> 4.5.
|
|
16783
|
-
v3Target: "v4.5.
|
|
16833
|
+
mechanism: "npm dist-tag latest -> 4.5.25 and redeploy the Hub Lambda from tag v4.5.25 (d7c572c38021); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
16834
|
+
v3Target: "v4.5.25 (@mutmutco/cli@4.5.25, tag commit d7c572c38021 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
16784
16835
|
}
|
|
16785
16836
|
},
|
|
16786
16837
|
{
|
|
@@ -17108,7 +17159,7 @@ function ensureField(fields, field) {
|
|
|
17108
17159
|
return list.join(",");
|
|
17109
17160
|
}
|
|
17110
17161
|
function resolveIssueViewGhFields(normalized) {
|
|
17111
|
-
const requested = normalized.split(",").map((f) => f.trim()).filter((f) => f && f !== "parent");
|
|
17162
|
+
const requested = normalized.split(",").map((f) => f.trim()).filter((f) => f && f !== "parent" && f !== "closedBy");
|
|
17112
17163
|
if (requested.length === 0) return { ghFields: "number", numberIsTransport: true };
|
|
17113
17164
|
return { ghFields: requested.join(","), numberIsTransport: false };
|
|
17114
17165
|
}
|
|
@@ -17597,6 +17648,45 @@ async function runPrForIssue(deps, issueRef, opts) {
|
|
|
17597
17648
|
}
|
|
17598
17649
|
return extractPrForIssueResponse(resp);
|
|
17599
17650
|
}
|
|
17651
|
+
function closedByGraphqlArgs(owner, name, number) {
|
|
17652
|
+
const query = "query($owner:String!,$name:String!){repository(owner:$owner,name:$name){issue(number:" + number + "){number closedByPullRequestsReferences(first:100){nodes{number merged mergedAt mergeCommit{oid} repository{owner{login} name}}}}}}";
|
|
17653
|
+
return ["api", "graphql", "-f", `query=${query}`, "-f", `owner=${owner}`, "-f", `name=${name}`];
|
|
17654
|
+
}
|
|
17655
|
+
function extractClosedByResponse(resp) {
|
|
17656
|
+
const r = resp ?? {};
|
|
17657
|
+
if ((r.errors ?? []).some((e) => e?.type === "NOT_FOUND") || !r.data?.repository?.issue) {
|
|
17658
|
+
throw new QueryReadError("NOT_FOUND", "issue not found");
|
|
17659
|
+
}
|
|
17660
|
+
const out = [];
|
|
17661
|
+
for (const node of r.data.repository.issue.closedByPullRequestsReferences?.nodes ?? []) {
|
|
17662
|
+
const pr = node;
|
|
17663
|
+
if (typeof pr.number !== "number") continue;
|
|
17664
|
+
out.push({
|
|
17665
|
+
number: pr.number,
|
|
17666
|
+
merged: pr.merged === true,
|
|
17667
|
+
mergedAt: typeof pr.mergedAt === "string" ? pr.mergedAt : null,
|
|
17668
|
+
mergeCommit: pr.mergeCommit && typeof pr.mergeCommit.oid === "string" ? { oid: pr.mergeCommit.oid } : null,
|
|
17669
|
+
repository: {
|
|
17670
|
+
owner: { login: String(pr.repository?.owner?.login ?? "") },
|
|
17671
|
+
name: String(pr.repository?.name ?? "")
|
|
17672
|
+
}
|
|
17673
|
+
});
|
|
17674
|
+
}
|
|
17675
|
+
return out;
|
|
17676
|
+
}
|
|
17677
|
+
async function runClosedBy(deps, opts) {
|
|
17678
|
+
const { owner, name } = splitRepo(opts.repo);
|
|
17679
|
+
let resp;
|
|
17680
|
+
try {
|
|
17681
|
+
resp = await deps.ghJson(closedByGraphqlArgs(owner, name, opts.number), GH_LIST_TIMEOUT_MS);
|
|
17682
|
+
} catch (e) {
|
|
17683
|
+
if (isGhNotFound(e)) {
|
|
17684
|
+
throw new QueryReadError("NOT_FOUND", `#${opts.number} not found in ${opts.repo}`);
|
|
17685
|
+
}
|
|
17686
|
+
throw e;
|
|
17687
|
+
}
|
|
17688
|
+
return extractClosedByResponse(resp);
|
|
17689
|
+
}
|
|
17600
17690
|
async function runIssueViewContext(deps, opts) {
|
|
17601
17691
|
const baseFields = opts.context ? opts.jsonFields.split(",").map((field) => field.trim()).filter((field) => field !== "linkedPrs" && field !== "children").join(",") : opts.jsonFields;
|
|
17602
17692
|
const fields = opts.comments || opts.context ? ensureField(baseFields, "comments") : baseFields;
|
|
@@ -26463,6 +26553,10 @@ var TAG_ADDRESSABLE_CONTEXTS_LABEL = "rc/main required contexts are tag-addressa
|
|
|
26463
26553
|
var RELEASE_BRANCH_REACHABLE_LABEL = "rc/main required contexts are reachable from a release-base head (#5193)";
|
|
26464
26554
|
var RULESET_TRACK_SCOPE_LABEL = "product ruleset branch scope covers the release track (#6327)";
|
|
26465
26555
|
var RECONCILE_SEED_BRANCH_PREFIX = "bootstrap-seed-ruleset-ref";
|
|
26556
|
+
var NPM_PUBLISH_SAFE_FLOOR_SHA = "b5632c8b940d8025fdb5630e576502e1397c4552";
|
|
26557
|
+
var SHARED_ACTION_PINS = [
|
|
26558
|
+
{ path: "npm-publish", floor: NPM_PUBLISH_SAFE_FLOOR_SHA }
|
|
26559
|
+
];
|
|
26466
26560
|
function slugFromRepo(repo) {
|
|
26467
26561
|
return (repo.includes("/") ? repo.split("/")[1] : repo).toLowerCase();
|
|
26468
26562
|
}
|
|
@@ -26486,6 +26580,43 @@ async function restJson2(deps, path2, fallback) {
|
|
|
26486
26580
|
return fallback;
|
|
26487
26581
|
}
|
|
26488
26582
|
}
|
|
26583
|
+
var sharedPinComparisons = /* @__PURE__ */ new WeakMap();
|
|
26584
|
+
function pinContainsFloor(deps, floor, pin) {
|
|
26585
|
+
let comparisons = sharedPinComparisons.get(deps);
|
|
26586
|
+
if (!comparisons) {
|
|
26587
|
+
comparisons = /* @__PURE__ */ new Map();
|
|
26588
|
+
sharedPinComparisons.set(deps, comparisons);
|
|
26589
|
+
}
|
|
26590
|
+
const key = `${floor}...${pin}`;
|
|
26591
|
+
let comparison = comparisons.get(key);
|
|
26592
|
+
if (!comparison) {
|
|
26593
|
+
comparison = deps.client.rest("GET", `repos/${HUB_REPO4}/compare/${key}`).then((result) => result?.status === "ahead" || result?.status === "identical" ? true : result?.status === "behind" || result?.status === "diverged" ? false : void 0).catch(() => void 0);
|
|
26594
|
+
comparisons.set(key, comparison);
|
|
26595
|
+
}
|
|
26596
|
+
return comparison;
|
|
26597
|
+
}
|
|
26598
|
+
async function auditSharedActionPins(files, deps) {
|
|
26599
|
+
const checks = [];
|
|
26600
|
+
for (const { path: path2, body } of files) {
|
|
26601
|
+
for (const [index, line] of body.split(/\r?\n/).entries()) {
|
|
26602
|
+
const match = /^\s*-?\s*uses:\s*['"]?mutmutco\/MMI-Hub\/\.github\/actions\/([^@\s'"]+)@([0-9a-f]{40})['"]?(?:\s*(?:#.*)?)$/.exec(line);
|
|
26603
|
+
if (!match) continue;
|
|
26604
|
+
const action = SHARED_ACTION_PINS.find((item) => item.path === match[1]);
|
|
26605
|
+
if (!action || match[2] === action.floor) continue;
|
|
26606
|
+
const safe = await pinContainsFloor(deps, action.floor, match[2]);
|
|
26607
|
+
if (safe === true) continue;
|
|
26608
|
+
const location = `${path2}:${index + 1}`;
|
|
26609
|
+
const replacement = `mutmutco/MMI-Hub/.github/actions/${action.path}@${action.floor}`;
|
|
26610
|
+
checks.push({
|
|
26611
|
+
ok: safe !== false,
|
|
26612
|
+
label: `${action.path} shared action pin is current`,
|
|
26613
|
+
detail: `${location}: ${action.path} pinned to ${match[2]} \u2014 ` + (safe === false ? `lacks the minimum safe floor ${action.floor}` : "could not verify ancestry against the safe floor; refresh the pin"),
|
|
26614
|
+
remediation: safe === false ? `Replace the uses: pin at ${location} with ${replacement}` : void 0
|
|
26615
|
+
});
|
|
26616
|
+
}
|
|
26617
|
+
}
|
|
26618
|
+
return checks;
|
|
26619
|
+
}
|
|
26489
26620
|
async function rulesetDetails(deps, repo, list) {
|
|
26490
26621
|
const details = [];
|
|
26491
26622
|
for (const ruleset of list) {
|
|
@@ -27022,6 +27153,10 @@ async function auditRepoCi(repo, deps) {
|
|
|
27022
27153
|
}
|
|
27023
27154
|
}
|
|
27024
27155
|
}
|
|
27156
|
+
if (repoClass !== "hub") {
|
|
27157
|
+
const workflows = await getBranchWorkflows();
|
|
27158
|
+
if (workflows) checks.push(...await auditSharedActionPins(workflows, deps));
|
|
27159
|
+
}
|
|
27025
27160
|
const ok = checks.every((c) => c.ok);
|
|
27026
27161
|
return { repo, class: repoClass, mergePolicy: policy, ok, checks, explicitNoCi };
|
|
27027
27162
|
}
|
|
@@ -27585,7 +27720,7 @@ var COMMAND_METADATA = /* @__PURE__ */ Symbol.for("mmi.commandTaxonomy.metadata"
|
|
|
27585
27720
|
var PRIMARY_GROUPS = [
|
|
27586
27721
|
["Orient", ["onboard", "status", "next", "doctor", "whoami", "commands", "explain"]],
|
|
27587
27722
|
["Plan and work", ["board", "issue", "stage"]],
|
|
27588
|
-
["Review and ship", ["pr", "ci", "rcand", "release", "hotfix", "train"]],
|
|
27723
|
+
["Review and ship", ["pr", "ci", "rcand", "release", "hotfix", "train", "api"]],
|
|
27589
27724
|
// `tests` sits beside `docs` deliberately: both are deterministic, repo-local gates a workflow
|
|
27590
27725
|
// step invokes (`docs refs`, `tests policy`), not org-plane operations (#3605). `spawn policy`
|
|
27591
27726
|
// joins them on the same footing (#3979); `dist status` does too (#5576) — the checkout's own
|
|
@@ -27633,6 +27768,7 @@ var COMMAND_OWNERSHIP = {
|
|
|
27633
27768
|
issue: { module_owner: "cli/src/issue-commands.ts", consumer: "agent-workflow" },
|
|
27634
27769
|
stage: { module_owner: "cli/src/stage-commands.ts", consumer: "agent-workflow" },
|
|
27635
27770
|
pr: { module_owner: "cli/src/pr-commands.ts", consumer: "agent-workflow" },
|
|
27771
|
+
api: { module_owner: "cli/src/api-read.ts", consumer: "agent-workflow" },
|
|
27636
27772
|
ci: { module_owner: "cli/src/ci-audit.ts", consumer: "release-operator" },
|
|
27637
27773
|
rcand: { module_owner: "cli/src/train-commands.ts", consumer: "release-operator" },
|
|
27638
27774
|
release: { module_owner: "cli/src/train-commands.ts", consumer: "release-operator" },
|
|
@@ -28372,7 +28508,10 @@ var APP_VAULT_REPO = "mutmutco/MMI-Hub";
|
|
|
28372
28508
|
var APP_VAULT_KEYS = ["GITHUB_APP_ID", "GITHUB_APP_INSTALLATION_ID", "GITHUB_APP_PRIVATE_KEY"];
|
|
28373
28509
|
var APP_ELIGIBLE_COMMANDS = /* @__PURE__ */ new Set([
|
|
28374
28510
|
"pr checks-wait",
|
|
28375
|
-
"pr view"
|
|
28511
|
+
"pr view",
|
|
28512
|
+
"issue list",
|
|
28513
|
+
"issue view",
|
|
28514
|
+
"api get"
|
|
28376
28515
|
]);
|
|
28377
28516
|
var APP_POLL_PERMISSIONS = {
|
|
28378
28517
|
metadata: "read",
|
|
@@ -28602,8 +28741,8 @@ async function attachBoardIssues(selectors, repoOption, deps = {}) {
|
|
|
28602
28741
|
ref = `${parsed.repo}#${parsed.number}`;
|
|
28603
28742
|
const cfg = resolveBoardConfig(await (deps.config ?? loadConfigForBoardSelector2)(selector, repoOption));
|
|
28604
28743
|
const issue = await client.rest("GET", `repos/${parsed.repo}/issues/${parsed.number}`);
|
|
28605
|
-
if (!issue.
|
|
28606
|
-
const projectItemId = await (deps.attach ?? attachViaHubApp)(cfg, issue.
|
|
28744
|
+
if (!issue.node_id) throw new Error("issue node id could not be read");
|
|
28745
|
+
const projectItemId = await (deps.attach ?? attachViaHubApp)(cfg, issue.node_id);
|
|
28607
28746
|
return { ref, attached: true, projectItemId, credential: "app_installation" };
|
|
28608
28747
|
} catch (e) {
|
|
28609
28748
|
return { ref, attached: false, reason: e.message };
|
|
@@ -28652,7 +28791,8 @@ function registerBoardCommands(program3) {
|
|
|
28652
28791
|
repo: o.repo,
|
|
28653
28792
|
includeBundleDetails: o.bundleDetails,
|
|
28654
28793
|
includeAllBodies: o.bodies,
|
|
28655
|
-
allowPartial: o.allowPartial
|
|
28794
|
+
allowPartial: o.allowPartial,
|
|
28795
|
+
snapshotOnly: o.snapshotOnly
|
|
28656
28796
|
}, o.direct ? {} : { snapshot: registryClientDeps(config) });
|
|
28657
28797
|
const output = o.json ? JSON.stringify(report) : renderBoardReport(report);
|
|
28658
28798
|
if (!o.out) console.log(output);
|
|
@@ -28695,7 +28835,7 @@ function registerBoardCommands(program3) {
|
|
|
28695
28835
|
return `Claimed ${ref} for ${holder} - In Progress${reclaimed}`;
|
|
28696
28836
|
}
|
|
28697
28837
|
const board = program3.command("board").description("read, claim, show, and move Project v2 work items for the current repo");
|
|
28698
|
-
board.command("read", { isDefault: true }).alias("list").description("read the board and print user-owned, claimable, and taken items").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo (defaults to git origin)").option("--direct", "bypass the Hub snapshot and read the live board through direct GitHub GraphQL").option("--bundle-details", "fetch body/comments only for user-owned and claimable issues").option("--bodies", "fetch body/comments for EVERY scoped row, including taken and unowned in-flight ones \u2014 for consumers that scope by Status rather than ownership (#4861); implies --bundle-details and costs one extra read per row").option("--allow-partial", "return partial board results when later page/detail reads fail").option("--out <path>", "write the output to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5802)").addHelpText("after", "\nread is always the authoritative live GitHub Project v2 board (#4926).\n--direct skips the Hub snapshot and uses the existing direct GitHub GraphQL read immediately.\n--allow-partial applies to the paginated path and detail reads.\n\nWhen using Windows PowerShell 5.1, avoid shell redirects: PowerShell 5.1's `> file.json` is Out-File,\nwhich writes UTF-16LE with a BOM, and Node reading it as 'utf8' then fails JSON.parse at position 1\n(#5802). Use --out instead \u2014 the CLI writes the file itself as UTF-8:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n").action((o) => runBoardRead(o));
|
|
28838
|
+
board.command("read", { isDefault: true }).alias("list").description("read the board and print user-owned, claimable, and taken items").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo (defaults to git origin)").option("--direct", "bypass the Hub snapshot and read the live board through direct GitHub GraphQL").option("--snapshot-only", "fail instead of falling back to your personal GraphQL pool when the Hub snapshot is unavailable \u2014 for ambient readers such as sidebars (#7203)").option("--bundle-details", "fetch body/comments only for user-owned and claimable issues").option("--bodies", "fetch body/comments for EVERY scoped row, including taken and unowned in-flight ones \u2014 for consumers that scope by Status rather than ownership (#4861); implies --bundle-details and costs one extra read per row").option("--allow-partial", "return partial board results when later page/detail reads fail").option("--out <path>", "write the output to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5802)").addHelpText("after", "\nread is always the authoritative live GitHub Project v2 board (#4926).\n--direct skips the Hub snapshot and uses the existing direct GitHub GraphQL read immediately.\n--allow-partial applies to the paginated path and detail reads.\n\nWhen using Windows PowerShell 5.1, avoid shell redirects: PowerShell 5.1's `> file.json` is Out-File,\nwhich writes UTF-16LE with a BOM, and Node reading it as 'utf8' then fails JSON.parse at position 1\n(#5802). Use --out instead \u2014 the CLI writes the file itself as UTF-8:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n").action((o) => runBoardRead(o));
|
|
28699
28839
|
withExamples(mutating(
|
|
28700
28840
|
board.command("claim <issues...>").description("claim issues: assign them and move their Project v2 Status to In Progress \u2014 idempotent, so an item already yours and In Progress succeeds unchanged (one or more refs); the board scan rides the Hub snapshot, same as board read").addHelpText("after", "\nclaim reads the board through the same Hub snapshot leg as `board read` (the App-installation\ncredential, never your personal GraphQL pool); the direct user-auth read is an emergency fallback\nand is named in a Warning line after the verdict (#6162).\n\nevery claim stamps a lane-identity marker comment on the issue (`<!-- mmi-claim: \u2026 -->`,\nsurface/session@host) so other agents can attribute the hold (#3727). The session is the\nhost-exported id when the surface provides one, otherwise a per-process `synth-` fallback \u2014\na claim is never anonymous (#5245). `board show`, doctor and unclaim read the latest marker.\n\nsame-owner resume (#6035): when the prior marker was posted by YOUR login on THIS host and its\nlocal session is verifiably dead (transcript probe), the claim proceeds as a resume without\n--force and names the evidence. A live, foreign-host, or unprobeable prior lane still refuses.\n\nreclaim from In Review (#6339): an In Review item with no holder, or one held by the claiming\nlogin, is claimable \u2014 it moves back to In Progress and the receipt names the status it came from.\nAn In Review item another login holds is refused (ask the holder, or wait for the review to land),\nand Done stays refused. The board status is the authority; no PR state is consulted.\n").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--for <login>", "assign to this login instead of @me \u2014 agent claims on behalf of the master").option("--force", "take an item already claimed by another lane that shows live evidence of active work (#3727)").option("--check", "read-only: run every claim gate and report the verdict, writing nothing \u2014 exits 1 with the same refusal a real claim would raise (#4511)").option("--allow-partial", "return success JSON if assignment succeeds but the status move fails"),
|
|
28701
28841
|
(_opts, args) => ({ command: "oracle board claim", issues: args[0] ?? [] })
|
|
@@ -29618,6 +29758,43 @@ function parseGateVar(raw) {
|
|
|
29618
29758
|
}
|
|
29619
29759
|
return out;
|
|
29620
29760
|
}
|
|
29761
|
+
var SECRET_OUTPUT_PATH_RE = /^\/[A-Za-z0-9._/-]+$/;
|
|
29762
|
+
var SECRET_OUTPUT_REPO_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*\/[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
29763
|
+
var SECRET_OUTPUT_KEY_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
29764
|
+
function parseTenantTaskSecretOutput(raw, label) {
|
|
29765
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
29766
|
+
throw new Error(`org project set: ${label} must be an object {path,vault:{repo,key},overwrite?}`);
|
|
29767
|
+
}
|
|
29768
|
+
const value = raw;
|
|
29769
|
+
if (Object.keys(value).some((key) => !["path", "vault", "overwrite"].includes(key))) {
|
|
29770
|
+
throw new Error(`org project set: ${label} has an unknown field`);
|
|
29771
|
+
}
|
|
29772
|
+
if (typeof value.path !== "string" || value.path.length < 2 || value.path.length > 256 || !SECRET_OUTPUT_PATH_RE.test(value.path) || value.path.split("/").includes("..")) {
|
|
29773
|
+
throw new Error(`org project set: ${label}.path must be an absolute container path (no ".." segment)`);
|
|
29774
|
+
}
|
|
29775
|
+
const vault = value.vault;
|
|
29776
|
+
if (!vault || typeof vault !== "object" || Array.isArray(vault)) {
|
|
29777
|
+
throw new Error(`org project set: ${label}.vault must be an object {repo,key}`);
|
|
29778
|
+
}
|
|
29779
|
+
const vaultMap = vault;
|
|
29780
|
+
if (Object.keys(vaultMap).some((key) => !["repo", "key"].includes(key))) {
|
|
29781
|
+
throw new Error(`org project set: ${label}.vault has an unknown field`);
|
|
29782
|
+
}
|
|
29783
|
+
if (typeof vaultMap.repo !== "string" || !SECRET_OUTPUT_REPO_RE.test(vaultMap.repo)) {
|
|
29784
|
+
throw new Error(`org project set: ${label}.vault.repo must be owner/name`);
|
|
29785
|
+
}
|
|
29786
|
+
if (typeof vaultMap.key !== "string" || !SECRET_OUTPUT_KEY_RE.test(vaultMap.key)) {
|
|
29787
|
+
throw new Error(`org project set: ${label}.vault.key must be UPPER_SNAKE`);
|
|
29788
|
+
}
|
|
29789
|
+
if (value.overwrite !== void 0 && typeof value.overwrite !== "boolean") {
|
|
29790
|
+
throw new Error(`org project set: ${label}.overwrite must be true or false`);
|
|
29791
|
+
}
|
|
29792
|
+
return {
|
|
29793
|
+
path: value.path,
|
|
29794
|
+
vault: { repo: vaultMap.repo, key: vaultMap.key },
|
|
29795
|
+
...value.overwrite !== void 0 ? { overwrite: value.overwrite } : {}
|
|
29796
|
+
};
|
|
29797
|
+
}
|
|
29621
29798
|
function parseTenantTasksVar(raw) {
|
|
29622
29799
|
let parsed;
|
|
29623
29800
|
try {
|
|
@@ -29636,7 +29813,7 @@ function parseTenantTasksVar(raw) {
|
|
|
29636
29813
|
if (!nameRe.test(name)) throw new Error(`org project set: tenantTasks name ${JSON.stringify(name)} must be lowercase kebab-case`);
|
|
29637
29814
|
if (!rawTask || typeof rawTask !== "object" || Array.isArray(rawTask)) throw new Error(`org project set: tenantTasks.${name} must be an object`);
|
|
29638
29815
|
const task = rawTask;
|
|
29639
|
-
if (Object.keys(task).some((key) => !["service", "command", "stages", "timeoutSeconds", "artifact"].includes(key))) throw new Error(`org project set: tenantTasks.${name} has an unknown field`);
|
|
29816
|
+
if (Object.keys(task).some((key) => !["service", "command", "stages", "timeoutSeconds", "artifact", "secretOutput"].includes(key))) throw new Error(`org project set: tenantTasks.${name} has an unknown field`);
|
|
29640
29817
|
if (typeof task.service !== "string" || !serviceRe.test(task.service)) throw new Error(`org project set: tenantTasks.${name}.service has an unsafe shape`);
|
|
29641
29818
|
if (!Array.isArray(task.command) || task.command.length < 1 || task.command.length > 32 || task.command.some((arg) => typeof arg !== "string" || arg.length < 1 || arg.length > 256 || /[\u0000\r\n]/.test(arg))) throw new Error(`org project set: tenantTasks.${name}.command must be 1-32 bounded argv strings`);
|
|
29642
29819
|
if (!Array.isArray(task.stages) || task.stages.length < 1 || task.stages.length > 3 || task.stages.some((stage) => typeof stage !== "string" || !validStages.has(stage)) || new Set(task.stages).size !== task.stages.length) throw new Error(`org project set: tenantTasks.${name}.stages must be unique dev/rc/main values`);
|
|
@@ -29650,7 +29827,8 @@ function parseTenantTasksVar(raw) {
|
|
|
29650
29827
|
command: task.command,
|
|
29651
29828
|
stages: task.stages,
|
|
29652
29829
|
...task.timeoutSeconds !== void 0 ? { timeoutSeconds: task.timeoutSeconds } : {},
|
|
29653
|
-
...artifact !== "none" ? { artifact } : {}
|
|
29830
|
+
...artifact !== "none" ? { artifact } : {},
|
|
29831
|
+
...task.secretOutput !== void 0 ? { secretOutput: parseTenantTaskSecretOutput(task.secretOutput, `tenantTasks.${name}.secretOutput`) } : {}
|
|
29654
29832
|
};
|
|
29655
29833
|
}
|
|
29656
29834
|
return out;
|
|
@@ -29708,7 +29886,7 @@ var SETTABLE_VAR_HINTS = {
|
|
|
29708
29886
|
requiredGcpApis: "comma-string",
|
|
29709
29887
|
requiredRuntimeSecrets: 'JSON stage map, e.g. {"dev":["KEY"],"rc":["KEY"],"main":["KEY"]}',
|
|
29710
29888
|
requiredBuildSecrets: 'JSON flat array, e.g. ["NODE_AUTH_TOKEN=@github-packages-token"]',
|
|
29711
|
-
tenantTasks: 'JSON map {name:{service,command[],stages[],timeoutSeconds?,artifact?:"required"}}; use {artifact} in argv when required',
|
|
29889
|
+
tenantTasks: 'JSON map {name:{service,command[],stages[],timeoutSeconds?,artifact?:"required",secretOutput?:{path,vault:{repo,key},overwrite?}}}; use {artifact} in argv when required; a secretOutput is master-only',
|
|
29712
29890
|
secrets: "JSON catalog map keyed by KEY {key,purpose,group,owner,stages[],consumers[]} \u2014 merged per entry; drop ONE entry with --unset-secret KEY; clear ALL with --unset secrets --confirm-clear-secrets; prefer --secrets-file",
|
|
29713
29891
|
edgeDomains: "JSON {dev,rc,main} domain map",
|
|
29714
29892
|
statusOptions: "JSON name\u2192id map",
|
|
@@ -32321,9 +32499,7 @@ function registerBootstrapCommands(program3) {
|
|
|
32321
32499
|
deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
|
|
32322
32500
|
deployFacts,
|
|
32323
32501
|
deployFactsRead: deployFacts !== null,
|
|
32324
|
-
edgeCertificates: await Promise.all(
|
|
32325
|
-
const domain = meta?.edgeDomains?.[stage];
|
|
32326
|
-
if (!domain) return null;
|
|
32502
|
+
edgeCertificates: await Promise.all(edgeCertificateStagesForBootstrap(effectiveTrack, meta?.edgeDomains).map(async (stage) => {
|
|
32327
32503
|
try {
|
|
32328
32504
|
return { stage, status: await edgeCertificateForStage(slug, stage, reg) };
|
|
32329
32505
|
} catch (e) {
|
|
@@ -32823,7 +32999,18 @@ function registerBootstrapCommands(program3) {
|
|
|
32823
32999
|
if (isLegacyBlock) {
|
|
32824
33000
|
content = upsertManagedGitignoreBlock(preservedContent).content;
|
|
32825
33001
|
} else {
|
|
32826
|
-
const
|
|
33002
|
+
const rulesetVars = preserveRuleset ? gateSeedVars(
|
|
33003
|
+
o.class,
|
|
33004
|
+
effectiveTrack,
|
|
33005
|
+
"node",
|
|
33006
|
+
registryMeta?.requiredCheckBranches,
|
|
33007
|
+
registryMeta?.requiredChecks
|
|
33008
|
+
) : null;
|
|
33009
|
+
const writeContent = resolveSeedWriteContent(resolved, preserveRuleset && rulesetVars ? {
|
|
33010
|
+
...vars,
|
|
33011
|
+
...registryMeta?.requiredChecks?.length ? { GATE_RULESET_CONTEXTS_JSON: rulesetVars.GATE_RULESET_CONTEXTS_JSON } : {},
|
|
33012
|
+
...registryMeta?.requiredCheckBranches?.length ? { GATE_RULESET_BRANCH_REFS_JSON: rulesetVars.GATE_RULESET_BRANCH_REFS_JSON } : {}
|
|
33013
|
+
} : vars, readFile9, preservedContent);
|
|
32827
33014
|
if (!writeContent.ok) {
|
|
32828
33015
|
return fail(`bootstrap apply: ${resolved.target}: ${writeContent.reason} \u2014 refusing to overwrite repo-owned content`);
|
|
32829
33016
|
}
|
|
@@ -32831,7 +33018,10 @@ function registerBootstrapCommands(program3) {
|
|
|
32831
33018
|
if (preserveRuleset) {
|
|
32832
33019
|
if (content === null) return fail("bootstrap apply: canonical product ruleset seed is unreadable");
|
|
32833
33020
|
try {
|
|
32834
|
-
content = reseedProductRulesetStrictness(baseContent, content
|
|
33021
|
+
content = reseedProductRulesetStrictness(baseContent, content, {
|
|
33022
|
+
requiredChecks: !!registryMeta?.requiredChecks?.length,
|
|
33023
|
+
requiredCheckBranches: !!registryMeta?.requiredCheckBranches?.length
|
|
33024
|
+
});
|
|
32835
33025
|
} catch (e) {
|
|
32836
33026
|
return fail(`bootstrap apply: product ruleset preservation refused: ${e.message}`);
|
|
32837
33027
|
}
|
|
@@ -33054,8 +33244,7 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
|
|
|
33054
33244
|
if (res.ok) {
|
|
33055
33245
|
ddbWrites.push({ slug: registerPayload.slug, action: "register", record: registerPayload });
|
|
33056
33246
|
applied.push(`ddb register ${registerPayload.slug}`);
|
|
33057
|
-
for (const stage of
|
|
33058
|
-
if (!registerPayload.edgeDomains?.[stage]) continue;
|
|
33247
|
+
for (const stage of edgeCertificateStagesForBootstrap(registerPayload.releaseTrack, registerPayload.edgeDomains)) {
|
|
33059
33248
|
try {
|
|
33060
33249
|
const cert = await edgeCertificateForStage(slug, stage, reg, true);
|
|
33061
33250
|
if (cert.required) applied.push(`edge certificate ${stage}: ${cert.action ?? "checked"}`);
|
|
@@ -33135,11 +33324,25 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
|
|
|
33135
33324
|
});
|
|
33136
33325
|
}
|
|
33137
33326
|
}
|
|
33138
|
-
|
|
33327
|
+
let ancillaryPlan;
|
|
33328
|
+
let environmentPlan;
|
|
33329
|
+
if (!o.execute && !onlyTarget) {
|
|
33330
|
+
try {
|
|
33331
|
+
const liveLabels = JSON.parse((await gh(["label", "list", "-R", repo, "--limit", "1000", "--json", "name"])).stdout);
|
|
33332
|
+
if (!Array.isArray(liveLabels)) throw new Error("label list is not an array");
|
|
33333
|
+
ancillaryPlan = planBootstrapLabels(manifest.labels, liveLabels);
|
|
33334
|
+
environmentPlan = (await verifyRepoEnvironments(repo, effectiveTrack, controlClient)).map(classifyEnvironmentDrift);
|
|
33335
|
+
} catch (e) {
|
|
33336
|
+
return failGraceful(`bootstrap apply: cannot plan labels/environments; no writes made: ${e.message}`);
|
|
33337
|
+
}
|
|
33338
|
+
}
|
|
33339
|
+
if (o.json) console.log(JSON.stringify({ repo, class: o.class, sourceSha: seedSource.sha, only: onlyTarget || null, execute: o.execute, seedDelivery: seedPlan.mode, seedBranch: seedPlan.branch, seedBaseSha: scopedPlan?.baseSha, seedPrUrl, actions, controlPlane: controlRows, labels: ancillaryPlan, environments: environmentPlan, applied, ddbWrites }, null, 2));
|
|
33139
33340
|
else {
|
|
33140
33341
|
console.log(renderSeedPlan(actions));
|
|
33141
33342
|
if (controlRows.length) console.log(`
|
|
33142
33343
|
${renderBootstrapControlPlan(controlRows)}`);
|
|
33344
|
+
if (ancillaryPlan && environmentPlan) console.log(`
|
|
33345
|
+
${renderBootstrapAncillaryPlan(ancillaryPlan, environmentPlan)}`);
|
|
33143
33346
|
if (o.execute) console.log(`
|
|
33144
33347
|
LIVE apply to ${repo}:
|
|
33145
33348
|
${applied.join("\n ")}`);
|
|
@@ -33649,7 +33852,7 @@ function formatDeployStatus(r) {
|
|
|
33649
33852
|
var STAGES2 = ["dev", "rc", "main"];
|
|
33650
33853
|
function registerDeployCommands(program3) {
|
|
33651
33854
|
const deploy = program3.command("deploy").description("per-stage deploy observability \u2014 last run, health, and running version (#2688)");
|
|
33652
|
-
deploy.command("status <stage>").description("last deploy run + runtime health probe + running version for a stage (defaults to the current repo);
|
|
33855
|
+
deploy.command("status <stage>").description("last deploy run + runtime health probe + running version for a stage (defaults to the current repo); a root answering 401/403/404 is re-probed at /health and the `url` field names the endpoint that answered (#6137)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action(async (stage, o) => {
|
|
33653
33856
|
if (!STAGES2.includes(stage)) {
|
|
33654
33857
|
return fail(`runtime deploy status: <stage> must be dev, rc, or main`);
|
|
33655
33858
|
}
|
|
@@ -38035,6 +38238,70 @@ function registerOrgHealthQuery(program3, deps = defaultOrgHealthQueryDeps()) {
|
|
|
38035
38238
|
});
|
|
38036
38239
|
}
|
|
38037
38240
|
|
|
38241
|
+
// src/api-read.ts
|
|
38242
|
+
init_cli_shared();
|
|
38243
|
+
var API_ACCEPT_HEADERS = {
|
|
38244
|
+
json: "application/vnd.github+json",
|
|
38245
|
+
raw: "application/vnd.github.raw"
|
|
38246
|
+
};
|
|
38247
|
+
function acceptHeaderFor(accept) {
|
|
38248
|
+
const v = (accept ?? "json").trim().toLowerCase();
|
|
38249
|
+
const header = API_ACCEPT_HEADERS[v];
|
|
38250
|
+
if (!header) {
|
|
38251
|
+
throw new Error(`api get: --accept must be one of: json, raw (received '${accept}')`);
|
|
38252
|
+
}
|
|
38253
|
+
return header;
|
|
38254
|
+
}
|
|
38255
|
+
var NAME = "[A-Za-z0-9_.-]+";
|
|
38256
|
+
var FREE = "[^?#%\\\\\\s]+";
|
|
38257
|
+
var PULL_FILES_RE = new RegExp(`^repos/${NAME}/${NAME}/pulls/\\d+/files$`);
|
|
38258
|
+
var COMPARE_RE = new RegExp(`^repos/${NAME}/${NAME}/compare/${FREE}\\.\\.\\.${FREE}$`);
|
|
38259
|
+
var CONTENTS_BASE_RE = new RegExp(`^repos/${NAME}/${NAME}/contents/${FREE}$`);
|
|
38260
|
+
var CONTENTS_QUERY_RE = /^ref=[^&\s]+$/;
|
|
38261
|
+
var API_GET_ALLOWED_SHAPES = "repos/{owner}/{repo}/pulls/{n}/files, repos/{owner}/{repo}/compare/{a}...{b}, repos/{owner}/{repo}/contents/{path}[?ref=<branch>]";
|
|
38262
|
+
function parseApiGetPath(rawPath) {
|
|
38263
|
+
const path2 = rawPath.trim();
|
|
38264
|
+
const refuse = () => new Error(`api get: refusing '${rawPath}' \u2014 only these GET reads are allowed (#7219): ${API_GET_ALLOWED_SHAPES}`);
|
|
38265
|
+
if (/[%#\\]/.test(path2) || path2.split("?")[0].split("/").some((seg) => seg === "." || seg === ".." || seg === "")) throw refuse();
|
|
38266
|
+
if (PULL_FILES_RE.test(path2)) return { kind: "pull-files", path: path2, paginate: true };
|
|
38267
|
+
if (COMPARE_RE.test(path2)) return { kind: "compare", path: path2, paginate: false };
|
|
38268
|
+
const q = path2.indexOf("?");
|
|
38269
|
+
const base = q === -1 ? path2 : path2.slice(0, q);
|
|
38270
|
+
const query = q === -1 ? "" : path2.slice(q + 1);
|
|
38271
|
+
if (CONTENTS_BASE_RE.test(base) && (query === "" || CONTENTS_QUERY_RE.test(query))) {
|
|
38272
|
+
return { kind: "contents", path: path2, paginate: false };
|
|
38273
|
+
}
|
|
38274
|
+
throw new Error(`api get: refusing '${rawPath}' \u2014 only these GET reads are allowed (#7219): ${API_GET_ALLOWED_SHAPES}`);
|
|
38275
|
+
}
|
|
38276
|
+
function buildApiGetArgs(path2, accept) {
|
|
38277
|
+
const req = parseApiGetPath(path2);
|
|
38278
|
+
const args = ["api", req.path];
|
|
38279
|
+
if (req.paginate) args.push("--paginate");
|
|
38280
|
+
const header = acceptHeaderFor(accept);
|
|
38281
|
+
if (header !== API_ACCEPT_HEADERS.json) args.push("-H", `Accept: ${header}`);
|
|
38282
|
+
return args;
|
|
38283
|
+
}
|
|
38284
|
+
async function runApiGet(run, path2, accept) {
|
|
38285
|
+
return run(buildApiGetArgs(path2, accept), GIT_TIMEOUT_MS);
|
|
38286
|
+
}
|
|
38287
|
+
function registerApiReadCommands(program3) {
|
|
38288
|
+
const api = program3.command("api").description("bounded GitHub REST reads for automation \u2014 GET only, allowlisted paths (pull files, compare, contents)");
|
|
38289
|
+
api.command("get <path>").description("GET one allowlisted REST path and print the response body to stdout (JSON, or raw file bytes with --accept raw); refuses everything outside the allowlist").option("--accept <type>", "response media type: json (default) or raw (file bytes)", "json").action(async (path2, o) => {
|
|
38290
|
+
let body;
|
|
38291
|
+
try {
|
|
38292
|
+
body = await runApiGet(
|
|
38293
|
+
async (args, timeoutMs) => (await execFileP("gh", args, { timeout: timeoutMs })).stdout,
|
|
38294
|
+
path2,
|
|
38295
|
+
o.accept
|
|
38296
|
+
);
|
|
38297
|
+
} catch (e) {
|
|
38298
|
+
const err = e;
|
|
38299
|
+
return fail(`api get: ${(err.stderr || err.message || String(e)).trim()}`);
|
|
38300
|
+
}
|
|
38301
|
+
process.stdout.write(body);
|
|
38302
|
+
});
|
|
38303
|
+
}
|
|
38304
|
+
|
|
38038
38305
|
// src/pr-commands.ts
|
|
38039
38306
|
var import_promises4 = require("node:fs/promises");
|
|
38040
38307
|
init_cli_shared();
|
|
@@ -43515,6 +43782,14 @@ function registerCollaborationCommands(program3) {
|
|
|
43515
43782
|
}
|
|
43516
43783
|
return resolveParentField(payload);
|
|
43517
43784
|
}
|
|
43785
|
+
async function readClosedByField(number, repo) {
|
|
43786
|
+
try {
|
|
43787
|
+
return { closedBy: await runClosedBy(queryDeps(), { number, repo }) };
|
|
43788
|
+
} catch (e) {
|
|
43789
|
+
const err = e;
|
|
43790
|
+
return { closedByReadError: (err.stderr || err.message || String(e)).trim() };
|
|
43791
|
+
}
|
|
43792
|
+
}
|
|
43518
43793
|
function withoutTransportNumber(data) {
|
|
43519
43794
|
const rest = { ...data };
|
|
43520
43795
|
delete rest.number;
|
|
@@ -43533,7 +43808,7 @@ function registerCollaborationCommands(program3) {
|
|
|
43533
43808
|
return void 0;
|
|
43534
43809
|
}
|
|
43535
43810
|
}
|
|
43536
|
-
issue.command("view <ref>").aliases(["show", "get", "read"]).description('read an issue as structured JSON \u2014 the mmi-cli path for non-board issue reads (#2347). --comments folds in every comment; --context also adds linkedPrs + children (the one-shot "load the whole item" read, #2894). `show`, `get` and `read` alias view (#5483/#5706/#6078)').option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json [fields...]", 'gh --json field list (overrides the default field set). `parent` is synthetic: selectable here, composed by the wrapper via a REST parent read, never sent to gh (#5970). Accepts commas, spaces, or repeated --json flags \u2014 in PowerShell an unquoted comma list is an array literal, so QUOTE it: --json "number,title,body,url"').option("--comments", 'include every comment (body + comments in one call) \u2014 the agentic "read the whole item before working it" read (#2894)').option("--context", "full working context in one call: implies --comments and also adds linkedPrs and, for an epic, a children summary (#2894)").action(async (ref, o) => {
|
|
43811
|
+
issue.command("view <ref>").aliases(["show", "get", "read"]).description('read an issue as structured JSON \u2014 the mmi-cli path for non-board issue reads (#2347). --comments folds in every comment; --context also adds linkedPrs + children (the one-shot "load the whole item" read, #2894). `show`, `get` and `read` alias view (#5483/#5706/#6078)').option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json [fields...]", 'gh --json field list (overrides the default field set). `parent` is synthetic: selectable here, composed by the wrapper via a REST parent read, never sent to gh (#5970). `closedBy` is synthetic too: the closing PRs with merged/mergedAt/mergeCommit.oid, composed via one fixed GraphQL read (#7219). Accepts commas, spaces, or repeated --json flags \u2014 in PowerShell an unquoted comma list is an array literal, so QUOTE it: --json "number,title,body,url"').option("--comments", 'include every comment (body + comments in one call) \u2014 the agentic "read the whole item before working it" read (#2894)').option("--context", "full working context in one call: implies --comments and also adds linkedPrs and, for an epic, a children summary (#2894)").action(async (ref, o) => {
|
|
43537
43812
|
let parsed;
|
|
43538
43813
|
try {
|
|
43539
43814
|
parsed = parseIssueRef(ref, o.repo);
|
|
@@ -43544,6 +43819,7 @@ function registerCollaborationCommands(program3) {
|
|
|
43544
43819
|
const repo = await resolveRepo(parsed.repo ?? o.repo);
|
|
43545
43820
|
if (!repo) return fail("issue view: could not resolve repo (pass --repo <owner/repo>)");
|
|
43546
43821
|
const fields = normalizeIssueViewJsonFields(o.json);
|
|
43822
|
+
const wantsClosedBy = fields.split(",").map((f) => f.trim()).includes("closedBy");
|
|
43547
43823
|
const gh = resolveIssueViewGhFields(fields);
|
|
43548
43824
|
const emit = (data, parent) => console.log(JSON.stringify({ ...gh.numberIsTransport ? withoutTransportNumber(data) : data, ...parent }));
|
|
43549
43825
|
const readIssue = async (targetRepo2, targetNumber) => {
|
|
@@ -43555,9 +43831,10 @@ function registerCollaborationCommands(program3) {
|
|
|
43555
43831
|
const data = await ghJson(["issue", "view", String(targetNumber), "--repo", targetRepo2, "--json", gh.ghFields]);
|
|
43556
43832
|
return { data, parent: await readParentField(targetNumber, targetRepo2) };
|
|
43557
43833
|
};
|
|
43834
|
+
const readClosedBy = async (targetRepo2, targetNumber) => wantsClosedBy ? readClosedByField(targetNumber, targetRepo2) : {};
|
|
43558
43835
|
try {
|
|
43559
43836
|
const { data, parent } = await readIssue(repo, n);
|
|
43560
|
-
emit(data, parent);
|
|
43837
|
+
emit(data, { ...parent, ...await readClosedBy(repo, n) });
|
|
43561
43838
|
} catch (e) {
|
|
43562
43839
|
const err = e;
|
|
43563
43840
|
const raw = (err.stderr || err.message || String(e)).trim();
|
|
@@ -43566,7 +43843,7 @@ function registerCollaborationCommands(program3) {
|
|
|
43566
43843
|
if (moved) {
|
|
43567
43844
|
try {
|
|
43568
43845
|
const { data, parent } = await readIssue(moved.repo, moved.number);
|
|
43569
|
-
emit({ ...data, transferred: { from: `${repo}#${n}`, to: `${moved.repo}#${moved.number}`, url: moved.url } }, parent);
|
|
43846
|
+
emit({ ...data, transferred: { from: `${repo}#${n}`, to: `${moved.repo}#${moved.number}`, url: moved.url } }, { ...parent, ...await readClosedBy(moved.repo, moved.number) });
|
|
43570
43847
|
return;
|
|
43571
43848
|
} catch (reRead) {
|
|
43572
43849
|
const reErr = reRead;
|
|
@@ -48711,7 +48988,7 @@ tenant.command("reconcile <owner/repo> <stage>").description("re-render this ten
|
|
|
48711
48988
|
return failGraceful(`runtime tenant reconcile: ${e.message}`);
|
|
48712
48989
|
}
|
|
48713
48990
|
});
|
|
48714
|
-
tenant.command("status <owner/repo> <stage>").description("read tenant runtime readiness without dispatching tenant-control: DEPLOY row, last deploy run, public URL probe, and TLS/Caddy/Cloudflare hints;
|
|
48991
|
+
tenant.command("status <owner/repo> <stage>").description("read tenant runtime readiness without dispatching tenant-control: DEPLOY row, last deploy run, public URL probe, and TLS/Caddy/Cloudflare hints; a root answering 401/403/404 is re-probed at /health and the `url` field names the endpoint that answered (#6137)").action(async (repo, stage) => {
|
|
48715
48992
|
if (!["dev", "rc", "main"].includes(stage)) return fail("runtime tenant status: <stage> must be dev, rc, or main");
|
|
48716
48993
|
const cfg = await loadConfig();
|
|
48717
48994
|
const result = await buildTenantRuntimeStatusFor(repo, stage, cfg);
|
|
@@ -49195,7 +49472,7 @@ ${filelessTransitionGuide(target, o.stage)}`
|
|
|
49195
49472
|
return reportWrite("org project set-deploy", res);
|
|
49196
49473
|
});
|
|
49197
49474
|
var projectTenantTasks = project.command("tenant-tasks").description("project-admin self-declare of tenant tasks for every stage of your own repo \u2014 new-task-only; editing an existing task stays master-only (`org project set --var tenantTasks=...`)");
|
|
49198
|
-
projectTenantTasks.command("declare <name>").description("register a NEW tenant task \u2014 becomes runnable via `runtime tenant control \u2026 run-task` on its declared dev/rc/main stages. Body shape {service,command[],stages[],timeoutSeconds?,artifact?}; required artifact tasks must carry {artifact} in the command argv.").option("--service <name>", "service in the tenant runtime the task runs in (required unless --var carries it)").option("--command <argv...>", "task command as argv (repeatable; required unless --var carries it)").option("--stages <list>", "comma-separated dev,rc,main stages this task may run on (required unless --var carries it)").option("--timeout-seconds <n>", "optional wall-clock ceiling 1..3600").option("--artifact <none|required>", "artifact contract (default none; required tasks must contain {artifact} in argv)").option("--var <json>", "full task object {service,command[],stages[],timeoutSeconds?,artifact?} \u2014 use instead of the individual flags").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action(async (name, o) => {
|
|
49475
|
+
projectTenantTasks.command("declare <name>").description("register a NEW tenant task \u2014 becomes runnable via `runtime tenant control \u2026 run-task` on its declared dev/rc/main stages. Body shape {service,command[],stages[],timeoutSeconds?,artifact?}; required artifact tasks must carry {artifact} in the command argv.").option("--service <name>", "service in the tenant runtime the task runs in (required unless --var carries it)").option("--command <argv...>", "task command as argv (repeatable; required unless --var carries it)").option("--stages <list>", "comma-separated dev,rc,main stages this task may run on (required unless --var carries it)").option("--timeout-seconds <n>", "optional wall-clock ceiling 1..3600").option("--artifact <none|required>", "artifact contract (default none; required tasks must contain {artifact} in argv)").option("--var <json>", "full task object {service,command[],stages[],timeoutSeconds?,artifact?} \u2014 use instead of the individual flags. A task carrying secretOutput:{path,vault:{repo,key},overwrite?} is master-only; declare that via `org project set --var tenantTasks=...`").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action(async (name, o) => {
|
|
49199
49476
|
const cfg = await loadConfig();
|
|
49200
49477
|
let target;
|
|
49201
49478
|
try {
|
|
@@ -49223,6 +49500,9 @@ projectTenantTasks.command("declare <name>").description("register a NEW tenant
|
|
|
49223
49500
|
...o.artifact !== void 0 ? { artifact: o.artifact } : {}
|
|
49224
49501
|
};
|
|
49225
49502
|
}
|
|
49503
|
+
if (task && typeof task === "object" && task.secretOutput !== void 0) {
|
|
49504
|
+
return fail("org project tenant-tasks declare: a task with secretOutput names a vault destination and is master-only (#7213) \u2014 declare it via `mmi-cli oracle org project set <owner/repo> --var 'tenantTasks=...'`");
|
|
49505
|
+
}
|
|
49226
49506
|
const res = await postJson(`/projects/${encodeURIComponent(slug)}/tenant-tasks`, { repo: target, tenantTasks: { [name]: task } }, registryClientDeps(cfg), "POST", { noRetry: true });
|
|
49227
49507
|
if (o.json) {
|
|
49228
49508
|
console.log(JSON.stringify(res, null, 2));
|
|
@@ -49370,6 +49650,7 @@ registerDeployCommands(program2);
|
|
|
49370
49650
|
registerDiscoveryCommands(program2);
|
|
49371
49651
|
registerExplainCommand(program2);
|
|
49372
49652
|
registerPrLifecycleCommands(program2);
|
|
49653
|
+
registerApiReadCommands(program2);
|
|
49373
49654
|
registerSessionReport(program2);
|
|
49374
49655
|
registerLearningClosureRateCommand(program2);
|
|
49375
49656
|
registerLearningPickupCommand(program2);
|
package/package.json
CHANGED