@tablation/crew 0.13.1 → 0.14.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 +186 -14
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -86,6 +86,7 @@ var BRANCH_KEYS = /* @__PURE__ */ new Set(["base", "name", "push", "remote"]);
|
|
|
86
86
|
var PLACEHOLDER = /\{(key|number|slug|role|prefix|tag)\}/g;
|
|
87
87
|
var RELEASE_KEYS = /* @__PURE__ */ new Set([
|
|
88
88
|
"mode",
|
|
89
|
+
"ship",
|
|
89
90
|
"ci",
|
|
90
91
|
"verify",
|
|
91
92
|
"versioning",
|
|
@@ -202,6 +203,16 @@ function parseRepoConfig(text, file) {
|
|
|
202
203
|
if (!MODES.includes(mode)) {
|
|
203
204
|
throw new RepoConfigError(`${file}: release.mode must be one of ${MODES.join("|")} (got "${mode}")`);
|
|
204
205
|
}
|
|
206
|
+
let ship = null;
|
|
207
|
+
if (raw.release?.ship !== void 0 && raw.release?.ship !== null) {
|
|
208
|
+
ship = String(raw.release.ship).trim();
|
|
209
|
+
if (ship === "") throw new RepoConfigError(`${file}: release.ship is empty`);
|
|
210
|
+
if (mode !== "local" && mode !== "integrate") {
|
|
211
|
+
throw new RepoConfigError(
|
|
212
|
+
`${file}: release.ship only applies to release.mode "local" or "integrate" (got "${mode}") \u2014 CI-mode repos keep multi-ship merging, protected by the board lock and the fast-forward-only push`
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
205
216
|
const provider = String(raw.release?.ci?.provider ?? "none");
|
|
206
217
|
if (!PROVIDERS.includes(provider)) {
|
|
207
218
|
throw new RepoConfigError(
|
|
@@ -301,6 +312,7 @@ function parseRepoConfig(text, file) {
|
|
|
301
312
|
labels,
|
|
302
313
|
release: {
|
|
303
314
|
mode,
|
|
315
|
+
ship,
|
|
304
316
|
ci: { provider, ref: raw.release?.ci?.ref ? String(raw.release.ci.ref) : void 0 },
|
|
305
317
|
verify,
|
|
306
318
|
versioning,
|
|
@@ -451,6 +463,9 @@ function resolveRepoConfig(repo, ship, dir) {
|
|
|
451
463
|
labels,
|
|
452
464
|
release: {
|
|
453
465
|
mode,
|
|
466
|
+
// Repo-only, like `platform`: which ship releases a repo is a fact the
|
|
467
|
+
// repo declares (CREW-1384), never something a ship's own fallback sets.
|
|
468
|
+
ship: repo?.release.ship ?? null,
|
|
454
469
|
ci: {
|
|
455
470
|
provider,
|
|
456
471
|
ref: pick("release.ci.ref", repo?.release.ci.ref, ship?.release?.ci?.ref, void 0)
|
|
@@ -522,6 +537,11 @@ function validateEffective(cfg) {
|
|
|
522
537
|
if (cfg.release.mode === "local" && !cfg.hooks.deploy) {
|
|
523
538
|
problems.push('release.mode "local" needs a deploy hook');
|
|
524
539
|
}
|
|
540
|
+
if (cfg.release.ship && cfg.release.mode !== "local" && cfg.release.mode !== "integrate") {
|
|
541
|
+
problems.push(
|
|
542
|
+
`release.ship "${cfg.release.ship}" only applies to release.mode "local" or "integrate" (this ship's settings make it "${cfg.release.mode}") \u2014 CI-mode repos keep multi-ship merging`
|
|
543
|
+
);
|
|
544
|
+
}
|
|
525
545
|
if (cfg.release.mode === "external" && !cfg.hooks.merged) {
|
|
526
546
|
problems.push(
|
|
527
547
|
'release.mode "external" without a merged hook \u2014 the crew hands work off and then has no reliable way to learn whether it landed'
|
|
@@ -1962,6 +1982,16 @@ async function notify(route, ship, n, emit, dryRun = false) {
|
|
|
1962
1982
|
return false;
|
|
1963
1983
|
}
|
|
1964
1984
|
}
|
|
1985
|
+
function withHookTail(detail, f) {
|
|
1986
|
+
if (!f || !f.tail) return detail;
|
|
1987
|
+
return `${detail}
|
|
1988
|
+
|
|
1989
|
+
The \`${f.hook}\` hook (\`${f.label}\`) printed, last lines:
|
|
1990
|
+
|
|
1991
|
+
\`\`\`
|
|
1992
|
+
${f.tail}
|
|
1993
|
+
\`\`\``;
|
|
1994
|
+
}
|
|
1965
1995
|
function describeRelease(o, route) {
|
|
1966
1996
|
if (o.integrated) {
|
|
1967
1997
|
return {
|
|
@@ -1989,17 +2019,24 @@ function describeRelease(o, route) {
|
|
|
1989
2019
|
return {
|
|
1990
2020
|
level: "fail",
|
|
1991
2021
|
headline: `${route}: release blocked \u2014 setup failed`,
|
|
1992
|
-
detail:
|
|
2022
|
+
detail: withHookTail(
|
|
2023
|
+
"the setup hook failed in the release checkout; the target stays on the previous release",
|
|
2024
|
+
o.hookFailure
|
|
2025
|
+
)
|
|
1993
2026
|
};
|
|
1994
2027
|
}
|
|
1995
2028
|
if (o.stopped === "deploy failed") {
|
|
1996
|
-
return {
|
|
2029
|
+
return {
|
|
2030
|
+
level: "fail",
|
|
2031
|
+
headline: `${route}: deploy FAILED`,
|
|
2032
|
+
detail: withHookTail("nothing new is live", o.hookFailure)
|
|
2033
|
+
};
|
|
1997
2034
|
}
|
|
1998
2035
|
if (o.stopped === "build failed") {
|
|
1999
2036
|
return {
|
|
2000
2037
|
level: "fail",
|
|
2001
2038
|
headline: `${route}: build FAILED`,
|
|
2002
|
-
detail: "the target stays on the previous release \u2014 nothing was deployed"
|
|
2039
|
+
detail: withHookTail("the target stays on the previous release \u2014 nothing was deployed", o.hookFailure)
|
|
2003
2040
|
};
|
|
2004
2041
|
}
|
|
2005
2042
|
if (o.blockedCycles && o.blockedCycles >= 2) {
|
|
@@ -2622,14 +2659,16 @@ var SHELL_WORDS = /* @__PURE__ */ new Set([
|
|
|
2622
2659
|
|
|
2623
2660
|
// src/board-lock.ts
|
|
2624
2661
|
import { StaleWriteError } from "@tablation/client";
|
|
2662
|
+
function releaseLockScope(route, repo) {
|
|
2663
|
+
return `${route}/${repo}`;
|
|
2664
|
+
}
|
|
2625
2665
|
async function acquireBoardLock(client2, modelId, scope, holderLabel, ttlMs) {
|
|
2626
2666
|
if (!modelId) return { ok: true, release: async () => {
|
|
2627
2667
|
} };
|
|
2628
2668
|
const filters = JSON.stringify([{ columnName: "scope", operator: "EQ", value: scope }]);
|
|
2629
2669
|
const rows = await client2.list(modelId, { filters, limit: 1 });
|
|
2630
2670
|
const row = rows[0];
|
|
2631
|
-
if (!row) return { ok:
|
|
2632
|
-
} };
|
|
2671
|
+
if (!row) return { ok: false, reason: "missing" };
|
|
2633
2672
|
const age = Date.now() - new Date(row.updated_at).getTime();
|
|
2634
2673
|
const free = !row.holder || row.holder === holderLabel || age > ttlMs;
|
|
2635
2674
|
if (!free) return { ok: false, heldBy: row.holder ?? void 0, reason: "held" };
|
|
@@ -2650,6 +2689,39 @@ async function acquireBoardLock(client2, modelId, scope, holderLabel, ttlMs) {
|
|
|
2650
2689
|
}
|
|
2651
2690
|
};
|
|
2652
2691
|
}
|
|
2692
|
+
async function lockRowState(client2, modelId, scope) {
|
|
2693
|
+
const rows = await client2.list(modelId, { filters: scopeFilter(scope), limit: 1 });
|
|
2694
|
+
const row = rows[0];
|
|
2695
|
+
if (!row) return { exists: false };
|
|
2696
|
+
return { exists: true, ...row.holder ? { holder: row.holder, since: row.updated_at } : {} };
|
|
2697
|
+
}
|
|
2698
|
+
var scopeFilter = (scope) => JSON.stringify([{ columnName: "scope", operator: "EQ", value: scope }]);
|
|
2699
|
+
async function ensureLockRows(client2, modelId, scopes) {
|
|
2700
|
+
const created = [];
|
|
2701
|
+
for (const scope of scopes) {
|
|
2702
|
+
const filters = scopeFilter(scope);
|
|
2703
|
+
if ((await client2.list(modelId, { filters, limit: 1 })).length > 0) continue;
|
|
2704
|
+
let mine;
|
|
2705
|
+
try {
|
|
2706
|
+
mine = await client2.create(modelId, { scope, holder: "" });
|
|
2707
|
+
} catch (e) {
|
|
2708
|
+
const status2 = e.status;
|
|
2709
|
+
if (status2 === 409 || status2 === 400) {
|
|
2710
|
+
if ((await client2.list(modelId, { filters, limit: 1 })).length > 0) continue;
|
|
2711
|
+
}
|
|
2712
|
+
throw e;
|
|
2713
|
+
}
|
|
2714
|
+
const all = await client2.list(modelId, { filters, limit: 20 });
|
|
2715
|
+
const keeper = [...all].sort((a, b) => (a.created_at ?? "").localeCompare(b.created_at ?? "") || a.id.localeCompare(b.id))[0];
|
|
2716
|
+
if (keeper && keeper.id !== mine.id) {
|
|
2717
|
+
await client2.remove(modelId, mine.id).catch(() => {
|
|
2718
|
+
});
|
|
2719
|
+
continue;
|
|
2720
|
+
}
|
|
2721
|
+
created.push(scope);
|
|
2722
|
+
}
|
|
2723
|
+
return created;
|
|
2724
|
+
}
|
|
2653
2725
|
|
|
2654
2726
|
// src/tracker.ts
|
|
2655
2727
|
function ticketBranchContext(t) {
|
|
@@ -3019,13 +3091,18 @@ var Tracker = class {
|
|
|
3019
3091
|
/**
|
|
3020
3092
|
* Claim a board-visible lock for `scope` (ISSUE-394) — see board-lock.ts
|
|
3021
3093
|
* for the CAS mechanics. Resolves to `{ ok: true }` immediately, with a
|
|
3022
|
-
* no-op release, when this workspace has no Locks table
|
|
3023
|
-
*
|
|
3024
|
-
*
|
|
3094
|
+
* no-op release, only when this workspace has no Locks table at all. A
|
|
3095
|
+
* Locks table with no row for `scope` is `{ ok: false, reason: 'missing' }`
|
|
3096
|
+
* (CREW-1384): `crew connect` provisions the row.
|
|
3025
3097
|
*/
|
|
3026
3098
|
acquireBoardLock(scope, holderLabel, ttlMs) {
|
|
3027
3099
|
return acquireBoardLock(this.client.records, this.route.resolved?.locksModelId, scope, holderLabel, ttlMs);
|
|
3028
3100
|
}
|
|
3101
|
+
/** The Locks row for `scope` — present? held by whom? Undefined when this workspace has no Locks table. */
|
|
3102
|
+
async lockState(scope) {
|
|
3103
|
+
const model = this.route.resolved?.locksModelId;
|
|
3104
|
+
return model ? lockRowState(this.client.records, model, scope) : void 0;
|
|
3105
|
+
}
|
|
3029
3106
|
/**
|
|
3030
3107
|
* Reports one crew member's live status (ISSUE-325) — "Working" for the
|
|
3031
3108
|
* duration of an agent run, "Idle" once it ends.
|
|
@@ -5979,6 +6056,10 @@ function summarizeOutcome(o, scope, hadTestHook) {
|
|
|
5979
6056
|
detail: `${o.merged.length} merged${o.version ? `, ${o.version}` : ""}${o.tag ? `, tagged ${o.tag}` : ""}`
|
|
5980
6057
|
};
|
|
5981
6058
|
}
|
|
6059
|
+
var HOOK_FAILURE_TAIL_LINES = 15;
|
|
6060
|
+
function hookFailure(o, name, output) {
|
|
6061
|
+
return { hook: name, label: hookLabel(o.repo, name), tail: lastLines(output, HOOK_FAILURE_TAIL_LINES) };
|
|
6062
|
+
}
|
|
5982
6063
|
function looksLikeWorkerCrash(output) {
|
|
5983
6064
|
const crashSignature = /worker process was terminated by another process|signal=SIG(SEGV|ABRT|BUS)/i;
|
|
5984
6065
|
const assertionDiff = /(AssertionError|Expected:|Received:|✕|not ok \d)/;
|
|
@@ -6450,7 +6531,7 @@ async function mergeAndRelease(o, decision, fresh, releaseBranch) {
|
|
|
6450
6531
|
const r = await hook(o, "setup");
|
|
6451
6532
|
if (r && r.code !== 0) {
|
|
6452
6533
|
o.emit.error(`setup FAILED (exit ${r.code}) \u2014 not testing or deploying; the target stays on the previous release`);
|
|
6453
|
-
return { merged, conflicts, unbuildable, deployed: false, stopped: "setup failed", decision };
|
|
6534
|
+
return { merged, conflicts, unbuildable, deployed: false, stopped: "setup failed", decision, hookFailure: hookFailure(o, "setup", r.output) };
|
|
6454
6535
|
}
|
|
6455
6536
|
}
|
|
6456
6537
|
}
|
|
@@ -6504,7 +6585,7 @@ async function mergeAndRelease(o, decision, fresh, releaseBranch) {
|
|
|
6504
6585
|
git(o.cwd, ["reset", "--hard", preBump]);
|
|
6505
6586
|
o.emit.warn(`dropped the unreleased version commit for ${version} (reset ${o.repo.branch.base} to ${preBump.slice(0, 8)}) so a retry starts clean`);
|
|
6506
6587
|
}
|
|
6507
|
-
return { merged, conflicts, unbuildable, version, deployed: false, stopped: "build failed", decision };
|
|
6588
|
+
return { merged, conflicts, unbuildable, version, deployed: false, stopped: "build failed", decision, hookFailure: hookFailure(o, "build", r.output) };
|
|
6508
6589
|
}
|
|
6509
6590
|
}
|
|
6510
6591
|
}
|
|
@@ -6534,7 +6615,7 @@ async function mergeAndRelease(o, decision, fresh, releaseBranch) {
|
|
|
6534
6615
|
if (r && r.code !== 0) {
|
|
6535
6616
|
o.emit.error(`deploy FAILED (exit ${r.code}) \u2014 the target may be partially deployed`);
|
|
6536
6617
|
o.state?.noteDeployFailed(headSha(o.cwd));
|
|
6537
|
-
return { merged, conflicts, unbuildable, version, deployed: false, stopped: "deploy failed", decision };
|
|
6618
|
+
return { merged, conflicts, unbuildable, version, deployed: false, stopped: "deploy failed", decision, hookFailure: hookFailure(o, "deploy", r.output) };
|
|
6538
6619
|
}
|
|
6539
6620
|
deployed = true;
|
|
6540
6621
|
o.state?.clearDeployFailed();
|
|
@@ -6652,6 +6733,24 @@ Still failing: ${n.detail ?? n.headline}`, memberId);
|
|
|
6652
6733
|
// src/cli.ts
|
|
6653
6734
|
import { StaleWriteError as StaleWriteError8 } from "@tablation/client";
|
|
6654
6735
|
|
|
6736
|
+
// src/release-ship.ts
|
|
6737
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
6738
|
+
function releaserFor(release, shipName2) {
|
|
6739
|
+
const named = release.ship;
|
|
6740
|
+
if (!named || release.mode !== "local" && release.mode !== "integrate") return { releases: true, releaser: named ?? null };
|
|
6741
|
+
return { releases: named.trim() === shipName2.trim(), releaser: named };
|
|
6742
|
+
}
|
|
6743
|
+
function nonReleaserNote(repo, releaser) {
|
|
6744
|
+
return `release for ${repo} runs on ${releaser}`;
|
|
6745
|
+
}
|
|
6746
|
+
function missingReleaserWarning(repo, release, ships, now = Date.now()) {
|
|
6747
|
+
if (release.ship || release.mode !== "local" && release.mode !== "integrate") return null;
|
|
6748
|
+
const live = ships.filter((s) => s.last_seen && now - Date.parse(s.last_seen) <= DAY_MS);
|
|
6749
|
+
if (live.length < 2) return null;
|
|
6750
|
+
const names = live.map((s) => s.name ?? "?").join(", ");
|
|
6751
|
+
return `${repo}: release.mode "${release.mode}" with no release.ship, and ${live.length} ships seen in the last day (${names}) \u2014 every one of them will try to release it. Set release.ship in .crew.yaml.`;
|
|
6752
|
+
}
|
|
6753
|
+
|
|
6655
6754
|
// src/watch.ts
|
|
6656
6755
|
import { watch as watchFile, existsSync as existsSync10, statSync as statSync5, openSync as openSync2, readSync, closeSync as closeSync2 } from "fs";
|
|
6657
6756
|
var E = "\x1B[";
|
|
@@ -7434,6 +7533,35 @@ async function patch(o, path, body) {
|
|
|
7434
7533
|
if (!res.ok) throw new ConnectHttpError(res.status, path, res.statusText);
|
|
7435
7534
|
return await res.json();
|
|
7436
7535
|
}
|
|
7536
|
+
async function del(o, path) {
|
|
7537
|
+
const res = await fetch(`${o.baseUrl.replace(/\/+$/, "")}/api${path}`, {
|
|
7538
|
+
method: "DELETE",
|
|
7539
|
+
headers: {
|
|
7540
|
+
Authorization: `Bearer ${o.apiKey}`,
|
|
7541
|
+
"User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0"
|
|
7542
|
+
}
|
|
7543
|
+
});
|
|
7544
|
+
if (!res.ok) throw new ConnectHttpError(res.status, path, res.statusText);
|
|
7545
|
+
}
|
|
7546
|
+
async function provisionReleaseLocks(o, locksModelId, route, repoNames) {
|
|
7547
|
+
const scopes = [...new Set(repoNames)].sort().map((r) => releaseLockScope(route, r));
|
|
7548
|
+
if (scopes.length === 0) return [];
|
|
7549
|
+
const client2 = {
|
|
7550
|
+
// The Locks table is a handful of rows, so read it whole and match in
|
|
7551
|
+
// memory rather than depend on the HTTP filter syntax.
|
|
7552
|
+
list: async (model, params) => {
|
|
7553
|
+
const want = JSON.parse(params?.filters ?? "[]")[0]?.value;
|
|
7554
|
+
const rows = await get(
|
|
7555
|
+
o,
|
|
7556
|
+
`/data-models/${model}/records?limit=500`
|
|
7557
|
+
);
|
|
7558
|
+
return rows.filter((r) => r.scope === want).slice(0, params?.limit ?? rows.length);
|
|
7559
|
+
},
|
|
7560
|
+
create: (model, body) => post2(o, `/data-models/${model}/records`, body),
|
|
7561
|
+
remove: (model, id) => del(o, `/data-models/${model}/records/${id}`)
|
|
7562
|
+
};
|
|
7563
|
+
return ensureLockRows(client2, locksModelId, scopes);
|
|
7564
|
+
}
|
|
7437
7565
|
var ISSUES_TEMPLATE_IDENTIFIER = "crew.issues";
|
|
7438
7566
|
async function listLibraryTemplates(o) {
|
|
7439
7567
|
return get(o, "/library-templates");
|
|
@@ -8304,11 +8432,11 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
8304
8432
|
// src/stream-sweep.ts
|
|
8305
8433
|
import { existsSync as existsSync15, readdirSync as readdirSync3, rmSync as rmSync3, statSync as statSync6 } from "fs";
|
|
8306
8434
|
import { join as join14 } from "path";
|
|
8307
|
-
var
|
|
8435
|
+
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
8308
8436
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
8309
8437
|
const dir = join14(stateDir, "streams");
|
|
8310
8438
|
if (!existsSync15(dir)) return [];
|
|
8311
|
-
const cutoff = now - retentionDays *
|
|
8439
|
+
const cutoff = now - retentionDays * DAY_MS2;
|
|
8312
8440
|
return readdirSync3(dir).map((name) => join14(dir, name)).filter((path) => {
|
|
8313
8441
|
try {
|
|
8314
8442
|
return statSync6(path).mtimeMs < cutoff;
|
|
@@ -9996,9 +10124,20 @@ ${changed2.map((c) => ` - ${c}
|
|
|
9996
10124
|
shipName: cfg.ship.name
|
|
9997
10125
|
});
|
|
9998
10126
|
}
|
|
10127
|
+
async function releaserLine(tracker, routeName, repo, release) {
|
|
10128
|
+
const who2 = release.ship ? `released by ${release.ship}` : "any ship may release";
|
|
10129
|
+
let lock2;
|
|
10130
|
+
try {
|
|
10131
|
+
const st = await tracker.lockState(releaseLockScope(routeName, repo));
|
|
10132
|
+
lock2 = st === void 0 ? "no Locks table" : !st.exists ? "lock row MISSING (run crew connect)" : st.holder ? `lock held by ${st.holder}` : "lock row free";
|
|
10133
|
+
} catch (e) {
|
|
10134
|
+
lock2 = `lock unreadable (${e.message})`;
|
|
10135
|
+
}
|
|
10136
|
+
return `${who2}; ${lock2}`;
|
|
10137
|
+
}
|
|
9999
10138
|
const unplaceableReported = /* @__PURE__ */ new Set();
|
|
10000
10139
|
async function releasePhase(c, target, opts = {}) {
|
|
10001
|
-
const scope =
|
|
10140
|
+
const scope = releaseLockScope(c.route, target.name);
|
|
10002
10141
|
const remit = emit.forRoute(c.route);
|
|
10003
10142
|
const relLock = dryRun ? void 0 : state.acquire(`release-${scope}`);
|
|
10004
10143
|
if (relLock && !relLock.ok) {
|
|
@@ -10023,11 +10162,22 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10023
10162
|
remit.emit(`${scope}: skipping \u2014 ${detail}`, { step: "release" });
|
|
10024
10163
|
return { scope, tests: "skipped", outcome: "skipped", detail };
|
|
10025
10164
|
}
|
|
10165
|
+
const who2 = releaserFor(repo.release, cfg.ship.name);
|
|
10166
|
+
if (!who2.releases) {
|
|
10167
|
+
const note = nonReleaserNote(target.name, who2.releaser);
|
|
10168
|
+
remit.emit(note, { step: "release" });
|
|
10169
|
+
return { scope, tests: "skipped", outcome: "skipped", detail: note };
|
|
10170
|
+
}
|
|
10026
10171
|
if (!dryRun && !c.enabled) return { scope, tests: "skipped", outcome: "skipped", detail: "route not enabled" };
|
|
10027
10172
|
const tracker = new Tracker(c, cfg.ship);
|
|
10028
10173
|
if (!dryRun) {
|
|
10029
10174
|
const holderLabel = `${cfg.ship.name}:${process.pid}`;
|
|
10030
10175
|
const got = await tracker.acquireBoardLock(scope, holderLabel, RELEASE_LOCK_TTL_MS);
|
|
10176
|
+
if (!got.ok && got.reason === "missing") {
|
|
10177
|
+
const detail = `no Locks row for ${scope} \u2014 run \`crew connect ${c.route}\` to provision it`;
|
|
10178
|
+
remit.warn(`release for ${scope} refused: ${detail}`, { step: "release" });
|
|
10179
|
+
return { scope, tests: "skipped", outcome: "skipped", detail };
|
|
10180
|
+
}
|
|
10031
10181
|
if (!got.ok) {
|
|
10032
10182
|
const why = got.reason === "held" ? `held by ${got.heldBy ?? "another ship"}` : "claimed by another ship mid-check";
|
|
10033
10183
|
remit.emit(`release for ${scope} is ${why} on the board \u2014 skipping`, { step: "release" });
|
|
@@ -11257,6 +11407,18 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
11257
11407
|
}
|
|
11258
11408
|
const route2 = `${found.workspaceSlug}/${found.projectSlug ?? found.projectId}`;
|
|
11259
11409
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, route2);
|
|
11410
|
+
if (found.locksModelId && !dryRun) {
|
|
11411
|
+
const configured = cfg.routes.find((r) => r.route === route2);
|
|
11412
|
+
const repoNames = [...Object.values(found.repoNames ?? {}), ...configured ? reposOf(configured).map((t) => t.name) : []].filter((n) => !!n);
|
|
11413
|
+
try {
|
|
11414
|
+
const made = await provisionReleaseLocks(authOpts, found.locksModelId, route2, repoNames);
|
|
11415
|
+
for (const sc of made) process.stderr.write(`created release lock row ${sc}
|
|
11416
|
+
`);
|
|
11417
|
+
} catch (e) {
|
|
11418
|
+
process.stderr.write(`(could not provision release lock rows \u2014 ${e.message})
|
|
11419
|
+
`);
|
|
11420
|
+
}
|
|
11421
|
+
}
|
|
11260
11422
|
let operator = found.operator;
|
|
11261
11423
|
if (!operator && found.holds.length > 0 && process.stdin.isTTY && process.stdout.isTTY) {
|
|
11262
11424
|
operator = await pickOperator(found.holds);
|
|
@@ -11928,6 +12090,8 @@ watermark: ${state.watermark()}
|
|
|
11928
12090
|
for (const r of await resolvedRepos(route)) {
|
|
11929
12091
|
const bh = baseHealth(r);
|
|
11930
12092
|
if (bh.unsafe) process.stdout.write(`base: ${r.name} \u2014 UNSAFE, no worktree cuts or releases: ${bh.detail}
|
|
12093
|
+
`);
|
|
12094
|
+
process.stdout.write(`releaser: ${r.name} \u2014 ${await releaserLine(statusTracker, route.route, r.name, r.config.release)}
|
|
11931
12095
|
`);
|
|
11932
12096
|
}
|
|
11933
12097
|
if (waiting) {
|
|
@@ -11988,6 +12152,14 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
11988
12152
|
`);
|
|
11989
12153
|
if (bh.unsafe) process.exitCode = 1;
|
|
11990
12154
|
}
|
|
12155
|
+
if (config.release.ship) {
|
|
12156
|
+
process.stdout.write(` release.ship ${config.release.ship}${config.release.ship === cfg.ship.name ? " (this ship)" : ""}
|
|
12157
|
+
`);
|
|
12158
|
+
} else {
|
|
12159
|
+
const warn = missingReleaserWarning(r.name, config.release, await tracker.shipRows().catch(() => []));
|
|
12160
|
+
if (warn) process.stdout.write(` WARNING ${warn}
|
|
12161
|
+
`);
|
|
12162
|
+
}
|
|
11991
12163
|
}
|
|
11992
12164
|
const missingAuthorship = await tracker.missingAuthorshipColumns();
|
|
11993
12165
|
process.stdout.write(
|
package/package.json
CHANGED