@tablation/crew 0.1.4 → 0.1.6

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.
Files changed (2) hide show
  1. package/dist/cli.js +129 -81
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1613,7 +1613,8 @@ var DEFAULT_CONTRACT = {
1613
1613
  deployed: "closed_deployed",
1614
1614
  needsHuman: "needs_info",
1615
1615
  reviewing: null,
1616
- wontFix: "closed_wont_fix"
1616
+ wontFix: "closed_wont_fix",
1617
+ draft: "draft"
1617
1618
  },
1618
1619
  epics: {
1619
1620
  statusColumn: "status",
@@ -4962,6 +4963,85 @@ crew: cannot run ${shell.bin}: ${err.message}` });
4962
4963
  });
4963
4964
  }
4964
4965
 
4966
+ // src/stamp.ts
4967
+ function planStamp(cwd, tickets, contract, fromSha, toSha, merged = /* @__PURE__ */ new Map()) {
4968
+ const range = fromSha ? `${fromSha.slice(0, 8)}..${toSha.slice(0, 8)}` : null;
4969
+ const bodies = fromSha ? commitBodies(cwd, `${fromSha}..${toSha}`) : "";
4970
+ return tickets.filter((t) => t.status === contract.statuses.verified).map((t) => {
4971
+ if (merged.has(t.issue_id)) {
4972
+ return { ticket: t, reason: "merged by this release", sha: merged.get(t.issue_id) };
4973
+ }
4974
+ const keys = referenceKeys(t);
4975
+ const pattern = keys.map((k) => k.replace(/[.[\]{}()*+?^$|\\]/g, "\\$&")).join("|");
4976
+ if (range && new RegExp(`(^|[^0-9A-Za-z_-])(${pattern})([^0-9]|$)`).test(bodies)) {
4977
+ const sha = findKeyInRange(cwd, keys, range) ?? void 0;
4978
+ return { ticket: t, reason: `named in ${range}`, sha };
4979
+ }
4980
+ return null;
4981
+ }).filter((p) => p !== null);
4982
+ }
4983
+ async function applyStamp(tracker, plan, version, contract, emit, dryRun) {
4984
+ let stamped = 0;
4985
+ const at = (/* @__PURE__ */ new Date()).toISOString();
4986
+ for (const { ticket, sha } of plan) {
4987
+ const shaNote = sha ? `, commit_sha ${sha.slice(0, 8)}` : "";
4988
+ if (dryRun) {
4989
+ emit.emit(`would stamp -> ${contract.statuses.deployed}${version ? ` (${version})` : ""}${shaNote}`, {
4990
+ ticket: ticket.issue_id
4991
+ });
4992
+ stamped++;
4993
+ continue;
4994
+ }
4995
+ try {
4996
+ await tracker.updateTicket(ticket.id, {
4997
+ status: contract.statuses.deployed,
4998
+ // `released_at` is always a real release moment here; only the version is optional (versioning: none).
4999
+ released_at: at,
5000
+ ...version ? { released_version: version } : {},
5001
+ // Written here rather than only on the external path (below): a
5002
+ // ticket's record should mean the same thing regardless of which
5003
+ // release mode closed it (ISSUE-218). `merged_at` is stamped
5004
+ // alongside — not read off the commit itself — since that is the
5005
+ // moment the tracker learns of it, same as the external path.
5006
+ ...sha ? { commit_sha: sha, merged_at: at } : {}
5007
+ });
5008
+ emit.emit(`stamped ${contract.statuses.deployed}${version ? ` (${version})` : ""}${shaNote}`, {
5009
+ ticket: ticket.issue_id
5010
+ });
5011
+ stamped++;
5012
+ } catch (e) {
5013
+ emit.warn(`could not stamp: ${e.message}`, { ticket: ticket.issue_id });
5014
+ }
5015
+ }
5016
+ return stamped;
5017
+ }
5018
+ async function applyExternalClosures(tracker, closures, contract, emit, dryRun) {
5019
+ if (!closures?.length) return 0;
5020
+ let stamped = 0;
5021
+ const at = (/* @__PURE__ */ new Date()).toISOString();
5022
+ for (const { ticket, closure, confirmed, version } of closures) {
5023
+ if (closure.state !== "merged" || !closure.mergedAt) continue;
5024
+ const shippedNote = confirmed ? ` and ${contract.statuses.deployed}${version ? ` (${version})` : ""}` : "";
5025
+ if (dryRun) {
5026
+ emit.emit(`would stamp commit_sha (${closure.mergedAt.slice(0, 8)})${shippedNote}`, { ticket: ticket.issue_id });
5027
+ stamped++;
5028
+ continue;
5029
+ }
5030
+ try {
5031
+ await tracker.updateTicket(ticket.id, {
5032
+ commit_sha: closure.mergedAt,
5033
+ merged_at: at,
5034
+ ...confirmed ? { status: contract.statuses.deployed, released_at: at, ...version ? { released_version: version } : {} } : {}
5035
+ });
5036
+ emit.emit(`stamped commit_sha (${closure.mergedAt.slice(0, 8)})${shippedNote}`, { ticket: ticket.issue_id });
5037
+ stamped++;
5038
+ } catch (e) {
5039
+ emit.warn(`could not stamp commit_sha: ${e.message}`, { ticket: ticket.issue_id });
5040
+ }
5041
+ }
5042
+ return stamped;
5043
+ }
5044
+
4965
5045
  // src/release-run.ts
4966
5046
  function emitReleaseSummary(emit, summaries) {
4967
5047
  if (summaries.length <= 1) return;
@@ -5240,6 +5320,26 @@ function refreshBase(o) {
5240
5320
  return { ok: false, why: r.detail };
5241
5321
  }
5242
5322
  }
5323
+ async function reconfirmLastRelease(o, decision) {
5324
+ if (!canReconfirm(o, decision)) return void 0;
5325
+ const prevTag = latestReleaseTag(o.cwd, o.repo.release.tagPattern ?? "v*", `${decision.lastTag}^`);
5326
+ const from = prevTag ? tagCommit(o.cwd, prevTag) : null;
5327
+ if (!from) return void 0;
5328
+ const pending = planStamp(o.cwd, o.tickets, o.contract, from, decision.lastReleased);
5329
+ if (pending.length === 0) return void 0;
5330
+ o.emit.emit(
5331
+ `${pending.length} verified ticket(s) were in ${decision.lastTag} but never confirmed live \u2014 re-checking`
5332
+ );
5333
+ return { ...await confirmLastRelease(o, decision), from };
5334
+ }
5335
+ var canReconfirm = (o, decision) => !o.dryRun && (o.repo.release.mode === "ci_manual" || o.repo.release.mode === "ci_auto") && !!o.repo.hooks.released && !!decision.lastTag && !!decision.lastReleased;
5336
+ async function confirmLastRelease(o, decision) {
5337
+ const version = decision.lastTag.replace(/^v/, "");
5338
+ const expected = o.repo.release.verify.match === "version" ? version : decision.lastReleased;
5339
+ const confirmed = await confirm(o, expected) === true;
5340
+ if (!confirmed) o.emit.warn(`${decision.lastTag} is still not confirmed live \u2014 tickets stay verified`);
5341
+ return { confirmed, version };
5342
+ }
5243
5343
  async function runRelease(o) {
5244
5344
  const scoped = (msg) => o.scope ? `${o.scope}: ${msg}` : msg;
5245
5345
  const tagPattern = o.repo.release.tagPattern ?? "v*";
@@ -5319,6 +5419,20 @@ async function runRelease(o) {
5319
5419
  o.emit.enter("release");
5320
5420
  const head = o.dryRun ? decision.head : headSha(o.cwd);
5321
5421
  if (merged.length === 0 && decision.upToDate && !o.force) {
5422
+ const re = await reconfirmLastRelease(o, decision);
5423
+ if (re) {
5424
+ return {
5425
+ merged,
5426
+ conflicts,
5427
+ unbuildable,
5428
+ deployed: false,
5429
+ version: re.version,
5430
+ confirmed: re.confirmed || void 0,
5431
+ stampFrom: re.from,
5432
+ stopped: "nothing to release",
5433
+ decision
5434
+ };
5435
+ }
5322
5436
  o.emit.emit("nothing to release");
5323
5437
  return { merged, conflicts, unbuildable, deployed: false, stopped: "nothing to release", decision };
5324
5438
  }
@@ -5329,6 +5443,12 @@ async function runRelease(o) {
5329
5443
  return { merged, conflicts, unbuildable, deployed: false, stopped: "nothing merged and no release marker", decision };
5330
5444
  }
5331
5445
  if (applied === 0 && merged.length > 0 && decision.upToDate && !o.force) {
5446
+ if (canReconfirm(o, decision)) {
5447
+ const re = await confirmLastRelease(o, decision);
5448
+ if (!re.confirmed) {
5449
+ return { merged, conflicts, unbuildable, deployed: false, stopped: "release not yet confirmed live", decision };
5450
+ }
5451
+ }
5332
5452
  o.emit.emit(
5333
5453
  `nothing to release \u2014 ${merged.length} branch(es) were already contained in ${o.repo.branch.base}; stamping without cutting a version`
5334
5454
  );
@@ -5487,7 +5607,8 @@ async function runRelease(o) {
5487
5607
  }
5488
5608
  }
5489
5609
  }
5490
- const confirmed = await confirm(o, o.dryRun ? head : headSha(o.cwd));
5610
+ const expected = o.repo.release.verify.match === "version" && version ? version : o.dryRun ? head : headSha(o.cwd);
5611
+ const confirmed = await confirm(o, expected);
5491
5612
  o.emit.emit(
5492
5613
  `release ${o.dryRun ? "plan complete" : "complete"}: ${merged.length} merged${version ? `, ${version}` : ""}${tag ? `, tagged ${tag}` : ""}`,
5493
5614
  { data: { merged: merged.length, version, tag, deployed, integrated, confirmed } }
@@ -5495,83 +5616,6 @@ async function runRelease(o) {
5495
5616
  return { merged, conflicts, unbuildable, version, tag, deployed, integrated, confirmed, decision };
5496
5617
  }
5497
5618
 
5498
- // src/stamp.ts
5499
- function planStamp(cwd, tickets, contract, fromSha, toSha, merged = /* @__PURE__ */ new Map()) {
5500
- const range = fromSha ? `${fromSha.slice(0, 8)}..${toSha.slice(0, 8)}` : null;
5501
- const bodies = fromSha ? commitBodies(cwd, `${fromSha}..${toSha}`) : "";
5502
- return tickets.filter((t) => t.status === contract.statuses.verified).map((t) => {
5503
- if (merged.has(t.issue_id)) {
5504
- return { ticket: t, reason: "merged by this release", sha: merged.get(t.issue_id) };
5505
- }
5506
- const keys = referenceKeys(t);
5507
- const pattern = keys.map((k) => k.replace(/[.[\]{}()*+?^$|\\]/g, "\\$&")).join("|");
5508
- if (range && new RegExp(`(^|[^0-9A-Za-z_-])(${pattern})([^0-9]|$)`).test(bodies)) {
5509
- const sha = findKeyInRange(cwd, keys, range) ?? void 0;
5510
- return { ticket: t, reason: `named in ${range}`, sha };
5511
- }
5512
- return null;
5513
- }).filter((p) => p !== null);
5514
- }
5515
- async function applyStamp(tracker, plan, version, contract, emit, dryRun) {
5516
- let stamped = 0;
5517
- const at = (/* @__PURE__ */ new Date()).toISOString();
5518
- for (const { ticket, sha } of plan) {
5519
- const shaNote = sha ? `, commit_sha ${sha.slice(0, 8)}` : "";
5520
- if (dryRun) {
5521
- emit.emit(`would stamp -> ${contract.statuses.deployed}${version ? ` (${version})` : ""}${shaNote}`, {
5522
- ticket: ticket.issue_id
5523
- });
5524
- stamped++;
5525
- continue;
5526
- }
5527
- try {
5528
- await tracker.updateTicket(ticket.id, {
5529
- status: contract.statuses.deployed,
5530
- ...version ? { released_version: version, released_at: at } : {},
5531
- // Written here rather than only on the external path (below): a
5532
- // ticket's record should mean the same thing regardless of which
5533
- // release mode closed it (ISSUE-218). `merged_at` is stamped
5534
- // alongside — not read off the commit itself — since that is the
5535
- // moment the tracker learns of it, same as the external path.
5536
- ...sha ? { commit_sha: sha, merged_at: at } : {}
5537
- });
5538
- emit.emit(`stamped ${contract.statuses.deployed}${version ? ` (${version})` : ""}${shaNote}`, {
5539
- ticket: ticket.issue_id
5540
- });
5541
- stamped++;
5542
- } catch (e) {
5543
- emit.warn(`could not stamp: ${e.message}`, { ticket: ticket.issue_id });
5544
- }
5545
- }
5546
- return stamped;
5547
- }
5548
- async function applyExternalClosures(tracker, closures, contract, emit, dryRun) {
5549
- if (!closures?.length) return 0;
5550
- let stamped = 0;
5551
- const at = (/* @__PURE__ */ new Date()).toISOString();
5552
- for (const { ticket, closure, confirmed, version } of closures) {
5553
- if (closure.state !== "merged" || !closure.mergedAt) continue;
5554
- const shippedNote = confirmed ? ` and ${contract.statuses.deployed}${version ? ` (${version})` : ""}` : "";
5555
- if (dryRun) {
5556
- emit.emit(`would stamp commit_sha (${closure.mergedAt.slice(0, 8)})${shippedNote}`, { ticket: ticket.issue_id });
5557
- stamped++;
5558
- continue;
5559
- }
5560
- try {
5561
- await tracker.updateTicket(ticket.id, {
5562
- commit_sha: closure.mergedAt,
5563
- merged_at: at,
5564
- ...confirmed ? { status: contract.statuses.deployed, released_at: at, ...version ? { released_version: version } : {} } : {}
5565
- });
5566
- emit.emit(`stamped commit_sha (${closure.mergedAt.slice(0, 8)})${shippedNote}`, { ticket: ticket.issue_id });
5567
- stamped++;
5568
- } catch (e) {
5569
- emit.warn(`could not stamp commit_sha: ${e.message}`, { ticket: ticket.issue_id });
5570
- }
5571
- }
5572
- return stamped;
5573
- }
5574
-
5575
5619
  // src/notify.ts
5576
5620
  async function notify(route, ship, n, emit, dryRun = false) {
5577
5621
  const script = route.hooks.notify;
@@ -6580,7 +6624,11 @@ async function discover(o) {
6580
6624
  try {
6581
6625
  const issuesModel = await get(o, `/data-models/${out.models.issues}`);
6582
6626
  const statusField = issuesModel.fields.find((f) => f.columnName === DEFAULT_CONTRACT.columns.status);
6583
- const known = /* @__PURE__ */ new Set([...DEFAULT_CONTRACT.statuses.open, ...DEFAULT_CONTRACT.statuses.resolved]);
6627
+ const known = /* @__PURE__ */ new Set([
6628
+ ...DEFAULT_CONTRACT.statuses.open,
6629
+ ...DEFAULT_CONTRACT.statuses.resolved,
6630
+ DEFAULT_CONTRACT.statuses.draft
6631
+ ]);
6584
6632
  out.unrecognizedStatuses = (statusField?.fieldType?.choiceOptions ?? []).filter((opt) => !known.has(opt.value)).sort((a, b) => (a.position ?? 0) - (b.position ?? 0)).map((opt) => ({ value: opt.value, label: opt.label ?? void 0, position: opt.position ?? void 0 }));
6585
6633
  } catch {
6586
6634
  }
@@ -8770,7 +8818,7 @@ ${changed2.map((c) => ` - ${c}
8770
8818
  target.dir,
8771
8819
  tickets,
8772
8820
  tracker.contract,
8773
- outcome.decision.lastReleased,
8821
+ outcome.stampFrom ?? outcome.decision.lastReleased,
8774
8822
  outcome.decision.head,
8775
8823
  new Map(outcome.merged.map((m) => [m.ticket.issue_id, m.sha]))
8776
8824
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "A standing team of headless agents that picks work off a Tablation board, does it on a machine you control, and reports back.",
5
5
  "type": "module",
6
6
  "license": "MIT",