@tablation/crew 0.43.3 → 0.44.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
CHANGED
|
@@ -1432,6 +1432,7 @@ var RELEASE_KEYS = /* @__PURE__ */ new Set([
|
|
|
1432
1432
|
"ship",
|
|
1433
1433
|
"ci",
|
|
1434
1434
|
"verify",
|
|
1435
|
+
"isolateFailures",
|
|
1435
1436
|
"versioning",
|
|
1436
1437
|
"versionFiles",
|
|
1437
1438
|
"changelog",
|
|
@@ -1441,6 +1442,14 @@ var RELEASE_KEYS = /* @__PURE__ */ new Set([
|
|
|
1441
1442
|
var VERSIONINGS = ["auto", "none"];
|
|
1442
1443
|
var CI_KEYS = /* @__PURE__ */ new Set(["provider", "ref"]);
|
|
1443
1444
|
var VERIFY_KEYS = /* @__PURE__ */ new Set(["match", "timeoutSeconds", "intervalSeconds"]);
|
|
1445
|
+
var ISOLATE_KEYS = /* @__PURE__ */ new Set(["maxCandidates"]);
|
|
1446
|
+
var DEFAULT_ISOLATE_MAX_CANDIDATES = 4;
|
|
1447
|
+
function parseIsolateMax(raw, where, fail) {
|
|
1448
|
+
if (raw === void 0 || raw === null) return void 0;
|
|
1449
|
+
const n = Number(raw);
|
|
1450
|
+
if (!Number.isInteger(n) || n < 0) throw fail(`${where}.maxCandidates must be a non-negative integer (got "${raw}")`);
|
|
1451
|
+
return n;
|
|
1452
|
+
}
|
|
1444
1453
|
var MATCHES = ["commit", "version"];
|
|
1445
1454
|
var MODES = ["local", "integrate", "ci_manual", "ci_auto", "external"];
|
|
1446
1455
|
var PROVIDERS = ["github", "buildkite", "other", "none"];
|
|
@@ -1474,6 +1483,7 @@ function parseRepoConfig(text, file) {
|
|
|
1474
1483
|
rejectUnknown(raw.release, RELEASE_KEYS, "release", file);
|
|
1475
1484
|
rejectUnknown(raw.release?.ci, CI_KEYS, "release.ci", file);
|
|
1476
1485
|
rejectUnknown(raw.release?.verify, VERIFY_KEYS, "release.verify", file);
|
|
1486
|
+
rejectUnknown(raw.release?.isolateFailures, ISOLATE_KEYS, "release.isolateFailures", file);
|
|
1477
1487
|
rejectUnknown(raw.branch, BRANCH_KEYS, "branch", file);
|
|
1478
1488
|
rejectUnknown(raw.worktrees, /* @__PURE__ */ new Set(["copy", "prefix", "name"]), "worktrees", file);
|
|
1479
1489
|
rejectUnknown(raw.docs, /* @__PURE__ */ new Set(["triagePolicy", "designGuide"]), "docs", file);
|
|
@@ -1611,6 +1621,11 @@ function parseRepoConfig(text, file) {
|
|
|
1611
1621
|
`${file}: release.verify.intervalSeconds (${verify.intervalSeconds}) exceeds timeoutSeconds (${verify.timeoutSeconds}) \u2014 it would never be asked twice`
|
|
1612
1622
|
);
|
|
1613
1623
|
}
|
|
1624
|
+
const isolateMax = parseIsolateMax(
|
|
1625
|
+
raw.release?.isolateFailures?.maxCandidates,
|
|
1626
|
+
"release.isolateFailures",
|
|
1627
|
+
(m) => new RepoConfigError(`${file}: ${m}`)
|
|
1628
|
+
);
|
|
1614
1629
|
const versioning = String(raw.release?.versioning ?? "auto");
|
|
1615
1630
|
if (!VERSIONINGS.includes(versioning)) {
|
|
1616
1631
|
throw new RepoConfigError(`${file}: release.versioning must be auto|none (got "${versioning}")`);
|
|
@@ -1664,6 +1679,7 @@ function parseRepoConfig(text, file) {
|
|
|
1664
1679
|
ship,
|
|
1665
1680
|
ci: { provider, ref: raw.release?.ci?.ref ? String(raw.release.ci.ref) : void 0 },
|
|
1666
1681
|
verify,
|
|
1682
|
+
isolateFailures: isolateMax === void 0 ? void 0 : { maxCandidates: isolateMax },
|
|
1667
1683
|
versioning,
|
|
1668
1684
|
versionFiles,
|
|
1669
1685
|
tag,
|
|
@@ -1848,6 +1864,14 @@ function resolveRepoConfig(repo, ship, dir) {
|
|
|
1848
1864
|
DEFAULTS.intervalSeconds
|
|
1849
1865
|
)
|
|
1850
1866
|
},
|
|
1867
|
+
isolateFailures: {
|
|
1868
|
+
maxCandidates: pick(
|
|
1869
|
+
"release.isolateFailures.maxCandidates",
|
|
1870
|
+
repo?.release.isolateFailures?.maxCandidates,
|
|
1871
|
+
ship?.release?.isolateFailures?.maxCandidates,
|
|
1872
|
+
DEFAULT_ISOLATE_MAX_CANDIDATES
|
|
1873
|
+
)
|
|
1874
|
+
},
|
|
1851
1875
|
versioning: pick(
|
|
1852
1876
|
"release.versioning",
|
|
1853
1877
|
repo?.release.versioning === "auto" ? void 0 : repo?.release.versioning,
|
|
@@ -1999,6 +2023,7 @@ var RELEASE_OVERRIDE_KEYS = /* @__PURE__ */ new Set([
|
|
|
1999
2023
|
"mode",
|
|
2000
2024
|
"ci",
|
|
2001
2025
|
"verify",
|
|
2026
|
+
"isolateFailures",
|
|
2002
2027
|
"versioning",
|
|
2003
2028
|
"versionFiles",
|
|
2004
2029
|
"versionFile",
|
|
@@ -2008,6 +2033,7 @@ var RELEASE_OVERRIDE_KEYS = /* @__PURE__ */ new Set([
|
|
|
2008
2033
|
]);
|
|
2009
2034
|
var CI_OVERRIDE_KEYS = /* @__PURE__ */ new Set(["provider", "ref"]);
|
|
2010
2035
|
var VERIFY_OVERRIDE_KEYS = /* @__PURE__ */ new Set(["match", "timeoutSeconds", "intervalSeconds"]);
|
|
2036
|
+
var ISOLATE_OVERRIDE_KEYS = /* @__PURE__ */ new Set(["maxCandidates"]);
|
|
2011
2037
|
function rejectUnknownKeys(obj, allowed, where, file) {
|
|
2012
2038
|
if (!obj || typeof obj !== "object") return;
|
|
2013
2039
|
const unknown = Object.keys(obj).filter((k) => !allowed.has(k));
|
|
@@ -2022,6 +2048,7 @@ function parseReleaseOverride(raw, where, file) {
|
|
|
2022
2048
|
rejectUnknownKeys(raw, RELEASE_OVERRIDE_KEYS, `${where}.release`, file);
|
|
2023
2049
|
if (raw.ci !== void 0) rejectUnknownKeys(raw.ci, CI_OVERRIDE_KEYS, `${where}.release.ci`, file);
|
|
2024
2050
|
if (raw.verify !== void 0) rejectUnknownKeys(raw.verify, VERIFY_OVERRIDE_KEYS, `${where}.release.verify`, file);
|
|
2051
|
+
if (raw.isolateFailures !== void 0) rejectUnknownKeys(raw.isolateFailures, ISOLATE_OVERRIDE_KEYS, `${where}.release.isolateFailures`, file);
|
|
2025
2052
|
const mode = raw.mode === void 0 ? void 0 : String(raw.mode);
|
|
2026
2053
|
if (mode !== void 0 && !MODES.includes(mode)) {
|
|
2027
2054
|
throw new ConfigError(`${file}: ${where}.release.mode must be one of ${MODES.join("|")} (got "${raw.mode}")`);
|
|
@@ -2050,11 +2077,17 @@ function parseReleaseOverride(raw, where, file) {
|
|
|
2050
2077
|
};
|
|
2051
2078
|
const timeoutSeconds = num(raw.verify?.timeoutSeconds, "timeoutSeconds");
|
|
2052
2079
|
const intervalSeconds = num(raw.verify?.intervalSeconds, "intervalSeconds");
|
|
2080
|
+
const isolateMax = parseIsolateMax(
|
|
2081
|
+
raw.isolateFailures?.maxCandidates,
|
|
2082
|
+
`${where}.release.isolateFailures`,
|
|
2083
|
+
(m) => new ConfigError(`${file}: ${m}`)
|
|
2084
|
+
);
|
|
2053
2085
|
const versionFiles = Array.isArray(raw.versionFiles) ? raw.versionFiles : raw.versionFile ? [String(raw.versionFile)] : void 0;
|
|
2054
2086
|
return {
|
|
2055
2087
|
mode,
|
|
2056
2088
|
ci: provider !== void 0 || raw.ci?.ref !== void 0 ? { provider, ref: raw.ci?.ref !== void 0 ? String(raw.ci.ref) : void 0 } : void 0,
|
|
2057
2089
|
verify: match !== void 0 || timeoutSeconds !== void 0 || intervalSeconds !== void 0 ? { match, timeoutSeconds, intervalSeconds } : void 0,
|
|
2090
|
+
...isolateMax === void 0 ? {} : { isolateFailures: { maxCandidates: isolateMax } },
|
|
2058
2091
|
versioning,
|
|
2059
2092
|
versionFiles,
|
|
2060
2093
|
changelog: raw.changelog !== void 0 ? String(raw.changelog) : void 0,
|
|
@@ -2078,10 +2111,12 @@ function mergeRouteRelease(route, override) {
|
|
|
2078
2111
|
const match = override?.verify?.match ?? route.verify?.match;
|
|
2079
2112
|
const timeoutSeconds = override?.verify?.timeoutSeconds ?? route.verify?.timeoutSeconds;
|
|
2080
2113
|
const intervalSeconds = override?.verify?.intervalSeconds ?? route.verify?.intervalSeconds;
|
|
2114
|
+
const isolateMax = override?.isolateFailures?.maxCandidates ?? route.isolateFailures?.maxCandidates;
|
|
2081
2115
|
return {
|
|
2082
2116
|
mode: override?.mode ?? route.mode,
|
|
2083
2117
|
ci: provider !== void 0 || ref !== void 0 ? { provider, ref } : void 0,
|
|
2084
2118
|
verify: match !== void 0 || timeoutSeconds !== void 0 || intervalSeconds !== void 0 ? { match, timeoutSeconds, intervalSeconds } : void 0,
|
|
2119
|
+
...isolateMax === void 0 ? {} : { isolateFailures: { maxCandidates: isolateMax } },
|
|
2085
2120
|
versioning: override?.versioning ?? route.versioning,
|
|
2086
2121
|
versionFiles: override?.versionFiles ?? route.versionFiles,
|
|
2087
2122
|
changelog: override?.changelog ?? route.changelog,
|
|
@@ -6675,6 +6710,82 @@ async function applyConflictBounce(writer, b, contract, memberId, log, dryRun) {
|
|
|
6675
6710
|
}
|
|
6676
6711
|
}
|
|
6677
6712
|
|
|
6713
|
+
// src/build-failure.ts
|
|
6714
|
+
import { StaleWriteError as StaleWriteError6 } from "@tablation/client";
|
|
6715
|
+
var BUILD_FAILURE_MARKER = "<!-- crew:build-failure -->";
|
|
6716
|
+
function failingPaths(output, cwd) {
|
|
6717
|
+
const found = /* @__PURE__ */ new Set();
|
|
6718
|
+
const ext = String.raw`[\w.-]+\.[A-Za-z]{1,6}`;
|
|
6719
|
+
const dir = String.raw`(?:[\w@.~-]+/)*`;
|
|
6720
|
+
const patterns = [
|
|
6721
|
+
new RegExp(String.raw`^\s*(${dir}${ext})\(\d+,\d+\)\s*:`, "gm"),
|
|
6722
|
+
new RegExp(String.raw`(?:^|\s)(/?${dir}${ext}):\d+(?::\d+)?`, "gm"),
|
|
6723
|
+
new RegExp(String.raw`(?:FAIL|✖|✕|not ok \d+ -)\s+(/?${dir}${ext})`, "gm")
|
|
6724
|
+
];
|
|
6725
|
+
const root = cwd.endsWith("/") ? cwd : `${cwd}/`;
|
|
6726
|
+
for (const re of patterns) {
|
|
6727
|
+
for (const m of output.matchAll(re)) {
|
|
6728
|
+
let p = m[1];
|
|
6729
|
+
if (p.startsWith("/")) {
|
|
6730
|
+
if (!p.startsWith(root)) continue;
|
|
6731
|
+
p = p.slice(root.length);
|
|
6732
|
+
}
|
|
6733
|
+
p = p.replace(/^\.\//, "");
|
|
6734
|
+
if (p.includes("node_modules/")) continue;
|
|
6735
|
+
found.add(p);
|
|
6736
|
+
}
|
|
6737
|
+
}
|
|
6738
|
+
return [...found];
|
|
6739
|
+
}
|
|
6740
|
+
function culpritByFiles(candidates, paths) {
|
|
6741
|
+
if (paths.length === 0) return null;
|
|
6742
|
+
const hits = candidates.filter((c) => paths.some((p) => c.changed.has(p)));
|
|
6743
|
+
return hits.length === 1 ? hits[0] : null;
|
|
6744
|
+
}
|
|
6745
|
+
function planBuildFailureBounce(cwd, c, base, remote) {
|
|
6746
|
+
return { ...c, base, remote, worktree: worktreeForBranch(cwd, c.branch) };
|
|
6747
|
+
}
|
|
6748
|
+
function buildFailureComment(b) {
|
|
6749
|
+
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.`;
|
|
6750
|
+
return `${BUILD_FAILURE_MARKER}
|
|
6751
|
+
**Back with the dev lane \u2014 this branch builds on its own but not on current \`${b.base}\`.**
|
|
6752
|
+
|
|
6753
|
+
\`${b.branch}\` was verified, but the release's \`${b.hook}\` hook failed once it was merged with the other branches in the same release. ${how}
|
|
6754
|
+
|
|
6755
|
+
First step: \`git merge ${b.remote}/${b.base}\` in ${b.worktree ? `\`${b.worktree}\`` : "the branch's worktree"}, rebuild, and fix what disagrees \u2014 the merge is clean at the text level, so this is a semantic conflict (a duplicate identifier, a changed signature) with work that landed on \`${b.base}\` after this branch was cut. Push as a plain fast-forward and hand back to QA as usual.
|
|
6756
|
+
|
|
6757
|
+
\`\`\`
|
|
6758
|
+
${b.tail.trimEnd()}
|
|
6759
|
+
\`\`\`
|
|
6760
|
+
|
|
6761
|
+
Nothing was written to \`${b.base}\` for this ticket; the rest of the release shipped without it.`;
|
|
6762
|
+
}
|
|
6763
|
+
async function applyBuildFailureBounce(writer, b, contract, memberId, log, dryRun) {
|
|
6764
|
+
const t = { ticket: b.ticket.issue_id, step: "release" };
|
|
6765
|
+
if (dryRun) {
|
|
6766
|
+
log.emit(`would hand ${b.ticket.issue_id} back to ${contract.statuses.building} \u2014 its branch breaks the ${b.hook} hook`, t);
|
|
6767
|
+
return { kind: "handed-back" };
|
|
6768
|
+
}
|
|
6769
|
+
try {
|
|
6770
|
+
await writer.updateTicket(
|
|
6771
|
+
b.ticket.id,
|
|
6772
|
+
{ status: contract.statuses.building, assignee_id: null },
|
|
6773
|
+
b.ticket.updated_at
|
|
6774
|
+
);
|
|
6775
|
+
await writer.postEvent(b.ticket.id, buildFailureComment(b), memberId).catch(() => {
|
|
6776
|
+
log.warn("handed back, but the note failed to post", t);
|
|
6777
|
+
});
|
|
6778
|
+
log.emit(`handed back to ${contract.statuses.building} \u2014 breaks the ${b.hook} hook once merged with the rest`, t);
|
|
6779
|
+
return { kind: "handed-back" };
|
|
6780
|
+
} catch (e) {
|
|
6781
|
+
if (e instanceof StaleWriteError6) {
|
|
6782
|
+
log.emit(`${b.ticket.issue_id} changed on the board since this run read it \u2014 not handing it back over it`, t);
|
|
6783
|
+
return { kind: "stale" };
|
|
6784
|
+
}
|
|
6785
|
+
return { kind: "failed", why: e.message };
|
|
6786
|
+
}
|
|
6787
|
+
}
|
|
6788
|
+
|
|
6678
6789
|
// src/stranded-verified.ts
|
|
6679
6790
|
var STRANDED_MARKER = "<!-- crew:stranded-verified -->";
|
|
6680
6791
|
var STRANDED_ESCALATE_AFTER_MS = 30 * 60 * 1e3;
|
|
@@ -7196,12 +7307,12 @@ function expandSkillMarkers(text, lookup, opts = {}) {
|
|
|
7196
7307
|
import { readFileSync as readFileSync10, readdirSync as readdirSync3, existsSync as existsSync11 } from "fs";
|
|
7197
7308
|
import { join as join12 } from "path";
|
|
7198
7309
|
import { parse as parseYaml } from "yaml";
|
|
7199
|
-
import { TablationClient as TablationClient3, StaleWriteError as
|
|
7310
|
+
import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError8, TablationApiError as TablationApiError2 } from "@tablation/client";
|
|
7200
7311
|
|
|
7201
7312
|
// src/agents.ts
|
|
7202
7313
|
import { readFileSync as readFileSync9, existsSync as existsSync10 } from "fs";
|
|
7203
7314
|
import { join as join11 } from "path";
|
|
7204
|
-
import { TablationClient as TablationClient2, StaleWriteError as
|
|
7315
|
+
import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError7, TablationApiError } from "@tablation/client";
|
|
7205
7316
|
var PERSONA_NAME = {
|
|
7206
7317
|
dev: "Developer",
|
|
7207
7318
|
design: "Design",
|
|
@@ -7380,7 +7491,7 @@ async function syncPersonas(route, opts) {
|
|
|
7380
7491
|
try {
|
|
7381
7492
|
anchoredAt = (await client2.records.update(agentsModel.id, row.id, promptSourceFields(agentsModel, defaultPrompt), row.updated_at)).updated_at;
|
|
7382
7493
|
} catch (e) {
|
|
7383
|
-
if (!(e instanceof
|
|
7494
|
+
if (!(e instanceof StaleWriteError7) && !isPromptGateRejection(e)) throw e;
|
|
7384
7495
|
}
|
|
7385
7496
|
}
|
|
7386
7497
|
const historyId = await fetchLatestHistoryId(route, agentsModel.id, row.id, opts.userAgent);
|
|
@@ -7452,7 +7563,7 @@ async function syncPersonas(route, opts) {
|
|
|
7452
7563
|
agentPersonas[role] = { agentId: row.id, lastSyncedUpdatedAt: updated.updated_at, ...historyId ? { historyId } : {} };
|
|
7453
7564
|
outcomes.push({ role, action: "updated", agentId: row.id });
|
|
7454
7565
|
} catch (e) {
|
|
7455
|
-
if (e instanceof
|
|
7566
|
+
if (e instanceof StaleWriteError7) {
|
|
7456
7567
|
outcomes.push({ role, action: "diverged", agentId: row.id });
|
|
7457
7568
|
continue;
|
|
7458
7569
|
}
|
|
@@ -7636,7 +7747,7 @@ async function syncSkills(route, opts) {
|
|
|
7636
7747
|
row.updated_at
|
|
7637
7748
|
)).updated_at;
|
|
7638
7749
|
} catch (e) {
|
|
7639
|
-
if (!(e instanceof
|
|
7750
|
+
if (!(e instanceof StaleWriteError8) && !isPromptGateRejection(e)) throw e;
|
|
7640
7751
|
}
|
|
7641
7752
|
}
|
|
7642
7753
|
agentSkills[skill.name] = { skillId: row.id, lastSyncedUpdatedAt: anchoredAt };
|
|
@@ -7692,7 +7803,7 @@ async function syncSkills(route, opts) {
|
|
|
7692
7803
|
agentSkills[skill.name] = { skillId: row.id, lastSyncedUpdatedAt: updated.updated_at };
|
|
7693
7804
|
outcomes.push({ name: skill.name, action: "updated", skillId: row.id });
|
|
7694
7805
|
} catch (e) {
|
|
7695
|
-
if (e instanceof
|
|
7806
|
+
if (e instanceof StaleWriteError8) {
|
|
7696
7807
|
outcomes.push({ name: skill.name, action: "diverged", skillId: row.id });
|
|
7697
7808
|
continue;
|
|
7698
7809
|
}
|
|
@@ -9474,8 +9585,62 @@ async function runRelease(o) {
|
|
|
9474
9585
|
}
|
|
9475
9586
|
}
|
|
9476
9587
|
}
|
|
9477
|
-
|
|
9588
|
+
var isolateCap = (o) => o.repo.release.isolateFailures?.maxCandidates ?? DEFAULT_ISOLATE_MAX_CANDIDATES;
|
|
9589
|
+
async function isolateAndRetry(o, name, output, merged, preMerge, decision, fresh, releaseBranch) {
|
|
9590
|
+
const applied = merged.filter((c) => c.sha && c.branch);
|
|
9591
|
+
const cap = isolateCap(o);
|
|
9592
|
+
if (o.dryRun || !preMerge || applied.length < 2 || cap === 0) return null;
|
|
9593
|
+
const head = headSha(o.cwd);
|
|
9594
|
+
const withFiles = applied.map((c) => ({
|
|
9595
|
+
c,
|
|
9596
|
+
changed: new Set(
|
|
9597
|
+
(gitOk(o.cwd, ["diff", "--name-only", `${preMerge}...${c.branch}`]) ?? "").split("\n").filter(Boolean)
|
|
9598
|
+
)
|
|
9599
|
+
}));
|
|
9600
|
+
const culprits = [];
|
|
9601
|
+
const byFile = culpritByFiles(withFiles, failingPaths(output, o.cwd));
|
|
9602
|
+
if (byFile) {
|
|
9603
|
+
o.emit.warn(`${name} failed; its output names files only ${byFile.c.branch} changed \u2014 treating it as the culprit`, { ticket: byFile.c.ticket.issue_id });
|
|
9604
|
+
culprits.push({ c: byFile.c, how: "files", tail: lastLines(output, HOOK_FAILURE_TAIL_LINES) });
|
|
9605
|
+
} else if (applied.length > cap) {
|
|
9606
|
+
o.emit.warn(`${name} failed with ${applied.length} branches merged, over release.isolateFailures.maxCandidates (${cap}) \u2014 not isolating`);
|
|
9607
|
+
return null;
|
|
9608
|
+
} else {
|
|
9609
|
+
o.emit.warn(`${name} failed with ${applied.length} branches merged \u2014 building each alone to find the culprit`);
|
|
9610
|
+
for (const c of applied) {
|
|
9611
|
+
gitOk(o.cwd, ["reset", "--hard", preMerge]);
|
|
9612
|
+
const m = mergeOne(o, c);
|
|
9613
|
+
if (typeof m === "object" && "conflict" in m) continue;
|
|
9614
|
+
const r = await hook(o, name);
|
|
9615
|
+
if (r && r.code !== 0) {
|
|
9616
|
+
o.emit.warn(`${c.branch} fails the ${name} hook on its own`, { ticket: c.ticket.issue_id });
|
|
9617
|
+
culprits.push({ c, how: "solo", tail: lastLines(r.output, HOOK_FAILURE_TAIL_LINES) });
|
|
9618
|
+
}
|
|
9619
|
+
}
|
|
9620
|
+
}
|
|
9621
|
+
gitOk(o.cwd, ["reset", "--hard", preMerge]);
|
|
9622
|
+
if (culprits.length === 0) {
|
|
9623
|
+
o.emit.warn("no single branch could be blamed \u2014 failing the release as a whole");
|
|
9624
|
+
gitOk(o.cwd, ["reset", "--hard", head]);
|
|
9625
|
+
return null;
|
|
9626
|
+
}
|
|
9627
|
+
const out = new Set(culprits.map((x) => x.c.ticket.id));
|
|
9628
|
+
const rest = decision.merges.filter((c) => !out.has(c.ticket.id));
|
|
9629
|
+
const retried = await mergeAndRelease(o, { ...decision, merges: rest }, fresh, releaseBranch, true);
|
|
9630
|
+
return {
|
|
9631
|
+
...retried,
|
|
9632
|
+
buildCulprits: culprits.map((x) => ({
|
|
9633
|
+
ticket: x.c.ticket,
|
|
9634
|
+
branch: x.c.branch,
|
|
9635
|
+
hook: name,
|
|
9636
|
+
tail: x.tail,
|
|
9637
|
+
how: x.how
|
|
9638
|
+
}))
|
|
9639
|
+
};
|
|
9640
|
+
}
|
|
9641
|
+
async function mergeAndRelease(o, decision, fresh, releaseBranch, isolated = false) {
|
|
9478
9642
|
o.emit.enter("merge");
|
|
9643
|
+
const preMerge = o.dryRun ? "" : headSha(o.cwd);
|
|
9479
9644
|
const merged = [];
|
|
9480
9645
|
const conflicts = [];
|
|
9481
9646
|
const unbuildable = [];
|
|
@@ -9590,6 +9755,10 @@ async function mergeAndRelease(o, decision, fresh, releaseBranch) {
|
|
|
9590
9755
|
o.emit.warn("test gate failed with what looks like a worker crash, not a real failure \u2014 retrying once");
|
|
9591
9756
|
r = await hook(o, "test");
|
|
9592
9757
|
}
|
|
9758
|
+
if (r && r.code !== 0 && !isolated) {
|
|
9759
|
+
const out = await isolateAndRetry(o, "test", r.output, merged, preMerge, decision, fresh, releaseBranch);
|
|
9760
|
+
if (out) return out;
|
|
9761
|
+
}
|
|
9593
9762
|
if (r && r.code !== 0) {
|
|
9594
9763
|
o.emit.error(`test gate FAILED \u2014 not deploying; the target stays on the previous release`);
|
|
9595
9764
|
const sha = headSha(o.cwd);
|
|
@@ -9619,13 +9788,23 @@ async function mergeAndRelease(o, decision, fresh, releaseBranch) {
|
|
|
9619
9788
|
}
|
|
9620
9789
|
}
|
|
9621
9790
|
if (o.repo.hooks.build) {
|
|
9622
|
-
if (o.dryRun)
|
|
9623
|
-
|
|
9791
|
+
if (o.dryRun) {
|
|
9792
|
+
o.emit.emit(`would run: ${hookLabel(o.repo, "build")}`);
|
|
9793
|
+
if (applied > 1) {
|
|
9794
|
+
o.emit.emit(
|
|
9795
|
+
`if it fails, would find the one of ${applied} merged branches that broke it (by the files it names, else by building each alone, up to ${isolateCap(o)}), hand that back to the dev lane and ship the rest`
|
|
9796
|
+
);
|
|
9797
|
+
}
|
|
9798
|
+
} else {
|
|
9624
9799
|
let r = await hook(o, "build");
|
|
9625
9800
|
if (r && r.code !== 0 && looksLikeTransientResolveFailure(r.output)) {
|
|
9626
9801
|
o.emit.warn("build failed with what looks like a transient dependency-resolution glitch, not a real failure \u2014 retrying once");
|
|
9627
9802
|
r = await hook(o, "build");
|
|
9628
9803
|
}
|
|
9804
|
+
if (r && r.code !== 0 && !isolated) {
|
|
9805
|
+
const out = await isolateAndRetry(o, "build", r.output, merged, preMerge, decision, fresh, releaseBranch);
|
|
9806
|
+
if (out) return out;
|
|
9807
|
+
}
|
|
9629
9808
|
if (r && r.code !== 0) {
|
|
9630
9809
|
o.emit.error("build failed \u2014 not deploying");
|
|
9631
9810
|
if (version && !o.dryRun && headSha(o.cwd) !== preBump) {
|
|
@@ -9824,7 +10003,7 @@ function formatProjectsReport(report) {
|
|
|
9824
10003
|
}
|
|
9825
10004
|
|
|
9826
10005
|
// src/cli.ts
|
|
9827
|
-
import { StaleWriteError as
|
|
10006
|
+
import { StaleWriteError as StaleWriteError9 } from "@tablation/client";
|
|
9828
10007
|
|
|
9829
10008
|
// src/release-ship.ts
|
|
9830
10009
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -12963,6 +13142,20 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12963
13142
|
});
|
|
12964
13143
|
}
|
|
12965
13144
|
}
|
|
13145
|
+
if (outcome.buildCulprits?.length) {
|
|
13146
|
+
const seat = releaseSeat(c);
|
|
13147
|
+
for (const cu of outcome.buildCulprits) {
|
|
13148
|
+
try {
|
|
13149
|
+
const bounce = planBuildFailureBounce(target.dir, cu, repo.branch.base, repo.branch.remote);
|
|
13150
|
+
const r = await applyBuildFailureBounce(tracker, bounce, tracker.contract, seat, remit, dryRun);
|
|
13151
|
+
if (r.kind === "failed") {
|
|
13152
|
+
remit.warn(`could not hand back ${cu.ticket.issue_id}: ${r.why}`, { step: "release" });
|
|
13153
|
+
}
|
|
13154
|
+
} catch (e) {
|
|
13155
|
+
remit.warn(`could not hand back ${cu.ticket.issue_id}: ${e.message}`, { step: "release" });
|
|
13156
|
+
}
|
|
13157
|
+
}
|
|
13158
|
+
}
|
|
12966
13159
|
if (outcome.conflicts?.length || outcome.unbuildable?.length) {
|
|
12967
13160
|
const comments = await tracker.comments();
|
|
12968
13161
|
const seat = releaseSeat(c);
|
|
@@ -14658,7 +14851,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
14658
14851
|
await tracker3.updateTicket(ticket.id, { [c.columns.assignee]: null }, ticket.updated_at);
|
|
14659
14852
|
emit.emit(`cleared assignee on ${label} \u2014 back up for grabs next cycle`);
|
|
14660
14853
|
} catch (e) {
|
|
14661
|
-
if (e instanceof
|
|
14854
|
+
if (e instanceof StaleWriteError9) {
|
|
14662
14855
|
process.stderr.write(`unassign: ${label} changed on the board since it was read \u2014 re-run to retry
|
|
14663
14856
|
`);
|
|
14664
14857
|
process.exit(2);
|
package/package.json
CHANGED
|
@@ -161,6 +161,11 @@ pass.
|
|
|
161
161
|
base, the bounce says to merge `<remote>/<base>` into the branch, resolve and
|
|
162
162
|
push fast-forward; a rebase would need a force push the builder cannot make.
|
|
163
163
|
Or pass the ticket and let the release's conflict path do the merge.
|
|
164
|
+
- **A branch that came back from a conflict or build-failure bounce is
|
|
165
|
+
re-verified against the current base, not only its own tip.** Before
|
|
166
|
+
passing it, merge current `<remote>/<base>` into the worktree (or run the
|
|
167
|
+
build on a throwaway merge) and run the build: a clean text merge can still
|
|
168
|
+
fail to compile, and it is far cheaper to find that here than at release.
|
|
164
169
|
- **You only ever enter the worktree of the ticket you are verifying.**
|
|
165
170
|
- Throwaway verification scripts go in a scratch directory outside the
|
|
166
171
|
repository — not the worktree root either.
|
|
@@ -116,6 +116,10 @@ Map go active now, or stay a draft?**
|
|
|
116
116
|
- Any `draft` Issue tied to this Epic that this session's ticket
|
|
117
117
|
set does NOT include (i.e. dropped by this revision) → flip to
|
|
118
118
|
`status: closed_obsolete`.
|
|
119
|
+
5. Set the Epic itself to `status: planned` (Epics table) if it is
|
|
120
|
+
still `new`. An active Map is the planning decision for the Epic;
|
|
121
|
+
leave an Epic that has already moved on (`in_progress`, `done`,
|
|
122
|
+
`cancelled`) alone.
|
|
119
123
|
|
|
120
124
|
Do all of this yourself via direct MCP writes. There is no automation
|
|
121
125
|
watching for this — if you don't do it, it doesn't happen.
|
|
@@ -160,6 +160,11 @@ pass.
|
|
|
160
160
|
base, the bounce says to merge `<remote>/<base>` into the branch, resolve and
|
|
161
161
|
push fast-forward; a rebase would need a force push the builder cannot make.
|
|
162
162
|
Or pass the ticket and let the release's conflict path do the merge.
|
|
163
|
+
- **A branch that came back from a conflict or build-failure bounce is
|
|
164
|
+
re-verified against the current base, not only its own tip.** Before
|
|
165
|
+
passing it, merge current `<remote>/<base>` into the worktree (or run the
|
|
166
|
+
build on a throwaway merge) and run the build: a clean text merge can still
|
|
167
|
+
fail to compile, and it is far cheaper to find that here than at release.
|
|
163
168
|
- **You only ever enter the worktree of the ticket you are verifying.**
|
|
164
169
|
- Throwaway verification scripts go in a scratch directory outside the
|
|
165
170
|
repository — not the worktree root either.
|
|
@@ -116,6 +116,10 @@ Map go active now, or stay a draft?**
|
|
|
116
116
|
- Any `draft` Issue tied to this Epic that this session's ticket
|
|
117
117
|
set does NOT include (i.e. dropped by this revision) → flip to
|
|
118
118
|
`status: closed_obsolete`.
|
|
119
|
+
5. Set the Epic itself to `status: planned` (Epics table) if it is
|
|
120
|
+
still `new`. An active Map is the planning decision for the Epic;
|
|
121
|
+
leave an Epic that has already moved on (`in_progress`, `done`,
|
|
122
|
+
`cancelled`) alone.
|
|
119
123
|
|
|
120
124
|
Do all of this yourself via direct MCP writes. There is no automation
|
|
121
125
|
watching for this — if you don't do it, it doesn't happen.
|