Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/findings-dependency-chains.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@codacy/codacy-cloud-cli": minor
---

`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`.
173 changes: 156 additions & 17 deletions .claude/commands/ship-it.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
---
description: Changeset + branch + commit + push + PR for the current working tree
description: Changeset + branch + commit + push + PR, then wait for AI reviews and auto-run /pr-fixup
---

# Ship it

Take the current uncommitted changes on `main` (or on a branch already derived
from `main` for this task) and turn them into an open PR. End-to-end: make
sure there's a changeset, cut a branch, commit, push, open the PR. This is a
user-triggered action — invoking this command IS the explicit authorisation
required by the repo's "never commit, push, or open PRs without asking" rule,
so you can proceed without further confirmation once you've sanity-checked
what's about to be shipped.
from `main` for this task) and turn them into an open PR, **then wait for the
AI reviewers and automatically run `/pr-fixup` on their feedback**. End-to-end:
make sure there's a changeset, cut a branch, commit, push, open the PR (Phases
0–5); then poll for the real AI reviews and chain into `/pr-fixup` (Phases 6–7);
finally report (Phase 8). This is a user-triggered action — invoking this
command IS the explicit authorisation required by the repo's "never commit,
push, or open PRs without asking" rule, so you can proceed without further
confirmation once you've sanity-checked what's about to be shipped.

**The wait-for-reviews + auto-fixup stage runs by default.** It commits, pushes,
and posts reply comments on the PR without a separate prompt — that is the
intended behavior. Pass `--no-fixup` to skip it and get the classic
"open the PR and stop" flow. Note that the auto-fixup step depends on the
personal `/pr-fixup` command being installed; if it isn't, ship-it still opens
the PR and waits for reviews but skips fixup with a note (see Phase 7).

**Arguments:** `$ARGUMENTS`

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

---

Expand Down Expand Up @@ -164,7 +176,14 @@ CI on `main` fails any PR without a changeset, so this step is mandatory.

## Phase 5: Push and open the PR

1. Push with upstream tracking:
1. Capture the moment just before pushing — the review poller in Phase 6 uses
it to ignore any stale reviews from earlier pushes (matters on re-runs):

```bash
START="$(date -u +%Y-%m-%dT%H:%M:%SZ)" # remember this value for Phase 6
```

Then push with upstream tracking:

```bash
git push -u origin <branch>
Expand Down Expand Up @@ -204,16 +223,136 @@ CI on `main` fails any PR without a changeset, so this step is mandatory.
- Use the argument if provided; otherwise derive from the changeset title
or the commit subject.

4. Capture the PR URL from `gh pr create`'s stdout and print it in the
end-of-turn summary.
4. Capture the PR **URL and number** from `gh pr create`'s stdout (or
`gh pr view --json number,url`). You need the number for Phase 6 and the URL
for the final report.

---

## Phase 6: Wait for the AI reviews

**If `--no-fixup` was passed, skip this phase and Phase 7 — go straight to
Phase 8** (classic "open the PR and stop" behavior).

This repo has three AI reviewers wired up. Each one posts an **immediate
summary/help comment that is NOT the review**, then its real review a few
minutes later. You must wait for the _real_ review, not the placeholder:

| Reviewer | Bot login | Immediate comment (ignore) | Real review (wait for) |
|----------|-----------|----------------------------|------------------------|
| Gemini Code Assist | `gemini-code-assist[bot]` | issue comment: "## Summary of Changes … I'll post my feedback shortly" | review: "## Code Review …" |
| Codacy | `codacy-production[bot]` | issue comment: "## Up to standards …" | review: "### Pull Request Overview …" |
| GitHub Copilot | `copilot-pull-request-reviewer[bot]` | (none) | review: "## Pull request overview …" |

**The reliable signal:** a reviewer's real review is a submitted entry in the
Pull-Request *reviews* API (`pulls/{n}/reviews`). The immediate summary/help
comments only ever land as *issue* comments (`issues/{n}/comments`) — they never
appear in the reviews API. So "all reviews are in" = every expected bot login
appears in `pulls/{n}/reviews` with a `submitted_at` at/after the push from
Phase 5. (Historically all three land within ~6 minutes of opening.)

Launch a background poller and **do not block the foreground** — the harness
re-invokes you when it exits (one completion notification). Substitute the PR
number from Phase 5 and the `START` timestamp you captured before pushing, then
run this with `run_in_background: true`:

```bash
OWNER="codacy"; REPO="codacy-cloud-cli"
PR="__PR_NUMBER__" # from Phase 5
START="__START_ISO8601_UTC__" # from Phase 5, e.g. 2026-06-24T12:30:00Z
MAX_WAIT=900 # 15-minute hard cap
POLL=90 # seconds between polls (never below ~30s — GitHub rate limits)
# AI reviewers configured on this repo. Their *real* reviews land in the reviews
# API; their immediate "summary/help" comments do not. Edit this list if the
# repo's reviewer set changes.
EXPECTED=("gemini-code-assist[bot]" "copilot-pull-request-reviewer[bot]" "codacy-production[bot]")

