@spardutti/claude-skills 2.60.3 → 2.60.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.
- package/hooks/ship-gate-hook.sh +15 -3
- package/hooks/ship-log.mjs +25 -4
- package/package.json +1 -1
package/hooks/ship-gate-hook.sh
CHANGED
|
@@ -80,11 +80,18 @@ git rev-parse --git-dir >/dev/null 2>&1 || exit 0
|
|
|
80
80
|
pr_branch() {
|
|
81
81
|
case "$CMD" in
|
|
82
82
|
*"gh pr create"*) printf '%s\n' "$CMD" | sed -nE 's/.*(--head[ =]|-H )"?([^ ;&|"]+).*/\2/p' ;;
|
|
83
|
-
*"gh pr merge"*)
|
|
83
|
+
*"gh pr merge"*) merge_field headRefName ;;
|
|
84
84
|
esac
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
pr_base() {
|
|
88
|
+
case "$CMD" in
|
|
89
|
+
*"gh pr create"*) printf '%s\n' "$CMD" | sed -nE 's/.*(--base[ =]|-B )"?([^ ;&|"]+).*/\2/p' ;;
|
|
90
|
+
*"gh pr merge"*) merge_field baseRefName ;;
|
|
91
|
+
esac
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
merge_field() { # merge_field <gh pr field>
|
|
88
95
|
local pr="" repo="" next=""
|
|
89
96
|
for t in $(printf '%s\n' "$CMD" | sed 's/.*gh pr merge//; s/[;&|].*//'); do
|
|
90
97
|
if [ "$next" = repo ]; then repo=$t; next=""; continue; fi
|
|
@@ -96,7 +103,7 @@ merge_branch() {
|
|
|
96
103
|
esac
|
|
97
104
|
done
|
|
98
105
|
[ -n "$pr" ] || return 0
|
|
99
|
-
gh pr view "$pr" ${repo:+-R "$repo"} --json
|
|
106
|
+
gh pr view "$pr" ${repo:+-R "$repo"} --json "$1" -q ".$1" 2>/dev/null
|
|
100
107
|
}
|
|
101
108
|
|
|
102
109
|
worktree_of() {
|
|
@@ -111,6 +118,11 @@ case "$CMD" in *"gh pr create"*)
|
|
|
111
118
|
CWD_TOP=$(git -C "${CWD:-.}" rev-parse --show-toplevel 2>/dev/null) && [ -n "$CWD_TOP" ] && TARGET="$CWD_TOP" ;;
|
|
112
119
|
esac
|
|
113
120
|
BRANCH=$(pr_branch)
|
|
121
|
+
# A release PR carries only work each feature PR already gated, and the gate on the
|
|
122
|
+
# trunk measures it against itself, so a receipt there proves nothing.
|
|
123
|
+
case "$BRANCH" in dev|develop|development)
|
|
124
|
+
case "$(pr_base)" in main|master) exit 0 ;; esac ;;
|
|
125
|
+
esac
|
|
114
126
|
[ -n "$BRANCH" ] && WT=$(worktree_of "$BRANCH") && [ -n "$WT" ] && TARGET="$WT"
|
|
115
127
|
|
|
116
128
|
# Ask the gate for the key rather than recomputing it here. Two implementations
|
package/hooks/ship-log.mjs
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { basename, dirname, join, resolve } from "node:path";
|
|
5
5
|
import { git, parseReport, prSize } from "./ship-log-parse.mjs";
|
|
6
6
|
import { printReport, tally } from "./ship-log-report.mjs";
|
|
7
7
|
|
|
8
8
|
const HERE = dirname(resolve(process.argv[1]));
|
|
9
9
|
const VERDICTS = ["confirmed", "refuted", "unconfirmed", "skipped"];
|
|
10
|
+
// A release ships only work its feature PRs already logged.
|
|
11
|
+
const RELEASE = /^(main|master|dev|develop|development|chore\/release-.*)$/;
|
|
10
12
|
const now = () => new Date().toISOString().replace(/\.\d+Z$/, "Z");
|
|
11
13
|
|
|
12
14
|
// gate-config.sh owns the config files and their order, so the gate and this agree on "on".
|
|
@@ -45,7 +47,19 @@ function summarize(record) {
|
|
|
45
47
|
return { minutes: Math.round(minutes * 10) / 10, gateRuns: record.gate.length, gateSeconds: record.gate.reduce((n, r) => n + (r.seconds ?? 0), 0), findings: by };
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
// Survivors fixed but never logged: a FAIL that a later PASS cleared, with no mutation finding since.
|
|
51
|
+
function settleSurvivors(record) {
|
|
52
|
+
const i = record.gate.findLastIndex((g) => g.verdict === "FAIL" && g.projects?.some((p) => p.survivors?.length));
|
|
53
|
+
if (i < 0 || !record.gate.slice(i + 1).some((g) => g.verdict === "PASS")) return;
|
|
54
|
+
if (record.findings.some((f) => f.by === "mutation" && f.ts >= record.gate[i].ts)) return;
|
|
55
|
+
for (const s of record.gate[i].projects.flatMap((p) => p.survivors ?? [])) {
|
|
56
|
+
record.findings.push({ ts: now(), by: "mutation", what: `${s.mutator ?? s.kind} at ${s.at}`, file: s.at.split(":")[0],
|
|
57
|
+
verdict: "confirmed", fixed: true, note: "logged by ship-log: a later gate run passed" });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
48
61
|
function finalize(dir, path, record, tag, status, reason, at) {
|
|
62
|
+
settleSurvivors(record);
|
|
49
63
|
Object.assign(record, { status, reason: reason ?? null, finished: at ?? now() });
|
|
50
64
|
record.summary = summarize(record);
|
|
51
65
|
const stem = `${record.app}_${record.finished.slice(0, 10)}_${tag}`;
|
|
@@ -57,7 +71,7 @@ function finalize(dir, path, record, tag, status, reason, at) {
|
|
|
57
71
|
}
|
|
58
72
|
|
|
59
73
|
function prOf(branch) {
|
|
60
|
-
const r = spawnSync("gh", ["pr", "list", "--head", branch, "--state", "all", "--json", "number,createdAt,state", "--limit", "1"], { encoding: "utf8" });
|
|
74
|
+
const r = spawnSync("gh", ["pr", "list", "--head", branch, "--state", "all", "--json", "number,createdAt,closedAt,state", "--limit", "1"], { encoding: "utf8" });
|
|
61
75
|
try { return r.status === 0 ? JSON.parse(r.stdout)[0] : undefined; } catch { return undefined; }
|
|
62
76
|
}
|
|
63
77
|
|
|
@@ -68,8 +82,10 @@ function closeOpened(dir, app) {
|
|
|
68
82
|
const open = join(dir, "open");
|
|
69
83
|
for (const f of existsSync(open) ? readdirSync(open).filter((n) => n.startsWith(`${app}__`)) : []) {
|
|
70
84
|
const path = join(open, f), record = JSON.parse(readFileSync(path, "utf8"));
|
|
71
|
-
|
|
72
|
-
|
|
85
|
+
const pr = prOf(record.branch), empty = !record.gate.length && !record.findings.length;
|
|
86
|
+
// A /ship that only merged fills nothing, so once its PR closes there is nothing to keep.
|
|
87
|
+
if (empty && pr?.closedAt >= record.started) { rmSync(path); continue; }
|
|
88
|
+
if (empty) continue;
|
|
73
89
|
const opened = pr && pr.createdAt >= record.started;
|
|
74
90
|
if (!pr || (!opened && pr.state !== "OPEN")) continue;
|
|
75
91
|
finalize(dir, path, Object.assign(record, { pr: pr.number }), `pr${pr.number}`,
|
|
@@ -144,6 +160,11 @@ function run(mode, args) {
|
|
|
144
160
|
if (mode === "on") return c.on ? 0 : 1;
|
|
145
161
|
if (!c.on) return 0;
|
|
146
162
|
const id = identity(c.root), path = openPath(c.dir, id), record = load(path, id);
|
|
163
|
+
if (mode === "start" && RELEASE.test(id.branch)) {
|
|
164
|
+
closeOpened(c.dir, id.app);
|
|
165
|
+
if (existsSync(path)) rmSync(path);
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
147
168
|
if (mode === "start") return restart(c.dir, path, record, id), 0;
|
|
148
169
|
if (mode === "ready") {
|
|
149
170
|
const todo = existsSync(path) ? missing(record, c) : [];
|