@tablation/crew 0.47.2 → 0.48.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 +128 -11
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4117,15 +4117,36 @@ function remoteBranchForIssue(cwd, remote, issueKey, templates = { name: "issue-
|
|
|
4117
4117
|
}
|
|
4118
4118
|
return null;
|
|
4119
4119
|
}
|
|
4120
|
-
function
|
|
4121
|
-
|
|
4122
|
-
|
|
4120
|
+
function levelRemoteBranch(cwd, remote, branch) {
|
|
4121
|
+
const local = gitOk(cwd, ["rev-parse", "-q", "--verify", `refs/heads/${branch}`]);
|
|
4122
|
+
if (!remoteBranchExists(cwd, remote, branch)) return { exists: local !== null };
|
|
4123
|
+
if (local === null) {
|
|
4124
|
+
return { exists: gitOk(cwd, ["branch", "--track", branch, `${remote}/${branch}`]) !== null };
|
|
4125
|
+
}
|
|
4126
|
+
const remoteTip2 = gitOk(cwd, ["rev-parse", "-q", "--verify", `refs/remotes/${remote}/${branch}`]);
|
|
4127
|
+
if (!remoteTip2 || remoteTip2 === local) return { exists: true };
|
|
4128
|
+
if (!isAncestor(cwd, local, remoteTip2)) return { exists: true };
|
|
4129
|
+
const holder = worktrees(cwd).find((w) => w.branch === branch && !w.prunable);
|
|
4130
|
+
if (holder) {
|
|
4131
|
+
if (gitOk(holder.path, ["status", "--porcelain"])) {
|
|
4132
|
+
return {
|
|
4133
|
+
exists: true,
|
|
4134
|
+
problem: `${branch} is behind ${remote}/${branch} and the worktree ${holder.path} that holds it has uncommitted or mid-merge changes`
|
|
4135
|
+
};
|
|
4136
|
+
}
|
|
4137
|
+
try {
|
|
4138
|
+
git(holder.path, ["merge", "--ff-only", "--quiet", `${remote}/${branch}`]);
|
|
4139
|
+
} catch (e) {
|
|
4140
|
+
return { exists: true, problem: `could not fast-forward ${branch} in ${holder.path}: ${e.message}` };
|
|
4141
|
+
}
|
|
4142
|
+
return { exists: true };
|
|
4123
4143
|
}
|
|
4124
|
-
|
|
4125
|
-
|
|
4144
|
+
try {
|
|
4145
|
+
git(cwd, ["fetch", "--quiet", remote, `${branch}:${branch}`]);
|
|
4146
|
+
} catch (e) {
|
|
4147
|
+
return { exists: true, problem: `could not move ${branch} to ${remote}/${branch}: ${e.message}` };
|
|
4126
4148
|
}
|
|
4127
|
-
|
|
4128
|
-
return true;
|
|
4149
|
+
return { exists: true };
|
|
4129
4150
|
}
|
|
4130
4151
|
function deleteRemoteBranch(cwd, remote, branch) {
|
|
4131
4152
|
if (!remoteBranchExists(cwd, remote, branch)) return true;
|
|
@@ -6285,10 +6306,11 @@ function locateBranchForTicket(dir, cfg, t, role, fetchedDirs) {
|
|
|
6285
6306
|
const found = remoteBranchForIssue(dir, remote, t.issue_id, branchTemplates(cfg, t), branchRenderer(t, role));
|
|
6286
6307
|
return found ? { branch: found, where: "remote" } : null;
|
|
6287
6308
|
}
|
|
6288
|
-
function materializeBranchForTicket(dir, cfg, t, role) {
|
|
6309
|
+
function materializeBranchForTicket(dir, cfg, t, role, onProblem) {
|
|
6289
6310
|
const loc = locateBranchForTicket(dir, cfg, t, role);
|
|
6290
6311
|
if (!loc) return null;
|
|
6291
|
-
|
|
6312
|
+
const r = levelRemoteBranch(dir, cfg.branch.remote, loc.branch);
|
|
6313
|
+
if (r.problem) onProblem?.(r.problem);
|
|
6292
6314
|
return loc.branch;
|
|
6293
6315
|
}
|
|
6294
6316
|
|
|
@@ -6746,6 +6768,11 @@ function planConflictBounce(cwd, ticket, branch, base, paths, events) {
|
|
|
6746
6768
|
function refreshBranch(worktree, base) {
|
|
6747
6769
|
const dirty = gitOk(worktree, ["status", "--porcelain"]);
|
|
6748
6770
|
if (dirty) return { clean: false, paths: [], why: "the worktree has uncommitted changes" };
|
|
6771
|
+
const upstream = gitOk(worktree, ["rev-parse", "-q", "--verify", "@{upstream}"]);
|
|
6772
|
+
const head = gitOk(worktree, ["rev-parse", "HEAD"]);
|
|
6773
|
+
if (upstream && head && upstream !== head && isAncestor(worktree, head, upstream)) {
|
|
6774
|
+
return { clean: false, paths: [], behind: true, why: "the branch is behind its remote; fast-forward it before merging the base in" };
|
|
6775
|
+
}
|
|
6749
6776
|
try {
|
|
6750
6777
|
git(worktree, ["merge", "--no-edit", base]);
|
|
6751
6778
|
return { clean: true, paths: [] };
|
|
@@ -6813,6 +6840,10 @@ async function applyConflictBounce(writer, b, contract, memberId, log, dryRun) {
|
|
|
6813
6840
|
log.emit(`merged ${b.base} into ${b.branch} cleanly \u2014 it was stale, not conflicting`, t);
|
|
6814
6841
|
return { kind: "refreshed" };
|
|
6815
6842
|
}
|
|
6843
|
+
if (r.behind) {
|
|
6844
|
+
log.warn(`not handing ${b.ticket.issue_id} back: ${r.why}`, t);
|
|
6845
|
+
return { kind: "failed", why: r.why ?? "branch behind its remote" };
|
|
6846
|
+
}
|
|
6816
6847
|
merged = r.paths.length > 0;
|
|
6817
6848
|
if (r.why) log.warn(`could not merge ${b.base} into ${b.branch}: ${r.why}`, t);
|
|
6818
6849
|
if (r.paths.length) b.paths = r.paths;
|
|
@@ -9050,6 +9081,30 @@ function releaseIdle(i) {
|
|
|
9050
9081
|
}
|
|
9051
9082
|
var renderTag = (template, version) => template.replace(/\{version\}/g, version);
|
|
9052
9083
|
|
|
9084
|
+
// src/verified-tip.ts
|
|
9085
|
+
var PUSHED_RE = /^pushed ([0-9a-f]{40}) to \S+$/;
|
|
9086
|
+
function parsePushedSha(message) {
|
|
9087
|
+
return PUSHED_RE.exec((message ?? "").trim())?.[1] ?? null;
|
|
9088
|
+
}
|
|
9089
|
+
function verifiedShaFor(ticket, verifiedShaColumn, pushedEvents) {
|
|
9090
|
+
const col = ticket[verifiedShaColumn];
|
|
9091
|
+
if (typeof col === "string" && /^[0-9a-f]{7,40}$/i.test(col.trim())) return col.trim();
|
|
9092
|
+
const when = (e) => Date.parse(e.occurred_at ?? e.created_at ?? "") || 0;
|
|
9093
|
+
const newest = pushedEvents.filter((e) => e.record_id === ticket.id && parsePushedSha(e.message)).sort((a, b) => when(b) - when(a))[0];
|
|
9094
|
+
return newest ? parsePushedSha(newest.message) : null;
|
|
9095
|
+
}
|
|
9096
|
+
function movedPastVerified(cwd, branch, verified) {
|
|
9097
|
+
const tip = gitOk(cwd, ["rev-parse", "-q", "--verify", `refs/heads/${branch}^{commit}`]);
|
|
9098
|
+
const ver = gitOk(cwd, ["rev-parse", "-q", "--verify", `${verified}^{commit}`]);
|
|
9099
|
+
if (!tip || !ver || tip === ver) return null;
|
|
9100
|
+
if (isAncestor(cwd, tip, ver)) return null;
|
|
9101
|
+
if (isAncestor(cwd, ver, tip)) {
|
|
9102
|
+
const own = gitOk(cwd, ["rev-list", "--first-parent", "--no-merges", "--count", `${ver}..${tip}`]);
|
|
9103
|
+
if (own === "0") return null;
|
|
9104
|
+
}
|
|
9105
|
+
return { tip, verified: ver };
|
|
9106
|
+
}
|
|
9107
|
+
|
|
9053
9108
|
// src/stamp.ts
|
|
9054
9109
|
function planStamp(cwd, tickets, contract, fromSha, toSha, merged = /* @__PURE__ */ new Map()) {
|
|
9055
9110
|
const range = fromSha ? `${fromSha.slice(0, 8)}..${toSha.slice(0, 8)}` : null;
|
|
@@ -9249,7 +9304,7 @@ ${h.prFailure.tail}
|
|
|
9249
9304
|
}
|
|
9250
9305
|
for (const q of outcome.requeues) {
|
|
9251
9306
|
const t = q.ticket;
|
|
9252
|
-
const body = `The branch is no longer what was verified:
|
|
9307
|
+
const body = `The branch is no longer what was verified: QA verified ${q.verified.slice(0, 8)}, the branch is now at ${q.head.slice(0, 8)}. Returned to ${contract.statuses.handoff} for QA to check what changed.`;
|
|
9253
9308
|
if (dryRun) {
|
|
9254
9309
|
emit.emit(`would return to ${contract.statuses.handoff} \u2014 branch moved past ${q.verified.slice(0, 8)}`, { ticket: t.issue_id });
|
|
9255
9310
|
continue;
|
|
@@ -9660,11 +9715,45 @@ function reportHeld(o, held) {
|
|
|
9660
9715
|
o.state?.noteHeldReported?.(signature);
|
|
9661
9716
|
}
|
|
9662
9717
|
async function runRelease(o) {
|
|
9718
|
+
const movedPast2 = [];
|
|
9719
|
+
const out = await runReleaseCycle(o, movedPast2);
|
|
9720
|
+
return movedPast2.length ? { ...out, movedPast: movedPast2 } : out;
|
|
9721
|
+
}
|
|
9722
|
+
async function runReleaseCycle(o, movedPast2) {
|
|
9663
9723
|
const scoped = (msg) => o.scope ? `${o.scope}: ${msg}` : msg;
|
|
9664
9724
|
const tagPattern = o.repo.release.tagPattern ?? "v*";
|
|
9665
9725
|
o.emit.enter("release");
|
|
9666
9726
|
const fresh = refreshBase(o);
|
|
9667
|
-
|
|
9727
|
+
let releasable = o.tickets;
|
|
9728
|
+
if (!o.dryRun) {
|
|
9729
|
+
const skipped = /* @__PURE__ */ new Set();
|
|
9730
|
+
for (const t of o.tickets) {
|
|
9731
|
+
if (t.status !== o.contract.statuses.verified) continue;
|
|
9732
|
+
materializeBranchForTicket(o.cwd, o.repo, t, void 0, (problem) => {
|
|
9733
|
+
skipped.add(t.id);
|
|
9734
|
+
o.emit.error(`not releasing ${t.issue_id} this cycle: ${problem}`, { ticket: t.issue_id });
|
|
9735
|
+
});
|
|
9736
|
+
}
|
|
9737
|
+
if (skipped.size) releasable = o.tickets.filter((t) => !skipped.has(t.id));
|
|
9738
|
+
}
|
|
9739
|
+
if (o.verifiedShas?.size) {
|
|
9740
|
+
const drop = /* @__PURE__ */ new Set();
|
|
9741
|
+
for (const t of releasable) {
|
|
9742
|
+
const verified = o.verifiedShas.get(t.id);
|
|
9743
|
+
if (t.status !== o.contract.statuses.verified || !verified) continue;
|
|
9744
|
+
const branch = locateBranchForTicket(o.cwd, o.repo, t)?.branch;
|
|
9745
|
+
const moved = branch ? movedPastVerified(o.cwd, branch, verified) : null;
|
|
9746
|
+
if (!moved) continue;
|
|
9747
|
+
drop.add(t.id);
|
|
9748
|
+
movedPast2.push({ ticket: t, verified: moved.verified, head: moved.tip });
|
|
9749
|
+
o.emit.error(
|
|
9750
|
+
`not releasing ${t.issue_id}: ${branch} is at ${moved.tip.slice(0, 8)}, past the ${moved.verified.slice(0, 8)} QA verified`,
|
|
9751
|
+
{ ticket: t.issue_id }
|
|
9752
|
+
);
|
|
9753
|
+
}
|
|
9754
|
+
if (drop.size) releasable = releasable.filter((t) => !drop.has(t.id));
|
|
9755
|
+
}
|
|
9756
|
+
const decision = decideRelease(o.cwd, releasable, o.contract, {
|
|
9668
9757
|
tagPattern,
|
|
9669
9758
|
base: o.repo.branch.base,
|
|
9670
9759
|
blockerInfo: o.blockerInfo,
|
|
@@ -13419,6 +13508,21 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13419
13508
|
);
|
|
13420
13509
|
reportUnplaceable(c.route, unplaceable, tracker.contract.statuses.verified, remit);
|
|
13421
13510
|
const blockerInfo = blockerInfoMap(all, await tracker.ticketsByIds(missingBlockerIds(all)));
|
|
13511
|
+
const verifiedStatus = tracker.contract.statuses.verified;
|
|
13512
|
+
const verifiedTickets = tickets.filter((t) => t.status === verifiedStatus);
|
|
13513
|
+
const verifiedShas = /* @__PURE__ */ new Map();
|
|
13514
|
+
if (verifiedTickets.length && repo.release.mode !== "external") {
|
|
13515
|
+
let pushedEvents = [];
|
|
13516
|
+
try {
|
|
13517
|
+
pushedEvents = await tracker.events("pushed");
|
|
13518
|
+
} catch (e) {
|
|
13519
|
+
remit.warn(`could not read hand-off events; not checking branches against what QA verified: ${e.message}`);
|
|
13520
|
+
}
|
|
13521
|
+
for (const t of verifiedTickets) {
|
|
13522
|
+
const sha = verifiedShaFor(t, tracker.contract.columns.verifiedSha, pushedEvents);
|
|
13523
|
+
if (sha) verifiedShas.set(t.id, sha);
|
|
13524
|
+
}
|
|
13525
|
+
}
|
|
13422
13526
|
const outcome = await runRelease({
|
|
13423
13527
|
cwd: target.dir,
|
|
13424
13528
|
repo,
|
|
@@ -13427,6 +13531,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13427
13531
|
blockerInfo,
|
|
13428
13532
|
emit: remit,
|
|
13429
13533
|
scope,
|
|
13534
|
+
verifiedShas,
|
|
13430
13535
|
state: state.release(scope),
|
|
13431
13536
|
reviewColumns: repo.release.mode === "external" ? await tracker.reviewColumns() : void 0,
|
|
13432
13537
|
dryRun,
|
|
@@ -13522,6 +13627,18 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13522
13627
|
dryRun
|
|
13523
13628
|
);
|
|
13524
13629
|
}
|
|
13630
|
+
if (outcome.movedPast?.length) {
|
|
13631
|
+
await applyReview(
|
|
13632
|
+
tracker,
|
|
13633
|
+
tracker.contract,
|
|
13634
|
+
{ handoffs: [], requeues: outcome.movedPast },
|
|
13635
|
+
repo.branch.remote,
|
|
13636
|
+
remit,
|
|
13637
|
+
releaseSeat(c),
|
|
13638
|
+
{ verifiedSha: false, prRef: false },
|
|
13639
|
+
dryRun
|
|
13640
|
+
);
|
|
13641
|
+
}
|
|
13525
13642
|
const news = describeRelease(outcome, scope);
|
|
13526
13643
|
if (news) {
|
|
13527
13644
|
const notified = await notify(c, cfg.ship, news, remit, dryRun);
|
package/package.json
CHANGED