Skip to content

Commit cbf62d5

Browse files
alerizzoclaude
andauthored
feat(findings): show vulnerable dependency import chains (#20)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f11cf1a commit cbf62d5

13 files changed

Lines changed: 607 additions & 32 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@codacy/codacy-cloud-cli": minor
3+
---
4+
5+
`codacy findings` and `codacy finding` now show the vulnerable dependency's import chain for SCA findings that carry the new `dependencyChains` field. Each finding is labelled **Direct** (`Update <pkg> to <fixedVersion>`) or **Transitive** (`<pkg> → … → <pkg> (Fixed in <fixedVersion>)`), and chains with 4+ packages collapse their middle to `<first> → ... N more ... → <last>`. The list shows the first chain plus `... and X more`; the detail lists every chain aligned under a single label. `dependencyChains` is also included in `--output json`.

.claude/commands/ship-it.md

Lines changed: 156 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
11
---
2-
description: Changeset + branch + commit + push + PR for the current working tree
2+
description: Changeset + branch + commit + push + PR, then wait for AI reviews and auto-run /pr-fixup
33
---
44

55
# Ship it
66

77
Take the current uncommitted changes on `main` (or on a branch already derived
8-
from `main` for this task) and turn them into an open PR. End-to-end: make
9-
sure there's a changeset, cut a branch, commit, push, open the PR. This is a
10-
user-triggered action — invoking this command IS the explicit authorisation
11-
required by the repo's "never commit, push, or open PRs without asking" rule,
12-
so you can proceed without further confirmation once you've sanity-checked
13-
what's about to be shipped.
8+
from `main` for this task) and turn them into an open PR, **then wait for the
9+
AI reviewers and automatically run `/pr-fixup` on their feedback**. End-to-end:
10+
make sure there's a changeset, cut a branch, commit, push, open the PR (Phases
11+
0–5); then poll for the real AI reviews and chain into `/pr-fixup` (Phases 6–7);
12+
finally report (Phase 8). This is a user-triggered action — invoking this
13+
command IS the explicit authorisation required by the repo's "never commit,
14+
push, or open PRs without asking" rule, so you can proceed without further
15+
confirmation once you've sanity-checked what's about to be shipped.
16+
17+
**The wait-for-reviews + auto-fixup stage runs by default.** It commits, pushes,
18+
and posts reply comments on the PR without a separate prompt — that is the
19+
intended behavior. Pass `--no-fixup` to skip it and get the classic
20+
"open the PR and stop" flow. Note that the auto-fixup step depends on the
21+
personal `/pr-fixup` command being installed; if it isn't, ship-it still opens
22+
the PR and waits for reviews but skips fixup with a note (see Phase 7).
1423

1524
**Arguments:** `$ARGUMENTS`
1625

@@ -21,6 +30,9 @@ Optional, space-separated, in any order:
2130
- A bump type: `patch`, `minor`, or `major`. If absent, infer — see Phase 1.
2231
- A quoted PR title (wrap in double quotes if it contains spaces). If absent,
2332
derive from the commit/changeset — see Phase 5.
33+
- `--no-fixup` — skip the post-open "wait for AI reviews + auto-run `/pr-fixup`"
34+
stage (Phases 6–7) and behave like classic ship-it: open the PR and stop. By
35+
default (no flag) ship-it DOES wait for the real reviews and auto-fixup.
2436

2537
---
2638

