@tablation/crew 0.18.0 → 0.19.1
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 +143 -11
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2575,6 +2575,9 @@ function detectSystemd() {
|
|
|
2575
2575
|
return !res.error;
|
|
2576
2576
|
}
|
|
2577
2577
|
|
|
2578
|
+
// src/ship-attention.ts
|
|
2579
|
+
import { statfsSync } from "fs";
|
|
2580
|
+
|
|
2578
2581
|
// src/git.ts
|
|
2579
2582
|
import { execFileSync } from "child_process";
|
|
2580
2583
|
import {
|
|
@@ -2732,7 +2735,20 @@ function patchIdOf(cwd, from, to) {
|
|
|
2732
2735
|
return null;
|
|
2733
2736
|
}
|
|
2734
2737
|
}
|
|
2738
|
+
var BRANCH_TIP_TRAILER = "Branch-tip";
|
|
2739
|
+
function findSquashByBranchTip(cwd, key, branch, range) {
|
|
2740
|
+
const tip = gitOk(cwd, ["rev-parse", "--verify", "-q", `${branch}^{commit}`]);
|
|
2741
|
+
if (!tip) return null;
|
|
2742
|
+
const wanted = `${BRANCH_TIP_TRAILER}: ${tip}`;
|
|
2743
|
+
for (const sha of keyCommitsInRange(cwd, key, range)) {
|
|
2744
|
+
const body = gitOk(cwd, ["show", "-s", "--format=%B", sha]) ?? "";
|
|
2745
|
+
if (body.split("\n").some((l) => l.trim() === wanted)) return sha;
|
|
2746
|
+
}
|
|
2747
|
+
return null;
|
|
2748
|
+
}
|
|
2735
2749
|
function findSquashOfBranch(cwd, key, branch, base, range) {
|
|
2750
|
+
const byTip = findSquashByBranchTip(cwd, key, branch, range);
|
|
2751
|
+
if (byTip) return byTip;
|
|
2736
2752
|
const candidates = keyCommitsInRange(cwd, key, range);
|
|
2737
2753
|
if (candidates.length === 0) return null;
|
|
2738
2754
|
const mb = gitOk(cwd, ["merge-base", base, branch]);
|
|
@@ -3049,14 +3065,94 @@ function baseAttention(repo, outcome, remote, base, nowIso = (/* @__PURE__ */ ne
|
|
|
3049
3065
|
if (outcome.action === "level" || outcome.action === "fast-forwarded") return { clear: baseUnsafeKey(repo) };
|
|
3050
3066
|
return null;
|
|
3051
3067
|
}
|
|
3068
|
+
var diskLowKey = (repo) => `disk_low:${repo}`;
|
|
3069
|
+
var LOW_DISK_BYTES = 2 * 1024 ** 3;
|
|
3070
|
+
var gib = (bytes) => (bytes / 1024 ** 3).toFixed(1);
|
|
3071
|
+
function diskAttention(repo, freeBytes, thresholdBytes = LOW_DISK_BYTES, nowIso = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
3072
|
+
if (freeBytes >= thresholdBytes) return { clear: diskLowKey(repo) };
|
|
3073
|
+
return {
|
|
3074
|
+
raise: {
|
|
3075
|
+
kind: "disk_low",
|
|
3076
|
+
key: diskLowKey(repo),
|
|
3077
|
+
since: nowIso,
|
|
3078
|
+
message: `${repo}: only ${gib(freeBytes)} GiB free beside the checkout (need ${gib(thresholdBytes)}) \u2014 no worktree can be set up until space is freed. Resumes automatically once there is room`
|
|
3079
|
+
}
|
|
3080
|
+
};
|
|
3081
|
+
}
|
|
3082
|
+
function freeBytesAt(dir) {
|
|
3083
|
+
try {
|
|
3084
|
+
const s = statfsSync(dir);
|
|
3085
|
+
return Number(s.bavail) * Number(s.bsize);
|
|
3086
|
+
} catch {
|
|
3087
|
+
return null;
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
async function checkRepoDisks(o, freeAt = freeBytesAt, thresholdBytes = LOW_DISK_BYTES) {
|
|
3091
|
+
for (const target of reposOf(o.route)) {
|
|
3092
|
+
const free = freeAt(target.dir);
|
|
3093
|
+
if (free === null) continue;
|
|
3094
|
+
const att = diskAttention(target.name, free, thresholdBytes);
|
|
3095
|
+
if ("raise" in att) await raiseShipAttention(o, att.raise);
|
|
3096
|
+
else clearShipAttention(o.state, att.clear);
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
function parseHookMissing(key, routeName) {
|
|
3100
|
+
const prefix = `hook_missing:${routeName}/`;
|
|
3101
|
+
if (!key.startsWith(prefix)) return null;
|
|
3102
|
+
const rest = key.slice(prefix.length);
|
|
3103
|
+
const slash = rest.lastIndexOf("/");
|
|
3104
|
+
if (slash <= 0) return null;
|
|
3105
|
+
return { repo: rest.slice(0, slash), hook: rest.slice(slash + 1) };
|
|
3106
|
+
}
|
|
3052
3107
|
function repoStopFor(items, route) {
|
|
3053
|
-
const stops =
|
|
3108
|
+
const stops = /* @__PURE__ */ new Map();
|
|
3109
|
+
for (const i of items) {
|
|
3110
|
+
let repo = null;
|
|
3111
|
+
if (i.kind === "base_unsafe") repo = i.key.slice("base_unsafe:".length);
|
|
3112
|
+
else if (i.kind === "disk_low") repo = i.key.slice("disk_low:".length);
|
|
3113
|
+
else if (i.kind === "hook_missing") {
|
|
3114
|
+
const h = parseHookMissing(i.key, route.route);
|
|
3115
|
+
if (h?.hook === "setup") repo = h.repo;
|
|
3116
|
+
}
|
|
3117
|
+
if (repo && !stops.has(repo)) stops.set(repo, i.message);
|
|
3118
|
+
}
|
|
3054
3119
|
if (stops.size === 0) return () => null;
|
|
3055
3120
|
return (t) => {
|
|
3056
3121
|
const name = repoTargetFor(route, t.repo_id)?.name;
|
|
3057
|
-
return name ? stops.get(
|
|
3122
|
+
return name ? stops.get(name) ?? null : null;
|
|
3058
3123
|
};
|
|
3059
3124
|
}
|
|
3125
|
+
function recheckEnvironmentStops(state, route, ship, deps = {}) {
|
|
3126
|
+
const onPath = deps.onPath ?? findOnPath;
|
|
3127
|
+
const dockerProblem = deps.dockerProblem ?? dockerPathProblem;
|
|
3128
|
+
const cleared = [];
|
|
3129
|
+
for (const item of state.shipAttention().previous()) {
|
|
3130
|
+
if (item.kind === "docker_missing") {
|
|
3131
|
+
if (!dockerProblem(ship)) {
|
|
3132
|
+
clearShipAttention(state, item.key);
|
|
3133
|
+
cleared.push("docker");
|
|
3134
|
+
}
|
|
3135
|
+
continue;
|
|
3136
|
+
}
|
|
3137
|
+
if (item.kind !== "hook_missing") continue;
|
|
3138
|
+
const h = parseHookMissing(item.key, route.route);
|
|
3139
|
+
const target = h && reposOf(route).find((t) => t.name === h.repo);
|
|
3140
|
+
if (!h || !target) continue;
|
|
3141
|
+
let cfg;
|
|
3142
|
+
try {
|
|
3143
|
+
cfg = resolveRepoConfig(loadRepoConfig(target.dir), void 0, target.dir);
|
|
3144
|
+
} catch {
|
|
3145
|
+
continue;
|
|
3146
|
+
}
|
|
3147
|
+
const script = cfg.hooks[h.hook];
|
|
3148
|
+
const cmd = script ? hookCommand(script) : null;
|
|
3149
|
+
if (!cmd || onPath(cmd, pathFor(ship))) {
|
|
3150
|
+
clearShipAttention(state, item.key);
|
|
3151
|
+
cleared.push(`${h.repo}/${h.hook}`);
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
return cleared;
|
|
3155
|
+
}
|
|
3060
3156
|
function recheckBaseStops(state, route) {
|
|
3061
3157
|
const open = state.shipAttention().previous().filter((i) => i.kind === "base_unsafe");
|
|
3062
3158
|
const cleared = [];
|
|
@@ -4695,6 +4791,10 @@ async function decideCycle(o) {
|
|
|
4695
4791
|
for (const repo of recheckBaseStops(state, route)) {
|
|
4696
4792
|
emit.emit(`${repo}: primary checkout is level again \u2014 building lanes resume for that repo`, { step: "select" });
|
|
4697
4793
|
}
|
|
4794
|
+
for (const what of recheckEnvironmentStops(state, route, ship)) {
|
|
4795
|
+
emit.emit(`${what}: no longer missing from the scheduler PATH \u2014 stop cleared`, { step: "select" });
|
|
4796
|
+
}
|
|
4797
|
+
await checkRepoDisks({ state, emit, route, ship });
|
|
4698
4798
|
const selectionInput = {
|
|
4699
4799
|
repoStop: repoStopFor(openShipAttention(state), route),
|
|
4700
4800
|
tickets,
|
|
@@ -6391,14 +6491,9 @@ function planMerge(cwd, tickets, contract, lastReleased, branchFor = (t) => bran
|
|
|
6391
6491
|
skipReason: "never-built"
|
|
6392
6492
|
};
|
|
6393
6493
|
}
|
|
6394
|
-
const squash = findSquashOfBranch(
|
|
6395
|
-
cwd,
|
|
6396
|
-
referenceKeys(ticket),
|
|
6397
|
-
branch,
|
|
6398
|
-
base,
|
|
6399
|
-
lastReleased ? `${lastReleased}..${base}` : base
|
|
6400
|
-
);
|
|
6494
|
+
const squash = findSquashOfBranch(cwd, referenceKeys(ticket), branch, base, base);
|
|
6401
6495
|
if (squash) {
|
|
6496
|
+
const released = !!lastReleased && isAncestor(cwd, squash, lastReleased);
|
|
6402
6497
|
return {
|
|
6403
6498
|
ticket,
|
|
6404
6499
|
branch,
|
|
@@ -6406,7 +6501,7 @@ function planMerge(cwd, tickets, contract, lastReleased, branchFor = (t) => bran
|
|
|
6406
6501
|
usedFallback: false,
|
|
6407
6502
|
bump: "patch",
|
|
6408
6503
|
majorRequested: false,
|
|
6409
|
-
skipReason: "already-merged",
|
|
6504
|
+
skipReason: released ? "already-released" : "already-merged",
|
|
6410
6505
|
mergedSha: squash
|
|
6411
6506
|
};
|
|
6412
6507
|
}
|
|
@@ -6424,6 +6519,20 @@ function planMerge(cwd, tickets, contract, lastReleased, branchFor = (t) => bran
|
|
|
6424
6519
|
};
|
|
6425
6520
|
});
|
|
6426
6521
|
}
|
|
6522
|
+
function planReleasedBranches(cwd, tickets, contract, branchFor, base = "main", tagPattern = "v*") {
|
|
6523
|
+
const eligible = /* @__PURE__ */ new Set([contract.statuses.building, contract.statuses.needsHuman]);
|
|
6524
|
+
const out = [];
|
|
6525
|
+
for (const ticket of tickets) {
|
|
6526
|
+
if (!eligible.has(ticket.status)) continue;
|
|
6527
|
+
const branch = branchFor(ticket);
|
|
6528
|
+
if (!branch) continue;
|
|
6529
|
+
const sha = findSquashByBranchTip(cwd, referenceKeys(ticket), branch, base);
|
|
6530
|
+
if (!sha) continue;
|
|
6531
|
+
const tag = firstReleaseTagContaining(cwd, sha, tagPattern);
|
|
6532
|
+
if (tag) out.push({ ticket, sha, tag });
|
|
6533
|
+
}
|
|
6534
|
+
return out;
|
|
6535
|
+
}
|
|
6427
6536
|
function requestedBump(bodies) {
|
|
6428
6537
|
const asks = [...bodies.matchAll(BUMP_LINE)].map((m) => m[1].toLowerCase());
|
|
6429
6538
|
const majorRequested = asks.includes("major");
|
|
@@ -6886,9 +6995,13 @@ function mergeOne(o, c) {
|
|
|
6886
6995
|
);
|
|
6887
6996
|
return "noop";
|
|
6888
6997
|
}
|
|
6998
|
+
const tip = gitOk(o.cwd, ["rev-parse", "--verify", "-q", `${c.branch}^{commit}`]);
|
|
6999
|
+
const trailer = tip ? `
|
|
7000
|
+
|
|
7001
|
+
${BRANCH_TIP_TRAILER}: ${tip}` : "";
|
|
6889
7002
|
git(o.cwd, ["commit", "-m", `${subject}
|
|
6890
7003
|
|
|
6891
|
-
Closes ${c.ticket.issue_id}
|
|
7004
|
+
Closes ${c.ticket.issue_id}.${trailer}`]);
|
|
6892
7005
|
const sha = headSha(o.cwd);
|
|
6893
7006
|
o.emit.emit(`merged ${c.branch}`, { ticket: c.ticket.issue_id });
|
|
6894
7007
|
return { applied: true, sha };
|
|
@@ -10903,6 +11016,25 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10903
11016
|
target.dir
|
|
10904
11017
|
);
|
|
10905
11018
|
}
|
|
11019
|
+
for (const r of planReleasedBranches(
|
|
11020
|
+
target.dir,
|
|
11021
|
+
tickets,
|
|
11022
|
+
tracker.contract,
|
|
11023
|
+
(t) => locateBranchForTicket(target.dir, repo, t)?.branch ?? null,
|
|
11024
|
+
repo.branch.base,
|
|
11025
|
+
repo.release.tagPattern ?? "v*"
|
|
11026
|
+
)) {
|
|
11027
|
+
remit.enter("reconcile");
|
|
11028
|
+
await applyStamp(
|
|
11029
|
+
tracker,
|
|
11030
|
+
[{ ticket: r.ticket, reason: `branch already released in ${r.tag}`, sha: r.sha }],
|
|
11031
|
+
r.tag.replace(/^v/, ""),
|
|
11032
|
+
tracker.contract,
|
|
11033
|
+
remit,
|
|
11034
|
+
dryRun,
|
|
11035
|
+
target.dir
|
|
11036
|
+
);
|
|
11037
|
+
}
|
|
10906
11038
|
if (outcome.gateRed && !dryRun) {
|
|
10907
11039
|
try {
|
|
10908
11040
|
const g = outcome.gateRed;
|
package/package.json
CHANGED