deadline=$(( $(date +%s) + MAX_WAIT ))
while :; do
# Distinct bot logins that have SUBMITTED a review at/after the push.
arrived="$(gh api "repos/$OWNER/$REPO/pulls/$PR/reviews" --paginate \
--jq '.[] | select(.submitted_at != null and .submitted_at >= "'"$START"'") | .user.login' \
2>/dev/null | sort -u)"
missing=()
for bot in "${EXPECTED[@]}"; do
grep -qxF "$bot" <<<"$arrived" || missing+=("$bot")
done
if [ "${#missing[@]}" -eq 0 ]; then
echo "READY arrived=[$(paste -sd, - <<<"$arrived")]"
exit 0
fi
if [ "$(date +%s)" -ge "$deadline" ]; then
echo "TIMEOUT after ${MAX_WAIT}s arrived=[$(paste -sd, - <<<"$arrived")] missing=[$(IFS=,; echo "${missing[*]}")]"
exit 0
fi
sleep "$POLL"
done
```

When the poller exits you are re-invoked with its final stdout line. Read it:

- `READY arrived=[…]` → all three real reviews are in. Proceed to Phase 7.
- `TIMEOUT … missing=[…]` → not everyone posted within 15 min. **Proceed to
Phase 7 anyway** against the reviews that did arrive, and carry the `missing`
list into the Phase 8 report so the user knows to re-run later.

Why a background poller and not a Haiku subagent: the "real review vs. summary
comment" distinction is fully deterministic (presence in the reviews API), so no
model judgment is needed during the wait — a background shell loop costs zero
tokens and the harness wakes you the instant it finishes. A transient `gh api`
failure just yields an empty poll; the loop retries on the next tick.

---

## Phase 7: Auto-run /pr-fixup (if available)

(Reached only when `--no-fixup` was NOT passed.)

**Dependency check first.** `/pr-fixup` is a *personal* command — it normally
lives in `~/.claude/commands/pr-fixup.md` and is **not** vendored into this repo.
ship-it is committed and shared, so don't assume it's present. Check both the
project and user locations:

```bash
{ test -f .claude/commands/pr-fixup.md || test -f ~/.claude/commands/pr-fixup.md; } \
&& echo "pr-fixup: available" || echo "pr-fixup: MISSING"
```

- **MISSING** → skip the rest of this phase. The PR is open and the reviews are
in; there's just no fixup command to run here. Carry this into the Phase 8
report: state that auto-fixup was skipped because `/pr-fixup` isn't installed
in this environment, and that the user should run their own fixup (or vendor
`pr-fixup` into the repo) to continue. Do not try to hand-roll the triage.
- **Available** → continue with the steps below.

1. Invoke the `/pr-fixup` command (via the Skill tool, skill `pr-fixup`) for the
PR you just opened. It triages every review comment, replies with decisions,
pulls in Codacy analysis, and applies the fixes worth making.
2. When `/pr-fixup` has applied its fixes, **commit and push them** — never
leave them uncommitted (this is the standing `/pr-fixup` expectation). Use a
`fix:` / `chore:` commit that references the review round, then `git push`.
Include a fresh changeset only if the fixes change package code in a way the
existing changeset doesn't already cover.
3. Do **one** pass only. Do NOT loop back to Phase 6 to wait for re-reviews of
the pushed fixes — that risks an endless ship→review→fix cycle. Report and
stop; the user can re-run `/ship-it` or `/pr-fixup` if they want another round.

---

## Phase 6: Report
## Phase 8: Report

Lead with one sentence on what shipped, plus the PR URL. Don't re-summarise the
diff — the PR body already does. Then, unless `--no-fixup` was passed, add a
short recap of the review round:

- Which reviewers' real reviews arrived (and, if the poller timed out, which
were still `missing` — call this out so the user can re-run later).
- What `/pr-fixup` did: comments addressed vs. dismissed, any Codacy issues
fixed/ignored, and whether fixup changes were committed and pushed — or, if
`/pr-fixup` wasn't installed in this environment, that auto-fixup was skipped
for that reason and the user should run their own.

One sentence on what shipped, plus the PR URL. Don't re-summarise the diff —
the PR body already does. If anything was skipped or changed from the
defaults (e.g. bump type defaulted to patch because ambiguous, branch name
had a suffix appended because of collision, pre-commit hook required a
retry), mention it in a single parenthetical line so the user can course-
correct if needed.
If anything was skipped or changed from the defaults (e.g. bump type defaulted
to patch because ambiguous, branch name had a suffix appended because of
collision, pre-commit hook required a retry, reviews timed out), mention it in a
single parenthetical line so the user can course-correct if needed.
1 change: 1 addition & 0 deletions SPECS/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,4 @@ _No pending tasks._ All commands implemented.
| 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) |
| 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) |
| 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) |
| 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) |
14 changes: 13 additions & 1 deletion SPECS/commands/finding.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ The `findingId` is the UUID shown in dim gray at the end of each findings card.
{Finding title}

{Status} {DueAt} | {Optional: CVE/CWE} | {Optional: AffectedVersion → FixedVersion} | {Optional: Application} | {Optional: AffectedTargets}
{Optional: Dependency import chains (SCA findings with dependencyChains)}

{Optional: Ignored by {name} on {date}}
{Optional: Ignored reason}
Expand All @@ -69,6 +70,17 @@ When `item.cve` is present, fetch CVE data from `https://cveawg.mitre.org/api/cv

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.

