@tablation/crew 0.79.10 → 0.80.0
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 +164 -24
- 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) {
|
|
@@ -12672,6 +12721,25 @@ async function runReleaseCycle(o, movedPast2, localRefSkips) {
|
|
|
12672
12721
|
}
|
|
12673
12722
|
}
|
|
12674
12723
|
}
|
|
12724
|
+
var MAX_ISOLATE_PAIRS = 10;
|
|
12725
|
+
function pairOrder(n, max = MAX_ISOLATE_PAIRS) {
|
|
12726
|
+
const pairs = [];
|
|
12727
|
+
let truncated = false;
|
|
12728
|
+
outer: for (let j = 1; j < n; j++) {
|
|
12729
|
+
for (let i = 0; i < j; i++) {
|
|
12730
|
+
if (pairs.length >= max) {
|
|
12731
|
+
truncated = true;
|
|
12732
|
+
break outer;
|
|
12733
|
+
}
|
|
12734
|
+
pairs.push([i, j]);
|
|
12735
|
+
}
|
|
12736
|
+
}
|
|
12737
|
+
return { pairs, truncated };
|
|
12738
|
+
}
|
|
12739
|
+
function selfRepairLine(maxCandidates) {
|
|
12740
|
+
const cap = maxCandidates ?? DEFAULT_ISOLATE_MAX_CANDIDATES;
|
|
12741
|
+
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`;
|
|
12742
|
+
}
|
|
12675
12743
|
var isolateCap = (o) => o.repo.release.isolateFailures?.maxCandidates ?? DEFAULT_ISOLATE_MAX_CANDIDATES;
|
|
12676
12744
|
async function isolateAndRetry(o, name, output, merged, preMerge, decision, fresh, releaseBranch) {
|
|
12677
12745
|
const applied = merged.filter((c) => c.sha && c.branch);
|
|
@@ -12696,13 +12764,13 @@ async function isolateAndRetry(o, name, output, merged, preMerge, decision, fres
|
|
|
12696
12764
|
if (baseRun && baseRun.code !== 0) {
|
|
12697
12765
|
o.emit.warn(`${name} fails on the base without ${only.branch} \u2014 not blaming the branch`, { ticket: only.ticket.issue_id });
|
|
12698
12766
|
gitOk(o.cwd, ["reset", "--hard", head]);
|
|
12699
|
-
return
|
|
12767
|
+
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
12768
|
}
|
|
12701
12769
|
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
12770
|
culprits.push({ c: only, how: "solo", tail: lastLines(output, HOOK_FAILURE_TAIL_LINES) });
|
|
12703
12771
|
} else if (applied.length > cap) {
|
|
12704
12772
|
o.emit.warn(`${name} failed with ${applied.length} branches merged, over release.isolateFailures.maxCandidates (${cap}) \u2014 not isolating`);
|
|
12705
|
-
return
|
|
12773
|
+
return { tried: `Isolation was skipped: ${applied.length} branches were merged, over release.isolateFailures.maxCandidates (${cap}).` };
|
|
12706
12774
|
} else {
|
|
12707
12775
|
o.emit.warn(`${name} failed with ${applied.length} branches merged \u2014 building each alone to find the culprit`);
|
|
12708
12776
|
for (const c of applied) {
|
|
@@ -12715,12 +12783,51 @@ async function isolateAndRetry(o, name, output, merged, preMerge, decision, fres
|
|
|
12715
12783
|
culprits.push({ c, how: "solo", tail: lastLines(r.output, HOOK_FAILURE_TAIL_LINES) });
|
|
12716
12784
|
}
|
|
12717
12785
|
}
|
|
12786
|
+
if (culprits.length === 0) {
|
|
12787
|
+
const { pairs, truncated } = pairOrder(applied.length);
|
|
12788
|
+
for (const [i, j] of pairs) {
|
|
12789
|
+
gitOk(o.cwd, ["reset", "--hard", preMerge]);
|
|
12790
|
+
const a = mergeOne(o, applied[i]);
|
|
12791
|
+
const b = typeof a === "object" && "conflict" in a ? a : mergeOne(o, applied[j]);
|
|
12792
|
+
if (typeof b === "object" && "conflict" in b) continue;
|
|
12793
|
+
const r = await hook(o, name);
|
|
12794
|
+
if (r && r.code !== 0) {
|
|
12795
|
+
o.emit.warn(`${applied[j].branch} fails the ${name} hook together with ${applied[i].branch}`, { ticket: applied[j].ticket.issue_id });
|
|
12796
|
+
culprits.push({ c: applied[j], how: "pair", sibling: applied[i], tail: lastLines(r.output, HOOK_FAILURE_TAIL_LINES) });
|
|
12797
|
+
break;
|
|
12798
|
+
}
|
|
12799
|
+
}
|
|
12800
|
+
if (culprits.length === 0 && truncated) {
|
|
12801
|
+
let lo = 2, hi = applied.length;
|
|
12802
|
+
let tail = lastLines(output, HOOK_FAILURE_TAIL_LINES);
|
|
12803
|
+
while (lo < hi) {
|
|
12804
|
+
const mid = Math.floor((lo + hi) / 2);
|
|
12805
|
+
gitOk(o.cwd, ["reset", "--hard", preMerge]);
|
|
12806
|
+
let clean = true;
|
|
12807
|
+
for (const c of applied.slice(0, mid)) {
|
|
12808
|
+
const m = mergeOne(o, c);
|
|
12809
|
+
if (typeof m === "object" && "conflict" in m) {
|
|
12810
|
+
clean = false;
|
|
12811
|
+
break;
|
|
12812
|
+
}
|
|
12813
|
+
}
|
|
12814
|
+
const r = clean ? await hook(o, name) : null;
|
|
12815
|
+
if (r && r.code !== 0) {
|
|
12816
|
+
hi = mid;
|
|
12817
|
+
tail = lastLines(r.output, HOOK_FAILURE_TAIL_LINES);
|
|
12818
|
+
} else lo = mid + 1;
|
|
12819
|
+
}
|
|
12820
|
+
const owner = applied[hi - 1];
|
|
12821
|
+
o.emit.warn(`${owner.branch} is the first branch whose prefix fails the ${name} hook (bisected over ${applied.length})`, { ticket: owner.ticket.issue_id });
|
|
12822
|
+
culprits.push({ c: owner, how: "pair", sibling: applied[hi - 2], tail });
|
|
12823
|
+
}
|
|
12824
|
+
}
|
|
12718
12825
|
}
|
|
12719
12826
|
gitOk(o.cwd, ["reset", "--hard", preMerge]);
|
|
12720
12827
|
if (culprits.length === 0) {
|
|
12721
12828
|
o.emit.warn("no single branch could be blamed \u2014 failing the release as a whole");
|
|
12722
12829
|
gitOk(o.cwd, ["reset", "--hard", head]);
|
|
12723
|
-
return
|
|
12830
|
+
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
12831
|
}
|
|
12725
12832
|
const out = new Set(culprits.map((x) => x.c.ticket.id));
|
|
12726
12833
|
const rest = decision.merges.filter((c) => !out.has(c.ticket.id));
|
|
@@ -12732,7 +12839,8 @@ async function isolateAndRetry(o, name, output, merged, preMerge, decision, fres
|
|
|
12732
12839
|
branch: x.c.branch,
|
|
12733
12840
|
hook: name,
|
|
12734
12841
|
tail: x.tail,
|
|
12735
|
-
how: x.how
|
|
12842
|
+
how: x.how,
|
|
12843
|
+
...x.sibling ? { sibling: { key: x.sibling.ticket.issue_tag ?? x.sibling.ticket.issue_id, branch: x.sibling.branch } } : {}
|
|
12736
12844
|
}))
|
|
12737
12845
|
};
|
|
12738
12846
|
}
|
|
@@ -12743,6 +12851,7 @@ async function mergeAndRelease(o, decision, fresh, releaseBranch, isolated = fal
|
|
|
12743
12851
|
const conflicts = [];
|
|
12744
12852
|
const unbuildable = [];
|
|
12745
12853
|
let applied = 0;
|
|
12854
|
+
let isolation;
|
|
12746
12855
|
for (const c of decision.merges) {
|
|
12747
12856
|
if (!c.branch || c.mergedSha) {
|
|
12748
12857
|
if (c.skipReason === "already-released") {
|
|
@@ -12855,7 +12964,8 @@ async function mergeAndRelease(o, decision, fresh, releaseBranch, isolated = fal
|
|
|
12855
12964
|
}
|
|
12856
12965
|
if (r && r.code !== 0 && !isolated) {
|
|
12857
12966
|
const out = await isolateAndRetry(o, "test", r.output, merged, preMerge, decision, fresh, releaseBranch);
|
|
12858
|
-
if (out)
|
|
12967
|
+
if (out && "tried" in out) isolation = out.tried;
|
|
12968
|
+
else if (out) return out;
|
|
12859
12969
|
}
|
|
12860
12970
|
if (r && r.code !== 0) {
|
|
12861
12971
|
o.emit.error(`test gate FAILED \u2014 not deploying; the target stays on the previous release`);
|
|
@@ -12876,7 +12986,7 @@ async function mergeAndRelease(o, decision, fresh, releaseBranch, isolated = fal
|
|
|
12876
12986
|
label: hookLabel(o.repo, "test"),
|
|
12877
12987
|
tail: failureSummary(r.output, HOOK_FAILURE_TAIL_LINES)
|
|
12878
12988
|
};
|
|
12879
|
-
return { merged, conflicts, unbuildable, deployed: false, stopped: "tests failed", decision, gateRed, hookFailure: hookFailure2, composition };
|
|
12989
|
+
return { merged, conflicts, unbuildable, deployed: false, stopped: "tests failed", decision, gateRed, hookFailure: hookFailure2, composition, ...isolation ? { isolation } : {} };
|
|
12880
12990
|
}
|
|
12881
12991
|
o.state?.clearTestGateFailed?.();
|
|
12882
12992
|
}
|
|
@@ -12915,7 +13025,8 @@ async function mergeAndRelease(o, decision, fresh, releaseBranch, isolated = fal
|
|
|
12915
13025
|
}
|
|
12916
13026
|
if (r && r.code !== 0 && !isolated) {
|
|
12917
13027
|
const out = await isolateAndRetry(o, "build", r.output, merged, preMerge, decision, fresh, releaseBranch);
|
|
12918
|
-
if (out)
|
|
13028
|
+
if (out && "tried" in out) isolation = out.tried;
|
|
13029
|
+
else if (out) return out;
|
|
12919
13030
|
}
|
|
12920
13031
|
if (r && r.code !== 0) {
|
|
12921
13032
|
o.emit.error("build failed \u2014 not deploying");
|
|
@@ -12923,7 +13034,7 @@ async function mergeAndRelease(o, decision, fresh, releaseBranch, isolated = fal
|
|
|
12923
13034
|
git(o.cwd, ["reset", "--hard", preBump]);
|
|
12924
13035
|
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
13036
|
}
|
|
12926
|
-
return { merged, conflicts, unbuildable, version, deployed: false, stopped: "build failed", decision, hookFailure: hookFailure(o, "build", r.output) };
|
|
13037
|
+
return { merged, conflicts, unbuildable, version, deployed: false, stopped: "build failed", decision, hookFailure: hookFailure(o, "build", r.output), ...isolation ? { isolation } : {} };
|
|
12927
13038
|
}
|
|
12928
13039
|
}
|
|
12929
13040
|
}
|
|
@@ -18294,21 +18405,25 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18294
18405
|
dryRun
|
|
18295
18406
|
);
|
|
18296
18407
|
}
|
|
18408
|
+
const alertPlacement = async () => {
|
|
18409
|
+
const placement = {};
|
|
18410
|
+
try {
|
|
18411
|
+
const repoId = repoIdForName(c, target.name);
|
|
18412
|
+
if (repoId) {
|
|
18413
|
+
placement.repo_id = repoId;
|
|
18414
|
+
const projectId = (await tracker.repoRow(repoId))?.project_id;
|
|
18415
|
+
if (projectId) placement.project_id = projectId;
|
|
18416
|
+
}
|
|
18417
|
+
} catch {
|
|
18418
|
+
}
|
|
18419
|
+
return placement;
|
|
18420
|
+
};
|
|
18297
18421
|
const news = describeRelease(outcome, scope);
|
|
18298
18422
|
if (news) {
|
|
18299
18423
|
const notified = await notify(c, cfg.ship, news, remit, dryRun);
|
|
18300
18424
|
if (!notified && news.level === "fail") {
|
|
18301
18425
|
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
|
-
}
|
|
18426
|
+
const placement = await alertPlacement();
|
|
18312
18427
|
await applyFailureAlert(tracker, news, scope, all, memberId, remit, dryRun, placement).catch((e) => {
|
|
18313
18428
|
remit.warn(`could not file/update a failure ticket: ${e.message}`, { step: "release" });
|
|
18314
18429
|
});
|
|
@@ -18332,13 +18447,36 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18332
18447
|
}
|
|
18333
18448
|
if (outcome.buildCulprits?.length) {
|
|
18334
18449
|
const seat = releaseSeat(c);
|
|
18450
|
+
const prior = [...await tracker.events("build_failed"), ...await tracker.events("pushed")];
|
|
18335
18451
|
for (const cu of outcome.buildCulprits) {
|
|
18336
18452
|
try {
|
|
18337
18453
|
const bounce = planBuildFailureBounce(target.dir, cu, repo.branch.base, repo.branch.remote);
|
|
18338
|
-
const r = await applyBuildFailureBounce(
|
|
18454
|
+
const r = await applyBuildFailureBounce(
|
|
18455
|
+
tracker,
|
|
18456
|
+
bounce,
|
|
18457
|
+
tracker.contract,
|
|
18458
|
+
seat,
|
|
18459
|
+
remit,
|
|
18460
|
+
dryRun,
|
|
18461
|
+
{ count: priorBuildFailures(cu.ticket, prior), operatorId: c.resolved?.operator }
|
|
18462
|
+
);
|
|
18339
18463
|
if (r.kind === "failed") {
|
|
18340
18464
|
remit.warn(`could not hand back ${cu.ticket.issue_id}: ${r.why}`, { step: "release" });
|
|
18341
18465
|
}
|
|
18466
|
+
if (r.kind === "escalated" && !dryRun) {
|
|
18467
|
+
const key = cu.ticket.issue_tag ?? cu.ticket.issue_id;
|
|
18468
|
+
await applyFailureAlert(tracker, {
|
|
18469
|
+
level: "fail",
|
|
18470
|
+
headline: `${scope}: release blocked \u2014 ${key} broke the ${cu.hook} gate twice`,
|
|
18471
|
+
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.
|
|
18472
|
+
|
|
18473
|
+
\`\`\`
|
|
18474
|
+
${cu.tail.trimEnd()}
|
|
18475
|
+
\`\`\``
|
|
18476
|
+
}, scope, all, seat, remit, dryRun, await alertPlacement()).catch((e) => {
|
|
18477
|
+
remit.warn(`could not file/update a failure ticket: ${e.message}`, { step: "release" });
|
|
18478
|
+
});
|
|
18479
|
+
}
|
|
18342
18480
|
} catch (e) {
|
|
18343
18481
|
remit.warn(`could not hand back ${cu.ticket.issue_id}: ${e.message}`, { step: "release" });
|
|
18344
18482
|
}
|
|
@@ -21546,6 +21684,8 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
21546
21684
|
process.stdout.write(` - release readiness: ${p2}
|
|
21547
21685
|
`);
|
|
21548
21686
|
}
|
|
21687
|
+
process.stdout.write(` release self-repair: ${selfRepairLine(config.release.isolateFailures?.maxCandidates)}
|
|
21688
|
+
`);
|
|
21549
21689
|
const dh = doctorBaseReport(r, config.branch.base, baseHealth);
|
|
21550
21690
|
process.stdout.write(dh.text);
|
|
21551
21691
|
if (dh.failed) process.exitCode = 1;
|
package/package.json
CHANGED