@tablation/crew 0.54.3 → 0.55.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 +185 -38
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3283,6 +3283,7 @@ var DEFAULT_CONTRACT = {
|
|
|
3283
3283
|
author: "filed_by_id",
|
|
3284
3284
|
ship: "ship_id",
|
|
3285
3285
|
heldBy: "held_by_ship_id",
|
|
3286
|
+
resolutionNote: "resolution_note",
|
|
3286
3287
|
verifiedSha: "verified_sha",
|
|
3287
3288
|
prRef: "pr_ref",
|
|
3288
3289
|
position: "position"
|
|
@@ -3404,6 +3405,69 @@ function validateContract(c) {
|
|
|
3404
3405
|
}
|
|
3405
3406
|
var EVENT_LOG_MISSING = "the Issues table has no event log \u2014 crew writes its audit trail there (CREW-1574). In the app, open the Issues table's Data & lifecycle tab and turn on Event log, then rerun.";
|
|
3406
3407
|
|
|
3408
|
+
// src/callout.ts
|
|
3409
|
+
var calloutHeader = (kind, label) => `> [!${kind}]
|
|
3410
|
+
> **Crew** \u2014 ${label}`;
|
|
3411
|
+
var calloutLabelLine = (label) => `> **Crew** \u2014 ${label}`;
|
|
3412
|
+
|
|
3413
|
+
// src/gate-red.ts
|
|
3414
|
+
var GATE_RED_MARKER = calloutHeader("CAUTION", "Test Gate Red");
|
|
3415
|
+
var shaLine = (g) => `Head \`${g.sha.slice(0, 8)}\`.`;
|
|
3416
|
+
var GATE_RED_AFTER = 3;
|
|
3417
|
+
var lastLines = (output, n) => output.replace(/\u001b\[[0-9;]*m/g, "").trimEnd().split("\n").slice(-n).join("\n");
|
|
3418
|
+
function gateRedTickets(merged, merges) {
|
|
3419
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3420
|
+
const out = [];
|
|
3421
|
+
for (const c of [...merged, ...merges.filter((m) => m.skipReason === "already-merged")]) {
|
|
3422
|
+
if (seen.has(c.ticket.id)) continue;
|
|
3423
|
+
seen.add(c.ticket.id);
|
|
3424
|
+
out.push(c.ticket);
|
|
3425
|
+
}
|
|
3426
|
+
return out;
|
|
3427
|
+
}
|
|
3428
|
+
function gateRedComment(g) {
|
|
3429
|
+
return `${GATE_RED_MARKER}
|
|
3430
|
+
> ${shaLine(g)}
|
|
3431
|
+
|
|
3432
|
+
**This ticket is merged on the base, but the release test gate is red \u2014 nothing has shipped.**
|
|
3433
|
+
|
|
3434
|
+
The \`test\` hook (\`${g.hook}\`) has failed ${g.count} cycles running on the same head (\`${g.sha.slice(0, 8)}\`). The ticket's commit is on the base and will ship once the gate passes; it is not stranded, and re-setting it to \`verified\` will not help. Last lines of the hook's output:
|
|
3435
|
+
|
|
3436
|
+
\`\`\`
|
|
3437
|
+
${g.tail}
|
|
3438
|
+
\`\`\`
|
|
3439
|
+
`;
|
|
3440
|
+
}
|
|
3441
|
+
var alreadyReported = (ticket, g, events) => events.some((e) => e.record_id === ticket.id && e.event_type === "gate_red" && (e.message ?? "").includes(shaLine(g)));
|
|
3442
|
+
|
|
3443
|
+
// src/test-evidence.ts
|
|
3444
|
+
var MAX_FAIL_LINES = 20;
|
|
3445
|
+
var ANSI = /\u001b\[[0-9;]*m/g;
|
|
3446
|
+
var FAIL_LINE = /^\s*(FAIL\b|●\s|not ok\b)/;
|
|
3447
|
+
var ERROR_LINE = /\b(?:[A-Z]\w*)?Error\b[:\s]/;
|
|
3448
|
+
function failureSummary(output, tailLines = 15) {
|
|
3449
|
+
const lines = output.replace(ANSI, "").trimEnd().split("\n");
|
|
3450
|
+
const names = [];
|
|
3451
|
+
for (const l of lines) {
|
|
3452
|
+
if (FAIL_LINE.test(l) && !names.includes(l.trim())) names.push(l.trim());
|
|
3453
|
+
if (names.length >= MAX_FAIL_LINES) break;
|
|
3454
|
+
}
|
|
3455
|
+
const at = lines.findIndex((l) => ERROR_LINE.test(l));
|
|
3456
|
+
if (names.length === 0 && at < 0) return lastLines(output, tailLines);
|
|
3457
|
+
const parts = [...names];
|
|
3458
|
+
if (at >= 0) parts.push("", ...lines.slice(at, at + 4).map((l) => l.trimEnd()));
|
|
3459
|
+
return parts.join("\n").trim();
|
|
3460
|
+
}
|
|
3461
|
+
var headLine = (head) => `Candidate head \`${head.slice(0, 8)}\`.`;
|
|
3462
|
+
function describeComposition(c) {
|
|
3463
|
+
const rows = c.branches.length ? c.branches.map((b) => `- ${b.key} \`${b.branch}\` @ \`${b.tip.slice(0, 8)}\``).join("\n") : "- (none merged this cycle)";
|
|
3464
|
+
return `${headLine(c.head)} Base before merging \`${c.base.slice(0, 8)}\`. Merged branches:
|
|
3465
|
+
|
|
3466
|
+
${rows}
|
|
3467
|
+
|
|
3468
|
+
The gate runs on the base plus every verified branch above, not on the base alone: reproduce on that composition.`;
|
|
3469
|
+
}
|
|
3470
|
+
|
|
3407
3471
|
// src/shell.ts
|
|
3408
3472
|
import { spawn } from "child_process";
|
|
3409
3473
|
import { platform } from "process";
|
|
@@ -3515,7 +3579,13 @@ function describeRelease(o, route) {
|
|
|
3515
3579
|
return {
|
|
3516
3580
|
level: "fail",
|
|
3517
3581
|
headline: `${route}: release blocked \u2014 tests failed`,
|
|
3518
|
-
detail:
|
|
3582
|
+
detail: withHookTail(
|
|
3583
|
+
o.composition ? `the target stays on the previous release
|
|
3584
|
+
|
|
3585
|
+
${describeComposition(o.composition)}` : "the target stays on the previous release",
|
|
3586
|
+
o.hookFailure
|
|
3587
|
+
),
|
|
3588
|
+
...o.composition ? { head: o.composition.head } : {}
|
|
3519
3589
|
};
|
|
3520
3590
|
}
|
|
3521
3591
|
if (o.stopped === "setup failed") {
|
|
@@ -3554,6 +3624,30 @@ function describeRelease(o, route) {
|
|
|
3554
3624
|
}
|
|
3555
3625
|
return null;
|
|
3556
3626
|
}
|
|
3627
|
+
function recoveredFailureHeadlines(o, route) {
|
|
3628
|
+
const setup = `${route}: release blocked \u2014 setup failed`;
|
|
3629
|
+
const tests = `${route}: release blocked \u2014 tests failed`;
|
|
3630
|
+
const build = `${route}: build FAILED`;
|
|
3631
|
+
const deploy = `${route}: deploy FAILED`;
|
|
3632
|
+
if (o.deployed || o.integrated) {
|
|
3633
|
+
return [
|
|
3634
|
+
setup,
|
|
3635
|
+
tests,
|
|
3636
|
+
build,
|
|
3637
|
+
deploy,
|
|
3638
|
+
`${route}: release blocked \u2014 checkout is not on the base branch`,
|
|
3639
|
+
`${route}: release blocked \u2014 working tree is not clean`
|
|
3640
|
+
];
|
|
3641
|
+
}
|
|
3642
|
+
switch (o.stopped) {
|
|
3643
|
+
case "build failed":
|
|
3644
|
+
case "deploy failed":
|
|
3645
|
+
case "tag push failed":
|
|
3646
|
+
return o.stopped === "deploy failed" ? [setup, tests, build] : [setup, tests];
|
|
3647
|
+
default:
|
|
3648
|
+
return [];
|
|
3649
|
+
}
|
|
3650
|
+
}
|
|
3557
3651
|
|
|
3558
3652
|
// src/install.ts
|
|
3559
3653
|
import { dirname as dirname4, join as join9, extname } from "path";
|
|
@@ -6497,11 +6591,6 @@ function materializeBranchForTicket(dir, cfg, t, role, onProblem) {
|
|
|
6497
6591
|
import { writeFileSync as writeFileSync7 } from "fs";
|
|
6498
6592
|
import { join as join11 } from "path";
|
|
6499
6593
|
|
|
6500
|
-
// src/callout.ts
|
|
6501
|
-
var calloutHeader = (kind, label) => `> [!${kind}]
|
|
6502
|
-
> **Crew** \u2014 ${label}`;
|
|
6503
|
-
var calloutLabelLine = (label) => `> **Crew** \u2014 ${label}`;
|
|
6504
|
-
|
|
6505
6594
|
// src/stalled.ts
|
|
6506
6595
|
var STALLED_AFTER_CYCLES = 30;
|
|
6507
6596
|
var CYCLE_MS = 120 * 1e3;
|
|
@@ -7229,36 +7318,6 @@ async function applyStrandedVerified(writer, s, contract, operatorId, memberId,
|
|
|
7229
7318
|
}
|
|
7230
7319
|
}
|
|
7231
7320
|
|
|
7232
|
-
// src/gate-red.ts
|
|
7233
|
-
var GATE_RED_MARKER = calloutHeader("CAUTION", "Test Gate Red");
|
|
7234
|
-
var shaLine = (g) => `Head \`${g.sha.slice(0, 8)}\`.`;
|
|
7235
|
-
var GATE_RED_AFTER = 3;
|
|
7236
|
-
var lastLines = (output, n) => output.replace(/\u001b\[[0-9;]*m/g, "").trimEnd().split("\n").slice(-n).join("\n");
|
|
7237
|
-
function gateRedTickets(merged, merges) {
|
|
7238
|
-
const seen = /* @__PURE__ */ new Set();
|
|
7239
|
-
const out = [];
|
|
7240
|
-
for (const c of [...merged, ...merges.filter((m) => m.skipReason === "already-merged")]) {
|
|
7241
|
-
if (seen.has(c.ticket.id)) continue;
|
|
7242
|
-
seen.add(c.ticket.id);
|
|
7243
|
-
out.push(c.ticket);
|
|
7244
|
-
}
|
|
7245
|
-
return out;
|
|
7246
|
-
}
|
|
7247
|
-
function gateRedComment(g) {
|
|
7248
|
-
return `${GATE_RED_MARKER}
|
|
7249
|
-
> ${shaLine(g)}
|
|
7250
|
-
|
|
7251
|
-
**This ticket is merged on the base, but the release test gate is red \u2014 nothing has shipped.**
|
|
7252
|
-
|
|
7253
|
-
The \`test\` hook (\`${g.hook}\`) has failed ${g.count} cycles running on the same head (\`${g.sha.slice(0, 8)}\`). The ticket's commit is on the base and will ship once the gate passes; it is not stranded, and re-setting it to \`verified\` will not help. Last lines of the hook's output:
|
|
7254
|
-
|
|
7255
|
-
\`\`\`
|
|
7256
|
-
${g.tail}
|
|
7257
|
-
\`\`\`
|
|
7258
|
-
`;
|
|
7259
|
-
}
|
|
7260
|
-
var alreadyReported = (ticket, g, events) => events.some((e) => e.record_id === ticket.id && e.event_type === "gate_red" && (e.message ?? "").includes(shaLine(g)));
|
|
7261
|
-
|
|
7262
7321
|
// src/agent.ts
|
|
7263
7322
|
import { spawn as spawn2, spawnSync as spawnSync5 } from "child_process";
|
|
7264
7323
|
import { readFileSync as readFileSync12, existsSync as existsSync14, statSync as statSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync8, createWriteStream, openSync as openSync3, closeSync as closeSync3, readSync } from "fs";
|
|
@@ -10693,7 +10752,21 @@ async function mergeAndRelease(o, decision, fresh, releaseBranch, isolated = fal
|
|
|
10693
10752
|
const sha = headSha(o.cwd);
|
|
10694
10753
|
const count = o.state?.noteTestGateFailed?.(sha) ?? 1;
|
|
10695
10754
|
const gateRed = count >= GATE_RED_AFTER && !o.state?.testGateReported?.(sha) ? { sha, count, hook: hookLabel(o.repo, "test"), tail: lastLines(r.output, 15) } : void 0;
|
|
10696
|
-
|
|
10755
|
+
const composition = {
|
|
10756
|
+
head: sha,
|
|
10757
|
+
base: preMerge,
|
|
10758
|
+
branches: merged.filter((c) => c.branch).map((c) => ({
|
|
10759
|
+
key: c.ticket.issue_tag ?? c.ticket.issue_id,
|
|
10760
|
+
branch: c.branch,
|
|
10761
|
+
tip: resolve2(o.cwd, c.branch) ?? ""
|
|
10762
|
+
}))
|
|
10763
|
+
};
|
|
10764
|
+
const hookFailure2 = {
|
|
10765
|
+
hook: "test",
|
|
10766
|
+
label: hookLabel(o.repo, "test"),
|
|
10767
|
+
tail: failureSummary(r.output, HOOK_FAILURE_TAIL_LINES)
|
|
10768
|
+
};
|
|
10769
|
+
return { merged, conflicts, unbuildable, deployed: false, stopped: "tests failed", decision, gateRed, hookFailure: hookFailure2, composition };
|
|
10697
10770
|
}
|
|
10698
10771
|
o.state?.clearTestGateFailed?.();
|
|
10699
10772
|
}
|
|
@@ -10865,9 +10938,15 @@ async function applyFailureAlert(writer, n, route, openTickets, memberId, log, d
|
|
|
10865
10938
|
return { kind: "commented", issueId: label };
|
|
10866
10939
|
}
|
|
10867
10940
|
try {
|
|
10941
|
+
let body = n.detail ?? n.headline;
|
|
10942
|
+
if (n.head && writer.events) {
|
|
10943
|
+
const line = headLine(n.head);
|
|
10944
|
+
const shown = (await writer.events("failure_alert")).some((e) => e.record_id === existing.id && (e.message ?? "").includes(line));
|
|
10945
|
+
if (shown) body = `${n.headline} \u2014 ${line}`;
|
|
10946
|
+
}
|
|
10868
10947
|
await writer.postEvent(existing.id, `${FAILURE_ALERT_MARKER}
|
|
10869
10948
|
|
|
10870
|
-
Still failing: ${
|
|
10949
|
+
Still failing: ${body}`, memberId, "failure_alert");
|
|
10871
10950
|
log.warn(`release failure recurring \u2014 commented on ${label}`, { step: "release" });
|
|
10872
10951
|
return { kind: "commented", issueId: label };
|
|
10873
10952
|
} catch (e) {
|
|
@@ -10889,12 +10968,64 @@ Still failing: ${n.detail ?? n.headline}`, memberId, "failure_alert");
|
|
|
10889
10968
|
...placement.project_id ? { project_id: placement.project_id } : {}
|
|
10890
10969
|
}, memberId);
|
|
10891
10970
|
const label = created.issue_id ? displayKey(created) : created.id;
|
|
10971
|
+
if (n.head) {
|
|
10972
|
+
await writer.postEvent(created.id, `${FAILURE_ALERT_MARKER}
|
|
10973
|
+
|
|
10974
|
+
Filed with the evidence above. ${headLine(n.head)}`, memberId, "failure_alert").catch(() => {
|
|
10975
|
+
});
|
|
10976
|
+
}
|
|
10892
10977
|
log.warn(`release failure \u2014 filed ${label}`, { step: "release" });
|
|
10893
10978
|
return { kind: "filed", issueId: label };
|
|
10894
10979
|
} catch (e) {
|
|
10895
10980
|
return { kind: "failed", why: e.message };
|
|
10896
10981
|
}
|
|
10897
10982
|
}
|
|
10983
|
+
function alertResolvedStatus(c) {
|
|
10984
|
+
return c.statuses.resolved.find((s) => s === "closed_completed");
|
|
10985
|
+
}
|
|
10986
|
+
async function applyFailureRecovery(writer, headlines, openTickets, contract, memberId, passed, log, dryRun) {
|
|
10987
|
+
const out = { closed: [], noted: [], failed: [] };
|
|
10988
|
+
const closedStatus = alertResolvedStatus(contract);
|
|
10989
|
+
const closable = /* @__PURE__ */ new Set(["new", contract.statuses.approved, contract.statuses.needsHuman]);
|
|
10990
|
+
for (const headline of headlines) {
|
|
10991
|
+
const alert = findOpenFailureAlert(openTickets, headline);
|
|
10992
|
+
if (!alert) continue;
|
|
10993
|
+
const label = displayKey(alert);
|
|
10994
|
+
const willClose = closedStatus !== void 0 && closable.has(alert.status);
|
|
10995
|
+
if (dryRun) {
|
|
10996
|
+
log.emit(`would ${willClose ? "close" : "note on"} ${label} \u2014 ${passed}`, { step: "release" });
|
|
10997
|
+
continue;
|
|
10998
|
+
}
|
|
10999
|
+
try {
|
|
11000
|
+
await writer.postEvent(
|
|
11001
|
+
alert.id,
|
|
11002
|
+
`${FAILURE_ALERT_MARKER}
|
|
11003
|
+
|
|
11004
|
+
Passed: ${passed}.${willClose ? " Closing this alert." : ""}`,
|
|
11005
|
+
memberId,
|
|
11006
|
+
"failure_alert"
|
|
11007
|
+
);
|
|
11008
|
+
if (willClose) {
|
|
11009
|
+
const col = contract.columns;
|
|
11010
|
+
await writer.updateTicket(alert.id, {
|
|
11011
|
+
[contract.columns.status]: closedStatus,
|
|
11012
|
+
[col.resolutionNote]: `Resolved automatically: ${passed}.`,
|
|
11013
|
+
[col.assignee]: null,
|
|
11014
|
+
[col.heldBy]: null
|
|
11015
|
+
});
|
|
11016
|
+
out.closed.push(label);
|
|
11017
|
+
log.emit(`${label} closed \u2014 ${passed}`, { step: "release" });
|
|
11018
|
+
} else {
|
|
11019
|
+
out.noted.push(label);
|
|
11020
|
+
log.emit(`${label} left open \u2014 ${passed}`, { step: "release" });
|
|
11021
|
+
}
|
|
11022
|
+
} catch (e) {
|
|
11023
|
+
out.failed.push(label);
|
|
11024
|
+
log.warn(`could not resolve failure alert ${label}: ${e.message}`, { step: "release" });
|
|
11025
|
+
}
|
|
11026
|
+
}
|
|
11027
|
+
return out;
|
|
11028
|
+
}
|
|
10898
11029
|
|
|
10899
11030
|
// src/projects-cmd.ts
|
|
10900
11031
|
async function buildProjectsReport(routes, open) {
|
|
@@ -14792,6 +14923,22 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14792
14923
|
});
|
|
14793
14924
|
}
|
|
14794
14925
|
}
|
|
14926
|
+
const recovered = recoveredFailureHeadlines(outcome, scope);
|
|
14927
|
+
if (recovered.length) {
|
|
14928
|
+
const passed = outcome.version ? `${scope} released ${outcome.version}` : outcome.deployed || outcome.integrated ? `${scope} release passed` : `${scope} release got past this stage`;
|
|
14929
|
+
await applyFailureRecovery(
|
|
14930
|
+
tracker,
|
|
14931
|
+
recovered,
|
|
14932
|
+
all,
|
|
14933
|
+
tracker.contract,
|
|
14934
|
+
releaseSeat(c),
|
|
14935
|
+
passed,
|
|
14936
|
+
remit,
|
|
14937
|
+
dryRun
|
|
14938
|
+
).catch((e) => {
|
|
14939
|
+
remit.warn(`could not resolve failure tickets: ${e.message}`, { step: "release" });
|
|
14940
|
+
});
|
|
14941
|
+
}
|
|
14795
14942
|
if (outcome.buildCulprits?.length) {
|
|
14796
14943
|
const seat = releaseSeat(c);
|
|
14797
14944
|
for (const cu of outcome.buildCulprits) {
|
package/package.json
CHANGED