clud-bug 0.5.6 → 0.5.8
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/README.md +48 -4
- package/bin/clud-bug.js +15 -3
- package/lib/update.js +73 -6
- package/package.json +1 -1
- package/templates/audit.yml.tmpl +1 -0
- package/templates/self-update.yml.tmpl +1 -0
- package/templates/skills/baseline/clud-bug-collaboration.md +49 -0
- package/templates/workflow-py.yml.tmpl +5 -18
- package/templates/workflow-ts.yml.tmpl +5 -18
- package/templates/workflow.yml.tmpl +12 -33
package/README.md
CHANGED
|
@@ -164,20 +164,64 @@ If you want clud-bug to review fork PRs too, you have two options:
|
|
|
164
164
|
1. **Maintainer re-pushes the branch** to your repo as a non-fork branch, and the review runs.
|
|
165
165
|
2. **Switch the trigger to `pull_request_target`** (advanced) — this gives the workflow access to secrets but runs against the *base* ref, not the PR's code. To safely review the PR's actual code, follow [`anthropics/claude-code-action` security.md](https://github.com/anthropics/claude-code-action/blob/main/docs/security.md): check out the PR head into a **subdirectory** (not the workspace root) and pass it via `--add-dir`. Skipping this is a code-execution risk.
|
|
166
166
|
|
|
167
|
-
|
|
167
|
+
Concretely, the safe shape:
|
|
168
|
+
|
|
169
|
+
```yaml
|
|
170
|
+
on:
|
|
171
|
+
pull_request_target:
|
|
172
|
+
types: [opened, synchronize]
|
|
173
|
+
|
|
174
|
+
jobs:
|
|
175
|
+
clud-bug-review:
|
|
176
|
+
steps:
|
|
177
|
+
- uses: actions/checkout@v6 # base ref — trusted
|
|
178
|
+
- uses: actions/checkout@v6 # PR head — UNTRUSTED, into a subdir
|
|
179
|
+
with:
|
|
180
|
+
ref: ${{ github.event.pull_request.head.sha }}
|
|
181
|
+
path: pr-head
|
|
182
|
+
- uses: anthropics/claude-code-action@v1
|
|
183
|
+
with:
|
|
184
|
+
claude_args: --add-dir pr-head
|
|
185
|
+
# ... rest of args
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The key invariant: the base checkout (with secrets in scope) lives at the workspace root; the PR head (untrusted user code) only ever lives in a subdirectory the action explicitly opts into via `--add-dir`. Any deviation — checking out the PR head at the root, running `npm install` from the subdir, etc. — re-opens the code-execution risk.
|
|
189
|
+
|
|
190
|
+
clud-bug's generated workflow uses `pull_request` (not `pull_request_target`) by default. If you understand the trade-offs and want to handle fork PRs, edit the trigger yourself using the shape above.
|
|
168
191
|
|
|
169
192
|
## When you edit the workflow
|
|
170
193
|
|
|
171
|
-
|
|
194
|
+
> **TL;DR:** if you see `App token exchange failed: Workflow validation failed (401)` on a PR that edits a clud-bug workflow file, that's **expected and protective** — not a bug in your PR. Read on.
|
|
195
|
+
|
|
196
|
+
clud-bug uses [`anthropics/claude-code-action`](https://github.com/anthropics/claude-code-action), which **refuses to run when the PR being reviewed modifies the action's own workflow file**. That's a security guard: without it, a PR could neuter the reviewer or exfiltrate secrets via prompt injection in the workflow file itself.
|
|
197
|
+
|
|
198
|
+
### What you'll see
|
|
199
|
+
|
|
200
|
+
When you push a PR that touches `.github/workflows/clud-bug-review.yml` (or any other clud-bug workflow):
|
|
201
|
+
|
|
202
|
+
- The `clud-bug-review` check fails with `App token exchange failed: 401 Unauthorized — Workflow validation failed. The workflow file must exist and have identical content to the version on the repository's default branch.`
|
|
203
|
+
- You'll get a GitHub email titled something like **"[thrillmot/your-repo] Run failed: Clud Bug 🐛 Crawls Your Code — `<branch-name>`"** — same wording for every workflow failure, so it doesn't visually distinguish "this is the expected self-mod guard" from "real failure."
|
|
204
|
+
|
|
205
|
+
### How to merge
|
|
206
|
+
|
|
207
|
+
If the PR contains **only** workflow edits, this is the expected path:
|
|
208
|
+
|
|
209
|
+
1. A maintainer reviews the diff directly (the bot can't).
|
|
210
|
+
2. Merge via admin override (`gh pr merge --admin` or the "Merge without waiting for requirements" button) — the failing `clud-bug-review` check is the bot refusing to review *itself*, not a real defect.
|
|
211
|
+
3. Subsequent PRs on the new workflow work normally — the validation gate compares against `main`, so once your edit is on `main`, the gate passes.
|
|
212
|
+
|
|
213
|
+
If the PR contains workflow edits **mixed with other code changes**, split them. The bot can't review either half while the workflow edit is in the diff, so any real findings get masked.
|
|
214
|
+
|
|
215
|
+
### The helper command
|
|
172
216
|
|
|
173
|
-
|
|
217
|
+
`clud-bug edit-workflow` packages the workflow change into a clean PR for you, refusing to run if your working tree has any non-workflow changes:
|
|
174
218
|
|
|
175
219
|
```bash
|
|
176
220
|
# Edit .github/workflows/clud-bug-*.yml as you like, then:
|
|
177
221
|
clud-bug edit-workflow
|
|
178
222
|
```
|
|
179
223
|
|
|
180
|
-
|
|
224
|
+
This keeps the merge ceremony scoped to just the workflow edit.
|
|
181
225
|
|
|
182
226
|
## Verifying it works
|
|
183
227
|
|
package/bin/clud-bug.js
CHANGED
|
@@ -608,16 +608,28 @@ async function runUpdateCmd(_args) {
|
|
|
608
608
|
return;
|
|
609
609
|
}
|
|
610
610
|
|
|
611
|
-
|
|
611
|
+
const skipped = result.skipped ?? [];
|
|
612
|
+
|
|
613
|
+
if (result.changed.length === 0 && skipped.length === 0) {
|
|
612
614
|
log(' Already current. Nothing to update.');
|
|
613
615
|
return;
|
|
614
616
|
}
|
|
615
617
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
+
if (result.changed.length > 0) {
|
|
619
|
+
log(` ✓ Updated ${result.changed.length} file${result.changed.length === 1 ? '' : 's'}:`);
|
|
620
|
+
for (const c of result.changed) {
|
|
621
|
+
const versionNote = c.from && c.to && c.from !== c.to ? ` (${c.label}, ${c.from} → ${c.to})` : ` (${c.label})`;
|
|
622
|
+
log(` • ${rel(cwd, c.path)}${versionNote}`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
618
625
|
if (result.unchanged.length > 0) {
|
|
619
626
|
log(` ${result.unchanged.length} file${result.unchanged.length === 1 ? ' was' : 's were'} already current.`);
|
|
620
627
|
}
|
|
628
|
+
if (skipped.length > 0) {
|
|
629
|
+
log('');
|
|
630
|
+
log(` ! Skipped ${skipped.length} markerless file${skipped.length === 1 ? '' : 's'} (treated as user-customized):`);
|
|
631
|
+
for (const s of skipped) log(` • ${rel(cwd, s.path)} — ${s.reason}`);
|
|
632
|
+
}
|
|
621
633
|
log('');
|
|
622
634
|
log('Commit + push to apply the refreshed kit on the next PR.');
|
|
623
635
|
}
|
package/lib/update.js
CHANGED
|
@@ -8,14 +8,18 @@ import { applyToRepo as applyAgentDocs } from './agents-md.js';
|
|
|
8
8
|
// Re-render the user's workflow + refresh baseline skills using the
|
|
9
9
|
// templates / baseline shipped with the currently-installed clud-bug.
|
|
10
10
|
//
|
|
11
|
-
// Honors
|
|
11
|
+
// Honors four protections:
|
|
12
12
|
// - Custom skills (anything in .claude/skills/ not in the manifest) are
|
|
13
13
|
// never modified.
|
|
14
14
|
// - Remote skills (from skills.sh, kind: 'remote' in manifest) are left
|
|
15
15
|
// alone unless { refreshRemote: true }.
|
|
16
|
-
// - The audit
|
|
16
|
+
// - The audit + self-update workflows are also refreshed if installed.
|
|
17
|
+
// - Markerless workflow files (no `# clud-bug-template-version:` header)
|
|
18
|
+
// are treated as user-customized and left alone — the user gets a
|
|
19
|
+
// printed warning + the documented "delete + clud-bug init" recovery
|
|
20
|
+
// path. Mirrors logmind v0.2.1's refresh-mode pattern.
|
|
17
21
|
//
|
|
18
|
-
// Returns
|
|
22
|
+
// Returns { changed, unchanged, skipped, ourVersion }.
|
|
19
23
|
export async function runUpdate({
|
|
20
24
|
cwd,
|
|
21
25
|
templatesDir,
|
|
@@ -35,6 +39,7 @@ export async function runUpdate({
|
|
|
35
39
|
|
|
36
40
|
const changed = [];
|
|
37
41
|
const unchanged = [];
|
|
42
|
+
const skipped = [];
|
|
38
43
|
|
|
39
44
|
// 1. Re-render review workflow with the latest template.
|
|
40
45
|
const signals = await detect(cwd);
|
|
@@ -43,13 +48,20 @@ export async function runUpdate({
|
|
|
43
48
|
PROJECT_DESCRIPTION: buildDescriptionLine(signals),
|
|
44
49
|
LANGUAGE_HINTS: '',
|
|
45
50
|
});
|
|
46
|
-
await
|
|
51
|
+
await maybeRefreshVersioned(join(cwd, '.github/workflows/clud-bug-review.yml'), newReview, changed, unchanged, skipped, 'review workflow');
|
|
47
52
|
|
|
48
53
|
// 2. Re-render audit workflow if it's installed (init from v0.3+ ships it).
|
|
49
54
|
const auditPath = join(cwd, '.github/workflows/clud-bug-audit.yml');
|
|
50
55
|
if (await pathExists(auditPath)) {
|
|
51
56
|
const newAudit = await readFile(join(templatesDir, 'audit.yml.tmpl'), 'utf8');
|
|
52
|
-
await
|
|
57
|
+
await maybeRefreshVersioned(auditPath, newAudit, changed, unchanged, skipped, 'audit workflow');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 2b. Re-render self-update workflow if installed (init from v0.4+ ships it).
|
|
61
|
+
const selfUpdatePath = join(cwd, '.github/workflows/clud-bug-self-update.yml');
|
|
62
|
+
if (await pathExists(selfUpdatePath)) {
|
|
63
|
+
const newSelfUpdate = await readFile(join(templatesDir, 'self-update.yml.tmpl'), 'utf8');
|
|
64
|
+
await maybeRefreshVersioned(selfUpdatePath, newSelfUpdate, changed, unchanged, skipped, 'self-update workflow');
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
// 3. Refresh baseline skills (always controlled by clud-bug).
|
|
@@ -88,7 +100,7 @@ export async function runUpdate({
|
|
|
88
100
|
manifest.lastUpdateVersion = ourVersion;
|
|
89
101
|
await writeManifest(skillsDir, manifest);
|
|
90
102
|
|
|
91
|
-
return { changed, unchanged, ourVersion };
|
|
103
|
+
return { changed, unchanged, skipped, ourVersion };
|
|
92
104
|
}
|
|
93
105
|
|
|
94
106
|
async function maybeWrite(path, contents, changed, unchanged, label) {
|
|
@@ -102,6 +114,61 @@ async function maybeWrite(path, contents, changed, unchanged, label) {
|
|
|
102
114
|
changed.push({ path, label });
|
|
103
115
|
}
|
|
104
116
|
|
|
117
|
+
// Refresh a versioned template (one that carries `# clud-bug-template-version:`
|
|
118
|
+
// on line 1). If the installed file lacks that marker, treat it as
|
|
119
|
+
// user-customized and leave it alone — recovery path is delete + `clud-bug init`.
|
|
120
|
+
// Mirrors logmind v0.2.1's refresh-mode contract.
|
|
121
|
+
async function maybeRefreshVersioned(path, contents, changed, unchanged, skipped, label) {
|
|
122
|
+
const tmplVersion = extractTemplateVersion(contents);
|
|
123
|
+
if (!tmplVersion) {
|
|
124
|
+
// Defensive: every versioned template is supposed to carry a marker.
|
|
125
|
+
// Falling back to byte-compare write here would silently mass-overwrite
|
|
126
|
+
// every installed file (including marker-bearing ones) the moment a
|
|
127
|
+
// future template regressed — the inverse of the protection contract
|
|
128
|
+
// this function exists to enforce. Throw so the regression surfaces
|
|
129
|
+
// in CI instead.
|
|
130
|
+
throw new Error(`Template for ${label} has no # clud-bug-template-version marker — refusing to refresh (templates must declare a marker so refresh-mode can reason about ownership).`);
|
|
131
|
+
}
|
|
132
|
+
const prior = await readSafe(path);
|
|
133
|
+
if (prior === null) {
|
|
134
|
+
// First time writing here; nothing to preserve.
|
|
135
|
+
await mkdir(dirname(path), { recursive: true });
|
|
136
|
+
await writeFile(path, contents);
|
|
137
|
+
changed.push({ path, label });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const priorVersion = extractTemplateVersion(prior);
|
|
141
|
+
if (priorVersion === null) {
|
|
142
|
+
// Markerless installed file = customized. Preserve and warn.
|
|
143
|
+
skipped.push({
|
|
144
|
+
path,
|
|
145
|
+
label,
|
|
146
|
+
reason: 'markerless (user-customized); delete the file + run `clud-bug init` to refresh',
|
|
147
|
+
});
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (prior === contents) {
|
|
151
|
+
unchanged.push({ path, label });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// Marker present (current or stale) AND content drifted: refresh.
|
|
155
|
+
await mkdir(dirname(path), { recursive: true });
|
|
156
|
+
await writeFile(path, contents);
|
|
157
|
+
changed.push({ path, label, from: priorVersion, to: tmplVersion });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Extract the template-version marker. Templates put it on line 1, but
|
|
161
|
+
// scan the first 5 lines so a leading blank or stray header doesn't hide it.
|
|
162
|
+
// Anchoring near the top means a stray `# clud-bug-template-version:` lower
|
|
163
|
+
// in the file (in a comment inside a heredoc, say) can't be mistaken for the
|
|
164
|
+
// authoritative marker. Returns null if not present.
|
|
165
|
+
function extractTemplateVersion(text) {
|
|
166
|
+
if (!text) return null;
|
|
167
|
+
const head = text.split('\n', 5).join('\n');
|
|
168
|
+
const m = head.match(/^# clud-bug-template-version:\s*(\S+)/m);
|
|
169
|
+
return m ? m[1] : null;
|
|
170
|
+
}
|
|
171
|
+
|
|
105
172
|
async function readSafe(path) {
|
|
106
173
|
try { return await readFile(path, 'utf8'); } catch { return null; }
|
|
107
174
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clud-bug",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.8",
|
|
4
4
|
"description": "Claude PR review with project-aware skills. CLI installs a working GitHub Actions workflow and curates skills from skills.sh.",
|
|
5
5
|
"homepage": "https://cludbug.dev",
|
|
6
6
|
"bugs": "https://github.com/thrillmot/clud-bug/issues",
|
package/templates/audit.yml.tmpl
CHANGED
|
@@ -71,6 +71,55 @@ When you write a custom skill, follow the SKILL.md frontmatter format
|
|
|
71
71
|
Generic advice gets ignored; rules with examples and quoted-line evidence
|
|
72
72
|
move the bot's behavior.
|
|
73
73
|
|
|
74
|
+
### Example: a good custom skill
|
|
75
|
+
|
|
76
|
+
Don't write generic prose like "be careful with database code." That's
|
|
77
|
+
not actionable. Instead, anchor to specific files + behaviors:
|
|
78
|
+
|
|
79
|
+
````markdown
|
|
80
|
+
---
|
|
81
|
+
name: db-query-review
|
|
82
|
+
description: How to review changes under lib/db/. Always flag missing parameterization, N+1 query patterns, and missing transaction boundaries on multi-statement writes.
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
# Reviewing lib/db/ changes
|
|
86
|
+
|
|
87
|
+
When the diff touches `lib/db/queries.ts` or any file under `lib/db/`,
|
|
88
|
+
apply these rules:
|
|
89
|
+
|
|
90
|
+
## Always flag
|
|
91
|
+
|
|
92
|
+
1. **SQL string interpolation** — anything that builds a query via `+`,
|
|
93
|
+
template literals, or `string.format` rather than parameterized
|
|
94
|
+
queries. Example to flag:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
// BAD — flag this
|
|
98
|
+
db.query(`SELECT * FROM users WHERE id = ${userId}`)
|
|
99
|
+
// GOOD — uses parameterization
|
|
100
|
+
db.query('SELECT * FROM users WHERE id = ?', [userId])
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
2. **N+1 patterns** — flag any `await` inside a `for`/`map` loop that
|
|
104
|
+
calls `db.query` or `db.queryOne`. The fix is usually a single
|
|
105
|
+
`WHERE id IN (...)` query or a join.
|
|
106
|
+
|
|
107
|
+
3. **Multi-statement writes without `db.transaction`** — if a diff
|
|
108
|
+
adds two or more `db.query` calls that all write, demand a
|
|
109
|
+
transaction wrapper. Quote the lines.
|
|
110
|
+
|
|
111
|
+
## Style of finding
|
|
112
|
+
|
|
113
|
+
Cite the specific line. "This is a SQL injection risk" alone isn't
|
|
114
|
+
enough — quote the unsafe interpolation directly and propose the
|
|
115
|
+
parameterized alternative.
|
|
116
|
+
````
|
|
117
|
+
|
|
118
|
+
That's what "evidence-anchored" looks like: specific file paths, runnable
|
|
119
|
+
code examples for both bad and good, and explicit instructions on what
|
|
120
|
+
to quote in the finding. The bot loads this and uses it as concrete
|
|
121
|
+
review criteria, not vague guidance.
|
|
122
|
+
|
|
74
123
|
## When you edit `.github/workflows/clud-bug-*.yml`
|
|
75
124
|
|
|
76
125
|
`anthropics/claude-code-action` **refuses to run on PRs that modify its
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# clud-bug-template-version: v2
|
|
1
2
|
name: Clud Bug 🐛 Crawls Your Code
|
|
2
3
|
|
|
3
4
|
on:
|
|
@@ -167,23 +168,9 @@ jobs:
|
|
|
167
168
|
with confirmed: true.
|
|
168
169
|
If there are no critical issues, post a one-line comment saying so.
|
|
169
170
|
|
|
170
|
-
# Strict-mode gate — see workflow.yml.tmpl for
|
|
171
|
+
# Strict-mode gate — composite action; see workflow.yml.tmpl for design notes.
|
|
171
172
|
- name: Strict mode — fail check on critical findings
|
|
172
173
|
if: success()
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
run: |
|
|
177
|
-
BASE_MANIFEST=$(git show "origin/${{ github.base_ref }}:.claude/skills/.clud-bug.json" 2>&1) || {
|
|
178
|
-
echo "::warning::Base manifest not found on ${{ github.base_ref }} — strict mode disabled for this run."
|
|
179
|
-
exit 0
|
|
180
|
-
}
|
|
181
|
-
STRICT=$(echo "$BASE_MANIFEST" | node -e "let s='';process.stdin.on('data',c=>s+=c);process.stdin.on('end',()=>{try{console.log(JSON.parse(s).strictMode===true)}catch(e){console.log('false')}})")
|
|
182
|
-
[ "$STRICT" = "true" ] || exit 0
|
|
183
|
-
LATEST=$(gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments?sort=created&direction=desc&per_page=100" \
|
|
184
|
-
--jq '[.[] | select(.user.login == "claude[bot]" and (.body | startswith("## 🐛 Clud Bug review")))][0].body // ""')
|
|
185
|
-
if echo "$LATEST" | head -n1 | grep -q "Clud Bug review — critical findings"; then
|
|
186
|
-
echo "::error title=Clud Bug 🐛::Critical issues found and strictMode is enabled — failing this check."
|
|
187
|
-
echo "::error::See the latest Clud Bug review comment for details. Push a fix and the gate will clear on the next run."
|
|
188
|
-
exit 1
|
|
189
|
-
fi
|
|
174
|
+
uses: thrillmot/clud-bug/.github/actions/strict-mode-gate@v0.5.8
|
|
175
|
+
with:
|
|
176
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# clud-bug-template-version: v2
|
|
1
2
|
name: Clud Bug 🐛 Crawls Your Code
|
|
2
3
|
|
|
3
4
|
on:
|
|
@@ -168,23 +169,9 @@ jobs:
|
|
|
168
169
|
with confirmed: true.
|
|
169
170
|
If there are no critical issues, post a one-line comment saying so.
|
|
170
171
|
|
|
171
|
-
# Strict-mode gate — see workflow.yml.tmpl for
|
|
172
|
+
# Strict-mode gate — composite action; see workflow.yml.tmpl for design notes.
|
|
172
173
|
- name: Strict mode — fail check on critical findings
|
|
173
174
|
if: success()
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
run: |
|
|
178
|
-
BASE_MANIFEST=$(git show "origin/${{ github.base_ref }}:.claude/skills/.clud-bug.json" 2>&1) || {
|
|
179
|
-
echo "::warning::Base manifest not found on ${{ github.base_ref }} — strict mode disabled for this run."
|
|
180
|
-
exit 0
|
|
181
|
-
}
|
|
182
|
-
STRICT=$(echo "$BASE_MANIFEST" | node -e "let s='';process.stdin.on('data',c=>s+=c);process.stdin.on('end',()=>{try{console.log(JSON.parse(s).strictMode===true)}catch(e){console.log('false')}})")
|
|
183
|
-
[ "$STRICT" = "true" ] || exit 0
|
|
184
|
-
LATEST=$(gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments?sort=created&direction=desc&per_page=100" \
|
|
185
|
-
--jq '[.[] | select(.user.login == "claude[bot]" and (.body | startswith("## 🐛 Clud Bug review")))][0].body // ""')
|
|
186
|
-
if echo "$LATEST" | head -n1 | grep -q "Clud Bug review — critical findings"; then
|
|
187
|
-
echo "::error title=Clud Bug 🐛::Critical issues found and strictMode is enabled — failing this check."
|
|
188
|
-
echo "::error::See the latest Clud Bug review comment for details. Push a fix and the gate will clear on the next run."
|
|
189
|
-
exit 1
|
|
190
|
-
fi
|
|
175
|
+
uses: thrillmot/clud-bug/.github/actions/strict-mode-gate@v0.5.8
|
|
176
|
+
with:
|
|
177
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# clud-bug-template-version: v2
|
|
1
2
|
name: Clud Bug 🐛 Crawls Your Code
|
|
2
3
|
|
|
3
4
|
on:
|
|
@@ -188,12 +189,14 @@ jobs:
|
|
|
188
189
|
with confirmed: true.
|
|
189
190
|
If there are no critical issues, post a one-line comment saying so.
|
|
190
191
|
|
|
191
|
-
# Strict-mode gate. Fails the check when
|
|
192
|
-
#
|
|
193
|
-
#
|
|
194
|
-
#
|
|
195
|
-
#
|
|
196
|
-
#
|
|
192
|
+
# Strict-mode gate. Fails the check when the BASE ref's manifest
|
|
193
|
+
# has { "strictMode": true } AND the latest clud-bug review's first
|
|
194
|
+
# line starts with "## 🐛 Clud Bug review — critical findings".
|
|
195
|
+
#
|
|
196
|
+
# Logic lives in the composite action so it's revised once across
|
|
197
|
+
# all 3 templates + the App runtime. Pinned to the same clud-bug
|
|
198
|
+
# tag the user installed (rendered by `clud-bug init`), so the
|
|
199
|
+
# action's contract is stable for the lifetime of that install.
|
|
197
200
|
#
|
|
198
201
|
# if: success() — only run when claude-code-action succeeded. If the
|
|
199
202
|
# action errored, no new comment was posted for this run; falling back
|
|
@@ -201,30 +204,6 @@ jobs:
|
|
|
201
204
|
# Letting the action's own failure fail the check is louder and right.
|
|
202
205
|
- name: Strict mode — fail check on critical findings
|
|
203
206
|
if: success()
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
run: |
|
|
208
|
-
# Loud failure if the base manifest can't be read — silently falling
|
|
209
|
-
# back to advisory would silently disable strict mode for every
|
|
210
|
-
# opted-in repo. (Requires fetch-depth: 0 on the checkout above.)
|
|
211
|
-
BASE_MANIFEST=$(git show "origin/${{ github.base_ref }}:.claude/skills/.clud-bug.json" 2>&1) || {
|
|
212
|
-
echo "::warning::Base manifest not found on ${{ github.base_ref }} — strict mode disabled for this run."
|
|
213
|
-
exit 0
|
|
214
|
-
}
|
|
215
|
-
STRICT=$(echo "$BASE_MANIFEST" | node -e "let s='';process.stdin.on('data',c=>s+=c);process.stdin.on('end',()=>{try{console.log(JSON.parse(s).strictMode===true)}catch(e){console.log('false')}})")
|
|
216
|
-
[ "$STRICT" = "true" ] || exit 0
|
|
217
|
-
|
|
218
|
-
# Use startswith (not regex contains) so comments that *quote* the
|
|
219
|
-
# sentinel header (other reviews, @claude responses, meta-PRs about
|
|
220
|
-
# strict mode itself) don't get picked up as "the latest review."
|
|
221
|
-
LATEST=$(gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments?sort=created&direction=desc&per_page=100" \
|
|
222
|
-
--jq '[.[] | select(.user.login == "claude[bot]" and (.body | startswith("## 🐛 Clud Bug review")))][0].body // ""')
|
|
223
|
-
|
|
224
|
-
# Scope the critical-findings match to the FIRST LINE so quoted
|
|
225
|
-
# sentinels deeper in the review can't trip the gate.
|
|
226
|
-
if echo "$LATEST" | head -n1 | grep -q "Clud Bug review — critical findings"; then
|
|
227
|
-
echo "::error title=Clud Bug 🐛::Critical issues found and strictMode is enabled — failing this check."
|
|
228
|
-
echo "::error::See the latest Clud Bug review comment for details. Push a fix and the gate will clear on the next run."
|
|
229
|
-
exit 1
|
|
230
|
-
fi
|
|
207
|
+
uses: thrillmot/clud-bug/.github/actions/strict-mode-gate@v0.5.8
|
|
208
|
+
with:
|
|
209
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|