@tablation/crew 0.19.3 → 0.19.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 +64 -16
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2706,7 +2706,9 @@ function pushTag(cwd, remote, tag) {
2706
2706
  git(cwd, ["push", remote, tag]);
2707
2707
  }
2708
2708
  function fetchRemote(cwd, remote = "origin") {
2709
- return gitOk(cwd, ["fetch", "--prune", "--quiet", remote]) !== null;
2709
+ if (gitOk(cwd, ["fetch", "--prune", "--quiet", remote]) === null) return false;
2710
+ gitOk(cwd, ["fetch", "--quiet", remote, "refs/tags/*:refs/tags/*"]);
2711
+ return true;
2710
2712
  }
2711
2713
  function remoteConfigured(cwd, remote = "origin") {
2712
2714
  return gitOk(cwd, ["remote", "get-url", remote]) !== null;
@@ -6432,6 +6434,15 @@ function describeUnplaceable(unplaceable, verifiedStatus) {
6432
6434
  }
6433
6435
  return notes;
6434
6436
  }
6437
+ var unplaceableReported = /* @__PURE__ */ new Set();
6438
+ function reportUnplaceable(routeName, unplaceable, verified, emit) {
6439
+ if (unplaceableReported.has(routeName)) return;
6440
+ unplaceableReported.add(routeName);
6441
+ for (const note of describeUnplaceable(unplaceable, verified)) {
6442
+ if (note.level === "warn") emit.warn(note.message, { step: "release" });
6443
+ else emit.emit(note.message, { step: "release" });
6444
+ }
6445
+ }
6435
6446
  function checkGuards(cwd, base = "main") {
6436
6447
  const branch = currentBranch(cwd);
6437
6448
  if (branch !== base) {
@@ -6561,6 +6572,7 @@ ${section}`;
6561
6572
  }
6562
6573
  return [...lines.slice(0, anchor), section, ...lines.slice(anchor)].join("\n");
6563
6574
  }
6575
+ var RELEASE_COMMIT_SUBJECT = /^Release \S+$/;
6564
6576
  function decideRelease(cwd, tickets, contract, opts = {}) {
6565
6577
  const pattern = opts.tagPattern ?? "v*";
6566
6578
  const base = opts.base ?? "main";
@@ -6570,6 +6582,7 @@ function decideRelease(cwd, tickets, contract, opts = {}) {
6570
6582
  const head = resolve2(cwd, "HEAD") ?? "";
6571
6583
  const merges = block ? [] : planMerge(cwd, tickets, contract, lastReleased, opts.branchFor ?? ((t) => branchForIssue(cwd, t.issue_id)), base);
6572
6584
  const unreleasedCommits = lastReleased ? countCommits(cwd, `${lastReleased}..HEAD`) : 0;
6585
+ const unreleasedWork = lastReleased ? (gitOk(cwd, ["log", "--format=%s", `${lastReleased}..HEAD`]) ?? "").split("\n").filter((l) => l && !RELEASE_COMMIT_SUBJECT.test(l)).length : 0;
6573
6586
  return {
6574
6587
  block,
6575
6588
  merges,
@@ -6577,10 +6590,14 @@ function decideRelease(cwd, tickets, contract, opts = {}) {
6577
6590
  lastTag,
6578
6591
  lastReleased,
6579
6592
  unreleasedCommits,
6580
- upToDate: lastReleased === head,
6593
+ upToDate: lastReleased === head || lastReleased !== null && unreleasedWork === 0,
6581
6594
  unseeded: lastTag === null
6582
6595
  };
6583
6596
  }
6597
+ function releaseIdle(i) {
6598
+ if (i.force || i.external || i.unseeded || !i.upToDate || i.unstamped > 0) return false;
6599
+ return !i.tickets.some((t) => t.status === i.verifiedStatus);
6600
+ }
6584
6601
  var renderTag = (template, version) => template.replace(/\{version\}/g, version);
6585
6602
 
6586
6603
  // src/stamp.ts
@@ -7489,7 +7506,8 @@ async function mergeAndRelease(o, decision, fresh, releaseBranch) {
7489
7506
  o.emit.emit(`tagged ${tag}`);
7490
7507
  }
7491
7508
  }
7492
- if (tag && (o.repo.release.mode === "ci_manual" || o.repo.release.mode === "ci_auto")) {
7509
+ const tagIsCiTrigger = o.repo.release.mode === "ci_manual" || o.repo.release.mode === "ci_auto";
7510
+ if (tag && (tagIsCiTrigger || remoteConfigured(o.cwd, o.repo.branch.remote))) {
7493
7511
  if (o.dryRun) {
7494
7512
  o.emit.emit(`would push tag ${tag} to ${o.repo.branch.remote}`);
7495
7513
  } else {
@@ -7497,8 +7515,12 @@ async function mergeAndRelease(o, decision, fresh, releaseBranch) {
7497
7515
  pushTag(o.cwd, o.repo.branch.remote, tag);
7498
7516
  o.emit.emit(`pushed tag ${tag} to ${o.repo.branch.remote}`);
7499
7517
  } catch (e) {
7500
- o.emit.error(`failed to push tag ${tag} to ${o.repo.branch.remote} \u2014 ${e.message}`);
7501
- return { merged, conflicts, unbuildable, version, tag, deployed, integrated, decision, stopped: "tag push failed" };
7518
+ const msg = `failed to push tag ${tag} to ${o.repo.branch.remote} \u2014 ${e.message}`;
7519
+ if (tagIsCiTrigger) {
7520
+ o.emit.error(msg);
7521
+ return { merged, conflicts, unbuildable, version, tag, deployed, integrated, decision, stopped: "tag push failed" };
7522
+ }
7523
+ o.emit.warn(`${msg}; other ships will not see this release marker`);
7502
7524
  }
7503
7525
  }
7504
7526
  }
@@ -10517,15 +10539,8 @@ if (command === "passenger-sync-daemon") {
10517
10539
  const out = refreshBaseBranch(r.dir, remote, base, true);
10518
10540
  const unsafe = baseBranchUnsafe(out);
10519
10541
  return { action: out.action, unsafe, detail: unsafe ? describeUnsafeBase(out, remote, base) : out.detail };
10520
- }, reportUnplaceable = function(c, unplaceable, verified, emit2) {
10521
- if (unplaceableReported.has(c.route)) return;
10522
- unplaceableReported.add(c.route);
10523
- for (const note of describeUnplaceable(unplaceable, verified)) {
10524
- if (note.level === "warn") emit2.warn(note.message, { step: "release" });
10525
- else emit2.emit(note.message, { step: "release" });
10526
- }
10527
10542
  };
10528
- requireArmed2 = requireArmed, parseTicketNumber2 = parseTicketNumber, passengerApiKeys2 = passengerApiKeys, endpointApiFor2 = endpointApiFor, endpointReconcileDeps2 = endpointReconcileDeps, releaseRefused2 = releaseRefused, baseHealth2 = baseHealth, reportUnplaceable2 = reportUnplaceable;
10543
+ requireArmed2 = requireArmed, parseTicketNumber2 = parseTicketNumber, passengerApiKeys2 = passengerApiKeys, endpointApiFor2 = endpointApiFor, endpointReconcileDeps2 = endpointReconcileDeps, releaseRefused2 = releaseRefused, baseHealth2 = baseHealth;
10529
10544
  let cfg;
10530
10545
  let firstRun;
10531
10546
  try {
@@ -10659,6 +10674,7 @@ if (command === "passenger-sync-daemon") {
10659
10674
  }
10660
10675
  async function releaseFleet(opts = {}) {
10661
10676
  if (releaseRefused()) return;
10677
+ precheckTickets.clear();
10662
10678
  const summaries = [];
10663
10679
  if (!dryRun) state.releaseHeartbeat().touch();
10664
10680
  for (const c of cfg.routes) {
@@ -10670,6 +10686,7 @@ if (command === "passenger-sync-daemon") {
10670
10686
  }
10671
10687
  async function releaseTargets(opts = {}) {
10672
10688
  if (releaseRefused()) return;
10689
+ precheckTickets.clear();
10673
10690
  const only = value("repo");
10674
10691
  const targets = reposOf(route);
10675
10692
  const chosen = only ? targets.filter((t) => t.name === only) : targets;
@@ -10906,7 +10923,7 @@ ${changed2.map((c) => ` - ${c}
10906
10923
  }
10907
10924
  return `${who2}; ${lock2}`;
10908
10925
  }
