@tablation/crew 0.1.4 → 0.1.5

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 +127 -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,83 @@ 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
+ ...version ? { released_version: version, released_at: at } : {},
4999
+ // Written here rather than only on the external path (below): a
5000
+ // ticket's record should mean the same thing regardless of which
5001
+ // release mode closed it (ISSUE-218). `merged_at` is stamped
5002
+ // alongside — not read off the commit itself — since that is the
5003
+ // moment the tracker learns of it, same as the external path.
5004
+ ...sha ? { commit_sha: sha, merged_at: at } : {}
5005
+ });
5006
+ emit.emit(`stamped ${contract.statuses.deployed}${version ? ` (${version})` : ""}${shaNote}`, {
5007
+ ticket: ticket.issue_id
5008
+ });
5009
+ stamped++;
5010
+ } catch (e) {
5011
+ emit.warn(`could not stamp: ${e.message}`, { ticket: ticket.issue_id });
5012
+ }
5013
+ }
5014
+ return stamped;
5015
+ }
5016
+ async function applyExternalClosures(tracker, closures, contract, emit, dryRun) {
5017
+ if (!closures?.length) return 0;
5018
+ let stamped = 0;
5019
+ const at = (/* @__PURE__ */ new Date()).toISOString();
5020
+ for (const { ticket, closure, confirmed, version } of closures) {
5021
+ if (closure.state !== "merged" || !closure.mergedAt) continue;
5022
+ const shippedNote = confirmed ? ` and ${contract.statuses.deployed}${version ? ` (${version})` : ""}` : "";
5023
+ if (dryRun) {
5024
+ emit.emit(`would stamp commit_sha (${closure.mergedAt.slice(0, 8)})${shippedNote}`, { ticket: ticket.issue_id });
5025
+ stamped++;
5026
+ continue;
5027
+ }
5028
+ try {
5029
+ await tracker.updateTicket(ticket.id, {
5030
+ commit_sha: closure.mergedAt,
5031
+ merged_at: at,
5032
+ ...confirmed ? { status: contract.statuses.deployed, released_at: at, ...version ? { released_version: version } : {} } : {}
5033
+ });
5034
+ emit.emit(`stamped commit_sha (${closure.mergedAt.slice(0, 8)})${shippedNote}`, { ticket: ticket.issue_id });
5035
+ stamped++;
5036
+ } catch (e) {
5037
+ emit.warn(`could not stamp commit_sha: ${e.message}`, { ticket: ticket.issue_id });
5038
+ }
5039
+ }
5040
+ return stamped;
5041
+ }
5042
+
4965
5043
  // src/release-run.ts
4966
5044
  function emitReleaseSummary(emit, summaries) {
4967
5045
  if (summaries.length <= 1) return;
@@ -5240,6 +5318,26 @@ function refreshBase(o) {
5240
5318
  return { ok: false, why: r.detail };
5241
5319
  }
5242
5320
  }
