@olegkoval/agent-skills 1.66.0 → 1.68.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/adapters/claude/olko-github-pr/skills/lekker-review/SKILL.md +73 -29
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/agents/implementation.md +16 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/agents/triage-logic.md +16 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/output-format.md +1 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/reviewed-before-push.md +94 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/reviewed-before-push.yml +57 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/review-label.mjs +90 -0
- package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/risk-paths.mjs +67 -0
- package/package.json +1 -1
- package/plugins/olko-apple-kit/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-creative/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-garmin-kit/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-git-tools/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-github-pr/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-github-pr/skills/lekker-review/README.md +10 -0
- package/plugins/olko-github-pr/skills/lekker-review/SKILL.md +73 -29
- package/plugins/olko-github-pr/skills/lekker-review/references/agents/implementation.md +16 -0
- package/plugins/olko-github-pr/skills/lekker-review/references/agents/triage-logic.md +16 -0
- package/plugins/olko-github-pr/skills/lekker-review/references/output-format.md +1 -0
- package/plugins/olko-github-pr/skills/lekker-review/references/reviewed-before-push.md +94 -0
- package/plugins/olko-github-pr/skills/lekker-review/references/reviewed-before-push.yml +57 -0
- package/plugins/olko-github-pr/skills/lekker-review/scripts/review-label.mjs +90 -0
- package/plugins/olko-github-pr/skills/lekker-review/scripts/risk-paths.mjs +67 -0
- package/plugins/olko-obsidian/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-product/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-reflection/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-release/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-skill-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-web-ops/.claude-plugin/plugin.json +1 -1
|
@@ -285,23 +285,49 @@ Then:
|
|
|
285
285
|
`LOCAL_BRANCH` is `main`, `master`, `develop`, `staging`, or `production`, stop
|
|
286
286
|
and say so.
|
|
287
287
|
|
|
288
|
-
**Depth:** explicit
|
|
289
|
-
|
|
290
|
-
`gh pr view <PR_NUMBER> --repo <REPO_SLUG> --json additions,deletions,changedFiles,files,baseRefName,state,mergeCommit
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
288
|
+
**Depth:** record an explicit `scan`, `medium`, `deep` or `deps` request;
|
|
289
|
+
otherwise select a provisional depth from the per-file stats. In `pr` mode use
|
|
290
|
+
`gh pr view <PR_NUMBER> --repo <REPO_SLUG> --json additions,deletions,changedFiles,files,baseRefName,state,mergeCommit`.
|
|
291
|
+
Keep `baseRefName` as `BASE_BRANCH`, `changedFiles` as `EXPECTED_FILES`, and
|
|
292
|
+
`mergeCommit.oid` as `MERGE_COMMIT` when merged. In `branch` mode use
|
|
293
|
+
`git diff --numstat <MERGE_BASE> HEAD`.
|
|
294
|
+
|
|
295
|
+
**Mandatory risk check (every depth, including explicit requests):** after
|
|
296
|
+
Track B has saved `pr.diff`, and before finalizing depth or starting Step 2,
|
|
297
|
+
run `scripts/risk-paths.mjs` with that diff and the exact changed paths.
|
|
298
|
+
Resolve the script beside the selected `SKILL.md`: in Codex use
|
|
299
|
+
`<directory containing selected SKILL.md>/scripts/risk-paths.mjs`; in Claude
|
|
300
|
+
use `${CLAUDE_PLUGIN_ROOT}/skills/lekker-review/scripts/risk-paths.mjs`.
|
|
301
|
+
Use a shell-free process API with separate arguments:
|
|
302
|
+
`execFileSync('node', [riskScript, '--diff', diffFile, '--', ...changedPaths])`.
|
|
303
|
+
Never interpolate PR-controlled paths into shell source. Alternatively pipe
|
|
304
|
+
`git diff --name-only -z <MERGE_BASE> HEAD` into
|
|
305
|
+
`node "<resolved riskScript>" --stdin0 --diff "<scratchpad>/pr.diff"`.
|
|
306
|
+
Do not continue on classifier failure; report risk classification unavailable.
|
|
307
|
+
|
|
308
|
+
The classifier detects configured path patterns for CI workflows, auth,
|
|
309
|
+
billing, installers/setup, and P21, plus `p21` in added lines. These are path
|
|
310
|
+
heuristics, not complete detection of auth/billing behavior in generic files;
|
|
311
|
+
reviewers still inspect that behavior in the diff. Whenever
|
|
312
|
+
`needsHumanReview: true`, always put `NEEDS HUMAN REVIEW: <categories>` in the
|
|
313
|
+
review header, even for an explicit scan. A human must read those paths.
|
|
314
|
+
|
|
315
|
+
Explicit depth wins, but never suppresses this marker. Without an explicit
|
|
316
|
+
request, apply AUTO-DEPTH in this order after the risk check:
|
|
317
|
+
- `deep` if the classifier returns `deep: true`, additions+deletions > 800,
|
|
318
|
+
changedFiles > 25, or the diff touches `migrations/` or `*.sql`.
|
|
319
|
+
- `deps` (candidate) if every changed file is a lockfile or `package.json`.
|
|
320
|
+
Confirm after Track A with `scripts/deps-only.mjs`; until then run Track B
|
|
321
|
+
as for `scan`.
|
|
322
|
+
- `scan` if additions+deletions < 150 AND changedFiles <= 5.
|
|
323
|
+
- `medium` otherwise.
|
|
324
|
+
|
|
325
|
+
Lockfiles (`package-lock.json`, `npm-shrinkwrap.json`, `yarn.lock`,
|
|
326
|
+
`pnpm-lock.yaml`, `bun.lock`, `bun.lockb`) do not count toward additions+deletions.
|
|
327
|
+
Step 0 depth is provisional: if the mandatory check upgrades scan, run Track A
|
|
328
|
+
and gather the newly required Track B sources before Step 2. Branch mode uses
|
|
329
|
+
Track A's merge-base diff. State final depth and whether auto-selected in the
|
|
330
|
+
review header.
|
|
305
331
|
|
|
306
332
|
**`--post` flag:** parse and store as `POST_REVIEW=true`.
|
|
307
333
|
|
|
@@ -673,7 +699,7 @@ Write `<scratchpad>/context.json` with keys:
|
|
|
673
699
|
"botPatternsFile": "<BOT_PATTERNS_FILE, or null>",
|
|
674
700
|
"depsReport": "<deps-report.json contents at depth deps, else null>",
|
|
675
701
|
"depsInstall": "<worktree.json depsInstall>",
|
|
676
|
-
"prBody": "<PR body at depth
|
|
702
|
+
"prBody": "<full PR body at every depth; empty string in branch mode>"
|
|
677
703
|
}
|
|
678
704
|
```
|
|
679
705
|
|
|
@@ -1113,8 +1139,17 @@ printf '%s\n' "$REVIEW_FILE" > ~/.cache/lekker-review/reviewed/<headSha>
|
|
|
1113
1139
|
```
|
|
1114
1140
|
|
|
1115
1141
|
Write it against the sha that was actually reviewed. When fix mode later adds
|
|
1116
|
-
commits, write a marker for the
|
|
1117
|
-
|
|
1142
|
+
commits, write a marker for the new head only after fix verification and
|
|
1143
|
+
relevant tests pass. Any unrelated edits require a fresh review; never copy a
|
|
1144
|
+
marker to an unreviewed SHA.
|
|
1145
|
+
|
|
1146
|
+
**PR label for branch reviews:** follow `references/reviewed-before-push.md`.
|
|
1147
|
+
After a complete, non-degraded local review and settling all Critical/Important
|
|
1148
|
+
findings, save the final-head receipt as `<scratchpad>/pre-push-review.json`
|
|
1149
|
+
BEFORE pushing. That reference defines the receipt schema and one-time cleanup
|
|
1150
|
+
workflow setup. The label is exactly `reviewed-before-push` (no AI prefix).
|
|
1151
|
+
A legacy SHA/path marker alone does not qualify. PR-mode reviews cannot earn
|
|
1152
|
+
this label.
|
|
1118
1153
|
|
|
1119
1154
|
**Close the run log** right after the receipt, then print the review:
|
|
1120
1155
|
|
|
@@ -1288,33 +1323,42 @@ Ask once, via AskUserQuestion:
|
|
|
1288
1323
|
|
|
1289
1324
|
Rules for each answer:
|
|
1290
1325
|
|
|
1291
|
-
- **Yes:**
|
|
1326
|
+
- **Yes:** set `PREDICTED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)` before pushing
|
|
1327
|
+
or publishing, including when a PR already exists. Then push with
|
|
1328
|
+
`git push -u origin "$local_branch"` and create or locate the PR. Create it
|
|
1292
1329
|
with `gh pr create --repo "$repo_slug" --base "$base_ref" --head
|
|
1293
1330
|
"$local_branch" --title "$pr_title" --body-file "$pr_body_file"`. Derive the title
|
|
1294
1331
|
from the commit log (the recommended prefix plus a summary of the change) and
|
|
1295
1332
|
the body from a short summary of what the branch does - do not leave either
|
|
1296
|
-
as a placeholder. Report the PR url.
|
|
1297
|
-
|
|
1298
|
-
|
|
1333
|
+
as a placeholder. Report the PR url. If an eligible pre-push receipt was
|
|
1334
|
+
saved, publish it using `scripts/review-label.mjs` as described in
|
|
1335
|
+
`references/reviewed-before-push.md`. This posts the commit/engine/depth/result
|
|
1336
|
+
evidence and applies `reviewed-before-push` only to the matching live head.
|
|
1337
|
+
If cleanup is not installed, omit the label and report the setup requirement;
|
|
1338
|
+
do not undo the successful PR creation. For an existing PR found after a
|
|
1339
|
+
branch-mode push, use the same helper and head check. Then record the prediction
|
|
1340
|
+
against the created or existing PR number using the saved `PREDICTED_AT`;
|
|
1341
|
+
scoring uses it to include bot comments triggered by the push or PR creation,
|
|
1342
|
+
even if they arrive before the record step. Record only the findings fix
|
|
1299
1343
|
mode did NOT apply (every row of the fixes table that is not `✅ applied`):
|
|
1300
1344
|
an applied fix is gone from the code the bots read, so they cannot raise it,
|
|
1301
1345
|
and recording it would sink precision for doing exactly what branch mode is
|
|
1302
1346
|
for. Build `<scratchpad>/unfixed.json` (a bare array is fine) from the merged
|
|
1303
1347
|
`findings.json` minus the findings fix mode applied; when fix mode did not
|
|
1304
|
-
run or applied nothing, it is the whole merged set.
|
|
1305
|
-
|
|
1306
|
-
a bot comment that lands before the record step still counts:
|
|
1348
|
+
run or applied nothing, it is the whole merged set. Keep the timestamp captured
|
|
1349
|
+
before the push; do not replace it at the record step:
|
|
1307
1350
|
|
|
1308
1351
|
```bash
|
|
1309
1352
|
node ${SKILL_ROOT}/scripts/bot-precision.mjs record \
|
|
1310
|
-
--repo "$repo_slug" --pr <
|
|
1353
|
+
--repo "$repo_slug" --pr <PR_NUMBER> --predicted <scratchpad>/unfixed.json \
|
|
1311
1354
|
--at "$PREDICTED_AT"
|
|
1312
1355
|
```
|
|
1313
1356
|
|
|
1314
1357
|
The review is already saved locally; offer `--post` on the new PR number only if the user asks - findings that fix
|
|
1315
1358
|
mode already applied must never be posted as review comments.
|
|
1316
1359
|
- **Push only:** `git push -u origin "$local_branch"` and stop. Say in one line
|
|
1317
|
-
that no PR was created.
|
|
1360
|
+
that no PR was created. Keep any eligible pre-push receipt for publication
|
|
1361
|
+
when a matching PR is later created; no PR means no label yet.
|
|
1318
1362
|
- **No:** stop. Print the branch name and the review file path.
|
|
1319
1363
|
|
|
1320
1364
|
**Unattended runs never ask and never create a PR** (see the operating contract
|
package/adapters/claude/olko-github-pr/skills/lekker-review/references/agents/implementation.md
CHANGED
|
@@ -48,6 +48,22 @@ Axes to cover:
|
|
|
48
48
|
fixed, the mitigation fully resolves the defect, or the PR is not a bug fix.
|
|
49
49
|
- Integration Contracts: Shopify API misuse, BC API assumptions, webhook
|
|
50
50
|
idempotency, external API pagination not handled.
|
|
51
|
+
- Rollout safety (external writes): when the diff changes what is WRITTEN to
|
|
52
|
+
another system (an API payload, field mapping, or a DB write into a system
|
|
53
|
+
this service does not own), state whether the PR has a feature flag
|
|
54
|
+
(Reflag / `getShopFlag` / `isEnabled` style check around the new write) or a
|
|
55
|
+
documented revert plan (PR body or code comment). Look in the diff and in the
|
|
56
|
+
PR body in CONTEXT_FILE. When WORKTREE_PATH is available, trace the changed
|
|
57
|
+
write through unchanged callers and flag guards before deciding either is
|
|
58
|
+
absent. Without a worktree, absence from the diff is not proof: report a
|
|
59
|
+
missing rollout control only when the diff demonstrates that the changed
|
|
60
|
+
write bypasses a guard and the PR body provides no revert plan. Otherwise
|
|
61
|
+
mark rollout verification unavailable rather than inventing a finding.
|
|
62
|
+
If neither control exists on that evidence, report an `important` finding:
|
|
63
|
+
`title` starts "No flag or revert plan for external write", `badCode` quotes
|
|
64
|
+
the changed write, `fix` names the flag guard to add. A pure UI/copy change or
|
|
65
|
+
a write that is trivially undone by deploying again is exempt. Quiet when the
|
|
66
|
+
flag or plan is present; do not report its presence.
|
|
51
67
|
|
|
52
68
|
Boundary contract and lifecycle map (required for every Critical/Important
|
|
53
69
|
finding involving routing, persistence, caching, fallback, proxying, or an
|
package/adapters/claude/olko-github-pr/skills/lekker-review/references/agents/triage-logic.md
CHANGED
|
@@ -11,6 +11,22 @@ Axes to cover (Critical/Important only):
|
|
|
11
11
|
- Scalability: N+1 queries, missing pagination, unbounded collections, missing rate-limit
|
|
12
12
|
- Integration Contracts: Shopify API misuse, webhook idempotency not handled
|
|
13
13
|
- Correctness: off-by-one, wrong operator, missing null guard
|
|
14
|
+
- Rollout safety (external writes): when the diff changes what is WRITTEN to
|
|
15
|
+
another system (an API payload, field mapping, or a DB write into a system
|
|
16
|
+
this service does not own), state whether the PR has a feature flag
|
|
17
|
+
(Reflag / `getShopFlag` / `isEnabled` style check around the new write) or a
|
|
18
|
+
documented revert plan (PR body or code comment). Look in the diff and in the
|
|
19
|
+
PR body in CONTEXT_FILE. When WORKTREE_PATH is available, trace the changed
|
|
20
|
+
write through unchanged callers and flag guards before deciding either is
|
|
21
|
+
absent. Without a worktree, absence from the diff is not proof: report a
|
|
22
|
+
missing rollout control only when the diff demonstrates that the changed
|
|
23
|
+
write bypasses a guard and the PR body provides no revert plan. Otherwise
|
|
24
|
+
mark rollout verification unavailable rather than inventing a finding.
|
|
25
|
+
If neither control exists on that evidence, report an `important` finding:
|
|
26
|
+
`title` starts "No flag or revert plan for external write", `badCode` quotes
|
|
27
|
+
the changed write, `fix` names the flag guard to add. A pure UI/copy change or
|
|
28
|
+
a write that is trivially undone by deploying again is exempt. Quiet when the
|
|
29
|
+
flag or plan is present; do not report its presence.
|
|
14
30
|
|
|
15
31
|
CI_STATUS: read key "ciStatus" from the JSON file CONTEXT_FILE.
|
|
16
32
|
EXISTING_REVIEWS: read key "existingReviews" from CONTEXT_FILE (awareness only, skip findings at same file:line)
|
|
@@ -24,6 +24,7 @@ Output the review as a markdown response (not via printf). No ANSI escapes.
|
|
|
24
24
|
**Proofs:** <M proven / N attempted> *(include only when N > 0)*
|
|
25
25
|
**CI:** <✅ All passing | ⚠️ N failing: check-name | ⏳ Pending | N/A>
|
|
26
26
|
**Depth:** <📦 deps | ⚡ scan | 🔍 medium | 🔬 deep>
|
|
27
|
+
**NEEDS HUMAN REVIEW:** <categories from risk-paths.mjs> *(only when `needsHumanReview` is true: a person must read these paths whatever the verdict)*
|
|
27
28
|
**Engine:** <workflow | codex | revmux>
|
|
28
29
|
|
|
29
30
|
---
|
package/adapters/claude/olko-github-pr/skills/lekker-review/references/reviewed-before-push.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Reviewed before push
|
|
2
|
+
|
|
3
|
+
`reviewed-before-push` records a completed local branch review of the current
|
|
4
|
+
PR head. It is informational: it does not satisfy CI, required remote review,
|
|
5
|
+
or the human-review marker for risky changes. The review comment records the
|
|
6
|
+
full head/base SHAs, engine, depth, timestamp, and result. It is an attestation
|
|
7
|
+
by the publishing user, not cryptographic proof that review preceded a push.
|
|
8
|
+
|
|
9
|
+
## Install stale-label cleanup once per repository
|
|
10
|
+
|
|
11
|
+
Copy `references/reviewed-before-push.yml` into the target repository as
|
|
12
|
+
`.github/workflows/reviewed-before-push.yml` and land it on the default branch
|
|
13
|
+
through that repository's normal review process. Do this before the local
|
|
14
|
+
review whose receipt will be published. Do not add a workflow commit after
|
|
15
|
+
review and then label that unreviewed commit.
|
|
16
|
+
|
|
17
|
+
The workflow uses only GitHub API data, with no PR checkout. On new commits or
|
|
18
|
+
receipt edits/deletion it removes the label unless a receipt from an owner,
|
|
19
|
+
member, or collaborator matches the live head. Pagination includes all receipt
|
|
20
|
+
comments. It does not add labels or replace merge gates. A newly reviewed head
|
|
21
|
+
can be labeled again by the publisher. Cleanup is asynchronous; the label is
|
|
22
|
+
not a merge-authorization signal. Workflows disabled by repository policy or
|
|
23
|
+
Actions outages can delay removal.
|
|
24
|
+
|
|
25
|
+
The helper refuses to publish if the cleanup workflow is absent. If setup is
|
|
26
|
+
missing, keep the local review and PR creation successful, omit the label, and
|
|
27
|
+
report the missing setup. Never silently install a write-capable workflow in
|
|
28
|
+
another repository. A push-only run keeps the receipt locally until a PR exists.
|
|
29
|
+
|
|
30
|
+
## Record before pushing
|
|
31
|
+
|
|
32
|
+
Only in `MODE=branch`, after the review engine completed without degraded or
|
|
33
|
+
missing required dimensions, settle every Critical/Important finding. Either
|
|
34
|
+
fix it with verifier/test evidence or explicitly rebut it with a reason. Open
|
|
35
|
+
observations may remain. A file containing only a review path or a SHA marker
|
|
36
|
+
is not sufficient evidence.
|
|
37
|
+
|
|
38
|
+
Save `<scratchpad>/pre-push-review.json` before the push:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"version": 1,
|
|
43
|
+
"mode": "branch",
|
|
44
|
+
"result": "passed",
|
|
45
|
+
"headSha": "<full reviewed final local HEAD>",
|
|
46
|
+
"baseSha": "<full merge-base used by this review>",
|
|
47
|
+
"engine": "codex",
|
|
48
|
+
"depth": "medium",
|
|
49
|
+
"reviewedAt": "<UTC ISO timestamp when this review completed>",
|
|
50
|
+
"findings": [
|
|
51
|
+
{
|
|
52
|
+
"severity": "important",
|
|
53
|
+
"disposition": "fixed",
|
|
54
|
+
"evidence": "<actual fix verification and relevant test result>"
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Use an empty `findings` array when the review found nothing. Otherwise include
|
|
61
|
+
every finding in the merged review and lint set. Allowed dispositions are
|
|
62
|
+
`fixed`, `rebutted`, and `open`; `open` is permitted only for observations and
|
|
63
|
+
idiomatic suggestions. Rebuttals require specific evidence too. Set engine and
|
|
64
|
+
depth to the values actually used. Keep the saved full review locally; finding
|
|
65
|
+
text, evidence text, and local paths are not uploaded by the helper.
|
|
66
|
+
|
|
67
|
+
`reviewedAt` must be a valid UTC ISO timestamp in `YYYY-MM-DDTHH:mm:ssZ`
|
|
68
|
+
format, optionally with fractional seconds before `Z` (for example,
|
|
69
|
+
`2026-10-29T09:00:00.123Z`). Date-only strings, timezone offsets, and invalid
|
|
70
|
+
calendar dates are rejected by the publisher and cleanup workflow.
|
|
71
|
+
|
|
72
|
+
After fixes, the receipt must describe the final SHA: fix verification and
|
|
73
|
+
relevant tests must pass, and the only intervening edits must be those verified
|
|
74
|
+
fixes. Any unrelated edit requires a fresh review. Never copy a receipt to a
|
|
75
|
+
new SHA just to make the label apply.
|
|
76
|
+
|
|
77
|
+
## Publish after pushing and creating or locating the PR
|
|
78
|
+
|
|
79
|
+
Resolve the helper relative to the selected `SKILL.md` (same directory's
|
|
80
|
+
`scripts/review-label.mjs`, including in Codex). Invoke with separate process
|
|
81
|
+
arguments, or independently shell-quote each value:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
node "<SKILL_ROOT>/scripts/review-label.mjs" \
|
|
85
|
+
--repo "<owner/repo>" --pr "<number>" \
|
|
86
|
+
--receipt "<scratchpad>/pre-push-review.json"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The helper checks the live open PR head, posts the receipt, creates the label
|
|
90
|
+
if needed, and applies it. It checks the head before and after labeling, and
|
|
91
|
+
removes the label if a concurrent push changed the head. Errors are failures
|
|
92
|
+
to publish the label, not a failed local review. Do not claim the label was
|
|
93
|
+
applied when the helper failed. Do not use this helper for a review performed
|
|
94
|
+
in `MODE=pr` after pushing.
|
package/adapters/claude/olko-github-pr/skills/lekker-review/references/reviewed-before-push.yml
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Install this file on the target repository's default branch as
|
|
2
|
+
# .github/workflows/reviewed-before-push.yml before publishing review labels.
|
|
3
|
+
name: Reviewed before push
|
|
4
|
+
on:
|
|
5
|
+
pull_request_target:
|
|
6
|
+
types: [opened, reopened, synchronize, edited, labeled]
|
|
7
|
+
issue_comment:
|
|
8
|
+
types: [created, edited, deleted]
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
issues: write
|
|
12
|
+
pull-requests: read
|
|
13
|
+
concurrency:
|
|
14
|
+
group: reviewed-before-push-${{ github.event.pull_request.number || github.event.issue.number }}
|
|
15
|
+
cancel-in-progress: false
|
|
16
|
+
jobs:
|
|
17
|
+
reconcile:
|
|
18
|
+
if: github.event.pull_request || github.event.issue.pull_request
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
# No checkout: never execute code from the PR with this write token.
|
|
22
|
+
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
23
|
+
with:
|
|
24
|
+
script: |
|
|
25
|
+
const label = 'reviewed-before-push';
|
|
26
|
+
function isUtcTimestamp(value) {
|
|
27
|
+
const match = typeof value === 'string' && value.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d+))?Z$/);
|
|
28
|
+
if (!match || match[0] !== value) return false;
|
|
29
|
+
const time = Date.parse(value);
|
|
30
|
+
const canonical = `${match[1]}.${(match[2] || '').padEnd(3, '0').slice(0, 3)}Z`;
|
|
31
|
+
return Number.isFinite(time) && new Date(time).toISOString() === canonical;
|
|
32
|
+
}
|
|
33
|
+
const { owner, repo } = context.repo;
|
|
34
|
+
const issue_number = context.payload.pull_request?.number || context.payload.issue.number;
|
|
35
|
+
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: issue_number });
|
|
36
|
+
if (!pr.labels.some(item => item.name === label)) return;
|
|
37
|
+
const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number, per_page: 100 });
|
|
38
|
+
const valid = comments.some(comment => {
|
|
39
|
+
if (!['OWNER', 'MEMBER', 'COLLABORATOR'].includes(comment.author_association)) return false;
|
|
40
|
+
const match = comment.body?.match(/<!-- lekker:reviewed-before-push (\{[^\n]*\}) -->/);
|
|
41
|
+
if (!match) return false;
|
|
42
|
+
try {
|
|
43
|
+
const r = JSON.parse(match[1]);
|
|
44
|
+
return r.version === 1 && r.mode === 'branch' && r.result === 'passed'
|
|
45
|
+
&& r.headSha === pr.head.sha && /^[a-f0-9]{40}$/.test(r.baseSha)
|
|
46
|
+
&& ['workflow', 'codex', 'revmux'].includes(r.engine)
|
|
47
|
+
&& ['scan', 'medium', 'deep', 'deps'].includes(r.depth)
|
|
48
|
+
&& isUtcTimestamp(r.reviewedAt);
|
|
49
|
+
} catch { return false; }
|
|
50
|
+
});
|
|
51
|
+
if (!valid) {
|
|
52
|
+
try {
|
|
53
|
+
await github.rest.issues.removeLabel({ owner, repo, issue_number, name: label });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (error.status !== 404) throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Publish a local branch-review receipt using gh; PR-controlled values never enter shell source.
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
export const LABEL = 'reviewed-before-push';
|
|
8
|
+
export const PREFIX = '<!-- lekker:reviewed-before-push ';
|
|
9
|
+
const SHA = /^[a-f0-9]{40}$/;
|
|
10
|
+
|
|
11
|
+
function isUtcTimestamp(value) {
|
|
12
|
+
const match = typeof value === 'string' && value.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d+))?Z$/);
|
|
13
|
+
if (!match || match[0] !== value) return false;
|
|
14
|
+
const time = Date.parse(value);
|
|
15
|
+
const canonical = `${match[1]}.${(match[2] || '').padEnd(3, '0').slice(0, 3)}Z`;
|
|
16
|
+
return Number.isFinite(time) && new Date(time).toISOString() === canonical;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function validateReceipt(receipt) {
|
|
20
|
+
if (receipt?.version !== 1 || receipt.mode !== 'branch' || receipt.result !== 'passed'
|
|
21
|
+
|| !SHA.test(receipt.headSha) || !SHA.test(receipt.baseSha)
|
|
22
|
+
|| !['workflow', 'codex', 'revmux'].includes(receipt.engine)
|
|
23
|
+
|| !['scan', 'medium', 'deep', 'deps'].includes(receipt.depth)
|
|
24
|
+
|| !isUtcTimestamp(receipt.reviewedAt)
|
|
25
|
+
|| !Array.isArray(receipt.findings)) {
|
|
26
|
+
throw new Error('Invalid branch-review receipt');
|
|
27
|
+
}
|
|
28
|
+
for (const finding of receipt.findings) {
|
|
29
|
+
if (!['critical', 'important', 'observation', 'idiomatic'].includes(finding.severity)
|
|
30
|
+
|| !['fixed', 'rebutted', 'open'].includes(finding.disposition)
|
|
31
|
+
|| (['critical', 'important'].includes(finding.severity) && finding.disposition === 'open')
|
|
32
|
+
|| (['fixed', 'rebutted'].includes(finding.disposition) && !finding.evidence?.trim())) {
|
|
33
|
+
throw new Error('Findings require dispositions and evidence; no open Critical/Important finding is allowed');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return receipt;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function reviewComment(receipt) {
|
|
40
|
+
validateReceipt(receipt);
|
|
41
|
+
// Only publish the attestation, not local paths or potentially sensitive finding text.
|
|
42
|
+
const { version, mode, result, headSha, baseSha, engine, depth, reviewedAt } = receipt;
|
|
43
|
+
const attestation = { version, mode, result, headSha, baseSha, engine, depth, reviewedAt };
|
|
44
|
+
return `Reviewed before push\n\nCommit: \`${headSha}\`\nBase: \`${baseSha}\`\nEngine: ${engine}\nDepth: ${depth}\nReviewed at: ${reviewedAt}\nResult: passed; no unresolved Critical/Important findings.\n\nThis records a local review; required CI and remote review gates still apply.\n\n${PREFIX}${JSON.stringify(attestation)} -->`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function publishReview({ repo, pr, receipt }, run = execFileSync) {
|
|
48
|
+
if (!/^[\w.-]+\/[\w.-]+$/.test(repo) || !/^[1-9]\d*$/.test(String(pr))) {
|
|
49
|
+
throw new Error('Expected --repo owner/repo and a positive --pr number');
|
|
50
|
+
}
|
|
51
|
+
const body = reviewComment(receipt);
|
|
52
|
+
const gh = (args, input) => run('gh', args, { encoding: 'utf8', input });
|
|
53
|
+
const readHead = () => JSON.parse(gh(['pr', 'view', String(pr), '--repo', repo, '--json', 'headRefOid,state']));
|
|
54
|
+
const ensureCurrent = () => {
|
|
55
|
+
const current = readHead();
|
|
56
|
+
if (current.state !== 'OPEN' || current.headRefOid !== receipt.headSha) {
|
|
57
|
+
throw new Error('PR is closed or its head differs from the reviewed commit; label withheld');
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
ensureCurrent();
|
|
61
|
+
// The installation is explicit: do not silently add a privileged workflow to someone else's repo.
|
|
62
|
+
gh(['api', `repos/${repo}/contents/.github/workflows/reviewed-before-push.yml`]);
|
|
63
|
+
gh(['api', '--method', 'POST', `repos/${repo}/issues/${pr}/comments`, '--input', '-'], JSON.stringify({ body }));
|
|
64
|
+
gh(['label', 'create', LABEL, '--repo', repo, '--color', '0E8A16', '--description', 'Local review completed for the current PR head before push', '--force']);
|
|
65
|
+
ensureCurrent();
|
|
66
|
+
gh(['pr', 'edit', String(pr), '--repo', repo, '--add-label', LABEL]);
|
|
67
|
+
// Catch a concurrent push after adding the label. The workflow also reconciles later pushes.
|
|
68
|
+
try {
|
|
69
|
+
ensureCurrent();
|
|
70
|
+
} catch (error) {
|
|
71
|
+
gh(['pr', 'edit', String(pr), '--repo', repo, '--remove-label', LABEL]);
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
return { label: LABEL, headSha: receipt.headSha };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function main(argv) {
|
|
78
|
+
const options = {};
|
|
79
|
+
for (let i = 0; i < argv.length; i += 2) {
|
|
80
|
+
if (!['--repo', '--pr', '--receipt'].includes(argv[i]) || !argv[i + 1] || options[argv[i]]) {
|
|
81
|
+
throw new Error('Usage: review-label.mjs --repo owner/repo --pr N --receipt receipt.json');
|
|
82
|
+
}
|
|
83
|
+
options[argv[i]] = argv[i + 1];
|
|
84
|
+
}
|
|
85
|
+
if (!options['--receipt']) throw new Error('--receipt is required');
|
|
86
|
+
const receipt = JSON.parse(readFileSync(options['--receipt'], 'utf8'));
|
|
87
|
+
console.log(JSON.stringify(publishReview({ repo: options['--repo'], pr: options['--pr'], receipt })));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) main(process.argv.slice(2));
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Path-based risk for AUTO-DEPTH. Size alone misses the small PRs that matter:
|
|
3
|
+
// a 10-line change to a workflow file, a session check or a P21 mapping.
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// node risk-paths.mjs [--diff <pr.diff>] -- <changed-file>...
|
|
7
|
+
// git diff --name-only -z <base>...HEAD | node risk-paths.mjs --stdin0 --diff <pr.diff>
|
|
8
|
+
// Use a shell-free process API for file arguments; never interpolate PR paths.
|
|
9
|
+
//
|
|
10
|
+
// Prints {"deep": bool, "needsHumanReview": bool, "categories": [{category, files}]}.
|
|
11
|
+
// `--diff` also lets the identifier `p21` count when it only appears in added lines.
|
|
12
|
+
|
|
13
|
+
import { readFileSync } from 'node:fs';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
|
|
16
|
+
export const RISK_RULES = [
|
|
17
|
+
{ category: 'ci-workflow', test: (f) => /^\.github\/workflows\//.test(f) },
|
|
18
|
+
{ category: 'auth', test: (f) => /(^|[/._-])(auth|authn|authz|authenticate|authenticated|authentication|authorize|authorized|authorization|oauth|session|sessions|permission|permissions|rbac|acl|jwt|login)([/._-]|$)/i.test(f.replace(/([a-z0-9])([A-Z])/g, '$1-$2')) },
|
|
19
|
+
{ category: 'billing', test: (f) => /(billing|payment|payments|invoice|invoices|checkout|stripe|subscription)/i.test(f) },
|
|
20
|
+
{ category: 'installer', test: (f) => /(^|\/)(install|setup|bootstrap|preinstall|postinstall)(?:[^/.]*|[^/]*\.(sh|bash|zsh|ps1|js|mjs|cjs|ts|py|bat|cmd))$/i.test(f) },
|
|
21
|
+
{ category: 'p21', test: (f) => /p21/i.test(f) },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export function addedLinesMention(diffText, pattern) {
|
|
25
|
+
return diffText.split('\n').some((l) => l.startsWith('+') && !l.startsWith('+++') && pattern.test(l));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function riskPaths(files, diffText = '') {
|
|
29
|
+
const categories = RISK_RULES
|
|
30
|
+
.map((rule) => ({ category: rule.category, files: files.filter(rule.test) }))
|
|
31
|
+
.map((c) => (c.category === 'p21' && c.files.length === 0 && addedLinesMention(diffText, /p21/i)
|
|
32
|
+
? { ...c, files: ['(identifier p21 in added lines)'] }
|
|
33
|
+
: c))
|
|
34
|
+
.filter((c) => c.files.length > 0);
|
|
35
|
+
return { deep: categories.length > 0, needsHumanReview: categories.length > 0, categories };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function main(argv) {
|
|
39
|
+
let diffText = '';
|
|
40
|
+
let inputMode = null;
|
|
41
|
+
const files = [];
|
|
42
|
+
for (let i = 0; i < argv.length; i++) {
|
|
43
|
+
const arg = argv[i];
|
|
44
|
+
if (arg === '--') {
|
|
45
|
+
files.push(...argv.slice(i + 1));
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
if (arg === '--diff') {
|
|
49
|
+
if (!argv[i + 1] || argv[i + 1] === '--') throw new Error('--diff requires a file');
|
|
50
|
+
diffText = readFileSync(argv[++i], 'utf8');
|
|
51
|
+
} else if (arg === '--stdin' || arg === '--stdin0') {
|
|
52
|
+
if (inputMode) throw new Error('Choose only one stdin mode');
|
|
53
|
+
inputMode = arg;
|
|
54
|
+
} else if (arg.startsWith('--')) {
|
|
55
|
+
throw new Error(`Unknown option: ${arg}; put filenames after --`);
|
|
56
|
+
} else {
|
|
57
|
+
files.push(arg);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (inputMode) {
|
|
61
|
+
if (files.length) throw new Error('Do not mix stdin and file arguments');
|
|
62
|
+
files.push(...readFileSync(0, 'utf8').split(inputMode === '--stdin0' ? '\0' : '\n').filter(Boolean));
|
|
63
|
+
}
|
|
64
|
+
console.log(JSON.stringify(riskPaths(files, diffText)));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) main(process.argv.slice(2));
|
package/package.json
CHANGED
|
@@ -60,3 +60,13 @@ the diff touches migrations or `*.sql`, `medium` otherwise.
|
|
|
60
60
|
|
|
61
61
|
The posters are self-contained HTML in `assets/`. Open either file in a browser
|
|
62
62
|
at a 1280x720 viewport and screenshot it to regenerate the PNG.
|
|
63
|
+
|
|
64
|
+
## Pre-push review label
|
|
65
|
+
|
|
66
|
+
Branch-mode reviews can apply `reviewed-before-push` after pushing and opening
|
|
67
|
+
or locating the PR. A comment records the reviewed commit, engine, depth, and
|
|
68
|
+
result; unresolved Critical/Important findings prevent labeling. Install the
|
|
69
|
+
[cleanup workflow](references/reviewed-before-push.yml) on the target repo's
|
|
70
|
+
default branch first so new, unreviewed commits lose the label. See the
|
|
71
|
+
[receipt and setup procedure](references/reviewed-before-push.md). This label
|
|
72
|
+
provides visibility and does not replace required remote review or CI.
|
|
@@ -284,23 +284,49 @@ Then:
|
|
|
284
284
|
`LOCAL_BRANCH` is `main`, `master`, `develop`, `staging`, or `production`, stop
|
|
285
285
|
and say so.
|
|
286
286
|
|
|
287
|
-
**Depth:** explicit
|
|
288
|
-
|
|
289
|
-
`gh pr view <PR_NUMBER> --repo <REPO_SLUG> --json additions,deletions,changedFiles,files,baseRefName,state,mergeCommit
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
287
|
+
**Depth:** record an explicit `scan`, `medium`, `deep` or `deps` request;
|
|
288
|
+
otherwise select a provisional depth from the per-file stats. In `pr` mode use
|
|
289
|
+
`gh pr view <PR_NUMBER> --repo <REPO_SLUG> --json additions,deletions,changedFiles,files,baseRefName,state,mergeCommit`.
|
|
290
|
+
Keep `baseRefName` as `BASE_BRANCH`, `changedFiles` as `EXPECTED_FILES`, and
|
|
291
|
+
`mergeCommit.oid` as `MERGE_COMMIT` when merged. In `branch` mode use
|
|
292
|
+
`git diff --numstat <MERGE_BASE> HEAD`.
|
|
293
|
+
|
|
294
|
+
**Mandatory risk check (every depth, including explicit requests):** after
|
|
295
|
+
Track B has saved `pr.diff`, and before finalizing depth or starting Step 2,
|
|
296
|
+
run `scripts/risk-paths.mjs` with that diff and the exact changed paths.
|
|
297
|
+
Resolve the script beside the selected `SKILL.md`: in Codex use
|
|
298
|
+
`<directory containing selected SKILL.md>/scripts/risk-paths.mjs`; in Claude
|
|
299
|
+
use `${CLAUDE_PLUGIN_ROOT}/skills/lekker-review/scripts/risk-paths.mjs`.
|
|
300
|
+
Use a shell-free process API with separate arguments:
|
|
301
|
+
`execFileSync('node', [riskScript, '--diff', diffFile, '--', ...changedPaths])`.
|
|
302
|
+
Never interpolate PR-controlled paths into shell source. Alternatively pipe
|
|
303
|
+
`git diff --name-only -z <MERGE_BASE> HEAD` into
|
|
304
|
+
`node "<resolved riskScript>" --stdin0 --diff "<scratchpad>/pr.diff"`.
|
|
305
|
+
Do not continue on classifier failure; report risk classification unavailable.
|
|
306
|
+
|
|
307
|
+
The classifier detects configured path patterns for CI workflows, auth,
|
|
308
|
+
billing, installers/setup, and P21, plus `p21` in added lines. These are path
|
|
309
|
+
heuristics, not complete detection of auth/billing behavior in generic files;
|
|
310
|
+
reviewers still inspect that behavior in the diff. Whenever
|
|
311
|
+
`needsHumanReview: true`, always put `NEEDS HUMAN REVIEW: <categories>` in the
|
|
312
|
+
review header, even for an explicit scan. A human must read those paths.
|
|
313
|
+
|
|
314
|
+
Explicit depth wins, but never suppresses this marker. Without an explicit
|
|
315
|
+
request, apply AUTO-DEPTH in this order after the risk check:
|
|
316
|
+
- `deep` if the classifier returns `deep: true`, additions+deletions > 800,
|
|
317
|
+
changedFiles > 25, or the diff touches `migrations/` or `*.sql`.
|
|
318
|
+
- `deps` (candidate) if every changed file is a lockfile or `package.json`.
|
|
319
|
+
Confirm after Track A with `scripts/deps-only.mjs`; until then run Track B
|
|
320
|
+
as for `scan`.
|
|
321
|
+
- `scan` if additions+deletions < 150 AND changedFiles <= 5.
|
|
322
|
+
- `medium` otherwise.
|
|
323
|
+
|
|
324
|
+
Lockfiles (`package-lock.json`, `npm-shrinkwrap.json`, `yarn.lock`,
|
|
325
|
+
`pnpm-lock.yaml`, `bun.lock`, `bun.lockb`) do not count toward additions+deletions.
|
|
326
|
+
Step 0 depth is provisional: if the mandatory check upgrades scan, run Track A
|
|
327
|
+
and gather the newly required Track B sources before Step 2. Branch mode uses
|
|
328
|
+
Track A's merge-base diff. State final depth and whether auto-selected in the
|
|
329
|
+
review header.
|
|
304
330
|
|
|
305
331
|
**`--post` flag:** parse and store as `POST_REVIEW=true`.
|
|
306
332
|
|
|
@@ -672,7 +698,7 @@ Write `<scratchpad>/context.json` with keys:
|
|
|
672
698
|
"botPatternsFile": "<BOT_PATTERNS_FILE, or null>",
|
|
673
699
|
"depsReport": "<deps-report.json contents at depth deps, else null>",
|
|
674
700
|
"depsInstall": "<worktree.json depsInstall>",
|
|
675
|
-
"prBody": "<PR body at depth
|
|
701
|
+
"prBody": "<full PR body at every depth; empty string in branch mode>"
|
|
676
702
|
}
|
|
677
703
|
```
|
|
678
704
|
|
|
@@ -1112,8 +1138,17 @@ printf '%s\n' "$REVIEW_FILE" > ~/.cache/lekker-review/reviewed/<headSha>
|
|
|
1112
1138
|
```
|
|
1113
1139
|
|
|
1114
1140
|
Write it against the sha that was actually reviewed. When fix mode later adds
|
|
1115
|
-
commits, write a marker for the
|
|
1116
|
-
|
|
1141
|
+
commits, write a marker for the new head only after fix verification and
|
|
1142
|
+
relevant tests pass. Any unrelated edits require a fresh review; never copy a
|
|
1143
|
+
marker to an unreviewed SHA.
|
|
1144
|
+
|
|
1145
|
+
**PR label for branch reviews:** follow `references/reviewed-before-push.md`.
|
|
1146
|
+
After a complete, non-degraded local review and settling all Critical/Important
|
|
1147
|
+
findings, save the final-head receipt as `<scratchpad>/pre-push-review.json`
|
|
1148
|
+
BEFORE pushing. That reference defines the receipt schema and one-time cleanup
|
|
1149
|
+
workflow setup. The label is exactly `reviewed-before-push` (no AI prefix).
|
|
1150
|
+
A legacy SHA/path marker alone does not qualify. PR-mode reviews cannot earn
|
|
1151
|
+
this label.
|
|
1117
1152
|
|
|
1118
1153
|
**Close the run log** right after the receipt, then print the review:
|
|
1119
1154
|
|
|
@@ -1287,33 +1322,42 @@ Ask once, via AskUserQuestion:
|
|
|
1287
1322
|
|
|
1288
1323
|
Rules for each answer:
|
|
1289
1324
|
|
|
1290
|
-
- **Yes:**
|
|
1325
|
+
- **Yes:** set `PREDICTED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)` before pushing
|
|
1326
|
+
or publishing, including when a PR already exists. Then push with
|
|
1327
|
+
`git push -u origin "$local_branch"` and create or locate the PR. Create it
|
|
1291
1328
|
with `gh pr create --repo "$repo_slug" --base "$base_ref" --head
|
|
1292
1329
|
"$local_branch" --title "$pr_title" --body-file "$pr_body_file"`. Derive the title
|
|
1293
1330
|
from the commit log (the recommended prefix plus a summary of the change) and
|
|
1294
1331
|
the body from a short summary of what the branch does - do not leave either
|
|
1295
|
-
as a placeholder. Report the PR url.
|
|
1296
|
-
|
|
1297
|
-
|
|
1332
|
+
as a placeholder. Report the PR url. If an eligible pre-push receipt was
|
|
1333
|
+
saved, publish it using `scripts/review-label.mjs` as described in
|
|
1334
|
+
`references/reviewed-before-push.md`. This posts the commit/engine/depth/result
|
|
1335
|
+
evidence and applies `reviewed-before-push` only to the matching live head.
|
|
1336
|
+
If cleanup is not installed, omit the label and report the setup requirement;
|
|
1337
|
+
do not undo the successful PR creation. For an existing PR found after a
|
|
1338
|
+
branch-mode push, use the same helper and head check. Then record the prediction
|
|
1339
|
+
against the created or existing PR number using the saved `PREDICTED_AT`;
|
|
1340
|
+
scoring uses it to include bot comments triggered by the push or PR creation,
|
|
1341
|
+
even if they arrive before the record step. Record only the findings fix
|
|
1298
1342
|
mode did NOT apply (every row of the fixes table that is not `✅ applied`):
|
|
1299
1343
|
an applied fix is gone from the code the bots read, so they cannot raise it,
|
|
1300
1344
|
and recording it would sink precision for doing exactly what branch mode is
|
|
1301
1345
|
for. Build `<scratchpad>/unfixed.json` (a bare array is fine) from the merged
|
|
1302
1346
|
`findings.json` minus the findings fix mode applied; when fix mode did not
|
|
1303
|
-
run or applied nothing, it is the whole merged set.
|
|
1304
|
-
|
|
1305
|
-
a bot comment that lands before the record step still counts:
|
|
1347
|
+
run or applied nothing, it is the whole merged set. Keep the timestamp captured
|
|
1348
|
+
before the push; do not replace it at the record step:
|
|
1306
1349
|
|
|
1307
1350
|
```bash
|
|
1308
1351
|
node ${SKILL_ROOT}/scripts/bot-precision.mjs record \
|
|
1309
|
-
--repo "$repo_slug" --pr <
|
|
1352
|
+
--repo "$repo_slug" --pr <PR_NUMBER> --predicted <scratchpad>/unfixed.json \
|
|
1310
1353
|
--at "$PREDICTED_AT"
|
|
1311
1354
|
```
|
|
1312
1355
|
|
|
1313
1356
|
The review is already saved locally; offer `--post` on the new PR number only if the user asks - findings that fix
|
|
1314
1357
|
mode already applied must never be posted as review comments.
|
|
1315
1358
|
- **Push only:** `git push -u origin "$local_branch"` and stop. Say in one line
|
|
1316
|
-
that no PR was created.
|
|
1359
|
+
that no PR was created. Keep any eligible pre-push receipt for publication
|
|
1360
|
+
when a matching PR is later created; no PR means no label yet.
|
|
1317
1361
|
- **No:** stop. Print the branch name and the review file path.
|
|
1318
1362
|
|
|
1319
1363
|
**Unattended runs never ask and never create a PR** (see the operating contract
|
|
@@ -48,6 +48,22 @@ Axes to cover:
|
|
|
48
48
|
fixed, the mitigation fully resolves the defect, or the PR is not a bug fix.
|
|
49
49
|
- Integration Contracts: Shopify API misuse, BC API assumptions, webhook
|
|
50
50
|
idempotency, external API pagination not handled.
|
|
51
|
+
- Rollout safety (external writes): when the diff changes what is WRITTEN to
|
|
52
|
+
another system (an API payload, field mapping, or a DB write into a system
|
|
53
|
+
this service does not own), state whether the PR has a feature flag
|
|
54
|
+
(Reflag / `getShopFlag` / `isEnabled` style check around the new write) or a
|
|
55
|
+
documented revert plan (PR body or code comment). Look in the diff and in the
|
|
56
|
+
PR body in CONTEXT_FILE. When WORKTREE_PATH is available, trace the changed
|
|
57
|
+
write through unchanged callers and flag guards before deciding either is
|
|
58
|
+
absent. Without a worktree, absence from the diff is not proof: report a
|
|
59
|
+
missing rollout control only when the diff demonstrates that the changed
|
|
60
|
+
write bypasses a guard and the PR body provides no revert plan. Otherwise
|
|
61
|
+
mark rollout verification unavailable rather than inventing a finding.
|
|
62
|
+
If neither control exists on that evidence, report an `important` finding:
|
|
63
|
+
`title` starts "No flag or revert plan for external write", `badCode` quotes
|
|
64
|
+
the changed write, `fix` names the flag guard to add. A pure UI/copy change or
|
|
65
|
+
a write that is trivially undone by deploying again is exempt. Quiet when the
|
|
66
|
+
flag or plan is present; do not report its presence.
|
|
51
67
|
|
|
52
68
|
Boundary contract and lifecycle map (required for every Critical/Important
|
|
53
69
|
finding involving routing, persistence, caching, fallback, proxying, or an
|
|
@@ -11,6 +11,22 @@ Axes to cover (Critical/Important only):
|
|
|
11
11
|
- Scalability: N+1 queries, missing pagination, unbounded collections, missing rate-limit
|
|
12
12
|
- Integration Contracts: Shopify API misuse, webhook idempotency not handled
|
|
13
13
|
- Correctness: off-by-one, wrong operator, missing null guard
|
|
14
|
+
- Rollout safety (external writes): when the diff changes what is WRITTEN to
|
|
15
|
+
another system (an API payload, field mapping, or a DB write into a system
|
|
16
|
+
this service does not own), state whether the PR has a feature flag
|
|
17
|
+
(Reflag / `getShopFlag` / `isEnabled` style check around the new write) or a
|
|
18
|
+
documented revert plan (PR body or code comment). Look in the diff and in the
|
|
19
|
+
PR body in CONTEXT_FILE. When WORKTREE_PATH is available, trace the changed
|
|
20
|
+
write through unchanged callers and flag guards before deciding either is
|
|
21
|
+
absent. Without a worktree, absence from the diff is not proof: report a
|
|
22
|
+
missing rollout control only when the diff demonstrates that the changed
|
|
23
|
+
write bypasses a guard and the PR body provides no revert plan. Otherwise
|
|
24
|
+
mark rollout verification unavailable rather than inventing a finding.
|
|
25
|
+
If neither control exists on that evidence, report an `important` finding:
|
|
26
|
+
`title` starts "No flag or revert plan for external write", `badCode` quotes
|
|
27
|
+
the changed write, `fix` names the flag guard to add. A pure UI/copy change or
|
|
28
|
+
a write that is trivially undone by deploying again is exempt. Quiet when the
|
|
29
|
+
flag or plan is present; do not report its presence.
|
|
14
30
|
|
|
15
31
|
CI_STATUS: read key "ciStatus" from the JSON file CONTEXT_FILE.
|
|
16
32
|
EXISTING_REVIEWS: read key "existingReviews" from CONTEXT_FILE (awareness only, skip findings at same file:line)
|
|
@@ -24,6 +24,7 @@ Output the review as a markdown response (not via printf). No ANSI escapes.
|
|
|
24
24
|
**Proofs:** <M proven / N attempted> *(include only when N > 0)*
|
|
25
25
|
**CI:** <✅ All passing | ⚠️ N failing: check-name | ⏳ Pending | N/A>
|
|
26
26
|
**Depth:** <📦 deps | ⚡ scan | 🔍 medium | 🔬 deep>
|
|
27
|
+
**NEEDS HUMAN REVIEW:** <categories from risk-paths.mjs> *(only when `needsHumanReview` is true: a person must read these paths whatever the verdict)*
|
|
27
28
|
**Engine:** <workflow | codex | revmux>
|
|
28
29
|
|
|
29
30
|
---
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Reviewed before push
|
|
2
|
+
|
|
3
|
+
`reviewed-before-push` records a completed local branch review of the current
|
|
4
|
+
PR head. It is informational: it does not satisfy CI, required remote review,
|
|
5
|
+
or the human-review marker for risky changes. The review comment records the
|
|
6
|
+
full head/base SHAs, engine, depth, timestamp, and result. It is an attestation
|
|
7
|
+
by the publishing user, not cryptographic proof that review preceded a push.
|
|
8
|
+
|
|
9
|
+
## Install stale-label cleanup once per repository
|
|
10
|
+
|
|
11
|
+
Copy `references/reviewed-before-push.yml` into the target repository as
|
|
12
|
+
`.github/workflows/reviewed-before-push.yml` and land it on the default branch
|
|
13
|
+
through that repository's normal review process. Do this before the local
|
|
14
|
+
review whose receipt will be published. Do not add a workflow commit after
|
|
15
|
+
review and then label that unreviewed commit.
|
|
16
|
+
|
|
17
|
+
The workflow uses only GitHub API data, with no PR checkout. On new commits or
|
|
18
|
+
receipt edits/deletion it removes the label unless a receipt from an owner,
|
|
19
|
+
member, or collaborator matches the live head. Pagination includes all receipt
|
|
20
|
+
comments. It does not add labels or replace merge gates. A newly reviewed head
|
|
21
|
+
can be labeled again by the publisher. Cleanup is asynchronous; the label is
|
|
22
|
+
not a merge-authorization signal. Workflows disabled by repository policy or
|
|
23
|
+
Actions outages can delay removal.
|
|
24
|
+
|
|
25
|
+
The helper refuses to publish if the cleanup workflow is absent. If setup is
|
|
26
|
+
missing, keep the local review and PR creation successful, omit the label, and
|
|
27
|
+
report the missing setup. Never silently install a write-capable workflow in
|
|
28
|
+
another repository. A push-only run keeps the receipt locally until a PR exists.
|
|
29
|
+
|
|
30
|
+
## Record before pushing
|
|
31
|
+
|
|
32
|
+
Only in `MODE=branch`, after the review engine completed without degraded or
|
|
33
|
+
missing required dimensions, settle every Critical/Important finding. Either
|
|
34
|
+
fix it with verifier/test evidence or explicitly rebut it with a reason. Open
|
|
35
|
+
observations may remain. A file containing only a review path or a SHA marker
|
|
36
|
+
is not sufficient evidence.
|
|
37
|
+
|
|
38
|
+
Save `<scratchpad>/pre-push-review.json` before the push:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"version": 1,
|
|
43
|
+
"mode": "branch",
|
|
44
|
+
"result": "passed",
|
|
45
|
+
"headSha": "<full reviewed final local HEAD>",
|
|
46
|
+
"baseSha": "<full merge-base used by this review>",
|
|
47
|
+
"engine": "codex",
|
|
48
|
+
"depth": "medium",
|
|
49
|
+
"reviewedAt": "<UTC ISO timestamp when this review completed>",
|
|
50
|
+
"findings": [
|
|
51
|
+
{
|
|
52
|
+
"severity": "important",
|
|
53
|
+
"disposition": "fixed",
|
|
54
|
+
"evidence": "<actual fix verification and relevant test result>"
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Use an empty `findings` array when the review found nothing. Otherwise include
|
|
61
|
+
every finding in the merged review and lint set. Allowed dispositions are
|
|
62
|
+
`fixed`, `rebutted`, and `open`; `open` is permitted only for observations and
|
|
63
|
+
idiomatic suggestions. Rebuttals require specific evidence too. Set engine and
|
|
64
|
+
depth to the values actually used. Keep the saved full review locally; finding
|
|
65
|
+
text, evidence text, and local paths are not uploaded by the helper.
|
|
66
|
+
|
|
67
|
+
`reviewedAt` must be a valid UTC ISO timestamp in `YYYY-MM-DDTHH:mm:ssZ`
|
|
68
|
+
format, optionally with fractional seconds before `Z` (for example,
|
|
69
|
+
`2026-10-29T09:00:00.123Z`). Date-only strings, timezone offsets, and invalid
|
|
70
|
+
calendar dates are rejected by the publisher and cleanup workflow.
|
|
71
|
+
|
|
72
|
+
After fixes, the receipt must describe the final SHA: fix verification and
|
|
73
|
+
relevant tests must pass, and the only intervening edits must be those verified
|
|
74
|
+
fixes. Any unrelated edit requires a fresh review. Never copy a receipt to a
|
|
75
|
+
new SHA just to make the label apply.
|
|
76
|
+
|
|
77
|
+
## Publish after pushing and creating or locating the PR
|
|
78
|
+
|
|
79
|
+
Resolve the helper relative to the selected `SKILL.md` (same directory's
|
|
80
|
+
`scripts/review-label.mjs`, including in Codex). Invoke with separate process
|
|
81
|
+
arguments, or independently shell-quote each value:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
node "<SKILL_ROOT>/scripts/review-label.mjs" \
|
|
85
|
+
--repo "<owner/repo>" --pr "<number>" \
|
|
86
|
+
--receipt "<scratchpad>/pre-push-review.json"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The helper checks the live open PR head, posts the receipt, creates the label
|
|
90
|
+
if needed, and applies it. It checks the head before and after labeling, and
|
|
91
|
+
removes the label if a concurrent push changed the head. Errors are failures
|
|
92
|
+
to publish the label, not a failed local review. Do not claim the label was
|
|
93
|
+
applied when the helper failed. Do not use this helper for a review performed
|
|
94
|
+
in `MODE=pr` after pushing.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Install this file on the target repository's default branch as
|
|
2
|
+
# .github/workflows/reviewed-before-push.yml before publishing review labels.
|
|
3
|
+
name: Reviewed before push
|
|
4
|
+
on:
|
|
5
|
+
pull_request_target:
|
|
6
|
+
types: [opened, reopened, synchronize, edited, labeled]
|
|
7
|
+
issue_comment:
|
|
8
|
+
types: [created, edited, deleted]
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
issues: write
|
|
12
|
+
pull-requests: read
|
|
13
|
+
concurrency:
|
|
14
|
+
group: reviewed-before-push-${{ github.event.pull_request.number || github.event.issue.number }}
|
|
15
|
+
cancel-in-progress: false
|
|
16
|
+
jobs:
|
|
17
|
+
reconcile:
|
|
18
|
+
if: github.event.pull_request || github.event.issue.pull_request
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
# No checkout: never execute code from the PR with this write token.
|
|
22
|
+
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
23
|
+
with:
|
|
24
|
+
script: |
|
|
25
|
+
const label = 'reviewed-before-push';
|
|
26
|
+
function isUtcTimestamp(value) {
|
|
27
|
+
const match = typeof value === 'string' && value.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d+))?Z$/);
|
|
28
|
+
if (!match || match[0] !== value) return false;
|
|
29
|
+
const time = Date.parse(value);
|
|
30
|
+
const canonical = `${match[1]}.${(match[2] || '').padEnd(3, '0').slice(0, 3)}Z`;
|
|
31
|
+
return Number.isFinite(time) && new Date(time).toISOString() === canonical;
|
|
32
|
+
}
|
|
33
|
+
const { owner, repo } = context.repo;
|
|
34
|
+
const issue_number = context.payload.pull_request?.number || context.payload.issue.number;
|
|
35
|
+
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: issue_number });
|
|
36
|
+
if (!pr.labels.some(item => item.name === label)) return;
|
|
37
|
+
const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number, per_page: 100 });
|
|
38
|
+
const valid = comments.some(comment => {
|
|
39
|
+
if (!['OWNER', 'MEMBER', 'COLLABORATOR'].includes(comment.author_association)) return false;
|
|
40
|
+
const match = comment.body?.match(/<!-- lekker:reviewed-before-push (\{[^\n]*\}) -->/);
|
|
41
|
+
if (!match) return false;
|
|
42
|
+
try {
|
|
43
|
+
const r = JSON.parse(match[1]);
|
|
44
|
+
return r.version === 1 && r.mode === 'branch' && r.result === 'passed'
|
|
45
|
+
&& r.headSha === pr.head.sha && /^[a-f0-9]{40}$/.test(r.baseSha)
|
|
46
|
+
&& ['workflow', 'codex', 'revmux'].includes(r.engine)
|
|
47
|
+
&& ['scan', 'medium', 'deep', 'deps'].includes(r.depth)
|
|
48
|
+
&& isUtcTimestamp(r.reviewedAt);
|
|
49
|
+
} catch { return false; }
|
|
50
|
+
});
|
|
51
|
+
if (!valid) {
|
|
52
|
+
try {
|
|
53
|
+
await github.rest.issues.removeLabel({ owner, repo, issue_number, name: label });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (error.status !== 404) throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Publish a local branch-review receipt using gh; PR-controlled values never enter shell source.
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
export const LABEL = 'reviewed-before-push';
|
|
8
|
+
export const PREFIX = '<!-- lekker:reviewed-before-push ';
|
|
9
|
+
const SHA = /^[a-f0-9]{40}$/;
|
|
10
|
+
|
|
11
|
+
function isUtcTimestamp(value) {
|
|
12
|
+
const match = typeof value === 'string' && value.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d+))?Z$/);
|
|
13
|
+
if (!match || match[0] !== value) return false;
|
|
14
|
+
const time = Date.parse(value);
|
|
15
|
+
const canonical = `${match[1]}.${(match[2] || '').padEnd(3, '0').slice(0, 3)}Z`;
|
|
16
|
+
return Number.isFinite(time) && new Date(time).toISOString() === canonical;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function validateReceipt(receipt) {
|
|
20
|
+
if (receipt?.version !== 1 || receipt.mode !== 'branch' || receipt.result !== 'passed'
|
|
21
|
+
|| !SHA.test(receipt.headSha) || !SHA.test(receipt.baseSha)
|
|
22
|
+
|| !['workflow', 'codex', 'revmux'].includes(receipt.engine)
|
|
23
|
+
|| !['scan', 'medium', 'deep', 'deps'].includes(receipt.depth)
|
|
24
|
+
|| !isUtcTimestamp(receipt.reviewedAt)
|
|
25
|
+
|| !Array.isArray(receipt.findings)) {
|
|
26
|
+
throw new Error('Invalid branch-review receipt');
|
|
27
|
+
}
|
|
28
|
+
for (const finding of receipt.findings) {
|
|
29
|
+
if (!['critical', 'important', 'observation', 'idiomatic'].includes(finding.severity)
|
|
30
|
+
|| !['fixed', 'rebutted', 'open'].includes(finding.disposition)
|
|
31
|
+
|| (['critical', 'important'].includes(finding.severity) && finding.disposition === 'open')
|
|
32
|
+
|| (['fixed', 'rebutted'].includes(finding.disposition) && !finding.evidence?.trim())) {
|
|
33
|
+
throw new Error('Findings require dispositions and evidence; no open Critical/Important finding is allowed');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return receipt;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function reviewComment(receipt) {
|
|
40
|
+
validateReceipt(receipt);
|
|
41
|
+
// Only publish the attestation, not local paths or potentially sensitive finding text.
|
|
42
|
+
const { version, mode, result, headSha, baseSha, engine, depth, reviewedAt } = receipt;
|
|
43
|
+
const attestation = { version, mode, result, headSha, baseSha, engine, depth, reviewedAt };
|
|
44
|
+
return `Reviewed before push\n\nCommit: \`${headSha}\`\nBase: \`${baseSha}\`\nEngine: ${engine}\nDepth: ${depth}\nReviewed at: ${reviewedAt}\nResult: passed; no unresolved Critical/Important findings.\n\nThis records a local review; required CI and remote review gates still apply.\n\n${PREFIX}${JSON.stringify(attestation)} -->`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function publishReview({ repo, pr, receipt }, run = execFileSync) {
|
|
48
|
+
if (!/^[\w.-]+\/[\w.-]+$/.test(repo) || !/^[1-9]\d*$/.test(String(pr))) {
|
|
49
|
+
throw new Error('Expected --repo owner/repo and a positive --pr number');
|
|
50
|
+
}
|
|
51
|
+
const body = reviewComment(receipt);
|
|
52
|
+
const gh = (args, input) => run('gh', args, { encoding: 'utf8', input });
|
|
53
|
+
const readHead = () => JSON.parse(gh(['pr', 'view', String(pr), '--repo', repo, '--json', 'headRefOid,state']));
|
|
54
|
+
const ensureCurrent = () => {
|
|
55
|
+
const current = readHead();
|
|
56
|
+
if (current.state !== 'OPEN' || current.headRefOid !== receipt.headSha) {
|
|
57
|
+
throw new Error('PR is closed or its head differs from the reviewed commit; label withheld');
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
ensureCurrent();
|
|
61
|
+
// The installation is explicit: do not silently add a privileged workflow to someone else's repo.
|
|
62
|
+
gh(['api', `repos/${repo}/contents/.github/workflows/reviewed-before-push.yml`]);
|
|
63
|
+
gh(['api', '--method', 'POST', `repos/${repo}/issues/${pr}/comments`, '--input', '-'], JSON.stringify({ body }));
|
|
64
|
+
gh(['label', 'create', LABEL, '--repo', repo, '--color', '0E8A16', '--description', 'Local review completed for the current PR head before push', '--force']);
|
|
65
|
+
ensureCurrent();
|
|
66
|
+
gh(['pr', 'edit', String(pr), '--repo', repo, '--add-label', LABEL]);
|
|
67
|
+
// Catch a concurrent push after adding the label. The workflow also reconciles later pushes.
|
|
68
|
+
try {
|
|
69
|
+
ensureCurrent();
|
|
70
|
+
} catch (error) {
|
|
71
|
+
gh(['pr', 'edit', String(pr), '--repo', repo, '--remove-label', LABEL]);
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
return { label: LABEL, headSha: receipt.headSha };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function main(argv) {
|
|
78
|
+
const options = {};
|
|
79
|
+
for (let i = 0; i < argv.length; i += 2) {
|
|
80
|
+
if (!['--repo', '--pr', '--receipt'].includes(argv[i]) || !argv[i + 1] || options[argv[i]]) {
|
|
81
|
+
throw new Error('Usage: review-label.mjs --repo owner/repo --pr N --receipt receipt.json');
|
|
82
|
+
}
|
|
83
|
+
options[argv[i]] = argv[i + 1];
|
|
84
|
+
}
|
|
85
|
+
if (!options['--receipt']) throw new Error('--receipt is required');
|
|
86
|
+
const receipt = JSON.parse(readFileSync(options['--receipt'], 'utf8'));
|
|
87
|
+
console.log(JSON.stringify(publishReview({ repo: options['--repo'], pr: options['--pr'], receipt })));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) main(process.argv.slice(2));
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Path-based risk for AUTO-DEPTH. Size alone misses the small PRs that matter:
|
|
3
|
+
// a 10-line change to a workflow file, a session check or a P21 mapping.
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// node risk-paths.mjs [--diff <pr.diff>] -- <changed-file>...
|
|
7
|
+
// git diff --name-only -z <base>...HEAD | node risk-paths.mjs --stdin0 --diff <pr.diff>
|
|
8
|
+
// Use a shell-free process API for file arguments; never interpolate PR paths.
|
|
9
|
+
//
|
|
10
|
+
// Prints {"deep": bool, "needsHumanReview": bool, "categories": [{category, files}]}.
|
|
11
|
+
// `--diff` also lets the identifier `p21` count when it only appears in added lines.
|
|
12
|
+
|
|
13
|
+
import { readFileSync } from 'node:fs';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
|
|
16
|
+
export const RISK_RULES = [
|
|
17
|
+
{ category: 'ci-workflow', test: (f) => /^\.github\/workflows\//.test(f) },
|
|
18
|
+
{ category: 'auth', test: (f) => /(^|[/._-])(auth|authn|authz|authenticate|authenticated|authentication|authorize|authorized|authorization|oauth|session|sessions|permission|permissions|rbac|acl|jwt|login)([/._-]|$)/i.test(f.replace(/([a-z0-9])([A-Z])/g, '$1-$2')) },
|
|
19
|
+
{ category: 'billing', test: (f) => /(billing|payment|payments|invoice|invoices|checkout|stripe|subscription)/i.test(f) },
|
|
20
|
+
{ category: 'installer', test: (f) => /(^|\/)(install|setup|bootstrap|preinstall|postinstall)(?:[^/.]*|[^/]*\.(sh|bash|zsh|ps1|js|mjs|cjs|ts|py|bat|cmd))$/i.test(f) },
|
|
21
|
+
{ category: 'p21', test: (f) => /p21/i.test(f) },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export function addedLinesMention(diffText, pattern) {
|
|
25
|
+
return diffText.split('\n').some((l) => l.startsWith('+') && !l.startsWith('+++') && pattern.test(l));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function riskPaths(files, diffText = '') {
|
|
29
|
+
const categories = RISK_RULES
|
|
30
|
+
.map((rule) => ({ category: rule.category, files: files.filter(rule.test) }))
|
|
31
|
+
.map((c) => (c.category === 'p21' && c.files.length === 0 && addedLinesMention(diffText, /p21/i)
|
|
32
|
+
? { ...c, files: ['(identifier p21 in added lines)'] }
|
|
33
|
+
: c))
|
|
34
|
+
.filter((c) => c.files.length > 0);
|
|
35
|
+
return { deep: categories.length > 0, needsHumanReview: categories.length > 0, categories };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function main(argv) {
|
|
39
|
+
let diffText = '';
|
|
40
|
+
let inputMode = null;
|
|
41
|
+
const files = [];
|
|
42
|
+
for (let i = 0; i < argv.length; i++) {
|
|
43
|
+
const arg = argv[i];
|
|
44
|
+
if (arg === '--') {
|
|
45
|
+
files.push(...argv.slice(i + 1));
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
if (arg === '--diff') {
|
|
49
|
+
if (!argv[i + 1] || argv[i + 1] === '--') throw new Error('--diff requires a file');
|
|
50
|
+
diffText = readFileSync(argv[++i], 'utf8');
|
|
51
|
+
} else if (arg === '--stdin' || arg === '--stdin0') {
|
|
52
|
+
if (inputMode) throw new Error('Choose only one stdin mode');
|
|
53
|
+
inputMode = arg;
|
|
54
|
+
} else if (arg.startsWith('--')) {
|
|
55
|
+
throw new Error(`Unknown option: ${arg}; put filenames after --`);
|
|
56
|
+
} else {
|
|
57
|
+
files.push(arg);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (inputMode) {
|
|
61
|
+
if (files.length) throw new Error('Do not mix stdin and file arguments');
|
|
62
|
+
files.push(...readFileSync(0, 'utf8').split(inputMode === '--stdin0' ? '\0' : '\n').filter(Boolean));
|
|
63
|
+
}
|
|
64
|
+
console.log(JSON.stringify(riskPaths(files, diffText)));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) main(process.argv.slice(2));
|