@tablation/crew 0.3.0 → 0.4.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 +124 -6
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1328,6 +1328,8 @@ var State = class _State {
|
|
|
1328
1328
|
const suffix = _State.safe(route);
|
|
1329
1329
|
const blocked = `.release-blocked-${suffix}`;
|
|
1330
1330
|
const failed = `.deploy-failed-sha-${suffix}`;
|
|
1331
|
+
const gateFailed = `.test-gate-failed-${suffix}`;
|
|
1332
|
+
const gateReported = `.test-gate-reported-${suffix}`;
|
|
1331
1333
|
const read = (f) => {
|
|
1332
1334
|
try {
|
|
1333
1335
|
return readFileSync3(this.path(f), "utf8").trim() || null;
|
|
@@ -1361,6 +1363,27 @@ var State = class _State {
|
|
|
1361
1363
|
},
|
|
1362
1364
|
clearDeployFailed: () => {
|
|
1363
1365
|
rmSync(this.path(failed), { force: true });
|
|
1366
|
+
},
|
|
1367
|
+
/**
|
|
1368
|
+
* Consecutive test-gate failures on the same head (CREW-1368). A failure
|
|
1369
|
+
* on a new head restarts the count. Held on disk because one process is
|
|
1370
|
+
* one cycle, and "failed three cycles running" only exists across them.
|
|
1371
|
+
*/
|
|
1372
|
+
noteTestGateFailed: (sha) => {
|
|
1373
|
+
const [prev, n] = (read(gateFailed) ?? "").split(" ");
|
|
1374
|
+
const next = prev === sha ? (Number.parseInt(n ?? "", 10) || 0) + 1 : 1;
|
|
1375
|
+
writeFileSync(this.path(gateFailed), `${sha} ${next}
|
|
1376
|
+
`);
|
|
1377
|
+
return next;
|
|
1378
|
+
},
|
|
1379
|
+
clearTestGateFailed: () => {
|
|
1380
|
+
rmSync(this.path(gateFailed), { force: true });
|
|
1381
|
+
},
|
|
1382
|
+
/** Whether the board was already told about this head's red gate. */
|
|
1383
|
+
testGateReported: (sha) => read(gateReported) === sha,
|
|
1384
|
+
noteTestGateReported: (sha) => {
|
|
1385
|
+
writeFileSync(this.path(gateReported), `${sha}
|
|
1386
|
+
`);
|
|
1364
1387
|
}
|
|
1365
1388
|
};
|
|
1366
1389
|
}
|
|
@@ -2869,6 +2892,10 @@ var countCommits = (cwd, range) => Number.parseInt(gitOk(cwd, ["rev-list", "--co
|
|
|
2869
2892
|
function latestReleaseTag(cwd, pattern = "v*", ref = "HEAD") {
|
|
2870
2893
|
return gitOk(cwd, ["describe", "--tags", "--abbrev=0", "--match", pattern, ref]);
|
|
2871
2894
|
}
|
|
2895
|
+
function firstReleaseTagContaining(cwd, sha, pattern = "v*") {
|
|
2896
|
+
const out = gitOk(cwd, ["tag", "--contains", sha, "--list", pattern, "--sort=creatordate"]);
|
|
2897
|
+
return out?.split("\n").map((l) => l.trim()).find(Boolean) ?? null;
|
|
2898
|
+
}
|
|
2872
2899
|
var tagCommit = (cwd, tag) => resolve2(cwd, tag);
|
|
2873
2900
|
function createReleaseTag(cwd, tag, sha, message) {
|
|
2874
2901
|
git(cwd, ["tag", "-a", tag, sha, "-m", message]);
|
|
@@ -3579,7 +3606,7 @@ function flagComment(s) {
|
|
|
3579
3606
|
return `${STRANDED_MARKER}
|
|
3580
3607
|
**Verified, but nothing to merge \u2014 flagging before this repeats forever.**
|
|
3581
3608
|
|
|
3582
|
-
\`${key}\` is \`verified\` with no branch on this ship, and its key names no commit
|
|
3609
|
+
\`${key}\` is \`verified\` with no branch on this ship, and its key names no commit anywhere in this ship's \`${s.base}\` history (searched all of it, local commits included, for a bounded \`${key}\` in a commit subject or body). One of three things is true: it landed under a commit that never named the key, the branch was deleted before it merged, or it was verified in error \u2014 the crew cannot tell these apart and will not guess.
|
|
3583
3610
|
|
|
3584
3611
|
If it is still unplaceable next cycle, this ticket is escalated to a person. If the work already landed, closing this ticket by hand now avoids that.`;
|
|
3585
3612
|
}
|
|
@@ -3588,7 +3615,7 @@ function escalateComment2(s) {
|
|
|
3588
3615
|
return `${STRANDED_MARKER}
|
|
3589
3616
|
**Still nothing to merge \u2014 stopping rather than repeating.**
|
|
3590
3617
|
|
|
3591
|
-
\`${key}\` was flagged as stranded at \`verified\` on an earlier cycle and still has no branch on this ship or on \`${s.remote}\` and no commit
|
|
3618
|
+
\`${key}\` was flagged as stranded at \`verified\` on an earlier cycle and still has no branch on this ship or on \`${s.remote}\` and no commit in this ship's \`${s.base}\` history naming its key (searched all of it, local commits included). This needs a person: either the work landed under a commit that doesn't name the key and this ticket can simply close, or the branch is gone and the work needs redoing.
|
|
3592
3619
|
|
|
3593
3620
|
The crew will not close this on its own.`;
|
|
3594
3621
|
}
|
|
@@ -3632,6 +3659,33 @@ async function applyStrandedVerified(writer, s, contract, operatorId, memberId,
|
|
|
3632
3659
|
}
|
|
3633
3660
|
}
|
|
3634
3661
|
|
|
3662
|
+
// src/gate-red.ts
|
|
3663
|
+
var GATE_RED_MARKER = "<!-- crew:test-gate-red";
|
|
3664
|
+
var GATE_RED_AFTER = 3;
|
|
3665
|
+
var lastLines = (output, n) => output.replace(/\u001b\[[0-9;]*m/g, "").trimEnd().split("\n").slice(-n).join("\n");
|
|
3666
|
+
function gateRedTickets(merged, merges) {
|
|
3667
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3668
|
+
const out = [];
|
|
3669
|
+
for (const c of [...merged, ...merges.filter((m) => m.skipReason === "already-merged")]) {
|
|
3670
|
+
if (seen.has(c.ticket.id)) continue;
|
|
3671
|
+
seen.add(c.ticket.id);
|
|
3672
|
+
out.push(c.ticket);
|
|
3673
|
+
}
|
|
3674
|
+
return out;
|
|
3675
|
+
}
|
|
3676
|
+
function gateRedComment(g) {
|
|
3677
|
+
return `${GATE_RED_MARKER} ${g.sha.slice(0, 8)} -->
|
|
3678
|
+
**This ticket is merged on the base, but the release test gate is red \u2014 nothing has shipped.**
|
|
3679
|
+
|
|
3680
|
+
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:
|
|
3681
|
+
|
|
3682
|
+
\`\`\`
|
|
3683
|
+
${g.tail}
|
|
3684
|
+
\`\`\`
|
|
3685
|
+
`;
|
|
3686
|
+
}
|
|
3687
|
+
var alreadyReported = (ticket, g, comments) => comments.some((c) => c.ticket_id === ticket.id && (c.body ?? "").includes(`${GATE_RED_MARKER} ${g.sha.slice(0, 8)}`));
|
|
3688
|
+
|
|
3635
3689
|
// src/agent.ts
|
|
3636
3690
|
import { spawn } from "child_process";
|
|
3637
3691
|
import { readFileSync as readFileSync5, existsSync as existsSync6, statSync as statSync2, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3, createWriteStream } from "fs";
|
|
@@ -4888,7 +4942,32 @@ function planMerge(cwd, tickets, contract, lastReleased, branchFor = (t) => bran
|
|
|
4888
4942
|
return verified.map((ticket) => {
|
|
4889
4943
|
const branch = branchFor(ticket);
|
|
4890
4944
|
if (!branch) {
|
|
4891
|
-
const
|
|
4945
|
+
const keys = referenceKeys(ticket);
|
|
4946
|
+
const unreleased = lastReleased ? findKeyInRange(cwd, keys, `${lastReleased}..HEAD`) : null;
|
|
4947
|
+
if (unreleased) {
|
|
4948
|
+
return {
|
|
4949
|
+
ticket,
|
|
4950
|
+
branch: null,
|
|
4951
|
+
entries: [],
|
|
4952
|
+
usedFallback: false,
|
|
4953
|
+
bump: "patch",
|
|
4954
|
+
majorRequested: false,
|
|
4955
|
+
skipReason: "already-merged"
|
|
4956
|
+
};
|
|
4957
|
+
}
|
|
4958
|
+
const anywhere = findKeyInRange(cwd, keys, "HEAD");
|
|
4959
|
+
if (anywhere) {
|
|
4960
|
+
return {
|
|
4961
|
+
ticket,
|
|
4962
|
+
branch: null,
|
|
4963
|
+
entries: [],
|
|
4964
|
+
usedFallback: false,
|
|
4965
|
+
bump: "patch",
|
|
4966
|
+
majorRequested: false,
|
|
4967
|
+
skipReason: lastReleased ? "already-released" : "already-merged",
|
|
4968
|
+
mergedSha: anywhere
|
|
4969
|
+
};
|
|
4970
|
+
}
|
|
4892
4971
|
return {
|
|
4893
4972
|
ticket,
|
|
4894
4973
|
branch: null,
|
|
@@ -4896,7 +4975,7 @@ function planMerge(cwd, tickets, contract, lastReleased, branchFor = (t) => bran
|
|
|
4896
4975
|
usedFallback: false,
|
|
4897
4976
|
bump: "patch",
|
|
4898
4977
|
majorRequested: false,
|
|
4899
|
-
skipReason:
|
|
4978
|
+
skipReason: "never-built"
|
|
4900
4979
|
};
|
|
4901
4980
|
}
|
|
4902
4981
|
const squash = findSquashOfBranch(
|
|
@@ -5465,7 +5544,12 @@ async function runRelease(o) {
|
|
|
5465
5544
|
let applied = 0;
|
|
5466
5545
|
for (const c of decision.merges) {
|
|
5467
5546
|
if (!c.branch || c.mergedSha) {
|
|
5468
|
-
if (c.skipReason === "already-
|
|
5547
|
+
if (c.skipReason === "already-released") {
|
|
5548
|
+
o.emit.emit(
|
|
5549
|
+
`verified and already released (on the base as ${c.mergedSha?.slice(0, 7)}), waiting to be stamped`,
|
|
5550
|
+
{ ticket: c.ticket.issue_id }
|
|
5551
|
+
);
|
|
5552
|
+
} else if (c.skipReason === "already-merged") {
|
|
5469
5553
|
o.emit.emit(
|
|
5470
5554
|
`verified and already merged${c.mergedSha ? ` (on the base as ${c.mergedSha.slice(0, 7)})` : ""}, waiting on a successful release`,
|
|
5471
5555
|
{ ticket: c.ticket.issue_id }
|
|
@@ -5570,8 +5654,12 @@ async function runRelease(o) {
|
|
|
5570
5654
|
}
|
|
5571
5655
|
if (r && r.code !== 0) {
|
|
5572
5656
|
o.emit.error(`test gate FAILED \u2014 not deploying; the target stays on the previous release`);
|
|
5573
|
-
|
|
5657
|
+
const sha = headSha(o.cwd);
|
|
5658
|
+
const count = o.state?.noteTestGateFailed?.(sha) ?? 1;
|
|
5659
|
+
const gateRed = count >= GATE_RED_AFTER && !o.state?.testGateReported?.(sha) ? { sha, count, hook: hookLabel(o.repo, "test"), tail: lastLines(r.output, 15) } : void 0;
|
|
5660
|
+
return { merged, conflicts, unbuildable, deployed: false, stopped: "tests failed", decision, gateRed };
|
|
5574
5661
|
}
|
|
5662
|
+
o.state?.clearTestGateFailed?.();
|
|
5575
5663
|
}
|
|
5576
5664
|
}
|
|
5577
5665
|
const size = merged.some((m) => m.bump === "minor") ? "minor" : "patch";
|
|
@@ -8983,6 +9071,36 @@ ${changed2.map((c) => ` - ${c}
|
|
|
8983
9071
|
} else if (outcome.stopped) {
|
|
8984
9072
|
remit.emit(`nothing stamped \u2014 ${outcome.stopped}`);
|
|
8985
9073
|
}
|
|
9074
|
+
for (const m of outcome.decision.merges) {
|
|
9075
|
+
if (m.skipReason !== "already-released" || !m.mergedSha) continue;
|
|
9076
|
+
const tag = firstReleaseTagContaining(target.dir, m.mergedSha, repo.release.tagPattern ?? "v*");
|
|
9077
|
+
if (!tag) continue;
|
|
9078
|
+
remit.enter("reconcile");
|
|
9079
|
+
await applyStamp(
|
|
9080
|
+
tracker,
|
|
9081
|
+
[{ ticket: m.ticket, reason: `carried by ${tag}`, sha: m.mergedSha }],
|
|
9082
|
+
tag.replace(/^v/, ""),
|
|
9083
|
+
tracker.contract,
|
|
9084
|
+
remit,
|
|
9085
|
+
dryRun,
|
|
9086
|
+
target.dir
|
|
9087
|
+
);
|
|
9088
|
+
}
|
|
9089
|
+
if (outcome.gateRed && !dryRun) {
|
|
9090
|
+
try {
|
|
9091
|
+
const g = outcome.gateRed;
|
|
9092
|
+
const comments = await tracker.comments();
|
|
9093
|
+
const seat = c.resolved?.seats.dev ?? c.resolved?.seats.qa ?? "";
|
|
9094
|
+
for (const t of gateRedTickets(outcome.merged, outcome.decision.merges)) {
|
|
9095
|
+
if (alreadyReported(t, g, comments)) continue;
|
|
9096
|
+
await tracker.postEvent(t.id, gateRedComment(g), seat);
|
|
9097
|
+
remit.warn(`test gate red for ${g.count} cycles \u2014 noted on the ticket`, { ticket: t.issue_id, step: "merge" });
|
|
9098
|
+
}
|
|
9099
|
+
state.release(scope).noteTestGateReported(g.sha);
|
|
9100
|
+
} catch (e) {
|
|
9101
|
+
remit.warn(`could not note the red test gate on the board: ${e.message}`, { step: "merge" });
|
|
9102
|
+
}
|
|
9103
|
+
}
|
|
8986
9104
|
if (outcome.externalClosures?.length) {
|
|
8987
9105
|
await applyExternalClosures(tracker, outcome.externalClosures, tracker.contract, remit, dryRun, target.dir);
|
|
8988
9106
|
}
|
package/package.json
CHANGED