5321
+ async function reconfirmLastRelease(o, decision) {
5322
+ if (!canReconfirm(o, decision)) return void 0;
5323
+ const prevTag = latestReleaseTag(o.cwd, o.repo.release.tagPattern ?? "v*", `${decision.lastTag}^`);
5324
+ const from = prevTag ? tagCommit(o.cwd, prevTag) : null;
5325
+ if (!from) return void 0;
5326
+ const pending = planStamp(o.cwd, o.tickets, o.contract, from, decision.lastReleased);
5327
+ if (pending.length === 0) return void 0;
5328
+ o.emit.emit(
5329
+ `${pending.length} verified ticket(s) were in ${decision.lastTag} but never confirmed live \u2014 re-checking`
5330
+ );
5331
+ return { ...await confirmLastRelease(o, decision), from };
5332
+ }
5333
+ 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;
5334
+ async function confirmLastRelease(o, decision) {
5335
+ const version = decision.lastTag.replace(/^v/, "");
5336
+ const expected = o.repo.release.verify.match === "version" ? version : decision.lastReleased;
5337
+ const confirmed = await confirm(o, expected) === true;
5338
+ if (!confirmed) o.emit.warn(`${decision.lastTag} is still not confirmed live \u2014 tickets stay verified`);
5339
+ return { confirmed, version };
5340
+ }
5243
5341
  async function runRelease(o) {
5244
5342
  const scoped = (msg) => o.scope ? `${o.scope}: ${msg}` : msg;
5245
5343
  const tagPattern = o.repo.release.tagPattern ?? "v*";
@@ -5319,6 +5417,20 @@ async function runRelease(o) {
5319
5417
  o.emit.enter("release");
5320
5418
  const head = o.dryRun ? decision.head : headSha(o.cwd);
5321
5419
  if (merged.length === 0 && decision.upToDate && !o.force) {
5420
+ const re = await reconfirmLastRelease(o, decision);
5421
+ if (re) {
5422
+ return {
5423
+ merged,
5424
+ conflicts,
5425
+ unbuildable,
5426
+ deployed: false,
5427
+ version: re.version,
5428
+ confirmed: re.confirmed || void 0,
5429
+ stampFrom: re.from,
5430
+ stopped: "nothing to release",
5431
+ decision
5432
+ };
5433
+ }
5322
5434
  o.emit.emit("nothing to release");
5323
5435
  return { merged, conflicts, unbuildable, deployed: false, stopped: "nothing to release", decision };
5324
5436
  }
@@ -5329,6 +5441,12 @@ async function runRelease(o) {
5329
5441
  return { merged, conflicts, unbuildable, deployed: false, stopped: "nothing merged and no release marker", decision };
5330
5442
  }
5331
5443
  if (applied === 0 && merged.length > 0 && decision.upToDate && !o.force) {
5444
+ if (canReconfirm(o, decision)) {
5445
+ const re = await confirmLastRelease(o, decision);
5446
+ if (!re.confirmed) {
5447
+ return { merged, conflicts, unbuildable, deployed: false, stopped: "release not yet confirmed live", decision };
5448
+ }
5449
+ }
5332
5450
  o.emit.emit(
5333
5451
  `nothing to release \u2014 ${merged.length} branch(es) were already contained in ${o.repo.branch.base}; stamping without cutting a version`
5334
5452
  );
@@ -5487,7 +5605,8 @@ async function runRelease(o) {
5487
5605
  }
5488
5606
  }
5489
5607
  }
5490
- const confirmed = await confirm(o, o.dryRun ? head : headSha(o.cwd));
5608
+ const expected = o.repo.release.verify.match === "version" && version ? version : o.dryRun ? head : headSha(o.cwd);
5609
+ const confirmed = await confirm(o, expected);
5491
5610
  o.emit.emit(
5492
5611
  `release ${o.dryRun ? "plan complete" : "complete"}: ${merged.length} merged${version ? `, ${version}` : ""}${tag ? `, tagged ${tag}` : ""}`,
5493
5612
  { data: { merged: merged.length, version, tag, deployed, integrated, confirmed } }
@@ -5495,83 +5614,6 @@ async function runRelease(o) {
5495
5614
  return { merged, conflicts, unbuildable, version, tag, deployed, integrated, confirmed, decision };
5496
5615
  }
5497
5616
 
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
5617
  // src/notify.ts
5576
5618
  async function notify(route, ship, n, emit, dryRun = false) {
5577
5619
  const script = route.hooks.notify;
@@ -6580,7 +6622,11 @@ async function discover(o) {
6580
6622
  try {
6581
6623
  const issuesModel = await get(o, `/data-models/${out.models.issues}`);
6582
6624
  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]);
6625
+ const known = /* @__PURE__ */ new Set([
6626
+ ...DEFAULT_CONTRACT.statuses.open,
6627
+ ...DEFAULT_CONTRACT.statuses.resolved,
6628
+ DEFAULT_CONTRACT.statuses.draft
6629
+ ]);
6584
6630
  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
6631
  } catch {
6586
6632
  }
@@ -8770,7 +8816,7 @@ ${changed2.map((c) => ` - ${c}
8770
8816
  target.dir,
8771
8817
  tickets,
8772
8818
  tracker.contract,
8773
- outcome.decision.lastReleased,
8819
+ outcome.stampFrom ?? outcome.decision.lastReleased,
8774
8820
  outcome.decision.head,
8775
8821
  new Map(outcome.merged.map((m) => [m.ticket.issue_id, m.sha]))
8776
8822
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
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",