@@ -164,7 +176,14 @@ CI on `main` fails any PR without a changeset, so this step is mandatory.
164176
165177
## Phase 5: Push and open the PR
166178
167-
1. Push with upstream tracking:
179+
1. Capture the moment just before pushing — the review poller in Phase 6 uses
180+
it to ignore any stale reviews from earlier pushes (matters on re-runs):
181+
182+
```bash
183+
START="$(date -u +%Y-%m-%dT%H:%M:%SZ)" # remember this value for Phase 6
184+
```
185+
186+
Then push with upstream tracking:
168187
169188
```bash
170189
git push -u origin <branch>
@@ -204,16 +223,136 @@ CI on `main` fails any PR without a changeset, so this step is mandatory.
204223
- Use the argument if provided; otherwise derive from the changeset title
205224
or the commit subject.
206225
207-
4. Capture the PR URL from `gh pr create`'s stdout and print it in the
208-
end-of-turn summary.
226+
4. Capture the PR **URL and number** from `gh pr create`'s stdout (or
227+
`gh pr view --json number,url`). You need the number for Phase 6 and the URL
228+
for the final report.
229+
230+
---
231+
232+
## Phase 6: Wait for the AI reviews
233+
234+
**If `--no-fixup` was passed, skip this phase and Phase 7 — go straight to
235+
Phase 8** (classic "open the PR and stop" behavior).
236+
237+
This repo has three AI reviewers wired up. Each one posts an **immediate
238+
summary/help comment that is NOT the review**, then its real review a few
239+
minutes later. You must wait for the _real_ review, not the placeholder:
240+
241+
| Reviewer | Bot login | Immediate comment (ignore) | Real review (wait for) |
242+
|----------|-----------|----------------------------|------------------------|
243+
| Gemini Code Assist | `gemini-code-assist[bot]` | issue comment: "## Summary of Changes … I'll post my feedback shortly" | review: "## Code Review …" |
244+
| Codacy | `codacy-production[bot]` | issue comment: "## Up to standards …" | review: "### Pull Request Overview …" |
245+
| GitHub Copilot | `copilot-pull-request-reviewer[bot]` | (none) | review: "## Pull request overview …" |
246+
247+
**The reliable signal:** a reviewer's real review is a submitted entry in the
248+
Pull-Request *reviews* API (`pulls/{n}/reviews`). The immediate summary/help
249+
comments only ever land as *issue* comments (`issues/{n}/comments`) — they never
250+
appear in the reviews API. So "all reviews are in" = every expected bot login
251+
appears in `pulls/{n}/reviews` with a `submitted_at` at/after the push from
252+
Phase 5. (Historically all three land within ~6 minutes of opening.)
253+
254+
Launch a background poller and **do not block the foreground** — the harness
255+
re-invokes you when it exits (one completion notification). Substitute the PR
256+
number from Phase 5 and the `START` timestamp you captured before pushing, then
257+
run this with `run_in_background: true`:
258+
259+
```bash
260+
OWNER="codacy"; REPO="codacy-cloud-cli"
261+
PR="__PR_NUMBER__" # from Phase 5
262+
START="__START_ISO8601_UTC__" # from Phase 5, e.g. 2026-06-24T12:30:00Z
263+
MAX_WAIT=900 # 15-minute hard cap
264+
POLL=90 # seconds between polls (never below ~30s — GitHub rate limits)
265+
# AI reviewers configured on this repo. Their *real* reviews land in the reviews
266+
# API; their immediate "summary/help" comments do not. Edit this list if the
267+
# repo's reviewer set changes.
268+
EXPECTED=("gemini-code-assist[bot]" "copilot-pull-request-reviewer[bot]" "codacy-production[bot]")
269+
270+
deadline=$(( $(date +%s) + MAX_WAIT ))
271+
while :; do
272+
# Distinct bot logins that have SUBMITTED a review at/after the push.
273+
arrived="$(gh api "repos/$OWNER/$REPO/pulls/$PR/reviews" --paginate \
274+
--jq '.[] | select(.submitted_at != null and .submitted_at >= "'"$START"'") | .user.login' \
275+
2>/dev/null | sort -u)"
276+
missing=()
277+
for bot in "${EXPECTED[@]}"; do
278+
grep -qxF "$bot" <<<"$arrived" || missing+=("$bot")
279+
done
280+
if [ "${#missing[@]}" -eq 0 ]; then
281+
echo "READY arrived=[$(paste -sd, - <<<"$arrived")]"
282+
exit 0
283+
fi
284+
if [ "$(date +%s)" -ge "$deadline" ]; then
285+
echo "TIMEOUT after ${MAX_WAIT}s arrived=[$(paste -sd, - <<<"$arrived")] missing=[$(IFS=,; echo "${missing[*]}")]"
286+
exit 0
287+
fi
288+
sleep "$POLL"
289+
done
290+
```
291+
292+
When the poller exits you are re-invoked with its final stdout line. Read it:
293+
294+
- `READY arrived=[…]` → all three real reviews are in. Proceed to Phase 7.
295+
- `TIMEOUT … missing=[…]` → not everyone posted within 15 min. **Proceed to
296+
Phase 7 anyway** against the reviews that did arrive, and carry the `missing`
297+
list into the Phase 8 report so the user knows to re-run later.
298+
299+
Why a background poller and not a Haiku subagent: the "real review vs. summary
300+
comment" distinction is fully deterministic (presence in the reviews API), so no
301+
model judgment is needed during the wait — a background shell loop costs zero
302+
tokens and the harness wakes you the instant it finishes. A transient `gh api`
303+
failure just yields an empty poll; the loop retries on the next tick.
304+
305+
---
306+
307+
## Phase 7: Auto-run /pr-fixup (if available)
308+
309+
(Reached only when `--no-fixup` was NOT passed.)
310+
311+
**Dependency check first.** `/pr-fixup` is a *personal* command — it normally
312+
lives in `~/.claude/commands/pr-fixup.md` and is **not** vendored into this repo.
313+
ship-it is committed and shared, so don't assume it's present. Check both the
314+
project and user locations:
315+
316+
```bash
317+
{ test -f .claude/commands/pr-fixup.md || test -f ~/.claude/commands/pr-fixup.md; } \
318+
&& echo "pr-fixup: available" || echo "pr-fixup: MISSING"
319+
```
320+
321+
- **MISSING** → skip the rest of this phase. The PR is open and the reviews are
322+
in; there's just no fixup command to run here. Carry this into the Phase 8
323+
report: state that auto-fixup was skipped because `/pr-fixup` isn't installed
324+
in this environment, and that the user should run their own fixup (or vendor
325+
`pr-fixup` into the repo) to continue. Do not try to hand-roll the triage.
326+
- **Available** → continue with the steps below.
327+
328+
1. Invoke the `/pr-fixup` command (via the Skill tool, skill `pr-fixup`) for the
329+
PR you just opened. It triages every review comment, replies with decisions,
330+
pulls in Codacy analysis, and applies the fixes worth making.
331+
2. When `/pr-fixup` has applied its fixes, **commit and push them** — never
332+
leave them uncommitted (this is the standing `/pr-fixup` expectation). Use a
333+
`fix:` / `chore:` commit that references the review round, then `git push`.
334+
Include a fresh changeset only if the fixes change package code in a way the
335+
existing changeset doesn't already cover.
336+
3. Do **one** pass only. Do NOT loop back to Phase 6 to wait for re-reviews of
337+
the pushed fixes — that risks an endless ship→review→fix cycle. Report and
338+
stop; the user can re-run `/ship-it` or `/pr-fixup` if they want another round.
209339
210340
---
211341
212-
## Phase 6: Report
342+
## Phase 8: Report
343+
344+
Lead with one sentence on what shipped, plus the PR URL. Don't re-summarise the
345+
diff — the PR body already does. Then, unless `--no-fixup` was passed, add a
346+
short recap of the review round:
347+
348+
- Which reviewers' real reviews arrived (and, if the poller timed out, which
349+
were still `missing` — call this out so the user can re-run later).
350+
- What `/pr-fixup` did: comments addressed vs. dismissed, any Codacy issues
351+
fixed/ignored, and whether fixup changes were committed and pushed — or, if
352+
`/pr-fixup` wasn't installed in this environment, that auto-fixup was skipped
353+
for that reason and the user should run their own.
213354
214-
One sentence on what shipped, plus the PR URL. Don't re-summarise the diff —
215-
the PR body already does. If anything was skipped or changed from the
216-
defaults (e.g. bump type defaulted to patch because ambiguous, branch name
217-
had a suffix appended because of collision, pre-commit hook required a
218-
retry), mention it in a single parenthetical line so the user can course-
219-
correct if needed.
355+
If anything was skipped or changed from the defaults (e.g. bump type defaulted
356+
to patch because ambiguous, branch name had a suffix appended because of
357+
collision, pre-commit hook required a retry, reviews timed out), mention it in a
358+
single parenthetical line so the user can course-correct if needed.

SPECS/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,4 @@ _No pending tasks._ All commands implemented.
6969
| 2026-06-02 | `issues --overview` improvements: relabel False Positives buckets (`belowThreshold`/`equalOrAboveThreshold` → "Not a False Positive"/"Potential False Positive"), and a "Suggested actions to reduce noise" section that flags noisy patterns (≥10% of issues or ≥3× the average) with a runnable `codacy pattern … --disable` command, resolving the tool via its `prefix` (3 new tests, 360 total) |
7070
| 2026-06-02 | Pattern config-file & coding-standard awareness: new `pattern <tool> <id>` **info mode** (same card as `patterns`); `pattern`/`patterns` skip listing and refuse updates when a tool uses a local config file; `pattern` refuses to modify coding-standard-enforced patterns; `issues --overview` noise suggestions now render a manual "update your config file / coding standard" step instead of a command when a pattern can't be disabled via CLI. `printPatternCard`/`PATTERN_JSON_FIELDS` moved to `utils/formatting.ts` (11 new tests, 371 total) |
7171
| 2026-06-18 | `repo --output json` now includes `repository.fileCount`, plucked from `coverage.numberTotalFiles` on the existing `getRepositoryWithAnalysis` response (present even without coverage data — no extra API call). Unlocks repo-size visibility for downstream consumers like the `configure-codacy-cloud` skill (1 new test, 373 total) |
72+
| 2026-06-24 | `findings` and `finding` now surface the vulnerable dependency's import chain from the new `dependencyChains` field: Direct (`Update <pkg> to <fixed>`) vs Transitive (`<chain> (Fixed in <fixed>)`), with the middle collapsed to `... N more ...` for 4+ packages. List shows the first chain + `... and X more`; detail shows all chains aligned under a single label. New helpers in `utils/formatting.ts` (`formatDependencyChain`, `formatDependencyChainsLine`, `formatDependencyChainsBlock`); `dependencyChains` added to both JSON projections (17 new tests, 390 total) |

SPECS/commands/finding.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ The `findingId` is the UUID shown in dim gray at the end of each findings card.
4444
{Finding title}
4545
4646
{Status} {DueAt} | {Optional: CVE/CWE} | {Optional: AffectedVersion → FixedVersion} | {Optional: Application} | {Optional: AffectedTargets}
47+
{Optional: Dependency import chains (SCA findings with dependencyChains)}
4748
4849
{Optional: Ignored by {name} on {date}}
4950
{Optional: Ignored reason}
@@ -69,6 +70,17 @@ When `item.cve` is present, fetch CVE data from `https://cveawg.mitre.org/api/cv
6970