10909
- const unplaceableReported = /* @__PURE__ */ new Set();
10926
+ const precheckTickets = /* @__PURE__ */ new Map();
10910
10927
  async function releasePhase(c, target, opts = {}) {
10911
10928
  const scope = releaseLockScope(c.route, target.name);
10912
10929
  const remit = emit.forRoute(c.route);
@@ -10941,6 +10958,38 @@ ${changed2.map((c) => ` - ${c}
10941
10958
  }
10942
10959
  if (!dryRun && !c.enabled) return { scope, tests: "skipped", outcome: "skipped", detail: "route not enabled" };
10943
10960
  const tracker = new Tracker(c, cfg.ship);
10961
+ if (!dryRun) {
10962
+ const force2 = command === "deploy" || flag("force");
10963
+ let seen = precheckTickets.get(c.route);
10964
+ if (!seen) precheckTickets.set(c.route, seen = tracker.openTickets());
10965
+ const { byRepo: preByRepo, unplaceable: preUnplaceable } = ticketsByRepo(c, await seen);
10966
+ const pre = preByRepo.get(target.name) ?? [];
10967
+ reportUnplaceable(c.route, preUnplaceable, tracker.contract.statuses.verified, remit);
10968
+ const d = decideRelease(target.dir, pre, tracker.contract, {
10969
+ tagPattern: repo.release.tagPattern ?? "v*",
10970
+ base: repo.branch.base
10971
+ });
10972
+ const unstamped = planReleasedBranches(
10973
+ target.dir,
10974
+ pre,
10975
+ tracker.contract,
10976
+ (t) => locateBranchForTicket(target.dir, repo, t)?.branch ?? null,
10977
+ repo.branch.base,
10978
+ repo.release.tagPattern ?? "v*"
10979
+ ).length;
10980
+ if (releaseIdle({
10981
+ tickets: pre,
10982
+ verifiedStatus: tracker.contract.statuses.verified,
10983
+ force: force2,
10984
+ external: repo.release.mode === "external",
10985
+ upToDate: d.upToDate,
10986
+ unseeded: d.unseeded,
10987
+ unstamped
10988
+ })) {
10989
+ remit.emit(`${target.name}: nothing verified \u2014 not locking`, { step: "release" });
10990
+ return { scope, tests: "skipped", outcome: "skipped", detail: "nothing verified \u2014 not locking" };
10991
+ }
10992
+ }
10944
10993
  if (!dryRun) {
10945
10994
  const holderLabel = `${cfg.ship.name}:${process.pid}`;
10946
10995
  const got = await tracker.acquireBoardLock(scope, holderLabel, RELEASE_LOCK_TTL_MS);
@@ -10963,7 +11012,7 @@ ${changed2.map((c) => ` - ${c}
10963
11012
  const all = await tracker.openTickets();
10964
11013
  const { byRepo, unplaceable } = ticketsByRepo(c, all);
10965
11014
  const tickets = byRepo.get(target.name) ?? [];
10966
- reportUnplaceable(c, unplaceable, tracker.contract.statuses.verified, remit);
11015
+ reportUnplaceable(c.route, unplaceable, tracker.contract.statuses.verified, remit);
10967
11016
  const outcome = await runRelease({
10968
11017
  cwd: target.dir,
10969
11018
  repo,
@@ -13249,4 +13298,3 @@ var endpointApiFor2;
13249
13298
  var endpointReconcileDeps2;
13250
13299
  var releaseRefused2;
13251
13300
  var baseHealth2;
13252
- var reportUnplaceable2;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.19.3",
3
+ "version": "0.19.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",