@tablation/crew 0.79.0 → 0.79.2
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 +89 -34
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -11388,14 +11388,14 @@ function dedupeChangelogEntries(entries, ticket) {
|
|
|
11388
11388
|
return out;
|
|
11389
11389
|
}
|
|
11390
11390
|
var BUMP_LINE = /^Bump:[ \t]*(major|minor|patch)\b/gim;
|
|
11391
|
-
function planMerge(cwd, tickets, contract, lastReleased, branchFor = (t) => branchForIssue(cwd, t.issue_id), base = "main", blockerInfo) {
|
|
11391
|
+
function planMerge(cwd, tickets, contract, lastReleased, branchFor = (t) => branchForIssue(cwd, t.issue_id), base = "main", blockerInfo, ref = "HEAD") {
|
|
11392
11392
|
const held = new Set(heldByBlockers(tickets, contract.statuses.verified, blockerInfo).map((h) => h.ticket.id));
|
|
11393
11393
|
const verified = tickets.filter((t) => t.status === contract.statuses.verified && !held.has(t.id)).sort((a, b) => Number.parseInt(a.issue_id.replace(/^\D+/, ""), 10) - Number.parseInt(b.issue_id.replace(/^\D+/, ""), 10));
|
|
11394
11394
|
return verified.map((ticket) => {
|
|
11395
11395
|
const branch = branchFor(ticket);
|
|
11396
11396
|
if (!branch) {
|
|
11397
11397
|
const keys = referenceKeys(ticket);
|
|
11398
|
-
const unreleased = lastReleased ? findKeyInRange(cwd, keys, `${lastReleased}
|
|
11398
|
+
const unreleased = lastReleased ? findKeyInRange(cwd, keys, `${lastReleased}..${ref}`) : null;
|
|
11399
11399
|
if (unreleased) {
|
|
11400
11400
|
return {
|
|
11401
11401
|
ticket,
|
|
@@ -11407,7 +11407,7 @@ function planMerge(cwd, tickets, contract, lastReleased, branchFor = (t) => bran
|
|
|
11407
11407
|
skipReason: "already-merged"
|
|
11408
11408
|
};
|
|
11409
11409
|
}
|
|
11410
|
-
const anywhere = findKeyInRange(cwd, keys,
|
|
11410
|
+
const anywhere = findKeyInRange(cwd, keys, ref);
|
|
11411
11411
|
if (anywhere) {
|
|
11412
11412
|
return {
|
|
11413
11413
|
ticket,
|
|
@@ -11504,21 +11504,22 @@ ${section}`;
|
|
|
11504
11504
|
return [...lines.slice(0, anchor), section, ...lines.slice(anchor)].join("\n");
|
|
11505
11505
|
}
|
|
11506
11506
|
var RELEASE_COMMIT_SUBJECT = /^Release \S+$/;
|
|
11507
|
-
function subjectsSince(cwd, sha) {
|
|
11508
|
-
if (!isAncestor(cwd, sha,
|
|
11509
|
-
return (gitOk(cwd, ["log", "--format=%h %s", `${sha}
|
|
11507
|
+
function subjectsSince(cwd, sha, ref = "HEAD") {
|
|
11508
|
+
if (!isAncestor(cwd, sha, ref)) return null;
|
|
11509
|
+
return (gitOk(cwd, ["log", "--format=%h %s", `${sha}..${ref}`]) ?? "").split("\n").filter(Boolean);
|
|
11510
11510
|
}
|
|
11511
11511
|
var isReleaseCommitLine = (line) => RELEASE_COMMIT_SUBJECT.test(line.replace(/^\S+ /, ""));
|
|
11512
11512
|
function decideRelease(cwd, tickets, contract, opts = {}) {
|
|
11513
11513
|
const pattern = opts.tagPattern ?? "v*";
|
|
11514
11514
|
const base = opts.base ?? "main";
|
|
11515
|
-
const
|
|
11516
|
-
const
|
|
11515
|
+
const ref = opts.ref ?? "HEAD";
|
|
11516
|
+
const block = opts.guard === false ? null : checkGuards(cwd, base);
|
|
11517
|
+
const lastTag = latestReleaseTag(cwd, pattern, ref);
|
|
11517
11518
|
const lastReleased = lastTag ? tagCommit(cwd, lastTag) : null;
|
|
11518
|
-
const head = resolve2(cwd,
|
|
11519
|
-
const merges = block ? [] : planMerge(cwd, tickets, contract, lastReleased, opts.branchFor ?? ((t) => branchForIssue(cwd, t.issue_id)), base, opts.blockerInfo);
|
|
11520
|
-
const unreleasedCommits = lastReleased ? countCommits(cwd, `${lastReleased}
|
|
11521
|
-
const unreleasedWork = lastReleased ? (gitOk(cwd, ["log", "--format=%s", `${lastReleased}
|
|
11519
|
+
const head = resolve2(cwd, ref) ?? "";
|
|
11520
|
+
const merges = block ? [] : planMerge(cwd, tickets, contract, lastReleased, opts.branchFor ?? ((t) => branchForIssue(cwd, t.issue_id)), opts.ref ?? base, opts.blockerInfo, ref);
|
|
11521
|
+
const unreleasedCommits = lastReleased ? countCommits(cwd, `${lastReleased}..${ref}`) : 0;
|
|
11522
|
+
const unreleasedWork = lastReleased ? (gitOk(cwd, ["log", "--format=%s", `${lastReleased}..${ref}`]) ?? "").split("\n").filter((l) => l && !RELEASE_COMMIT_SUBJECT.test(l)).length : 0;
|
|
11522
11523
|
return {
|
|
11523
11524
|
block,
|
|
11524
11525
|
merges,
|
|
@@ -12233,10 +12234,14 @@ async function runReleaseCycle(o, movedPast2, localRefSkips) {
|
|
|
12233
12234
|
}
|
|
12234
12235
|
if (drop.size) releasable = releasable.filter((t) => !drop.has(t.id));
|
|
12235
12236
|
}
|
|
12237
|
+
const { remote, base } = o.repo.branch;
|
|
12238
|
+
const inPlace = o.dryRun || !!o.mergeOnly || !remoteBranchExists(o.cwd, remote, base);
|
|
12239
|
+
const remoteRef = `${remote}/${base}`;
|
|
12236
12240
|
const decision = decideRelease(o.cwd, releasable, o.contract, {
|
|
12237
12241
|
tagPattern,
|
|
12238
|
-
base
|
|
12242
|
+
base,
|
|
12239
12243
|
blockerInfo: o.blockerInfo,
|
|
12244
|
+
...inPlace ? {} : { ref: remoteRef, guard: false },
|
|
12240
12245
|
// CREW-1364: a branch another ship built and pushed at `fixed` exists
|
|
12241
12246
|
// here only on the remote. Materialize it so the squash below has a ref.
|
|
12242
12247
|
// A dry run only looks.
|
|
@@ -12265,7 +12270,7 @@ async function runReleaseCycle(o, movedPast2, localRefSkips) {
|
|
|
12265
12270
|
o.state?.clearBlocked();
|
|
12266
12271
|
const failedAt = o.state?.deployFailedSha();
|
|
12267
12272
|
if (!o.force && failedAt) {
|
|
12268
|
-
const since2 = failedAt === decision.head ? [] : subjectsSince(o.cwd, failedAt);
|
|
12273
|
+
const since2 = failedAt === decision.head ? [] : subjectsSince(o.cwd, failedAt, inPlace ? "HEAD" : remoteRef);
|
|
12269
12274
|
const real = since2?.filter((l) => !isReleaseCommitLine(l)) ?? null;
|
|
12270
12275
|
if (real && real.length === 0) {
|
|
12271
12276
|
o.emit.warn(
|
|
@@ -12292,10 +12297,13 @@ async function runReleaseCycle(o, movedPast2, localRefSkips) {
|
|
|
12292
12297
|
const review = await reviewHandoff(o, externalClosures, decision.merges, reviewing);
|
|
12293
12298
|
return { merged: [], deployed: false, stopped: "external", decision, externalClosures, review };
|
|
12294
12299
|
}
|
|
12295
|
-
|
|
12296
|
-
if (o.dryRun || o.mergeOnly || !remoteBranchExists(o.cwd, remote, base)) {
|
|
12300
|
+
if (inPlace) {
|
|
12297
12301
|
return mergeAndRelease(o, decision, fresh);
|
|
12298
12302
|
}
|
|
12303
|
+
const primary = checkGuards(o.cwd, base);
|
|
12304
|
+
if (primary) {
|
|
12305
|
+
o.emit.emit(scoped(`primary checkout not fast-forwarded: ${primary.detail}; its dist/ is stale until rebuilt`), { step: "release" });
|
|
12306
|
+
}
|
|
12299
12307
|
let checkout;
|
|
12300
12308
|
const stranded = recoverStrandedReleaseCheckouts(o.cwd);
|
|
12301
12309
|
if (stranded.length > 0) o.emit.warn(scoped(`dropped release checkout(s) stranded by a crash: ${stranded.join(", ")}`));
|
|
@@ -13139,10 +13147,19 @@ async function canMintAgentKeys(route, o = {}) {
|
|
|
13139
13147
|
|
|
13140
13148
|
// src/seat-read-probe.ts
|
|
13141
13149
|
var SEAT_NO_READ_REMEDY = "the project has no default role for agent keys \u2014 update the Issues app's template, then re-mint with `crew agents sync --revoke-seat-keys`, or assign the Crew seat role to the agent members";
|
|
13142
|
-
|
|
13143
|
-
const
|
|
13144
|
-
if (!
|
|
13145
|
-
const
|
|
13150
|
+
function probeTables(route) {
|
|
13151
|
+
const m = route.resolved?.models;
|
|
13152
|
+
if (!m) return [];
|
|
13153
|
+
const r = route.resolved;
|
|
13154
|
+
return [
|
|
13155
|
+
{ name: "Issues", modelId: m.issues },
|
|
13156
|
+
{ name: "Comments", modelId: m.comments },
|
|
13157
|
+
{ name: "Crew", modelId: m.crew },
|
|
13158
|
+
{ name: "Ships", modelId: r?.shipsModelId ?? "" }
|
|
13159
|
+
].filter((t) => !!t.modelId);
|
|
13160
|
+
}
|
|
13161
|
+
async function probeTableRead(route, modelId, apiKey, o = {}) {
|
|
13162
|
+
const url = `${route.baseUrl.replace(/\/+$/, "")}/api/data-models/${modelId}/records?limit=1`;
|
|
13146
13163
|
try {
|
|
13147
13164
|
const res = await (o.fetchImpl ?? fetch)(url, {
|
|
13148
13165
|
headers: { Authorization: `Bearer ${apiKey}`, "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
|
|
@@ -13160,12 +13177,46 @@ async function probeSeatReads(route, o = {}) {
|
|
|
13160
13177
|
for (const role of roles) {
|
|
13161
13178
|
const id = role === "release" ? await resolveReleaseKey(route, o.getStore) : await resolveSeatKey(route, role, o.getStore);
|
|
13162
13179
|
if (id.kind !== "agent") continue;
|
|
13163
|
-
|
|
13180
|
+
const tables = [];
|
|
13181
|
+
for (const t of probeTables(route)) tables.push({ table: t.name, ...await probeTableRead(route, t.modelId, id.apiKey, o) });
|
|
13182
|
+
const bad = tables.find((t) => t.state === "forbidden") ?? tables.find((t) => t.state === "unknown");
|
|
13183
|
+
out.push({ role, state: bad?.state ?? (tables.length ? "ok" : "unknown"), detail: bad?.detail ?? (tables.length ? void 0 : "route has no resolved tables"), tables });
|
|
13164
13184
|
}
|
|
13165
13185
|
return out;
|
|
13166
13186
|
}
|
|
13167
|
-
function
|
|
13168
|
-
|
|
13187
|
+
function forbiddenDetail(e, route) {
|
|
13188
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
13189
|
+
const m = /^([A-Z]+) (\S+) -> 403/.exec(msg);
|
|
13190
|
+
const verb = /No (READ|WRITE) access/i.exec(msg)?.[1]?.toUpperCase();
|
|
13191
|
+
const modelId = /\/data-models\/([\w-]+)/i.exec(m?.[2] ?? "")?.[1];
|
|
13192
|
+
const r = route?.resolved;
|
|
13193
|
+
const known = [
|
|
13194
|
+
[r?.models?.issues, "Issues"],
|
|
13195
|
+
[r?.models?.comments, "Comments"],
|
|
13196
|
+
[r?.models?.crew, "Crew"],
|
|
13197
|
+
[r?.shipsModelId, "Ships"],
|
|
13198
|
+
[r?.locksModelId, "Locks"],
|
|
13199
|
+
[r?.epicsModelId, "Epics"],
|
|
13200
|
+
[r?.reposModelId, "Repos"]
|
|
13201
|
+
];
|
|
13202
|
+
const table2 = modelId ? known.find(([id]) => id && id === modelId)?.[1] ?? `table ${modelId}` : void 0;
|
|
13203
|
+
return { method: m?.[1], path: m?.[2], table: table2, verb };
|
|
13204
|
+
}
|
|
13205
|
+
function remedyFor(d, roleName = "the agent role") {
|
|
13206
|
+
if (d.table && d.verb && !(d.table === "Issues" && d.verb === "READ")) {
|
|
13207
|
+
return `grant ${d.verb} on ${d.table} to ${roleName}`;
|
|
13208
|
+
}
|
|
13209
|
+
return SEAT_NO_READ_REMEDY;
|
|
13210
|
+
}
|
|
13211
|
+
function describeSeatForbidden(role, tables = []) {
|
|
13212
|
+
const bad = tables.filter((t) => t.state === "forbidden").map((t) => t.table);
|
|
13213
|
+
const what = bad.length ? bad.join(", ") : "Issues";
|
|
13214
|
+
const remedy = bad.length && !(bad.length === 1 && bad[0] === "Issues") ? bad.map((t) => remedyFor({ table: t, verb: "READ" })).join("; ") : SEAT_NO_READ_REMEDY;
|
|
13215
|
+
return `seat read: ERROR ${role} agent key cannot read ${what} (403) \u2014 ${remedy}`;
|
|
13216
|
+
}
|
|
13217
|
+
function describeSeatTables(role, tables) {
|
|
13218
|
+
const cells = tables.map((t) => `${t.table} ${t.state === "ok" ? "ok" : t.state === "forbidden" ? "403" : `error (${t.detail ?? "?"})`}`);
|
|
13219
|
+
return `seat read: ${role}: ${cells.join(", ")}`;
|
|
13169
13220
|
}
|
|
13170
13221
|
function isForbiddenError(e) {
|
|
13171
13222
|
const status2 = e?.status;
|
|
@@ -13173,16 +13224,18 @@ function isForbiddenError(e) {
|
|
|
13173
13224
|
return /\b403\b/.test(e instanceof Error ? e.message : String(e));
|
|
13174
13225
|
}
|
|
13175
13226
|
var SEAT_FORBIDDEN_KEY = "seat_forbidden:release";
|
|
13176
|
-
function seatForbiddenItem(now = /* @__PURE__ */ new Date()) {
|
|
13227
|
+
function seatForbiddenItem(now = /* @__PURE__ */ new Date(), e, route) {
|
|
13228
|
+
const d = e === void 0 ? {} : forbiddenDetail(e, route);
|
|
13229
|
+
const what = d.method && d.table ? `${d.method} ${d.table}${d.verb ? ` -> 403 No ${d.verb} access` : " -> 403"}` : "cannot read the Issues table (403)";
|
|
13177
13230
|
return {
|
|
13178
13231
|
kind: "seat_forbidden",
|
|
13179
13232
|
key: SEAT_FORBIDDEN_KEY,
|
|
13180
|
-
message: `release seat
|
|
13233
|
+
message: `release seat: ${what} \u2014 ${remedyFor(d)}`,
|
|
13181
13234
|
since: now.toISOString()
|
|
13182
13235
|
};
|
|
13183
13236
|
}
|
|
13184
13237
|
function seatForbiddenRepoLine(scope) {
|
|
13185
|
-
return `${scope}: skipped \u2014 release seat
|
|
13238
|
+
return `${scope}: skipped \u2014 release seat was refused by the tracker (see attention)`;
|
|
13186
13239
|
}
|
|
13187
13240
|
|
|
13188
13241
|
// src/cli.ts
|
|
@@ -17952,9 +18005,11 @@ ${changed2.map((c) => ` - ${c}
|
|
|
17952
18005
|
preModes
|
|
17953
18006
|
);
|
|
17954
18007
|
reportUnplaceable(c.route, preUnplaceable, tracker.contract.statuses.verified, remit);
|
|
18008
|
+
const remoteBase = remoteBranchExists(target.dir, repo.branch.remote, repo.branch.base);
|
|
17955
18009
|
const d = decideRelease(target.dir, pre, tracker.contract, {
|
|
17956
18010
|
tagPattern: repo.release.tagPattern ?? "v*",
|
|
17957
|
-
base: repo.branch.base
|
|
18011
|
+
base: repo.branch.base,
|
|
18012
|
+
...remoteBase ? { ref: `${repo.branch.remote}/${repo.branch.base}`, guard: false } : {}
|
|
17958
18013
|
});
|
|
17959
18014
|
const unstamped = planReleasedBranches(
|
|
17960
18015
|
target.dir,
|
|
@@ -18253,7 +18308,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18253
18308
|
return summary;
|
|
18254
18309
|
} catch (e) {
|
|
18255
18310
|
if (isForbiddenError(e)) {
|
|
18256
|
-
await raiseShipAttention({ state, emit: remit, route: c, ship: cfg.ship }, seatForbiddenItem());
|
|
18311
|
+
await raiseShipAttention({ state, emit: remit, route: c, ship: cfg.ship }, seatForbiddenItem(/* @__PURE__ */ new Date(), e, c));
|
|
18257
18312
|
const line = seatForbiddenRepoLine(scope);
|
|
18258
18313
|
remit.warn(line, { step: "release" });
|
|
18259
18314
|
return { scope, tests: "skipped", outcome: "error", detail: line };
|
|
@@ -19014,7 +19069,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
19014
19069
|
if (!dryRun) {
|
|
19015
19070
|
for (const p of await probeSeatReads(target, { userAgent: cfg.ship.userAgent })) {
|
|
19016
19071
|
if (p.state === "forbidden") {
|
|
19017
|
-
process.stdout.write(`${describeSeatForbidden(p.role)}
|
|
19072
|
+
process.stdout.write(`${describeSeatForbidden(p.role, p.tables)}
|
|
19018
19073
|
`);
|
|
19019
19074
|
process.exitCode = 1;
|
|
19020
19075
|
}
|
|
@@ -19997,7 +20052,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19997
20052
|
for (const o of mintOutcomes) process.stderr.write(`${describeMintOutcome(o)}
|
|
19998
20053
|
`);
|
|
19999
20054
|
for (const p of await probeSeatReads({ ...existingRoute, resolved: { ...existingRoute.resolved, seats: found.seats } }, { userAgent: cfg.ship.userAgent })) {
|
|
20000
|
-
if (p.state === "forbidden") process.stderr.write(`${describeSeatForbidden(p.role)}
|
|
20055
|
+
if (p.state === "forbidden") process.stderr.write(`${describeSeatForbidden(p.role, p.tables)}
|
|
20001
20056
|
`);
|
|
20002
20057
|
}
|
|
20003
20058
|
} catch (e) {
|
|
@@ -20176,7 +20231,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20176
20231
|
for (const m of minted) process.stderr.write(` ${describeMintOutcome(m)}
|
|
20177
20232
|
`);
|
|
20178
20233
|
for (const p of await probeSeatReads(syncRoute, { userAgent: cfg.ship.userAgent })) {
|
|
20179
|
-
if (p.state === "forbidden") process.stderr.write(` ${describeSeatForbidden(p.role)}
|
|
20234
|
+
if (p.state === "forbidden") process.stderr.write(` ${describeSeatForbidden(p.role, p.tables)}
|
|
20180
20235
|
`);
|
|
20181
20236
|
}
|
|
20182
20237
|
if (run2.readOnly) {
|
|
@@ -21303,7 +21358,7 @@ watermark: ${state.watermark()}
|
|
|
21303
21358
|
if (!flag("offline")) {
|
|
21304
21359
|
for (const p of await probeSeatReads(route, { userAgent: cfg.ship.userAgent })) {
|
|
21305
21360
|
if (p.state === "forbidden") {
|
|
21306
|
-
problems.push({ severity: "blocker", kind: "seat_cannot_read_issues", route: route.route, message: `${p.role} agent key
|
|
21361
|
+
problems.push({ severity: "blocker", kind: "seat_cannot_read_issues", route: route.route, message: `${p.role} agent key refused on ${p.tables.filter((t) => t.state === "forbidden").map((t) => t.table).join(", ")} (403)`, remedy: describeSeatForbidden(p.role, p.tables).replace(/^.*?— /, "") });
|
|
21307
21362
|
} else if (p.state === "ok") passed++;
|
|
21308
21363
|
}
|
|
21309
21364
|
}
|
|
@@ -21449,11 +21504,11 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
21449
21504
|
if (!flag("offline")) {
|
|
21450
21505
|
for (const p of await probeSeatReads(roleRoute, { userAgent: cfg.ship.userAgent })) {
|
|
21451
21506
|
if (p.state === "forbidden") {
|
|
21452
|
-
process.stdout.write(`${describeSeatForbidden(p.role)}
|
|
21507
|
+
process.stdout.write(`${describeSeatForbidden(p.role, p.tables)}
|
|
21453
21508
|
`);
|
|
21454
21509
|
process.exitCode = 1;
|
|
21455
21510
|
} else {
|
|
21456
|
-
process.stdout.write(p.state === "ok" ?
|
|
21511
|
+
process.stdout.write(p.state === "ok" ? `${describeSeatTables(p.role, p.tables)}
|
|
21457
21512
|
` : `seat read: ${p.role} could not be checked (${p.detail})
|
|
21458
21513
|
`);
|
|
21459
21514
|
}
|
package/package.json
CHANGED