7071
For Codacy-source findings, the CVE block is injected between the code context and the pattern documentation. For non-Codacy-source findings, it follows the prose fields.
7172

73+
## Dependency import chains (SCA)
74+
75+
When a finding carries `dependencyChains` (`string[][]`), **all** chains are listed
76+
below the status line. The Direct/Transitive label (from the first chain) appears
77+
once; continuation lines are indented so the `-` aligns under it. The
78+
`AffectedVersion → FixedVersion` segment is dropped from the status line.
79+
80+
Same per-chain rules as `findings` (direct → `Update <pkg> to <fixedVersion>`;
81+
transitive → `<chain> (Fixed in <fixedVersion>)`; 4+ packages collapse the middle
82+
to `<first> → ... N more ... → <last>`). See `SPECS/commands/findings.md`.
83+
7284
## Tests
7385

74-
File: `src/commands/finding.test.ts`14 tests (9 original + 5 for CVE enrichment).
86+
File: `src/commands/finding.test.ts`23 tests.

SPECS/commands/findings.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Card-style format:
4545
{Optional: affectedTargets}
4646
4747
{Status} {DueAt} | {Optional: CVE or CWE} | {Optional: AffectedVersion → FixedVersion} | {Optional: Application}
48+
{Optional: Dependency import chain (SCA findings with dependencyChains)}
4849
4950
────────────────────────────────────────
5051
```
@@ -55,6 +56,18 @@ Priority colors: Critical=red, High=orange, Medium=yellow, Low=blue.
5556

5657
Shows pagination warning if more results exist.
5758

59+
### Dependency import chain (SCA)
60+
61+
When a finding carries `dependencyChains` (`string[][]` — one ordered import chain
62+
per entry, root → vulnerable package), a dedicated line is shown below the status
63+
line, built from the **first** chain. The `AffectedVersion → FixedVersion` segment
64+
is dropped from the status line (it would duplicate the chain line).
65+
66+
- **Direct** (chain has 1 package): `Direct - Update <pkg> to <fixedVersion>`
67+
- **Transitive** (2+ packages): `Transitive - <pkg> → … → <pkg> (Fixed in <fixedVersion>)`
68+
- Chains with **4+ packages** collapse their middle: `<first> → ... N more ... → <last>` (N = length − 2).
69+
- Multiple chains append `... and X more` (X = chains − 1).
70+
5871
## Tests
5972

60-
File: `src/commands/findings.test.ts`13 tests.
73+
File: `src/commands/findings.test.ts`24 tests.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"prepublishOnly": "npm run update-api && npm run build",
2727
"start": "npx ts-node src/index.ts",
2828
"start:dist": "node dist/index.js",
29-
"fetch-api": "curl https://artifacts.codacy.com/api/codacy-api/55.12.1/apiv3-bundled.yaml -o ./api-v3/api-swagger.yaml --create-dirs",
29+
"fetch-api": "curl https://artifacts.codacy.com/api/codacy-api/56.1.1/apiv3-bundled.yaml -o ./api-v3/api-swagger.yaml --create-dirs",
3030
"generate-api": "rm -rf ./src/api/client && openapi --input ./api-v3/api-swagger.yaml --output ./src/api/client --useUnionTypes --indent 2 --client fetch",
3131
"update-api": "npm run fetch-api && npm run generate-api",
3232
"check-types": "tsc --noEmit"

src/commands/AGENTS.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,12 @@ Several helpers are shared between `repository.ts` and `pull-request.ts` via `ut
122122
- `formatPrCoverage(pr, passing)` — diffCoverage% (+/-deltaCoverage%)
123123
- `formatPrIssues(pr, passing)` — +newIssues (colored by gate) / -fixedIssues (always gray)
124124