## Dependency import chains (SCA)

When a finding carries `dependencyChains` (`string[][]`), **all** chains are listed
below the status line. The Direct/Transitive label (from the first chain) appears
once; continuation lines are indented so the `-` aligns under it. The
`AffectedVersion → FixedVersion` segment is dropped from the status line.

Same per-chain rules as `findings` (direct → `Update <pkg> to <fixedVersion>`;
transitive → `<chain> (Fixed in <fixedVersion>)`; 4+ packages collapse the middle
to `<first> → ... N more ... → <last>`). See `SPECS/commands/findings.md`.

## Tests

File: `src/commands/finding.test.ts` — 14 tests (9 original + 5 for CVE enrichment).
File: `src/commands/finding.test.ts` — 23 tests.
15 changes: 14 additions & 1 deletion SPECS/commands/findings.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Card-style format:
{Optional: affectedTargets}

{Status} {DueAt} | {Optional: CVE or CWE} | {Optional: AffectedVersion → FixedVersion} | {Optional: Application}
{Optional: Dependency import chain (SCA findings with dependencyChains)}

────────────────────────────────────────
```
Expand All @@ -55,6 +56,18 @@ Priority colors: Critical=red, High=orange, Medium=yellow, Low=blue.

Shows pagination warning if more results exist.

### Dependency import chain (SCA)

When a finding carries `dependencyChains` (`string[][]` — one ordered import chain
per entry, root → vulnerable package), a dedicated line is shown below the status
line, built from the **first** chain. The `AffectedVersion → FixedVersion` segment
is dropped from the status line (it would duplicate the chain line).

- **Direct** (chain has 1 package): `Direct - Update <pkg> to <fixedVersion>`
- **Transitive** (2+ packages): `Transitive - <pkg> → … → <pkg> (Fixed in <fixedVersion>)`
- Chains with **4+ packages** collapse their middle: `<first> → ... N more ... → <last>` (N = length − 2).
- Multiple chains append `... and X more` (X = chains − 1).

## Tests

File: `src/commands/findings.test.ts` — 13 tests.
File: `src/commands/findings.test.ts` — 24 tests.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"prepublishOnly": "npm run update-api && npm run build",
"start": "npx ts-node src/index.ts",
"start:dist": "node dist/index.js",
"fetch-api": "curl https://artifacts.codacy.com/api/codacy-api/55.12.1/apiv3-bundled.yaml -o ./api-v3/api-swagger.yaml --create-dirs",
"fetch-api": "curl https://artifacts.codacy.com/api/codacy-api/56.1.1/apiv3-bundled.yaml -o ./api-v3/api-swagger.yaml --create-dirs",
"generate-api": "rm -rf ./src/api/client && openapi --input ./api-v3/api-swagger.yaml --output ./src/api/client --useUnionTypes --indent 2 --client fetch",
"update-api": "npm run fetch-api && npm run generate-api",
"check-types": "tsc --noEmit"
Expand Down
7 changes: 7 additions & 0 deletions src/commands/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@ Several helpers are shared between `repository.ts` and `pull-request.ts` via `ut
- `formatPrCoverage(pr, passing)` — diffCoverage% (+/-deltaCoverage%)
- `formatPrIssues(pr, passing)` — +newIssues (colored by gate) / -fixedIssues (always gray)

Dependency-chain helpers shared between `findings.ts` (list) and `finding.ts` (detail):
- `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
- `formatDependencyChain(chain)` — joins a chain with ` → `, collapsing the middle to `<first> → ... N more ... → <last>` when it has 4+ packages (≤ 3 shown in full)
- `formatDependencyChainsLine(chains, fixedVersion)` — one-line list summary: first chain with its Direct/Transitive label + `... and N more`; returns `null` for no chains
- `formatDependencyChainsBlock(chains, fixedVersion)` — multi-line detail block: all chains, label shown once, continuation lines aligned under the label; returns `null` for no chains

Pattern helpers shared between `patterns.ts` (list) and `pattern.ts` (single info):
- `printPatternCard(cp)` — the configured-pattern card (icons, enforced-by line, metadata, why/how, parameters)
- `PATTERN_JSON_FIELDS` — `pickDeep` paths for the JSON projection of a `ConfiguredPattern`
Expand Down Expand Up @@ -213,6 +219,7 @@ Keeps the two command handlers thin: they only supply the API-specific callbacks
- `-R, --ignore-reason`: `AcceptedUse` (default) | `FalsePositive` | `NotExploitable` | `TestCode` | `ExternalCode`
- `-m, --ignore-comment`: optional free-text comment
- **`--unignore` mode** (`-U`): calls `SecurityService.unignoreSecurityItem`; skips rendering finding details
- **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).

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

Expand Down
Loading
Loading