@mutmutco/cli 4.6.31 → 4.6.32
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 +265 -32
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -28301,10 +28301,10 @@ var rollout_plan_default = {
|
|
|
28301
28301
|
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)."
|
|
28302
28302
|
},
|
|
28303
28303
|
baseline: {
|
|
28304
|
-
version: "4.6.
|
|
28305
|
-
tag: "v4.6.
|
|
28306
|
-
commit: "
|
|
28307
|
-
npm: "@mutmutco/cli@4.6.
|
|
28304
|
+
version: "4.6.32",
|
|
28305
|
+
tag: "v4.6.32",
|
|
28306
|
+
commit: "0e477e5e2765",
|
|
28307
|
+
npm: "@mutmutco/cli@4.6.32"
|
|
28308
28308
|
},
|
|
28309
28309
|
exitCriterion: "fleet-n-of-n",
|
|
28310
28310
|
hubOnlyShortcut: "forbidden",
|
|
@@ -28321,14 +28321,14 @@ var rollout_plan_default = {
|
|
|
28321
28321
|
repo: "mutmutco/mmi-hub",
|
|
28322
28322
|
role: "canary",
|
|
28323
28323
|
schedule: "train",
|
|
28324
|
-
v3Target: "v4.6.
|
|
28324
|
+
v3Target: "v4.6.32"
|
|
28325
28325
|
}
|
|
28326
28326
|
],
|
|
28327
28327
|
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.",
|
|
28328
28328
|
rollback: {
|
|
28329
28329
|
independent: true,
|
|
28330
|
-
mechanism: "npm dist-tag latest -> 4.6.
|
|
28331
|
-
v3Target: "v4.6.
|
|
28330
|
+
mechanism: "npm dist-tag latest -> 4.6.32 and redeploy the Hub Lambda from tag v4.6.32 (0e477e5e2765); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
28331
|
+
v3Target: "v4.6.32 (@mutmutco/cli@4.6.32, tag commit 0e477e5e2765 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
28332
28332
|
}
|
|
28333
28333
|
},
|
|
28334
28334
|
{
|
|
@@ -42556,6 +42556,9 @@ async function runPrLandOnce(prNumber, options, deps) {
|
|
|
42556
42556
|
base.mergeStatus = merge.mergeStatus;
|
|
42557
42557
|
if (merge.credential) base.credential = merge.credential;
|
|
42558
42558
|
if (merge.credentialRole) base.credentialRole = merge.credentialRole;
|
|
42559
|
+
if (merge.ejectedThenReenqueued) base.ejectedThenReenqueued = true;
|
|
42560
|
+
if (merge.reconcileNeeded) base.reconcileNeeded = true;
|
|
42561
|
+
if (merge.queueRemovalEvidence) base.queueRemovalEvidence = merge.queueRemovalEvidence;
|
|
42559
42562
|
if (merge.mergeStatus === "queue-admission-pending") {
|
|
42560
42563
|
return { ...base, status: "queue-admission-pending", outcomeUnknown: true, queueAdmission: merge.queueAdmission, error: merge.error };
|
|
42561
42564
|
}
|
|
@@ -43727,6 +43730,7 @@ var autoMergeApi = async (pr, repo, head) => {
|
|
|
43727
43730
|
};
|
|
43728
43731
|
var ASYNC_HEADERS = ["-H", "Accept: application/vnd.github+json", "-H", "X-GitHub-Api-Version: 2026-03-10"];
|
|
43729
43732
|
var UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
|
|
43733
|
+
var UNWITNESSED_EJECTION_GRACE_MS = 6e4;
|
|
43730
43734
|
function admissionJournal(repo, pr, root) {
|
|
43731
43735
|
const stateRoot = root ?? (process.platform === "win32" ? (0, import_node_path31.join)(process.env.LOCALAPPDATA || (0, import_node_path31.join)((0, import_node_os18.homedir)(), "AppData", "Local"), "MMI Future", "mmi-cli", "queue-admissions") : (0, import_node_path31.join)(process.env.XDG_STATE_HOME || (0, import_node_path31.join)((0, import_node_os18.homedir)(), ".local", "state"), "mmi-cli", "queue-admissions"));
|
|
43732
43736
|
const directory = (0, import_node_path31.join)(stateRoot, (0, import_node_crypto12.createHash)("sha256").update(`${repo.toLowerCase()}#${pr}`).digest("hex"));
|
|
@@ -43740,12 +43744,12 @@ function admissionJournal(repo, pr, root) {
|
|
|
43740
43744
|
const path2 = (number) => (0, import_node_path31.join)(directory, `${number}.jsonl`);
|
|
43741
43745
|
const validate = (value, previous) => {
|
|
43742
43746
|
const row = value;
|
|
43743
|
-
if (!row || row.version !== 1 || row.repo !== repo.toLowerCase() || row.pr !== pr || !/^[a-f0-9]{40}$/i.test(row.head) || !UUID.test(row.intentId) || !Number.isSafeInteger(row.createdAt) || row.createdAt <= 0 || !Number.isSafeInteger(row.observedAt) || row.observedAt < row.createdAt || !["dispatching", "pending", "enqueued", "merged", "failed", "ejected"].includes(row.state) || row.credential !== void 0 && !["app_installation", "user"].includes(row.credential) || row.queueWitnessed !== void 0 && typeof row.queueWitnessed !== "boolean" || row.uuid !== void 0 && !UUID.test(row.uuid) || row.state === "pending" && !row.uuid || row.queueEntry !== void 0 && (!row.queueEntry || typeof row.queueEntry.headCommitOid !== "string" || !/^[a-f0-9]{40}$/i.test(row.queueEntry.headCommitOid) || typeof row.queueEntry.enqueuedAt !== "string" || !Number.isFinite(Date.parse(row.queueEntry.enqueuedAt)))) {
|
|
43747
|
+
if (!row || row.version !== 1 || row.repo !== repo.toLowerCase() || row.pr !== pr || !/^[a-f0-9]{40}$/i.test(row.head) || !UUID.test(row.intentId) || !Number.isSafeInteger(row.createdAt) || row.createdAt <= 0 || !Number.isSafeInteger(row.observedAt) || row.observedAt < row.createdAt || !["dispatching", "pending", "enqueued", "merged", "failed", "ejected", "abandoned"].includes(row.state) || row.credential !== void 0 && !["app_installation", "user"].includes(row.credential) || row.queueWitnessed !== void 0 && typeof row.queueWitnessed !== "boolean" || row.uuid !== void 0 && !UUID.test(row.uuid) || row.state === "pending" && !row.uuid || row.queueEntry !== void 0 && (!row.queueEntry || typeof row.queueEntry.headCommitOid !== "string" || !/^[a-f0-9]{40}$/i.test(row.queueEntry.headCommitOid) || typeof row.queueEntry.enqueuedAt !== "string" || !Number.isFinite(Date.parse(row.queueEntry.enqueuedAt)))) {
|
|
43744
43748
|
throw new Error("queue admission receipt is invalid; preserve it and reconcile the prior request");
|
|
43745
43749
|
}
|
|
43746
43750
|
if (!previous) {
|
|
43747
43751
|
if (row.state !== "dispatching" || row.uuid || row.queueEntry || row.observedAt !== row.createdAt) throw new Error("queue admission intent record is invalid");
|
|
43748
|
-
} else if (row.head !== previous.head || row.intentId !== previous.intentId || row.createdAt !== previous.createdAt || row.observedAt < previous.observedAt || row.state === "dispatching" || previous.uuid && row.uuid !== previous.uuid || previous.credential !== void 0 && row.credential !== previous.credential || ["enqueued", "merged", "failed", "ejected"].includes(previous.state) && (row.state !== previous.state || row.uuid !== previous.uuid) && !(previous.state === "enqueued" && row.state
|
|
43752
|
+
} else if (row.head !== previous.head || row.intentId !== previous.intentId || row.createdAt !== previous.createdAt || row.observedAt < previous.observedAt || row.state === "dispatching" || previous.uuid && row.uuid !== previous.uuid || previous.credential !== void 0 && row.credential !== previous.credential || ["enqueued", "merged", "failed", "ejected"].includes(previous.state) && (row.state !== previous.state || row.uuid !== previous.uuid) && !(previous.state === "enqueued" && ["ejected", "abandoned"].includes(row.state) && row.uuid === previous.uuid)) {
|
|
43749
43753
|
throw new Error("queue admission receipt has an illegal transition or changed operation identity");
|
|
43750
43754
|
}
|
|
43751
43755
|
return row;
|
|
@@ -43828,13 +43832,23 @@ function admissionRejected(reason, repo, pr) {
|
|
|
43828
43832
|
};
|
|
43829
43833
|
}
|
|
43830
43834
|
async function submitQueueAdmission(pr, repo, head, read, write, options, resume = false) {
|
|
43831
|
-
|
|
43835
|
+
let reenqueuedAfterEjection = false;
|
|
43836
|
+
let removalEvidence;
|
|
43837
|
+
const markReenqueued = (result) => reenqueuedAfterEjection ? {
|
|
43838
|
+
...result,
|
|
43839
|
+
ejectedThenReenqueued: true,
|
|
43840
|
+
...removalEvidence ? { queueRemovalEvidence: removalEvidence } : {},
|
|
43841
|
+
...result.error ? { error: `re-enqueued after ejection: ${result.error}` } : {}
|
|
43842
|
+
} : result;
|
|
43843
|
+
const unknown = (reason) => markReenqueued({
|
|
43832
43844
|
mergeStatus: "failed",
|
|
43833
43845
|
retryable: false,
|
|
43834
43846
|
outcomeUnknown: true,
|
|
43835
|
-
|
|
43847
|
+
reconcileNeeded: true,
|
|
43848
|
+
...removalEvidence ? { queueRemovalEvidence: removalEvidence } : {},
|
|
43849
|
+
error: `queue submission outcome unknown: ${reason}. Never hand-edit files under the mmi-cli queue-admissions receipt store (%LOCALAPPDATA%/MMI Future/mmi-cli/queue-admissions); read the queue entry with \`mmi-cli devops read pr queue ${pr} --repo ${repo} --json\`; once the PR is verifiably OPEN at the submitted head and absent from the queue, abandon the stuck admission with \`mmi-cli devops pr land ${pr} --repo ${repo} --abandon-admission\` so a new intent needs no new head \u2014 no new request was sent after this uncertainty.`
|
|
43836
43850
|
});
|
|
43837
|
-
const rejected = (reason) => admissionRejected(reason, repo, pr);
|
|
43851
|
+
const rejected = (reason) => markReenqueued(admissionRejected(reason, repo, pr));
|
|
43838
43852
|
const reconcile = async (reason, recordEntry = false, resumingSaved = false) => {
|
|
43839
43853
|
const maxReads = options.enqueueOnly ? 3 : 1;
|
|
43840
43854
|
const sleep3 = options.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms)));
|
|
@@ -43842,13 +43856,13 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
43842
43856
|
if (readAttempt) await sleep3(readAttempt * 1e3);
|
|
43843
43857
|
try {
|
|
43844
43858
|
const observed = await readPrMergeQueue(pr, repo, read);
|
|
43845
|
-
if (observed.state === "MERGED" && observed.headRefOid === head) return { mergeStatus: "merged" };
|
|
43859
|
+
if (observed.state === "MERGED" && observed.headRefOid === head) return markReenqueued({ mergeStatus: "merged" });
|
|
43846
43860
|
if (observed.isInMergeQueue && observed.headRefOid === head) {
|
|
43847
43861
|
const entry = observed.mergeQueueEntry;
|
|
43848
43862
|
if (recordEntry && receipt?.head === head && entry?.headCommit && entry.enqueuedAt) {
|
|
43849
43863
|
journal.save("enqueued", receipt?.uuid, { headCommitOid: entry.headCommit.oid, enqueuedAt: entry.enqueuedAt });
|
|
43850
43864
|
}
|
|
43851
|
-
return { mergeStatus: "auto-merge-enqueued" };
|
|
43865
|
+
return markReenqueued({ mergeStatus: "auto-merge-enqueued" });
|
|
43852
43866
|
}
|
|
43853
43867
|
const saved = recordEntry ? admissionJournal(repo, pr, options.receiptDirectory).receipt : void 0;
|
|
43854
43868
|
const boundMs = saved?.queueEntry ? Date.parse(saved.queueEntry.enqueuedAt) : resumingSaved && saved?.state === "enqueued" && saved.queueWitnessed === void 0 ? saved.observedAt : void 0;
|
|
@@ -43880,11 +43894,37 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
43880
43894
|
let receipt = journal.receipt;
|
|
43881
43895
|
let response;
|
|
43882
43896
|
let superseded = false;
|
|
43883
|
-
if (receipt && (receipt.state === "enqueued" || receipt.state === "merged")
|
|
43884
|
-
|
|
43885
|
-
|
|
43886
|
-
|
|
43887
|
-
|
|
43897
|
+
if (receipt && (receipt.state === "enqueued" || receipt.state === "merged")) {
|
|
43898
|
+
if (receipt.head.toLowerCase() !== head.toLowerCase()) {
|
|
43899
|
+
try {
|
|
43900
|
+
const observed = await readPrMergeQueue(pr, repo, read);
|
|
43901
|
+
superseded = observed.state === "OPEN" && !observed.isInMergeQueue;
|
|
43902
|
+
} catch {
|
|
43903
|
+
}
|
|
43904
|
+
} else {
|
|
43905
|
+
try {
|
|
43906
|
+
const observed = await readPrMergeQueue(pr, repo, read);
|
|
43907
|
+
if (observed.state === "OPEN" && !observed.isInMergeQueue && observed.headRefOid?.toLowerCase() === receipt.head.toLowerCase()) {
|
|
43908
|
+
const boundMs = receipt.queueEntry ? Date.parse(receipt.queueEntry.enqueuedAt) : receipt.observedAt + UNWITNESSED_EJECTION_GRACE_MS;
|
|
43909
|
+
const events = await readArrayPages(
|
|
43910
|
+
`repos/${repo}/issues/${pr}/timeline?per_page=100`,
|
|
43911
|
+
options.timelineRead ?? read,
|
|
43912
|
+
"queue ejection timeline unreadable"
|
|
43913
|
+
);
|
|
43914
|
+
const removals = events.filter((event) => event.event === "removed_from_merge_queue" && Number.isFinite(Date.parse(event.created_at))).map((event) => Date.parse(event.created_at)).filter((at) => at > boundMs).sort((a, b) => a - b);
|
|
43915
|
+
removalEvidence = {
|
|
43916
|
+
source: receipt.queueEntry ? "queue-entry" : "receipt-checkpoint",
|
|
43917
|
+
boundAt: new Date(boundMs).toISOString(),
|
|
43918
|
+
removalsAfterBound: removals.map((at) => new Date(at).toISOString()),
|
|
43919
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
43920
|
+
};
|
|
43921
|
+
if (removals.length > 0) {
|
|
43922
|
+
reenqueuedAfterEjection = true;
|
|
43923
|
+
superseded = true;
|
|
43924
|
+
}
|
|
43925
|
+
}
|
|
43926
|
+
} catch {
|
|
43927
|
+
}
|
|
43888
43928
|
}
|
|
43889
43929
|
}
|
|
43890
43930
|
if (superseded && receipt?.state === "enqueued") {
|
|
@@ -43894,7 +43934,7 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
43894
43934
|
return unknown("the superseded admission could not be checkpointed");
|
|
43895
43935
|
}
|
|
43896
43936
|
}
|
|
43897
|
-
if (receipt && receipt.state !== "failed" && receipt.state !== "ejected" && !superseded) {
|
|
43937
|
+
if (receipt && receipt.state !== "failed" && receipt.state !== "ejected" && receipt.state !== "abandoned" && !superseded) {
|
|
43898
43938
|
if (options.selection) {
|
|
43899
43939
|
options.selection.selected = true;
|
|
43900
43940
|
options.selection.intentId = receipt.intentId;
|
|
@@ -44040,13 +44080,13 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
|
|
|
44040
44080
|
if (receipt.head !== head) return reconcile("the previous head completed admission; inspect current head and membership before continuing");
|
|
44041
44081
|
return reconcile(`operation ${receipt.uuid} reported ${status}, but matching-head membership or merged state is unconfirmed`, status === "enqueued");
|
|
44042
44082
|
}
|
|
44043
|
-
return {
|
|
44083
|
+
return markReenqueued({
|
|
44044
44084
|
mergeStatus: "queue-admission-pending",
|
|
44045
44085
|
retryable: false,
|
|
44046
44086
|
outcomeUnknown: true,
|
|
44047
44087
|
queueAdmission: { state: "pending", uuid: receipt.uuid, head: receipt.head },
|
|
44048
44088
|
error: "GitHub accepted queue admission but has not confirmed queue membership. Rerun pr land to resume the saved operation read-only."
|
|
44049
|
-
};
|
|
44089
|
+
});
|
|
44050
44090
|
}
|
|
44051
44091
|
function githubQueueSquashMessage(title, pr, commits) {
|
|
44052
44092
|
const list = commits.map((commit) => `* ${commit.headline}
|
|
@@ -44251,6 +44291,38 @@ async function enqueuePrMergeQueue(pr, repo, options = {}) {
|
|
|
44251
44291
|
const result = await submitQueueAdmission(pr, repo, head, queueRead, write, { ...options, timelineRead: read, selection });
|
|
44252
44292
|
return { ...stamp(result, selection.selected, selection.intentId), ...inspected.warning ? { warning: inspected.warning } : {} };
|
|
44253
44293
|
}
|
|
44294
|
+
async function abandonQueueAdmission(pr, repo, options = {}) {
|
|
44295
|
+
if (!/^\d+$/.test(pr)) return { status: "refused", repo, pr, reason: "a bare PR number is required" };
|
|
44296
|
+
const refuse = (reason) => ({ status: "refused", repo, pr, reason });
|
|
44297
|
+
let journal;
|
|
44298
|
+
try {
|
|
44299
|
+
journal = admissionJournal(repo, pr, options.receiptDirectory);
|
|
44300
|
+
} catch (error) {
|
|
44301
|
+
return refuse(`the durable admission receipt cannot be read (${error.message})`);
|
|
44302
|
+
}
|
|
44303
|
+
const receipt = journal.receipt;
|
|
44304
|
+
if (!receipt) return refuse("no queue-admission receipt exists for this PR; nothing to abandon");
|
|
44305
|
+
if (!["pending", "enqueued"].includes(receipt.state)) {
|
|
44306
|
+
return refuse(`the saved admission is '${receipt.state}', not a stuck non-terminal 'pending'/'enqueued' receipt; nothing to abandon`);
|
|
44307
|
+
}
|
|
44308
|
+
let snapshot;
|
|
44309
|
+
try {
|
|
44310
|
+
snapshot = await readPrMergeQueue(pr, repo, options.read ?? actorReadApi);
|
|
44311
|
+
} catch (error) {
|
|
44312
|
+
return refuse(`the PR and queue state could not be read (${error.message}); the admission is unchanged`);
|
|
44313
|
+
}
|
|
44314
|
+
if (snapshot.state !== "OPEN") return refuse(`the PR is '${snapshot.state}', not OPEN; the admission is unchanged`);
|
|
44315
|
+
if (!snapshot.headRefOid || snapshot.headRefOid.toLowerCase() !== receipt.head.toLowerCase()) {
|
|
44316
|
+
return refuse(`the PR head is ${snapshot.headRefOid ?? "unreadable"}, not the receipt's submitted head ${receipt.head}; the admission is unchanged`);
|
|
44317
|
+
}
|
|
44318
|
+
if (snapshot.isInMergeQueue) return refuse("the PR is currently in the merge queue, so the admission is not stuck; the admission is unchanged");
|
|
44319
|
+
try {
|
|
44320
|
+
journal.save("abandoned");
|
|
44321
|
+
} catch (error) {
|
|
44322
|
+
return refuse(`the abandoned state could not be checkpointed (${error.message}); the admission is unchanged`);
|
|
44323
|
+
}
|
|
44324
|
+
return { status: "abandoned", repo, pr, head: receipt.head, ...receipt.uuid ? { operation: receipt.uuid } : {} };
|
|
44325
|
+
}
|
|
44254
44326
|
function closingGuardDiagnostic(message2) {
|
|
44255
44327
|
if (!message2) return "";
|
|
44256
44328
|
const remedy = message2.search(/GitHub's merge queue composes|If this PR is meant to close/);
|
|
@@ -46163,7 +46235,7 @@ function refuseNotClaimable(e, json) {
|
|
|
46163
46235
|
process.exitCode = 1;
|
|
46164
46236
|
return true;
|
|
46165
46237
|
}
|
|
46166
|
-
var BOARD_READ_HELP = "\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\nThe --json report is an OBJECT \u2014 never an array, and there is no top-level `items` list:\n {project, viewer, repo, primary, secondary, warnings, partial[, source, builtAt, eventCursor, ...]}\n`primary` is this repo and `secondary` the other repos in scope; each is a bucket object\n {userOwned, claimable, taken, unownedInFlight}\nand each bucket is an array of items. Filter the BUCKET, not the report:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n node -e \"const b=require('./.jerv/tmp/board.json');console.log(b.primary.claimable.length)\"\n\nAdd --flat with --json for one array across both scopes: {ref,number,title,status,assignees,labels,bucket,claim,openPullRequests}\n(#9595: claim is the parsed mmi-claim evidence object, null when unclaimed; openPullRequests lists the OPEN\n PRs whose closing-issue body links name the item. On the FULL JSON object that list lives at\n `report.pullRequests` \u2014 there is no top-level `openPullRequests`, and #9596 hand-written projections\n that assume one crash on `.filter`.)\n\nA cached snapshot read renders `source: cache \xB7 built <timestamp> \xB7 fresh|stale` (#9584) and carries\n`builtAt` under --json, so a same-turn re-read of the same builtAt buys nothing new.\n\nUnder --json, parse stdout ONLY and check the exit code first. A failure still prints a\nhuman-readable reason on stderr, so never pipe 2>&1 into a JSON parser (#9151) \u2014 the merged\ntext is not JSON and hides the real board failure.\n\nA zero-byte or empty .jerv/tmp/*.json means the read FAILED \u2014 `Unexpected end of JSON input` is\nthe symptom of shell-redirected empty stdout, not malformed JSON \u2014 re-run with --out (which writes\nthe file only on success) and check the exit code before parsing.\n\nChanging an existing item's Status is a move, never a hand-written `gh api graphql`\n`updateProjectV2ItemFieldValue` mutation with copied `PVT_`/`PVTI_` node ids and a manual Status\nfield-id lookup:\n mmi-cli oracle board move <status> <ref>\n";
|
|
46238
|
+
var BOARD_READ_HELP = "\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\nThe --json report is an OBJECT \u2014 never an array, and there is no top-level `items` list:\n {project, viewer, repo, primary, secondary, warnings, partial[, source, builtAt, eventCursor, ...]}\n`primary` is this repo and `secondary` the other repos in scope; each is a bucket object\n {userOwned, claimable, taken, unownedInFlight}\nand each bucket is an array of items. Filter the BUCKET, not the report:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n node -e \"const b=require('./.jerv/tmp/board.json');console.log(b.primary.claimable.length)\"\n\nAdd --flat with --json for one array across both scopes: {ref,number,title,status,assignees,labels,bucket,claim,openPullRequests}\n(#9595: claim is the parsed mmi-claim evidence object, null when unclaimed; openPullRequests lists the OPEN\n PRs whose closing-issue body links name the item. On the FULL JSON object that list lives at\n `report.pullRequests` \u2014 there is no top-level `openPullRequests`, and #9596 hand-written projections\n that assume one crash on `.filter`.)\n\nNever sniff the envelope \u2014 `j.items ?? j.rows ?? j`, `Object.values(j)` or any generic \"find the list\"\nwrapper throws `items is not iterable` before any row is read (#9956). For a number|status|claim|title\nprojection across non-Done rows, read --json --flat --out .jerv/tmp/board.json and filter that array locally:\n mmi-cli oracle board read --json --flat --out .jerv/tmp/board.json\n\nA cached snapshot read renders `source: cache \xB7 built <timestamp> \xB7 fresh|stale` (#9584) and carries\n`builtAt` under --json, so a same-turn re-read of the same builtAt buys nothing new.\n\nUnder --json, parse stdout ONLY and check the exit code first. A failure still prints a\nhuman-readable reason on stderr, so never pipe 2>&1 into a JSON parser (#9151) \u2014 the merged\ntext is not JSON and hides the real board failure.\n\nA zero-byte or empty .jerv/tmp/*.json means the read FAILED \u2014 `Unexpected end of JSON input` is\nthe symptom of shell-redirected empty stdout, not malformed JSON \u2014 re-run with --out (which writes\nthe file only on success) and check the exit code before parsing.\n\nChanging an existing item's Status is a move, never a hand-written `gh api graphql`\n`updateProjectV2ItemFieldValue` mutation with copied `PVT_`/`PVTI_` node ids and a manual Status\nfield-id lookup:\n mmi-cli oracle board move <status> <ref>\n";
|
|
46167
46239
|
function flattenBoardReport(report) {
|
|
46168
46240
|
const closingByRef = /* @__PURE__ */ new Map();
|
|
46169
46241
|
for (const pull of report.pullRequests ?? []) {
|
|
@@ -60899,6 +60971,32 @@ function rejectionReason(error) {
|
|
|
60899
60971
|
const text = (error instanceof Error ? error.message : String(error)).trim().split("\n")[0] || "no error detail";
|
|
60900
60972
|
return text.length > 300 ? `${text.slice(0, 300)}\u2026` : text;
|
|
60901
60973
|
}
|
|
60974
|
+
var PERMISSION_REFUSAL = /Resource not accessible|HTTP 40[13]|40[13] (?:Forbidden|Unauthorized)|Bad credentials|must have admin rights|insufficient (?:permissions?|scope)|not authorized/i;
|
|
60975
|
+
async function observePolicy(deps, candidate) {
|
|
60976
|
+
if (!candidate) return void 0;
|
|
60977
|
+
try {
|
|
60978
|
+
return await deps.policy(candidate);
|
|
60979
|
+
} catch (error) {
|
|
60980
|
+
const refusal = rejectionReason(error);
|
|
60981
|
+
if (!PERMISSION_REFUSAL.test(refusal)) throw error;
|
|
60982
|
+
await deps.fetchCandidate?.(candidate).catch(() => void 0);
|
|
60983
|
+
let retried;
|
|
60984
|
+
try {
|
|
60985
|
+
retried = await deps.policy(candidate);
|
|
60986
|
+
} catch (retryError) {
|
|
60987
|
+
const second = rejectionReason(retryError);
|
|
60988
|
+
if (!PERMISSION_REFUSAL.test(second)) throw retryError;
|
|
60989
|
+
return {
|
|
60990
|
+
check: "policy",
|
|
60991
|
+
state: "skipped",
|
|
60992
|
+
detail: `committed test policy was not observed: GitHub refused the policy read twice (${second}) \u2014 the identity running pr create lacks the permission that observation needs, repository Administration: read, which covers rulesets and the merge-queue configuration; grant it to the App installation (APP_POLL_PERMISSIONS or the installed App's permissions on ${candidate.repo}) or run with an owner GH_TOKEN. This is an unreadable guard, NOT a policy violation.`
|
|
60993
|
+
};
|
|
60994
|
+
}
|
|
60995
|
+
process.stderr.write(`pr create: WARNING \u2014 the committed test-policy read was refused (${refusal}); re-fetched the candidate objects and retried it once (#9946)
|
|
60996
|
+
`);
|
|
60997
|
+
return retried;
|
|
60998
|
+
}
|
|
60999
|
+
}
|
|
60902
61000
|
async function preparePrCreate(input, deps = createPreparationDeps(input)) {
|
|
60903
61001
|
const observations = [];
|
|
60904
61002
|
const refs = await deps.candidate().catch((error) => ({ findings: [`candidate refs unavailable (${rejectionReason(error)}); verify the repository and pushed head/base before retrying`] }));
|
|
@@ -60907,7 +61005,7 @@ async function preparePrCreate(input, deps = createPreparationDeps(input)) {
|
|
|
60907
61005
|
const reads = await Promise.allSettled([
|
|
60908
61006
|
deps.claims(refs.candidate),
|
|
60909
61007
|
refs.candidate ? deps.docs(refs.candidate) : Promise.resolve(void 0),
|
|
60910
|
-
|
|
61008
|
+
observePolicy(deps, refs.candidate)
|
|
60911
61009
|
]);
|
|
60912
61010
|
const [claims, docs, policy] = reads;
|
|
60913
61011
|
if (claims.status === "fulfilled") {
|
|
@@ -60944,7 +61042,7 @@ async function preparePrCreate(input, deps = createPreparationDeps(input)) {
|
|
|
60944
61042
|
await claims.value.restamp();
|
|
60945
61043
|
const beforeCreate = await deps.candidate();
|
|
60946
61044
|
if (beforeCreate.findings.length || JSON.stringify(beforeCreate.candidate) !== JSON.stringify(expected)) {
|
|
60947
|
-
throw new Error("pr create: candidate changed while restamping claims; prepare
|
|
61045
|
+
throw new Error("pr create: candidate identity changed during preparation while restamping claims; prepare the current head and base again");
|
|
60948
61046
|
}
|
|
60949
61047
|
}
|
|
60950
61048
|
};
|
|
@@ -61017,6 +61115,15 @@ function createPreparationDeps(input, io = {}) {
|
|
|
61017
61115
|
return {
|
|
61018
61116
|
candidate: readCandidate,
|
|
61019
61117
|
publish: (proof) => publishPreparedPrHead(proof, exec3),
|
|
61118
|
+
// #9946: the retry needs the exact candidate objects. Fetch each ref on its own so a fork-only head
|
|
61119
|
+
// that origin cannot resolve never costs the base fetch too, and never surface a fetch failure as the
|
|
61120
|
+
// reason the policy read stayed unreadable.
|
|
61121
|
+
fetchCandidate: async (candidate) => {
|
|
61122
|
+
if (!candidate.root) return;
|
|
61123
|
+
const branch = (name) => name.slice(name.lastIndexOf(":") + 1);
|
|
61124
|
+
const refspecs = [.../* @__PURE__ */ new Set([candidate.head, candidate.base])].map((name) => `+refs/heads/${branch(name)}:refs/remotes/origin/${branch(name)}`);
|
|
61125
|
+
await Promise.allSettled(refspecs.map((refspec) => exec3("git", ["-C", candidate.root, "fetch", "--no-tags", "origin", refspec], { timeout: GIT_TIMEOUT_MS })));
|
|
61126
|
+
},
|
|
61020
61127
|
claims: (candidate) => observePrCreateClaims(input.body, candidate?.repo ?? input.repo, { head: candidate?.head, headSha: candidate?.headSha }),
|
|
61021
61128
|
docs: (candidate) => checkDocsIndexAtHead({ repo: candidate.repo, head: candidate.headSha }, docs),
|
|
61022
61129
|
policy: async (candidate) => {
|
|
@@ -62672,6 +62779,46 @@ var RESIDUE_LEDGER_FILE = "mmi-worktree-residue.json";
|
|
|
62672
62779
|
function canonicalWorktreeRoot(primary) {
|
|
62673
62780
|
return (0, import_node_path49.join)((0, import_node_path49.dirname)(primary), "mmi-worktrees", (0, import_node_path49.basename)(primary));
|
|
62674
62781
|
}
|
|
62782
|
+
function lastSegment(p) {
|
|
62783
|
+
return (0, import_node_path49.basename)(p.split(/[\\/]+/).filter(Boolean).pop() ?? p);
|
|
62784
|
+
}
|
|
62785
|
+
function readProvenance(folder, gitCommonDir, exists) {
|
|
62786
|
+
const found = {
|
|
62787
|
+
path: folder,
|
|
62788
|
+
gitdirPresent: false,
|
|
62789
|
+
registrationId: (0, import_node_path49.basename)(folder),
|
|
62790
|
+
registrationPresent: false
|
|
62791
|
+
};
|
|
62792
|
+
try {
|
|
62793
|
+
const pointer = /^\s*gitdir:\s*(.+?)\s*$/m.exec((0, import_node_fs53.readFileSync)((0, import_node_path49.join)(folder, ".git"), "utf8"));
|
|
62794
|
+
if (pointer) {
|
|
62795
|
+
const target = pointer[1];
|
|
62796
|
+
found.gitdir = (0, import_node_path49.isAbsolute)(target) ? target : (0, import_node_path49.join)(folder, target);
|
|
62797
|
+
found.gitdirPresent = exists(found.gitdir);
|
|
62798
|
+
found.registrationId = lastSegment(found.gitdir);
|
|
62799
|
+
}
|
|
62800
|
+
} catch {
|
|
62801
|
+
}
|
|
62802
|
+
try {
|
|
62803
|
+
found.registrationPresent = exists((0, import_node_path49.join)(gitCommonDir, "worktrees", found.registrationId));
|
|
62804
|
+
} catch {
|
|
62805
|
+
}
|
|
62806
|
+
if (found.gitdir && found.gitdirPresent) {
|
|
62807
|
+
try {
|
|
62808
|
+
const head = (0, import_node_fs53.readFileSync)((0, import_node_path49.join)(found.gitdir, "HEAD"), "utf8").trim();
|
|
62809
|
+
const branch = /^ref:\s*(.+?)\s*$/.exec(head);
|
|
62810
|
+
if (branch) found.head = branch[1].startsWith("refs/heads/") ? branch[1].slice("refs/heads/".length) : `ref ${branch[1]}`;
|
|
62811
|
+
else if (/^[0-9a-f]{7,64}$/i.test(head)) found.head = `detached ${head.slice(0, 12)}`;
|
|
62812
|
+
else if (head) found.head = `ref ${head}`;
|
|
62813
|
+
} catch {
|
|
62814
|
+
}
|
|
62815
|
+
}
|
|
62816
|
+
try {
|
|
62817
|
+
found.mtime = (0, import_node_fs53.statSync)(folder).mtime.toISOString();
|
|
62818
|
+
} catch {
|
|
62819
|
+
}
|
|
62820
|
+
return found;
|
|
62821
|
+
}
|
|
62675
62822
|
function norm2(p) {
|
|
62676
62823
|
const s = p.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
62677
62824
|
return process.platform === "win32" ? s.toLowerCase() : s;
|
|
@@ -62766,7 +62913,7 @@ async function sweepWorktreeResidue(gitCommonDir, registered, deps = {}) {
|
|
|
62766
62913
|
} catch {
|
|
62767
62914
|
}
|
|
62768
62915
|
} else {
|
|
62769
|
-
report.unrecorded.push(child2);
|
|
62916
|
+
report.unrecorded.push(readProvenance(child2, gitCommonDir, exists));
|
|
62770
62917
|
}
|
|
62771
62918
|
}
|
|
62772
62919
|
}
|
|
@@ -62780,12 +62927,19 @@ function renderResidueSweepLines(report) {
|
|
|
62780
62927
|
if (cleaned) lines2.push(`pr merge: removed ${cleaned} leftover worktree folder(s) from earlier merges${where}`);
|
|
62781
62928
|
for (const k of report.kept) lines2.push(`pr merge: leftover folder still locked, retried on the next merge \u2014 ${k.path}`);
|
|
62782
62929
|
if (report.unrecorded.length) {
|
|
62783
|
-
const
|
|
62930
|
+
const listed = report.unrecorded.slice(0, 5);
|
|
62784
62931
|
const more = report.unrecorded.length > 5 ? `, \u2026 ${report.unrecorded.length - 5} more` : "";
|
|
62785
|
-
lines2.push(`pr merge: ${report.unrecorded.length} unregistered folder(s) with content under the worktree root${where} were left untouched (not recorded by a merge): ${
|
|
62932
|
+
lines2.push(`pr merge: ${report.unrecorded.length} unregistered folder(s) with content under the worktree root${where} were left untouched (not recorded by a merge): ${listed.map((u) => u.path).join(", ")}${more}`);
|
|
62933
|
+
for (const u of listed) lines2.push(`pr merge: left untouched ${u.path} \u2014 ${provenanceText(u)}`);
|
|
62786
62934
|
}
|
|
62787
62935
|
return lines2;
|
|
62788
62936
|
}
|
|
62937
|
+
function provenanceText(u) {
|
|
62938
|
+
const pointer = u.gitdir ? `.git \u2192 ${u.gitdir}${u.gitdirPresent ? "" : " (admin dir gone)"}` : ".git pointer missing";
|
|
62939
|
+
const registration = `worktrees/${u.registrationId} ${u.registrationPresent ? "registered" : "not registered"}`;
|
|
62940
|
+
const head = u.head ? `HEAD ${u.head}` : "HEAD unknown";
|
|
62941
|
+
return `${pointer}; ${registration}; ${head}; ${u.mtime ? `modified ${u.mtime}` : "mtime unknown"}`;
|
|
62942
|
+
}
|
|
62789
62943
|
|
|
62790
62944
|
// src/command-register-collaboration.ts
|
|
62791
62945
|
var DEFAULT_PRIORITY = "medium";
|
|
@@ -63935,9 +64089,19 @@ ${preparation.findings.map((finding) => `[${finding.check}/${finding.state}] ${f
|
|
|
63935
64089
|
}
|
|
63936
64090
|
class PrHeadBehindBaseError extends Error {
|
|
63937
64091
|
}
|
|
63938
|
-
pr.command("land <numbers...>").description("agent merge path (#1440): train probe \u2014 checks-wait \u2014 merge --auto \u2014 poll enqueued \u2014 development PRs only. Several PR numbers land together (#8322): all are enqueued, waited on together, a queue-ejected PR is updated and re-enqueued once, a queue merge is followed by worktree/branch cleanup and board Done, and one table (or --json) reports each outcome").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--enqueue-only", "return once queue membership is confirmed; without a merge queue, land normally").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").option("--force", "silence the leftover-scratch warning (scratch never blocks, #8785) and acknowledge a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal").option("--without-review <reason>", "compatibility option; shared merge helpers do not require a review verdict").option("--timeout <minutes>", `merge-queue wait per PR (default ${PR_LAND_QUEUE_TIMEOUT_MS / 6e4} minutes, one window per ${PR_LAND_QUEUE_BATCH_SIZE} PRs \u2014 per-repo ruleset check_response_timeout_minutes is 15 default with 30/45/60 overrides in .github/rulesets/mmi-development-merge-queue.json; agents never edit the ruleset to shorten a queue wait)`).option("--preserve-worktree", "after a queue merge keep the local worktree and branch (skips the post-merge cleanup)").option("--expect-head <sha>", "pin the PR head to this full 40-hex SHA (#8456): checked before landing, after checks and by GitHub at enqueue (--match-head-commit); never updates a BEHIND head and never retries an ejection \u2014 it stops with pinnedHead.refused instead. One PR only").action(async (numbers, opts) => {
|
|
64092
|
+
pr.command("land <numbers...>").description("agent merge path (#1440): train probe \u2014 checks-wait \u2014 merge --auto \u2014 poll enqueued \u2014 development PRs only. Several PR numbers land together (#8322): all are enqueued, waited on together, a queue-ejected PR is updated and re-enqueued once, a queue merge is followed by worktree/branch cleanup and board Done, and one table (or --json) reports each outcome").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--enqueue-only", "return once queue membership is confirmed; without a merge queue, land normally").option("--abandon-admission", "one stuck-receipt recovery, one PR per call: journal the saved non-terminal (pending/enqueued) queue-admission receipt terminal ONLY after positively observing the PR OPEN at the receipt head and absent from the queue; any unprovable observation refuses and changes nothing (#9951). The next pr land then submits a new intent with no new head").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").option("--force", "silence the leftover-scratch warning (scratch never blocks, #8785) and acknowledge a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal").option("--without-review <reason>", "compatibility option; shared merge helpers do not require a review verdict").option("--timeout <minutes>", `merge-queue wait per PR (default ${PR_LAND_QUEUE_TIMEOUT_MS / 6e4} minutes, one window per ${PR_LAND_QUEUE_BATCH_SIZE} PRs \u2014 per-repo ruleset check_response_timeout_minutes is 15 default with 30/45/60 overrides in .github/rulesets/mmi-development-merge-queue.json; agents never edit the ruleset to shorten a queue wait)`).option("--preserve-worktree", "after a queue merge keep the local worktree and branch (skips the post-merge cleanup)").option("--expect-head <sha>", "pin the PR head to this full 40-hex SHA (#8456): checked before landing, after checks and by GitHub at enqueue (--match-head-commit); never updates a BEHIND head and never retries an ejection \u2014 it stops with pinnedHead.refused instead. One PR only").action(async (numbers, opts) => {
|
|
63939
64093
|
const timeoutMinutes = opts.timeout === void 0 ? void 0 : Number(opts.timeout);
|
|
63940
64094
|
if (timeoutMinutes !== void 0 && !(timeoutMinutes > 0)) return fail("pr land: --timeout must be a positive number of minutes");
|
|
64095
|
+
if (opts.abandonAdmission) {
|
|
64096
|
+
if (numbers.length !== 1) return fail("pr land: --abandon-admission abandons one PR per call");
|
|
64097
|
+
const repo = await resolveRepo(opts.repo).catch(() => void 0) ?? opts.repo;
|
|
64098
|
+
if (!repo) return fail("pr land: --abandon-admission needs --repo <owner/repo>; the PR repo could not be resolved");
|
|
64099
|
+
const outcome = await abandonQueueAdmission(numbers[0], repo);
|
|
64100
|
+
if (opts.json) printLine(JSON.stringify(outcome));
|
|
64101
|
+
else printLine(outcome.status === "abandoned" ? `pr land: abandoned the stuck queue admission of PR #${outcome.pr} on ${repo}${outcome.operation ? ` (operation ${outcome.operation})` : ""} at head ${outcome.head} \u2014 the next pr land submits a new intent with no new head` : `pr land: refused to abandon the queue admission of PR #${outcome.pr} on ${repo} \u2014 ${outcome.reason}`);
|
|
64102
|
+
if (outcome.status !== "abandoned") process.exitCode = 1;
|
|
64103
|
+
return;
|
|
64104
|
+
}
|
|
63941
64105
|
if (opts.expectHead !== void 0) {
|
|
63942
64106
|
if (!/^[0-9a-f]{40}$/i.test(opts.expectHead)) return fail("pr land: --expect-head needs the full 40-hex head SHA");
|
|
63943
64107
|
if (numbers.length !== 1) return fail("pr land: --expect-head pins one PR head; land one PR per call");
|
|
@@ -71555,6 +71719,54 @@ function renderTenantLogs(r) {
|
|
|
71555
71719
|
return out.join("\n");
|
|
71556
71720
|
}
|
|
71557
71721
|
|
|
71722
|
+
// src/tenant-status-wait.ts
|
|
71723
|
+
var TENANT_STATUS_WAIT_INTERVAL_MS = 12e3;
|
|
71724
|
+
var TENANT_STATUS_WAIT_TIMEOUT_DEFAULT_S = 660;
|
|
71725
|
+
var TENANT_STATUS_WAIT_TIMEOUT_MAX_S = 1800;
|
|
71726
|
+
var TENANT_STATUS_WAIT_MIN_SHA_LENGTH = 7;
|
|
71727
|
+
function parseWaitForSha(raw) {
|
|
71728
|
+
const sha = raw.trim().toLowerCase();
|
|
71729
|
+
if (!new RegExp(`^[0-9a-f]{${TENANT_STATUS_WAIT_MIN_SHA_LENGTH},40}$`).test(sha)) {
|
|
71730
|
+
return { ok: false, reason: `--wait-for must be a commit SHA (${TENANT_STATUS_WAIT_MIN_SHA_LENGTH}-40 hex characters), got "${raw.trim()}"` };
|
|
71731
|
+
}
|
|
71732
|
+
return { ok: true, sha };
|
|
71733
|
+
}
|
|
71734
|
+
function parseWaitTimeoutSeconds(raw) {
|
|
71735
|
+
if (raw === void 0) return { ok: true, seconds: TENANT_STATUS_WAIT_TIMEOUT_DEFAULT_S, clamped: false };
|
|
71736
|
+
const seconds = Number(raw.trim());
|
|
71737
|
+
if (!Number.isInteger(seconds) || seconds <= 0) {
|
|
71738
|
+
return { ok: false, reason: `--timeout must be a positive whole number of seconds, got "${raw.trim()}"` };
|
|
71739
|
+
}
|
|
71740
|
+
if (seconds > TENANT_STATUS_WAIT_TIMEOUT_MAX_S) return { ok: true, seconds: TENANT_STATUS_WAIT_TIMEOUT_MAX_S, clamped: true };
|
|
71741
|
+
return { ok: true, seconds, clamped: false };
|
|
71742
|
+
}
|
|
71743
|
+
function deployedTagReaches(tag, sha) {
|
|
71744
|
+
const stamped = (tag ?? "").trim().toLowerCase();
|
|
71745
|
+
const needle = sha.trim().toLowerCase();
|
|
71746
|
+
if (!stamped || !needle) return false;
|
|
71747
|
+
if (stamped === needle) return true;
|
|
71748
|
+
const [shorter, longer] = stamped.length <= needle.length ? [stamped, needle] : [needle, stamped];
|
|
71749
|
+
return shorter.length >= TENANT_STATUS_WAIT_MIN_SHA_LENGTH && longer.startsWith(shorter);
|
|
71750
|
+
}
|
|
71751
|
+
async function waitForDeployedTag(deps) {
|
|
71752
|
+
const intervalMs = deps.intervalMs ?? TENANT_STATUS_WAIT_INTERVAL_MS;
|
|
71753
|
+
const now = deps.now ?? Date.now;
|
|
71754
|
+
const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms)));
|
|
71755
|
+
const startedAt = now();
|
|
71756
|
+
const deadline = startedAt + deps.timeoutMs;
|
|
71757
|
+
let polls = 0;
|
|
71758
|
+
for (; ; ) {
|
|
71759
|
+
const status = await deps.readStatus();
|
|
71760
|
+
polls += 1;
|
|
71761
|
+
if (deployedTagReaches(deps.deployedTagOf(status), deps.sha)) {
|
|
71762
|
+
return { matched: true, status, polls, elapsedMs: now() - startedAt };
|
|
71763
|
+
}
|
|
71764
|
+
const remaining = deadline - now();
|
|
71765
|
+
if (remaining <= 0) return { matched: false, status, polls, elapsedMs: now() - startedAt };
|
|
71766
|
+
await sleep2(Math.min(intervalMs, remaining));
|
|
71767
|
+
}
|
|
71768
|
+
}
|
|
71769
|
+
|
|
71558
71770
|
// src/tenant-sweep.ts
|
|
71559
71771
|
function isRcBearingTenant(p) {
|
|
71560
71772
|
if ((p.deployModel ?? "") !== "tenant-container") return false;
|
|
@@ -73613,12 +73825,33 @@ tenant.command("reconcile <owner/repo> <stage>").description("re-render this ten
|
|
|
73613
73825
|
return failGraceful(`runtime tenant reconcile: ${e.message}`);
|
|
73614
73826
|
}
|
|
73615
73827
|
});
|
|
73616
|
-
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) => {
|
|
73828
|
+
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); `--wait-for <sha>` polls the same read to a deadline (#9942)").option("--wait-for <sha>", "poll until DEPLOY#<stage> deployedTag reaches this commit SHA (7-40 hex, short form ok), then print the final status and exit 0; exits 1 on timeout \u2014 use this instead of a shell sleep loop (#9942)").option("--timeout <secs>", `with --wait-for: give up after this many seconds (default ${TENANT_STATUS_WAIT_TIMEOUT_DEFAULT_S}, max ${TENANT_STATUS_WAIT_TIMEOUT_MAX_S}, polled every ${TENANT_STATUS_WAIT_INTERVAL_MS / 1e3}s)`).action(async (repo, stage, o) => {
|
|
73617
73829
|
if (!["dev", "rc", "main"].includes(stage)) return fail("runtime tenant status: <stage> must be dev, rc, or main");
|
|
73830
|
+
let wait;
|
|
73831
|
+
if (o.waitFor !== void 0) {
|
|
73832
|
+
const sha = parseWaitForSha(o.waitFor);
|
|
73833
|
+
if (!sha.ok) return fail(`runtime tenant status: ${sha.reason}`);
|
|
73834
|
+
const timeout = parseWaitTimeoutSeconds(o.timeout);
|
|
73835
|
+
if (!timeout.ok) return fail(`runtime tenant status: ${timeout.reason}`);
|
|
73836
|
+
wait = { sha: sha.sha, seconds: timeout.seconds, clamped: timeout.clamped };
|
|
73837
|
+
} else if (o.timeout !== void 0) {
|
|
73838
|
+
return fail("runtime tenant status: --timeout only applies with --wait-for <sha>");
|
|
73839
|
+
}
|
|
73618
73840
|
const cfg = await loadConfig();
|
|
73619
|
-
const
|
|
73841
|
+
const readStatus = () => buildTenantRuntimeStatusFor(repo, stage, cfg);
|
|
73842
|
+
let result;
|
|
73843
|
+
let waitMatched;
|
|
73844
|
+
if (!wait) result = await readStatus();
|
|
73845
|
+
else {
|
|
73846
|
+
console.error(`runtime tenant status: waiting up to ${wait.seconds}s for ${repo} ${stage} deployedTag to reach ${wait.sha}${wait.clamped ? ` (--timeout clamped to the ${TENANT_STATUS_WAIT_TIMEOUT_MAX_S}s max)` : ""}`);
|
|
73847
|
+
const waited = await waitForDeployedTag({ sha: wait.sha, timeoutMs: wait.seconds * 1e3, readStatus, deployedTagOf: (status) => status.deploy?.deployedTag });
|
|
73848
|
+
result = waited.status;
|
|
73849
|
+
waitMatched = waited.matched;
|
|
73850
|
+
if (!waited.matched) console.error(`runtime tenant status: timed out after ${Math.round(waited.elapsedMs / 1e3)}s (limit ${wait.seconds}s, ${waited.polls} read(s)); deployedTag is ${waited.status.deploy?.deployedTag ?? "unset"}, not ${wait.sha}`);
|
|
73851
|
+
}
|
|
73620
73852
|
console.log(JSON.stringify(result, null, 2));
|
|
73621
|
-
if (
|
|
73853
|
+
if (waitMatched !== void 0) process.exitCode = waitMatched ? 0 : 1;
|
|
73854
|
+
else if (result.publicProbe?.ok === false || result.edgeCertificate?.missing) process.exitCode = 1;
|
|
73622
73855
|
});
|
|
73623
73856
|
tenant.command("logs <owner/repo> <stage>").description("READ-ONLY: read a tenant stage's application journal (journald, so a deploy-recreated container still answers) \u2014 the unit namespace comes from the tenant's DEPLOY coords and the blue/green slot is chosen on the box, never from a flag; bounded and redacted (#9230)").option("--tail <n>", `lines to return (1-${TENANT_LOG_TAIL_MAX}, default ${TENANT_LOG_TAIL_DEFAULT}); the applied bound is printed`).option("--since <window>", "window start: a relative window like 15m, 6h or 2d, or a UTC time like 2026-10-07T13:10Z (default 1h)").option("--until <time>", "window end: same forms as --since (default: now) \u2014 the box returns the newest N lines OF the window, so end it earlier to reach older lines on a busy tenant (#9878)").option("--grep <needle>", "keep only returned lines containing this literal text (applied to the bounded tail, not the whole journal)").option("--json", "machine-readable output").action(async (repo, stage, o) => {
|
|
73624
73857
|
try {
|
package/package.json
CHANGED