125+
Dependency-chain helpers shared between `findings.ts` (list) and `finding.ts` (detail):
126+
- `formatVersionSegment(affectedVersion, fixedVersion, { includeUpdatePrefix })` — the `affected → fixed` status-line segment shown when a finding has no dependency chains; `includeUpdatePrefix` prepends `Update ` (list uses it, detail doesn't); returns `null` when there's no affected version
127+
- `formatDependencyChain(chain)` — joins a chain with ``, collapsing the middle to `<first> → ... N more ... → <last>` when it has 4+ packages (≤ 3 shown in full)
128+
- `formatDependencyChainsLine(chains, fixedVersion)` — one-line list summary: first chain with its Direct/Transitive label + `... and N more`; returns `null` for no chains
129+
- `formatDependencyChainsBlock(chains, fixedVersion)` — multi-line detail block: all chains, label shown once, continuation lines aligned under the label; returns `null` for no chains
130+
125131
Pattern helpers shared between `patterns.ts` (list) and `pattern.ts` (single info):
126132
- `printPatternCard(cp)` — the configured-pattern card (icons, enforced-by line, metadata, why/how, parameters)
127133
- `PATTERN_JSON_FIELDS``pickDeep` paths for the JSON projection of a `ConfiguredPattern`
@@ -213,6 +219,7 @@ Keeps the two command handlers thin: they only supply the API-specific callbacks
213219
- `-R, --ignore-reason`: `AcceptedUse` (default) | `FalsePositive` | `NotExploitable` | `TestCode` | `ExternalCode`
214220
- `-m, --ignore-comment`: optional free-text comment
215221
- **`--unignore` mode** (`-U`): calls `SecurityService.unignoreSecurityItem`; skips rendering finding details
222+
- **Dependency import chains** (SCA findings): when `item.dependencyChains` (`string[][]`) is present, both `finding` (detail) and `findings` (list) render the vulnerable dependency's import path. A chain with a single package is a **direct** dependency (`Direct - Update <pkg> to <fixedVersion>`); 2+ packages is **transitive** (`Transitive - <chain> (Fixed in <fixedVersion>)`). Chains with **4+ packages** collapse the middle to `<first> → ... N more ... → <last>` (N = length − 2). The list shows only the first chain + `... and X more`; the detail lists **all** chains with the Direct/Transitive label shown once and continuation lines indented so the `-` aligns. When chains are present, the redundant `AffectedVersion → FixedVersion` segment is dropped from the status line. Mixed direct/transitive chains (rare) take their label from the first chain. Rendering lives in `formatDependencyChainsLine` / `formatDependencyChainsBlock` (see Shared Formatting Utilities).
216223

217224
## pull-request command (`pull-request.ts`)
218225

0 commit comments

Comments
 (0)