@tablation/crew 0.79.10 → 0.80.1
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/cli.js +207 -25
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2981,9 +2981,11 @@ function describeRelease(o, route) {
|
|
|
2981
2981
|
level: "fail",
|
|
2982
2982
|
headline: `${route}: release blocked \u2014 tests failed`,
|
|
2983
2983
|
detail: withHookTail(
|
|
2984
|
-
o.composition ? `the target stays on the previous release
|
|
2984
|
+
(o.composition ? `the target stays on the previous release
|
|
2985
2985
|
|
|
2986
|
-
${describeComposition(o.composition)}` : "the target stays on the previous release"
|
|
2986
|
+
${describeComposition(o.composition)}` : "the target stays on the previous release") + (o.isolation ? `
|
|
2987
|
+
|
|
2988
|
+
${o.isolation}` : ""),
|
|
2987
2989
|
o.hookFailure
|
|
2988
2990
|
),
|
|
2989
2991
|
...o.composition ? { head: o.composition.head } : {}
|
|
@@ -3013,7 +3015,9 @@ ${describeComposition(o.composition)}` : "the target stays on the previous relea
|
|
|
3013
3015
|
return {
|
|
3014
3016
|
level: "fail",
|
|
3015
3017
|
headline: `${route}: build FAILED`,
|
|
3016
|
-
detail: withHookTail(
|
|
3018
|
+
detail: withHookTail(`the target stays on the previous release \u2014 nothing was deployed${o.isolation ? `
|
|
3019
|
+
|
|
3020
|
+
${o.isolation}` : ""}`, o.hookFailure)
|
|
3017
3021
|
};
|
|
3018
3022
|
}
|
|
3019
3023
|
if (o.blockedCycles && o.blockedCycles >= 2) {
|
|
@@ -11311,7 +11315,7 @@ function planBuildFailureBounce(cwd, c, base, remote) {
|
|
|
11311
11315
|
return { ...c, base, remote, worktree: worktreeForBranch(cwd, c.branch) };
|
|
11312
11316
|
}
|
|
11313
11317
|
function buildFailureComment(b) {
|
|
11314
|
-
const how = b.how === "files" ? `The failing output names files that only \`${b.branch}\` changed among the branches in that release.` : `Built alone on the previous \`${b.base}\`, \`${b.branch}\` fails the same hook.`;
|
|
11318
|
+
const how = b.how === "pair" && b.sibling ? `Each branch passes the hook alone; \`${b.branch}\` together with ${b.sibling.key} (\`${b.sibling.branch}\`) fails it. ${b.sibling.key} was verified first, so the fix belongs on this branch: \`git merge ${b.remote}/${b.sibling.branch}\` first, then fix what disagrees. Fix it on this branch, never a fresh one.` : b.how === "files" ? `The failing output names files that only \`${b.branch}\` changed among the branches in that release.` : `Built alone on the previous \`${b.base}\`, \`${b.branch}\` fails the same hook.`;
|
|
11315
11319
|
return `${BUILD_FAILURE_MARKER}
|
|
11316
11320
|
|
|
11317
11321
|
**Back with the dev lane \u2014 this branch builds on its own but not on current \`${b.base}\`.**
|
|
@@ -11326,8 +11330,46 @@ ${b.tail.trimEnd()}
|
|
|
11326
11330
|
|
|
11327
11331
|
Nothing was written to \`${b.base}\` for this ticket; the rest of the release shipped without it.`;
|
|
11328
11332
|
}
|
|
11329
|
-
|
|
11333
|
+
function priorBuildFailures(ticket, events) {
|
|
11334
|
+
const stamp = (e) => e.occurred_at ?? e.created_at ?? "";
|
|
11335
|
+
const mine = events.filter((e) => e.record_id === ticket.id);
|
|
11336
|
+
const pushes = mine.filter((e) => e.event_type === "pushed").map(stamp);
|
|
11337
|
+
return mine.filter((e) => e.event_type === "build_failed" && pushes.some((p) => p > stamp(e))).length;
|
|
11338
|
+
}
|
|
11339
|
+
function buildFailureEscalation(b) {
|
|
11340
|
+
return `${BUILD_FAILURE_MARKER}
|
|
11341
|
+
|
|
11342
|
+
**Failed the release gate again after a hand-back \u2014 stopping rather than looping.**
|
|
11343
|
+
|
|
11344
|
+
\`${b.branch}\` was handed back for breaking the \`${b.hook}\` hook, pushed, verified again, and breaks it once more. That is a call for a person, not another lap through the lanes.
|
|
11345
|
+
|
|
11346
|
+
\`\`\`
|
|
11347
|
+
${b.tail.trimEnd()}
|
|
11348
|
+
\`\`\``;
|
|
11349
|
+
}
|
|
11350
|
+
async function applyBuildFailureBounce(writer, b, contract, memberId, log, dryRun, prior = { count: 0 }) {
|
|
11330
11351
|
const t = { ticket: b.ticket.issue_id, step: "release" };
|
|
11352
|
+
if (prior.count > 0) {
|
|
11353
|
+
if (dryRun) {
|
|
11354
|
+
log.emit(`would escalate to ${contract.statuses.needsHuman} \u2014 handed back for the ${b.hook} hook before`, t);
|
|
11355
|
+
return { kind: "escalated" };
|
|
11356
|
+
}
|
|
11357
|
+
try {
|
|
11358
|
+
await writer.updateTicket(
|
|
11359
|
+
b.ticket.id,
|
|
11360
|
+
{ status: contract.statuses.needsHuman, ...prior.operatorId ? { assignee_id: prior.operatorId } : {} },
|
|
11361
|
+
b.ticket.updated_at
|
|
11362
|
+
);
|
|
11363
|
+
await writer.postEvent(b.ticket.id, buildFailureEscalation(b), memberId, "build_failed_escalated").catch(() => {
|
|
11364
|
+
log.warn("escalated, but the note failed to post", t);
|
|
11365
|
+
});
|
|
11366
|
+
log.warn(`breaks the ${b.hook} hook again after a hand-back \u2014 escalated to ${contract.statuses.needsHuman}`, t);
|
|
11367
|
+
return { kind: "escalated" };
|
|
11368
|
+
} catch (e) {
|
|
11369
|
+
if (e instanceof StaleWriteError8) return { kind: "stale" };
|
|
11370
|
+
return { kind: "failed", why: e.message };
|
|
11371
|
+
}
|
|
11372
|
+
}
|
|
11331
11373
|
if (dryRun) {
|
|
11332
11374
|
log.emit(`would hand ${b.ticket.issue_id} back to ${contract.statuses.building} \u2014 its branch breaks the ${b.hook} hook`, t);
|
|
11333
11375
|
return { kind: "handed-back" };
|
|
@@ -11341,6 +11383,13 @@ async function applyBuildFailureBounce(writer, b, contract, memberId, log, dryRu
|
|
|
11341
11383
|
await writer.postEvent(b.ticket.id, buildFailureComment(b), memberId, "build_failed").catch(() => {
|
|
11342
11384
|
log.warn("handed back, but the note failed to post", t);
|
|
11343
11385
|
});
|
|
11386
|
+
await writer.postEvent(
|
|
11387
|
+
b.ticket.id,
|
|
11388
|
+
`${b.ticket.issue_tag ?? b.ticket.issue_id} left out of the release: ${b.hook} hook, found by ${b.how}${b.sibling ? `, together with ${b.sibling.key} (${b.sibling.branch})` : ""}`,
|
|
11389
|
+
memberId,
|
|
11390
|
+
"release_culprit"
|
|
11391
|
+
).catch(() => {
|
|
11392
|
+
});
|
|
11344
11393
|
log.emit(`handed back to ${contract.statuses.building} \u2014 breaks the ${b.hook} hook once merged with the rest`, t);
|
|
11345
11394
|
return { kind: "handed-back" };
|
|
11346
11395
|
} catch (e) {
|
|
@@ -11926,6 +11975,27 @@ function planStamp(cwd, tickets, contract, fromSha, toSha, merged = /* @__PURE__
|
|
|
11926
11975
|
function mergedAtFor(cwd, sha, fallback) {
|
|
11927
11976
|
return cwd && commitDate(cwd, sha) || fallback;
|
|
11928
11977
|
}
|
|
11978
|
+
async function applyMergeStamp(tracker, merged, tickets, emit, dryRun, cwd) {
|
|
11979
|
+
let stamped = 0;
|
|
11980
|
+
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
11981
|
+
for (const ticket of tickets) {
|
|
11982
|
+
const sha = merged.get(ticket.issue_id);
|
|
11983
|
+
if (dryRun) {
|
|
11984
|
+
emit.emit(`would stamp commit_sha${sha ? ` (${sha.slice(0, 8)})` : ""}`, { ticket: ticket.issue_id });
|
|
11985
|
+
stamped++;
|
|
11986
|
+
continue;
|
|
11987
|
+
}
|
|
11988
|
+
if (!sha || ticket.commit_sha === sha) continue;
|
|
11989
|
+
try {
|
|
11990
|
+
await tracker.updateTicket(ticket.id, { commit_sha: sha, merged_at: mergedAtFor(cwd, sha, at) });
|
|
11991
|
+
emit.emit(`stamped commit_sha (${sha.slice(0, 8)})`, { ticket: ticket.issue_id });
|
|
11992
|
+
stamped++;
|
|
11993
|
+
} catch (e) {
|
|
11994
|
+
emit.warn(`could not stamp commit_sha: ${e.message}`, { ticket: ticket.issue_id });
|
|
11995
|
+
}
|
|
11996
|
+
}
|
|
11997
|
+
return stamped;
|
|
11998
|
+
}
|
|
11929
11999
|
async function applyStamp(tracker, plan, version, contract, emit, dryRun, cwd) {
|
|
11930
12000
|
let stamped = 0;
|
|
11931
12001
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -11938,6 +12008,7 @@ async function applyStamp(tracker, plan, version, contract, emit, dryRun, cwd) {
|
|
|
11938
12008
|
stamped++;
|
|
11939
12009
|
continue;
|
|
11940
12010
|
}
|
|
12011
|
+
const recordMerge = !!sha && !ticket.commit_sha;
|
|
11941
12012
|
try {
|
|
11942
12013
|
await tracker.updateTicket(ticket.id, {
|
|
11943
12014
|
status: contract.statuses.deployed,
|
|
@@ -11948,7 +12019,7 @@ async function applyStamp(tracker, plan, version, contract, emit, dryRun, cwd) {
|
|
|
11948
12019
|
// ticket's record should mean the same thing regardless of which
|
|
11949
12020
|
// release mode closed it (ISSUE-218). `merged_at` is the commit's own
|
|
11950
12021
|
// date, not the stamp time (which trails the deploy in ci_* modes).
|
|
11951
|
-
...sha ? { commit_sha: sha, merged_at: mergedAtFor(cwd, sha, at) } : {}
|
|
12022
|
+
...recordMerge && sha ? { commit_sha: sha, merged_at: mergedAtFor(cwd, sha, at) } : {}
|
|
11952
12023
|
});
|
|
11953
12024
|
emit.emit(`stamped ${contract.statuses.deployed}${version ? ` (${version})` : ""}${shaNote}`, {
|
|
11954
12025
|
ticket: ticket.issue_id
|
|
@@ -12672,6 +12743,25 @@ async function runReleaseCycle(o, movedPast2, localRefSkips) {
|
|
|
12672
12743
|
}
|
|
12673
12744
|
}
|
|
12674
12745
|
}
|
|
12746
|
+
var MAX_ISOLATE_PAIRS = 10;
|
|
12747
|
+
function pairOrder(n, max = MAX_ISOLATE_PAIRS) {
|
|
12748
|
+
const pairs = [];
|
|
12749
|
+
let truncated = false;
|
|
12750
|
+
outer: for (let j = 1; j < n; j++) {
|
|
12751
|
+
for (let i = 0; i < j; i++) {
|
|
12752
|
+
if (pairs.length >= max) {
|
|
12753
|
+
truncated = true;
|
|
12754
|
+
break outer;
|
|
12755
|
+
}
|
|
12756
|
+
pairs.push([i, j]);
|
|
12757
|
+
}
|
|
12758
|
+
}
|
|
12759
|
+
return { pairs, truncated };
|
|
12760
|
+
}
|
|
12761
|
+
function selfRepairLine(maxCandidates) {
|
|
12762
|
+
const cap = maxCandidates ?? DEFAULT_ISOLATE_MAX_CANDIDATES;
|
|
12763
|
+
return cap === 0 ? "culprit isolation is off (release.isolateFailures.maxCandidates: 0); a failed gate files an alert" : `culprit isolation up to ${cap} merged branches, ${MAX_ISOLATE_PAIRS} pairs before bisecting; a ticket handed back twice after a push escalates with an alert`;
|
|
12764
|
+
}
|
|
12675
12765
|
var isolateCap = (o) => o.repo.release.isolateFailures?.maxCandidates ?? DEFAULT_ISOLATE_MAX_CANDIDATES;
|
|
12676
12766
|
async function isolateAndRetry(o, name, output, merged, preMerge, decision, fresh, releaseBranch) {
|
|
12677
12767
|
const applied = merged.filter((c) => c.sha && c.branch);
|
|
@@ -12696,13 +12786,13 @@ async function isolateAndRetry(o, name, output, merged, preMerge, decision, fres
|
|
|
12696
12786
|
if (baseRun && baseRun.code !== 0) {
|
|
12697
12787
|
o.emit.warn(`${name} fails on the base without ${only.branch} \u2014 not blaming the branch`, { ticket: only.ticket.issue_id });
|
|
12698
12788
|
gitOk(o.cwd, ["reset", "--hard", head]);
|
|
12699
|
-
return
|
|
12789
|
+
return { tried: `Isolation found no culprit: the ${name} hook also fails on the base alone, without ${only.branch}, so there is no branch to hand back.` };
|
|
12700
12790
|
}
|
|
12701
12791
|
o.emit.warn(`${name} failed with only ${only.branch} merged and passes without it \u2014 treating it as the culprit`, { ticket: only.ticket.issue_id });
|
|
12702
12792
|
culprits.push({ c: only, how: "solo", tail: lastLines(output, HOOK_FAILURE_TAIL_LINES) });
|
|
12703
12793
|
} else if (applied.length > cap) {
|
|
12704
12794
|
o.emit.warn(`${name} failed with ${applied.length} branches merged, over release.isolateFailures.maxCandidates (${cap}) \u2014 not isolating`);
|
|
12705
|
-
return
|
|
12795
|
+
return { tried: `Isolation was skipped: ${applied.length} branches were merged, over release.isolateFailures.maxCandidates (${cap}).` };
|
|
12706
12796
|
} else {
|
|
12707
12797
|
o.emit.warn(`${name} failed with ${applied.length} branches merged \u2014 building each alone to find the culprit`);
|
|
12708
12798
|
for (const c of applied) {
|
|
@@ -12715,12 +12805,51 @@ async function isolateAndRetry(o, name, output, merged, preMerge, decision, fres
|
|
|
12715
12805
|
culprits.push({ c, how: "solo", tail: lastLines(r.output, HOOK_FAILURE_TAIL_LINES) });
|
|
12716
12806
|
}
|
|
12717
12807
|
}
|
|
12808
|
+
if (culprits.length === 0) {
|
|
12809
|
+
const { pairs, truncated } = pairOrder(applied.length);
|
|
12810
|
+
for (const [i, j] of pairs) {
|
|
12811
|
+
gitOk(o.cwd, ["reset", "--hard", preMerge]);
|
|
12812
|
+
const a = mergeOne(o, applied[i]);
|
|
12813
|
+
const b = typeof a === "object" && "conflict" in a ? a : mergeOne(o, applied[j]);
|
|
12814
|
+
if (typeof b === "object" && "conflict" in b) continue;
|
|
12815
|
+
const r = await hook(o, name);
|
|
12816
|
+
if (r && r.code !== 0) {
|
|
12817
|
+
o.emit.warn(`${applied[j].branch} fails the ${name} hook together with ${applied[i].branch}`, { ticket: applied[j].ticket.issue_id });
|
|
12818
|
+
culprits.push({ c: applied[j], how: "pair", sibling: applied[i], tail: lastLines(r.output, HOOK_FAILURE_TAIL_LINES) });
|
|
12819
|
+
break;
|
|
12820
|
+
}
|
|
12821
|
+
}
|
|
12822
|
+
if (culprits.length === 0 && truncated) {
|
|
12823
|
+
let lo = 2, hi = applied.length;
|
|
12824
|
+
let tail = lastLines(output, HOOK_FAILURE_TAIL_LINES);
|
|
12825
|
+
while (lo < hi) {
|
|
12826
|
+
const mid = Math.floor((lo + hi) / 2);
|
|
12827
|
+
gitOk(o.cwd, ["reset", "--hard", preMerge]);
|
|
12828
|
+
let clean = true;
|
|
12829
|
+
for (const c of applied.slice(0, mid)) {
|
|
12830
|
+
const m = mergeOne(o, c);
|
|
12831
|
+
if (typeof m === "object" && "conflict" in m) {
|
|
12832
|
+
clean = false;
|
|
12833
|
+
break;
|
|
12834
|
+
}
|
|
12835
|
+
}
|
|
12836
|
+
const r = clean ? await hook(o, name) : null;
|
|
12837
|
+
if (r && r.code !== 0) {
|
|
12838
|
+
hi = mid;
|
|
12839
|
+
tail = lastLines(r.output, HOOK_FAILURE_TAIL_LINES);
|
|
12840
|
+
} else lo = mid + 1;
|
|
12841
|
+
}
|
|
12842
|
+
const owner = applied[hi - 1];
|
|
12843
|
+
o.emit.warn(`${owner.branch} is the first branch whose prefix fails the ${name} hook (bisected over ${applied.length})`, { ticket: owner.ticket.issue_id });
|
|
12844
|
+
culprits.push({ c: owner, how: "pair", sibling: applied[hi - 2], tail });
|
|
12845
|
+
}
|
|
12846
|
+
}
|
|
12718
12847
|
}
|
|
12719
12848
|
gitOk(o.cwd, ["reset", "--hard", preMerge]);
|
|
12720
12849
|
if (culprits.length === 0) {
|
|
12721
12850
|
o.emit.warn("no single branch could be blamed \u2014 failing the release as a whole");
|
|
12722
12851
|
gitOk(o.cwd, ["reset", "--hard", head]);
|
|
12723
|
-
return
|
|
12852
|
+
return { tried: `Isolation found no culprit: each of the ${applied.length} merged branches passes the ${name} hook alone and no pair among them fails it.` };
|
|
12724
12853
|
}
|
|
12725
12854
|
const out = new Set(culprits.map((x) => x.c.ticket.id));
|
|
12726
12855
|
const rest = decision.merges.filter((c) => !out.has(c.ticket.id));
|
|
@@ -12732,7 +12861,8 @@ async function isolateAndRetry(o, name, output, merged, preMerge, decision, fres
|
|
|
12732
12861
|
branch: x.c.branch,
|
|
12733
12862
|
hook: name,
|
|
12734
12863
|
tail: x.tail,
|
|
12735
|
-
how: x.how
|
|
12864
|
+
how: x.how,
|
|
12865
|
+
...x.sibling ? { sibling: { key: x.sibling.ticket.issue_tag ?? x.sibling.ticket.issue_id, branch: x.sibling.branch } } : {}
|
|
12736
12866
|
}))
|
|
12737
12867
|
};
|
|
12738
12868
|
}
|
|
@@ -12743,6 +12873,7 @@ async function mergeAndRelease(o, decision, fresh, releaseBranch, isolated = fal
|
|
|
12743
12873
|
const conflicts = [];
|
|
12744
12874
|
const unbuildable = [];
|
|
12745
12875
|
let applied = 0;
|
|
12876
|
+
let isolation;
|
|
12746
12877
|
for (const c of decision.merges) {
|
|
12747
12878
|
if (!c.branch || c.mergedSha) {
|
|
12748
12879
|
if (c.skipReason === "already-released") {
|
|
@@ -12855,7 +12986,8 @@ async function mergeAndRelease(o, decision, fresh, releaseBranch, isolated = fal
|
|
|
12855
12986
|
}
|
|
12856
12987
|
if (r && r.code !== 0 && !isolated) {
|
|
12857
12988
|
const out = await isolateAndRetry(o, "test", r.output, merged, preMerge, decision, fresh, releaseBranch);
|
|
12858
|
-
if (out)
|
|
12989
|
+
if (out && "tried" in out) isolation = out.tried;
|
|
12990
|
+
else if (out) return out;
|
|
12859
12991
|
}
|
|
12860
12992
|
if (r && r.code !== 0) {
|
|
12861
12993
|
o.emit.error(`test gate FAILED \u2014 not deploying; the target stays on the previous release`);
|
|
@@ -12876,7 +13008,7 @@ async function mergeAndRelease(o, decision, fresh, releaseBranch, isolated = fal
|
|
|
12876
13008
|
label: hookLabel(o.repo, "test"),
|
|
12877
13009
|
tail: failureSummary(r.output, HOOK_FAILURE_TAIL_LINES)
|
|
12878
13010
|
};
|
|
12879
|
-
return { merged, conflicts, unbuildable, deployed: false, stopped: "tests failed", decision, gateRed, hookFailure: hookFailure2, composition };
|
|
13011
|
+
return { merged, conflicts, unbuildable, deployed: false, stopped: "tests failed", decision, gateRed, hookFailure: hookFailure2, composition, ...isolation ? { isolation } : {} };
|
|
12880
13012
|
}
|
|
12881
13013
|
o.state?.clearTestGateFailed?.();
|
|
12882
13014
|
}
|
|
@@ -12915,7 +13047,8 @@ async function mergeAndRelease(o, decision, fresh, releaseBranch, isolated = fal
|
|
|
12915
13047
|
}
|
|
12916
13048
|
if (r && r.code !== 0 && !isolated) {
|
|
12917
13049
|
const out = await isolateAndRetry(o, "build", r.output, merged, preMerge, decision, fresh, releaseBranch);
|
|
12918
|
-
if (out)
|
|
13050
|
+
if (out && "tried" in out) isolation = out.tried;
|
|
13051
|
+
else if (out) return out;
|
|
12919
13052
|
}
|
|
12920
13053
|
if (r && r.code !== 0) {
|
|
12921
13054
|
o.emit.error("build failed \u2014 not deploying");
|
|
@@ -12923,7 +13056,7 @@ async function mergeAndRelease(o, decision, fresh, releaseBranch, isolated = fal
|
|
|
12923
13056
|
git(o.cwd, ["reset", "--hard", preBump]);
|
|
12924
13057
|
o.emit.warn(`dropped the unreleased version commit for ${version} (reset ${o.repo.branch.base} to ${preBump.slice(0, 8)}) so a retry starts clean`);
|
|
12925
13058
|
}
|
|
12926
|
-
return { merged, conflicts, unbuildable, version, deployed: false, stopped: "build failed", decision, hookFailure: hookFailure(o, "build", r.output) };
|
|
13059
|
+
return { merged, conflicts, unbuildable, version, deployed: false, stopped: "build failed", decision, hookFailure: hookFailure(o, "build", r.output), ...isolation ? { isolation } : {} };
|
|
12927
13060
|
}
|
|
12928
13061
|
}
|
|
12929
13062
|
}
|
|
@@ -12961,6 +13094,13 @@ async function mergeAndRelease(o, decision, fresh, releaseBranch, isolated = fal
|
|
|
12961
13094
|
}
|
|
12962
13095
|
}
|
|
12963
13096
|
}
|
|
13097
|
+
if (o.repo.release.mode === "ci_auto" || o.repo.release.mode === "ci_manual") {
|
|
13098
|
+
try {
|
|
13099
|
+
await o.onBasePushed?.(merged);
|
|
13100
|
+
} catch (e) {
|
|
13101
|
+
o.emit.warn(`could not record merged tickets: ${e.message}`);
|
|
13102
|
+
}
|
|
13103
|
+
}
|
|
12964
13104
|
let deployed = false;
|
|
12965
13105
|
let integrated = false;
|
|
12966
13106
|
if (o.repo.release.mode === "integrate") {
|
|
@@ -18181,6 +18321,19 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18181
18321
|
skipTests: flag("skip-tests"),
|
|
18182
18322
|
shell: cfg.ship.shell,
|
|
18183
18323
|
mergeOnly: command === "merge",
|
|
18324
|
+
// ci_* modes: record the merge on the board when it lands on base,
|
|
18325
|
+
// not only once CI confirms the release live (CREW-2820).
|
|
18326
|
+
onBasePushed: async (merged) => {
|
|
18327
|
+
remit.enter("reconcile");
|
|
18328
|
+
await applyMergeStamp(
|
|
18329
|
+
tracker,
|
|
18330
|
+
new Map(merged.map((m) => [m.ticket.issue_id, m.sha])),
|
|
18331
|
+
merged.map((m) => m.ticket),
|
|
18332
|
+
remit,
|
|
18333
|
+
dryRun,
|
|
18334
|
+
target.dir
|
|
18335
|
+
);
|
|
18336
|
+
},
|
|
18184
18337
|
// `deploy` is the "go now" button: it exists for a commit a previous
|
|
18185
18338
|
// deploy failed on, where nothing new will merge but the work is
|
|
18186
18339
|
// genuinely unreleased.
|
|
@@ -18294,21 +18447,25 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18294
18447
|
dryRun
|
|
18295
18448
|
);
|
|
18296
18449
|
}
|
|
18450
|
+
const alertPlacement = async () => {
|
|
18451
|
+
const placement = {};
|
|
18452
|
+
try {
|
|
18453
|
+
const repoId = repoIdForName(c, target.name);
|
|
18454
|
+
if (repoId) {
|
|
18455
|
+
placement.repo_id = repoId;
|
|
18456
|
+
const projectId = (await tracker.repoRow(repoId))?.project_id;
|
|
18457
|
+
if (projectId) placement.project_id = projectId;
|
|
18458
|
+
}
|
|
18459
|
+
} catch {
|
|
18460
|
+
}
|
|
18461
|
+
return placement;
|
|
18462
|
+
};
|
|
18297
18463
|
const news = describeRelease(outcome, scope);
|
|
18298
18464
|
if (news) {
|
|
18299
18465
|
const notified = await notify(c, cfg.ship, news, remit, dryRun);
|
|
18300
18466
|
if (!notified && news.level === "fail") {
|
|
18301
18467
|
const memberId = releaseSeat(c);
|
|
18302
|
-
const placement =
|
|
18303
|
-
try {
|
|
18304
|
-
const repoId = repoIdForName(c, target.name);
|
|
18305
|
-
if (repoId) {
|
|
18306
|
-
placement.repo_id = repoId;
|
|
18307
|
-
const projectId = (await tracker.repoRow(repoId))?.project_id;
|
|
18308
|
-
if (projectId) placement.project_id = projectId;
|
|
18309
|
-
}
|
|
18310
|
-
} catch {
|
|
18311
|
-
}
|
|
18468
|
+
const placement = await alertPlacement();
|
|
18312
18469
|
await applyFailureAlert(tracker, news, scope, all, memberId, remit, dryRun, placement).catch((e) => {
|
|
18313
18470
|
remit.warn(`could not file/update a failure ticket: ${e.message}`, { step: "release" });
|
|
18314
18471
|
});
|
|
@@ -18332,13 +18489,36 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18332
18489
|
}
|
|
18333
18490
|
if (outcome.buildCulprits?.length) {
|
|
18334
18491
|
const seat = releaseSeat(c);
|
|
18492
|
+
const prior = [...await tracker.events("build_failed"), ...await tracker.events("pushed")];
|
|
18335
18493
|
for (const cu of outcome.buildCulprits) {
|
|
18336
18494
|
try {
|
|
18337
18495
|
const bounce = planBuildFailureBounce(target.dir, cu, repo.branch.base, repo.branch.remote);
|
|
18338
|
-
const r = await applyBuildFailureBounce(
|
|
18496
|
+
const r = await applyBuildFailureBounce(
|
|
18497
|
+
tracker,
|
|
18498
|
+
bounce,
|
|
18499
|
+
tracker.contract,
|
|
18500
|
+
seat,
|
|
18501
|
+
remit,
|
|
18502
|
+
dryRun,
|
|
18503
|
+
{ count: priorBuildFailures(cu.ticket, prior), operatorId: c.resolved?.operator }
|
|
18504
|
+
);
|
|
18339
18505
|
if (r.kind === "failed") {
|
|
18340
18506
|
remit.warn(`could not hand back ${cu.ticket.issue_id}: ${r.why}`, { step: "release" });
|
|
18341
18507
|
}
|
|
18508
|
+
if (r.kind === "escalated" && !dryRun) {
|
|
18509
|
+
const key = cu.ticket.issue_tag ?? cu.ticket.issue_id;
|
|
18510
|
+
await applyFailureAlert(tracker, {
|
|
18511
|
+
level: "fail",
|
|
18512
|
+
headline: `${scope}: release blocked \u2014 ${key} broke the ${cu.hook} gate twice`,
|
|
18513
|
+
detail: `${key} (\`${cu.branch}\`) was handed back after a failed ${cu.hook} gate, pushed and re-verified, and failed it again. Isolation found it both times (${cu.how}), so it is escalated to the operator instead of another lap.
|
|
18514
|
+
|
|
18515
|
+
\`\`\`
|
|
18516
|
+
${cu.tail.trimEnd()}
|
|
18517
|
+
\`\`\``
|
|
18518
|
+
}, scope, all, seat, remit, dryRun, await alertPlacement()).catch((e) => {
|
|
18519
|
+
remit.warn(`could not file/update a failure ticket: ${e.message}`, { step: "release" });
|
|
18520
|
+
});
|
|
18521
|
+
}
|
|
18342
18522
|
} catch (e) {
|
|
18343
18523
|
remit.warn(`could not hand back ${cu.ticket.issue_id}: ${e.message}`, { step: "release" });
|
|
18344
18524
|
}
|
|
@@ -21546,6 +21726,8 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
21546
21726
|
process.stdout.write(` - release readiness: ${p2}
|
|
21547
21727
|
`);
|
|
21548
21728
|
}
|
|
21729
|
+
process.stdout.write(` release self-repair: ${selfRepairLine(config.release.isolateFailures?.maxCandidates)}
|
|
21730
|
+
`);
|
|
21549
21731
|
const dh = doctorBaseReport(r, config.branch.base, baseHealth);
|
|
21550
21732
|
process.stdout.write(dh.text);
|
|
21551
21733
|
if (dh.failed) process.exitCode = 1;
|
package/package.json
CHANGED