@tablation/crew 0.45.0 → 0.45.3
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 +133 -60
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4804,7 +4804,6 @@ function referenceKeys(t) {
|
|
|
4804
4804
|
}
|
|
4805
4805
|
var RESOLVED_STATUSES = new Set(DEFAULT_CONTRACT.statuses.resolved);
|
|
4806
4806
|
var encodeFilters = (f) => JSON.stringify(f);
|
|
4807
|
-
var encodeSort = (s) => JSON.stringify(s);
|
|
4808
4807
|
var Tracker = class {
|
|
4809
4808
|
// NB: explicit fields, not TypeScript parameter properties — node's
|
|
4810
4809
|
// --experimental-strip-types cannot transform those, and the test runner
|
|
@@ -4864,37 +4863,41 @@ var Tracker = class {
|
|
|
4864
4863
|
});
|
|
4865
4864
|
}
|
|
4866
4865
|
/**
|
|
4867
|
-
*
|
|
4868
|
-
*
|
|
4866
|
+
* The tickets behind a set of ticket numbers — the cleanup sweeps' input
|
|
4867
|
+
* (CREW-1593).
|
|
4869
4868
|
*
|
|
4870
|
-
* The
|
|
4871
|
-
*
|
|
4872
|
-
*
|
|
4873
|
-
*
|
|
4874
|
-
*
|
|
4869
|
+
* The sweeps used to fetch "every closed ticket, newest `updated_at` first,
|
|
4870
|
+
* capped at 500" and hope the tickets they cared about were in the page. They
|
|
4871
|
+
* were not once the board held more than 500 closed tickets, and a bulk
|
|
4872
|
+
* write gave 1457 of them one identical `updated_at`, so which ones made the
|
|
4873
|
+
* page was arbitrary and a deployed ticket's worktree/branch was silently
|
|
4874
|
+
* never swept. The caller now names the numbers it actually has a worktree
|
|
4875
|
+
* or remote branch for, so the request is bounded by those and not by how
|
|
4876
|
+
* many tickets are closed.
|
|
4875
4877
|
*
|
|
4876
|
-
*
|
|
4877
|
-
*
|
|
4878
|
-
*
|
|
4879
|
-
* `updatedAt` descending means a truncated page still holds the tickets
|
|
4880
|
-
* most likely to need their worktree swept — the ones that closed most
|
|
4881
|
-
* recently — rather than an arbitrary, possibly stale slice (ISSUE-403).
|
|
4878
|
+
* Matched on `issue_id`, in the raw, unpadded and 3-digit-padded spellings a
|
|
4879
|
+
* number has been stored under. `onTruncated` is told when a page came back
|
|
4880
|
+
* full, since anything past it would be missing without a trace.
|
|
4882
4881
|
*/
|
|
4883
|
-
async
|
|
4882
|
+
async ticketsByNumbers(nums, onTruncated) {
|
|
4884
4883
|
const c = this.contract;
|
|
4885
|
-
const
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
const
|
|
4891
|
-
|
|
4892
|
-
const
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4884
|
+
const keys = /* @__PURE__ */ new Set();
|
|
4885
|
+
for (const raw of nums) {
|
|
4886
|
+
const unpadded = String(Number.parseInt(raw, 10));
|
|
4887
|
+
for (const n of [raw, unpadded, unpadded.padStart(3, "0")]) keys.add(`ISSUE-${n}`);
|
|
4888
|
+
}
|
|
4889
|
+
const all = [...keys];
|
|
4890
|
+
const out = [];
|
|
4891
|
+
const limit = 500;
|
|
4892
|
+
for (let i = 0; i < all.length; i += 100) {
|
|
4893
|
+
const page = await this.client.records.list(this.models.issues, {
|
|
4894
|
+
filters: encodeFilters([{ columnName: c.columns.key, operator: "IN", value: all.slice(i, i + 100) }]),
|
|
4895
|
+
limit
|
|
4896
|
+
});
|
|
4897
|
+
if (page.length >= limit) onTruncated?.(limit);
|
|
4898
|
+
out.push(...page);
|
|
4899
|
+
}
|
|
4900
|
+
return out;
|
|
4898
4901
|
}
|
|
4899
4902
|
/**
|
|
4900
4903
|
* Tickets by id, for blockers that have already closed and so are absent
|
|
@@ -11098,6 +11101,14 @@ function planWorktreeSweep(target, tickets, contract) {
|
|
|
11098
11101
|
}
|
|
11099
11102
|
return actions;
|
|
11100
11103
|
}
|
|
11104
|
+
function worktreeTicketNumbers(target) {
|
|
11105
|
+
const nums = /* @__PURE__ */ new Set();
|
|
11106
|
+
for (const w of worktrees(target.dir)) {
|
|
11107
|
+
const m = /(?:^|-)(\d+)$/.exec(basename8(w.path));
|
|
11108
|
+
if (m) nums.add(m[1]);
|
|
11109
|
+
}
|
|
11110
|
+
return [...nums];
|
|
11111
|
+
}
|
|
11101
11112
|
async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin") {
|
|
11102
11113
|
let removed = 0;
|
|
11103
11114
|
let keptBranches = 0;
|
|
@@ -11156,8 +11167,28 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
|
|
|
11156
11167
|
}
|
|
11157
11168
|
return { removed, keptBranches };
|
|
11158
11169
|
}
|
|
11170
|
+
function remoteBranchTicketNumbers(cwd, remote, base) {
|
|
11171
|
+
if (!remoteConfigured(cwd, remote)) return [];
|
|
11172
|
+
const prefix = `refs/remotes/${remote}/`;
|
|
11173
|
+
const refs = (gitOk(cwd, ["for-each-ref", "--format=%(refname)", prefix]) ?? "").split("\n").filter(Boolean);
|
|
11174
|
+
const nums = /* @__PURE__ */ new Set();
|
|
11175
|
+
for (const ref of refs) {
|
|
11176
|
+
const name = ref.slice(prefix.length);
|
|
11177
|
+
if (name === "HEAD" || name === base) continue;
|
|
11178
|
+
const m = /^.+?-(\d+)(?:-.*)?$/.exec(name);
|
|
11179
|
+
if (m) nums.add(m[1]);
|
|
11180
|
+
}
|
|
11181
|
+
return [...nums];
|
|
11182
|
+
}
|
|
11159
11183
|
function planRemoteBranchCleanup(cwd, remote, base, tickets, contract) {
|
|
11160
11184
|
if (!remoteConfigured(cwd, remote)) return [];
|
|
11185
|
+
const stems = deployedBranchStems(tickets, contract);
|
|
11186
|
+
if (stems.size === 0) return [];
|
|
11187
|
+
const prefix = `refs/remotes/${remote}/`;
|
|
11188
|
+
const refs = (gitOk(cwd, ["for-each-ref", "--format=%(refname)", prefix]) ?? "").split("\n").filter(Boolean);
|
|
11189
|
+
return refs.map((ref) => ref.slice(prefix.length)).filter((name) => isTicketBranch(name, base, stems));
|
|
11190
|
+
}
|
|
11191
|
+
function deployedBranchStems(tickets, contract) {
|
|
11161
11192
|
const stems = /* @__PURE__ */ new Set();
|
|
11162
11193
|
for (const t of tickets) {
|
|
11163
11194
|
if (t.status !== contract.statuses.deployed) continue;
|
|
@@ -11169,17 +11200,48 @@ function planRemoteBranchCleanup(cwd, remote, base, tickets, contract) {
|
|
|
11169
11200
|
stems.add(`${p}-${unpadded}`);
|
|
11170
11201
|
}
|
|
11171
11202
|
}
|
|
11203
|
+
return stems;
|
|
11204
|
+
}
|
|
11205
|
+
function isTicketBranch(name, base, stems) {
|
|
11206
|
+
if (name === "HEAD" || name === base) return false;
|
|
11207
|
+
const m = /^(.+?-\d+)(?:-.*)?$/.exec(name);
|
|
11208
|
+
return !!m && stems.has(m[1]);
|
|
11209
|
+
}
|
|
11210
|
+
function sweepCandidatesExist(cwd, base) {
|
|
11211
|
+
const linked = worktrees(cwd).slice(1);
|
|
11212
|
+
if (linked.some((w) => /(?:^|-)\d+$/.test(basename8(w.path)))) return true;
|
|
11213
|
+
return localBranches(cwd).some((b) => b !== base && /-\d+(?:-.*)?$/.test(b));
|
|
11214
|
+
}
|
|
11215
|
+
function localBranchTicketNumbers(cwd, base) {
|
|
11216
|
+
const nums = /* @__PURE__ */ new Set();
|
|
11217
|
+
for (const b of localBranches(cwd)) {
|
|
11218
|
+
const m = b === base ? null : /^.+?-(\d+)(?:-.*)?$/.exec(b);
|
|
11219
|
+
if (m) nums.add(m[1]);
|
|
11220
|
+
}
|
|
11221
|
+
return [...nums];
|
|
11222
|
+
}
|
|
11223
|
+
function localBranches(cwd) {
|
|
11224
|
+
return (gitOk(cwd, ["branch", "--list", "--format=%(refname:short)"]) ?? "").split("\n").filter(Boolean);
|
|
11225
|
+
}
|
|
11226
|
+
function planLocalBranchCleanup(cwd, base, tickets, contract) {
|
|
11227
|
+
const stems = deployedBranchStems(tickets, contract);
|
|
11172
11228
|
if (stems.size === 0) return [];
|
|
11173
|
-
const
|
|
11174
|
-
|
|
11175
|
-
|
|
11176
|
-
|
|
11177
|
-
|
|
11178
|
-
|
|
11179
|
-
|
|
11180
|
-
|
|
11229
|
+
const checkedOut = new Set(worktrees(cwd).map((w) => w.branch).filter((b) => !!b));
|
|
11230
|
+
return localBranches(cwd).filter((b) => !checkedOut.has(b) && isTicketBranch(b, base, stems));
|
|
11231
|
+
}
|
|
11232
|
+
function applyLocalBranchCleanup(cwd, branches2, dryRun, log) {
|
|
11233
|
+
let deleted = 0;
|
|
11234
|
+
for (const b of branches2) {
|
|
11235
|
+
if (dryRun) {
|
|
11236
|
+
log.emit(`would delete local branch ${b} \u2014 its ticket is deployed`, { step: "worktree" });
|
|
11237
|
+
continue;
|
|
11238
|
+
}
|
|
11239
|
+
if (gitOk(cwd, ["branch", "-D", b]) !== null) {
|
|
11240
|
+
deleted++;
|
|
11241
|
+
log.emit(`deleted local branch ${b} \u2014 its ticket is deployed`, { step: "worktree" });
|
|
11242
|
+
} else log.warn(`could not delete local branch ${b} \u2014 leaving it for the next sweep`, { step: "worktree" });
|
|
11181
11243
|
}
|
|
11182
|
-
return
|
|
11244
|
+
return deleted;
|
|
11183
11245
|
}
|
|
11184
11246
|
function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
11185
11247
|
let deleted = 0;
|
|
@@ -12984,6 +13046,30 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12984
13046
|
}
|
|
12985
13047
|
return `${who2}; ${lock2}`;
|
|
12986
13048
|
}
|
|
13049
|
+
async function sweepClosedTickets(c, target, branch, tracker, dryRun2, remit, idle = false) {
|
|
13050
|
+
try {
|
|
13051
|
+
if (idle && !sweepCandidatesExist(target.dir, branch.base)) return;
|
|
13052
|
+
const truncated = (limit) => remit.warn(`ticket lookup for the sweep hit its ${limit}-row limit; some tickets may be missed`, { step: "worktree" });
|
|
13053
|
+
const nums = /* @__PURE__ */ new Set([
|
|
13054
|
+
...worktreeTicketNumbers(target),
|
|
13055
|
+
...localBranchTicketNumbers(target.dir, branch.base),
|
|
13056
|
+
...remoteBranchTicketNumbers(target.dir, branch.remote, branch.base)
|
|
13057
|
+
]);
|
|
13058
|
+
const scoped = ticketsByRepo(c, await tracker.ticketsByNumbers([...nums], truncated)).byRepo.get(target.name) ?? [];
|
|
13059
|
+
const actions = planWorktreeSweep(target, scoped, tracker.contract);
|
|
13060
|
+
if (actions.length) {
|
|
13061
|
+
remit.enter("worktree");
|
|
13062
|
+
const r = await applyWorktreeSweep(target.dir, actions, dryRun2, remit, branch.remote);
|
|
13063
|
+
remit.emit(`swept ${r.removed} worktree(s), kept ${r.keptBranches} branch(es)`, { step: "worktree" });
|
|
13064
|
+
}
|
|
13065
|
+
const orphaned = planLocalBranchCleanup(target.dir, branch.base, scoped, tracker.contract);
|
|
13066
|
+
if (orphaned.length) applyLocalBranchCleanup(target.dir, orphaned, dryRun2, remit);
|
|
13067
|
+
const stale = planRemoteBranchCleanup(target.dir, branch.remote, branch.base, scoped, tracker.contract);
|
|
13068
|
+
if (stale.length) applyRemoteBranchCleanup(target.dir, branch.remote, stale, dryRun2, remit);
|
|
13069
|
+
} catch (e) {
|
|
13070
|
+
remit.warn(`worktree sweep failed: ${e.message}`, { step: "worktree" });
|
|
13071
|
+
}
|
|
13072
|
+
}
|
|
12987
13073
|
const precheckTickets = /* @__PURE__ */ new Map();
|
|
12988
13074
|
async function releasePhase(c, target, opts = {}) {
|
|
12989
13075
|
const scope = releaseLockScope(c.route, target.name);
|
|
@@ -13053,6 +13139,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13053
13139
|
unstamped
|
|
13054
13140
|
})) {
|
|
13055
13141
|
remit.emit(`${target.name}: nothing verified \u2014 not locking`, { step: "release" });
|
|
13142
|
+
await sweepClosedTickets(c, target, repo.branch, tracker, dryRun, remit, true);
|
|
13056
13143
|
return { scope, tests: "skipped", outcome: "skipped", detail: "nothing verified \u2014 not locking" };
|
|
13057
13144
|
}
|
|
13058
13145
|
}
|
|
@@ -13257,21 +13344,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13257
13344
|
}
|
|
13258
13345
|
}
|
|
13259
13346
|
}
|
|
13260
|
-
|
|
13261
|
-
const terminal = await tracker.terminalTickets();
|
|
13262
|
-
const verifiedNow = (await tracker.openTickets()).filter((t) => t.status === tracker.contract.statuses.verified);
|
|
13263
|
-
const scoped = ticketsByRepo(c, terminal).byRepo.get(target.name) ?? [];
|
|
13264
|
-
const actions = planWorktreeSweep(target, [...scoped, ...ticketsByRepo(c, verifiedNow).byRepo.get(target.name) ?? []], tracker.contract);
|
|
13265
|
-
if (actions.length) {
|
|
13266
|
-
remit.enter("worktree");
|
|
13267
|
-
const r = await applyWorktreeSweep(target.dir, actions, dryRun, remit, repo.branch.remote);
|
|
13268
|
-
remit.emit(`swept ${r.removed} worktree(s), kept ${r.keptBranches} branch(es)`, { step: "worktree" });
|
|
13269
|
-
}
|
|
13270
|
-
const stale = planRemoteBranchCleanup(target.dir, repo.branch.remote, repo.branch.base, scoped, tracker.contract);
|
|
13271
|
-
if (stale.length) applyRemoteBranchCleanup(target.dir, repo.branch.remote, stale, dryRun, remit);
|
|
13272
|
-
} catch (e) {
|
|
13273
|
-
remit.warn(`worktree sweep failed: ${e.message}`, { step: "worktree" });
|
|
13274
|
-
}
|
|
13347
|
+
await sweepClosedTickets(c, target, repo.branch, tracker, dryRun, remit);
|
|
13275
13348
|
return summary;
|
|
13276
13349
|
} catch (e) {
|
|
13277
13350
|
remit.warn(`release phase failed: ${e.message}`, { step: "release" });
|
|
@@ -14672,15 +14745,15 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
14672
14745
|
}
|
|
14673
14746
|
if (!dryRun && orphans.length) await killGently(orphans.map((o) => o.pid));
|
|
14674
14747
|
const tracker = new Tracker(route, cfg.ship);
|
|
14675
|
-
const
|
|
14676
|
-
const { byRepo } = ticketsByRepo(route, terminal);
|
|
14677
|
-
const verifiedByRepo = ticketsByRepo(
|
|
14678
|
-
route,
|
|
14679
|
-
(await tracker.openTickets()).filter((t) => t.status === tracker.contract.statuses.verified)
|
|
14680
|
-
).byRepo;
|
|
14748
|
+
const truncated = (limit) => emit.warn(`ticket lookup for the sweep hit its ${limit}-row limit; some tickets may be missed`, { step: "worktree" });
|
|
14681
14749
|
for (const r of await resolvedRepos(route)) {
|
|
14682
|
-
const
|
|
14683
|
-
const
|
|
14750
|
+
const wtTickets = ticketsByRepo(route, await tracker.ticketsByNumbers(worktreeTicketNumbers(r), truncated)).byRepo.get(r.name) ?? [];
|
|
14751
|
+
const brTickets = ticketsByRepo(
|
|
14752
|
+
route,
|
|
14753
|
+
await tracker.ticketsByNumbers(remoteBranchTicketNumbers(r.dir, r.config.branch.remote, r.config.branch.base), truncated)
|
|
14754
|
+
).byRepo.get(r.name) ?? [];
|
|
14755
|
+
const actions = planWorktreeSweep(r, wtTickets, tracker.contract);
|
|
14756
|
+
const stale = planRemoteBranchCleanup(r.dir, r.config.branch.remote, r.config.branch.base, brTickets, tracker.contract);
|
|
14684
14757
|
if (stale.length) {
|
|
14685
14758
|
anything = true;
|
|
14686
14759
|
applyRemoteBranchCleanup(r.dir, r.config.branch.remote, stale, dryRun, emit);
|
package/package.json
CHANGED