Skip to content

Commit 7889fed

Browse files
feat(action): GitHub Marketplace Action — Slices 1-4 of #73 plan (#74)
* feat(audit): --format sarif on `codemap audit` (Slice 1a of #73 plan) First half of Slice 1 from the GitHub Marketplace Action plan. `audit` today only emits `--json`; this adds SARIF 2.1.0 output directly, no JSON→SARIF transform step needed in CI workflows. CLI: - New `--format <text|json|sarif>` flag (default `text`). - `--json` stays as backward-compat shortcut for `--format json`. - `--json` + `--format <other>` rejected as a contradiction with a helpful error message. - `--summary` is a no-op with `--format sarif` (SARIF results are per-row, not counts) and surfaces a stderr warning. - `runAuditCmd` signature updated: `json: boolean` → `format: AuditOutputFormat` (`"text" | "json" | "sarif"`). SARIF shape: - One rule per delta key (`codemap.audit.files-added`, `codemap.audit.dependencies-added`, `codemap.audit.deprecated-added`). - One result per `added` row (severity = `warning`; audit deltas are more actionable than per-recipe `note`). - Locations auto-detected via the existing `file_path`/`path`/`to_path`/`from_path` priority list (same as `query --format sarif`); `line_start`/`line_end` populate the SARIF `region`. - `removed` rows intentionally excluded — SARIF surfaces findings to act on, not cleanups. - Location-only rows (e.g. files-added has only `path`) get a "new files: src/foo.ts" message, not the generic "(no message)" fallback. Tests: - 4 new SARIF-formatter unit tests (rules/results shape; empty deltas; missing location column; line range). - 4 new audit-CLI parser tests (--format sarif/json/=json, unknown format value, --json + --format contradiction, --json + --format json reconcilable). - All existing audit / output-formatters tests updated for the `json: bool` → `format: AuditOutputFormat` field rename. Lockstep updates: - `.agents/rules/codemap.md` + `templates/agents/rules/codemap.md` audit-vs-git-ref row gains `--format sarif` mention. - New changeset (.changeset/audit-format-sarif.md, minor). Slice 1b (`--ci` aggregate flag on `query` + `audit`) lands in the follow-up PR. * feat(cli): --ci aggregate flag on `query` + `audit` (Slice 1b of #73 plan) Second half of Slice 1. `--ci` is the GitHub-Action-shaped CI flag: - Aliases `--format sarif` - Sets process.exitCode = 1 when ≥1 finding/addition surfaced - Suppresses the no-locatable-rows stderr warning (CI templates would surface it as red noise; the row-set itself is the gating signal) Lands on both `query` and `audit` (the two finding-producing verbs). Same parser / resolver semantics on both: - Mutually exclusive with `--json` (different format aliases) - Mutually exclusive with `--format <other>` (contradicts the alias) - `--ci --format sarif` redundant but accepted (consumers may set both for clarity in CI templates) Wiring: - `parseQueryRest` / `parseAuditRest` gain `--ci` token + `ci: boolean` in the run-shape union. - `runQueryCmd` / `runAuditCmd` gain `ci?: boolean` opt; threaded through to `printFormattedQuery` (query) and the post-render exit-code branch (audit). - `query`: exit 1 if `rows.length > 0` after SARIF emit. - `audit`: exit 1 if any delta has `added.length > 0` after SARIF emit. Tests: - 4 new `cmd-query` parser tests (--ci alias; --ci+--json reject; --ci+--format json reject; --ci+--format sarif accept). - 4 new `cmd-audit` parser tests (same matrix). - All existing toEqual tests updated for the `ci: false` field default. Smoke verified end-to-end: - `query --ci` with results → SARIF stdout, exit 1. - `audit --baseline X --ci` against identical baseline → 0 additions, exit 0. Diff with adds → exit 1. - Contradiction tests (`--ci --json`) emit clear error + exit 1. * feat(action): action.yml + scripts/detect-pm.mjs (Slice 2 of #73 plan) Composite GitHub Action wrapping codemap CLI for the Marketplace. ~16 declarative inputs per Q1 resolution; package-manager detection + codemap CLI invocation resolution via package-manager-detector (antfu/userquin, MIT, 0 transitive deps, 23 kB). action.yml shape: - Skip-on-non-PR-events for the headline α default (audit --base ${{ github.base_ref }} --ci). Other events (push, schedule, …) no-op + log "no PR context, skipping" + exit 0 unless an explicit command: input is passed. - 16 declarative inputs across 3 categories: - WHERE TO RUN: working-directory, package-manager (override), version (CLI pin), state-dir - WHAT TO RUN: mode (audit | recipe | aggregate | command), recipe, params, baseline, audit-base, changed-since, group-by, command (escape hatch) - WHAT TO DO WITH OUTPUT: format (default sarif), output-path, upload-sarif, pr-comment (Slice 3 stub for v1.0), fail-on, token - Validation precedence: command > mode > defaults; mode='aggregate' rejected (reserved for v1.x post-Q6 SARIF rule.id de-dup work). - 4 outputs: agent / exec / install_method (debug breadcrumbs) + output-file (echoes inputs.output-path). - Composite steps: gate → setup-node → detect-pm → validate → run → upload-sarif (if Code Scanning enabled) → pr-comment-stub. scripts/detect-pm.mjs: - Wraps `package-manager-detector`'s `detect()` + `resolveCommand()`. - Implements the Q3 invocation logic: - VERSION env var set → 'execute' intent (dlx-pinned) - codemap in devDependencies → 'execute-local' - else → 'execute' intent (dlx-latest) - Outputs to $GITHUB_OUTPUT per current Actions convention (set-output deprecated 2022-10). - Validates PACKAGE_MANAGER override against known agents. - 8 unit tests covering: pnpm/bun/npm autodetect, no-lockfile fallback, execute-local for project-installed, dlx-pinned override, manual PM override, unknown PM rejection, packageManager-field priority over lockfile. New runtime dep: package-manager-detector@1.6.0 (MIT, antfu/userquin, 0 transitive deps). * feat(cli): codemap pr-comment + action.yml integration (Slice 3 of #73 plan) Slice 3 — markdown PR-summary writer for the cases SARIF→Code-Scanning doesn't cover well: private repos without GHAS, repos that haven't enabled Code Scanning, aggregate audit deltas without a single file:line anchor, and bot-context seeding (review bots read PR conversation, not workflow artifacts). Architecture (engine + CLI separation, mirrors show / impact / audit): - src/application/pr-comment-engine.ts — pure transport-agnostic renderer. Auto-detects input shape (audit envelope vs SARIF doc) + renders markdown grouped by delta key (audit) or rule id (SARIF). Lists >50 entries collapse to `… and N more`. Removed rows surface in their own collapsed section (audit only). - src/cli/cmd-pr-comment.ts — CLI wrapper. Reads JSON from a file or stdin (`-`). `--shape audit|sarif` overrides autodetection; `--json` emits structured envelope `{ markdown, findings_count, kind }` for action.yml steps. - src/cli/main.ts + src/cli/bootstrap.ts wire the new `pr-comment` verb (whitelist + dispatch). action.yml integration (Slice 2 stub replaced): - pr-comment toggle now actually invokes `codemap pr-comment` against the SARIF / JSON output file produced by the run step, then posts via `gh pr comment <PR> -F -`. Same binary that produced the output renders the comment — version stream stays coherent. Tests: - 12 new pr-comment-engine unit tests (input shape detection, no-drift / no-findings ✅ rendering, audit summary line + per-delta sections, SARIF rule grouping, location formatting, >50 collapse). - Smoke verified: real audit envelope produces markdown with file: links + delta sections; SARIF doc groups findings by rule id. Lockstep updates (per docs/README.md Rule 10): - .agents/rules/codemap.md + templates/agents/rules/codemap.md gain rows for `--ci` aggregate flag (Slice 1b) and `pr-comment` renderer (Slice 3). * ci(action): dogfood action-smoke job + bundle changeset (Slice 4 of #73 plan) Adds an `action-smoke` job to .github/workflows/ci.yml that runs `uses: ./` from this repo on every PR. Validates: - action.yml YAML syntax + composite-step flow - gate step (skip-on-non-PR; command-set-overrides-skip) - setup-node + npm install of package-manager-detector - scripts/detect-pm.mjs execution + GITHUB_OUTPUT writes - detect-pm command resolution (`<pm> dlx codemap@latest --version`) Smoke uses `command: --version` to avoid the real-audit dependency chain (audit baselines etc.) — codemap audit logic is covered by the unit-test suite. The action-smoke validates the wrapper, not the underlying CLI. Non-blocking (`continue-on-error: true`) until v1.0.0 of codemap is published — today the action pulls codemap@latest from npm (0.4.0), which works for `--version` but doesn't validate v1.x flags. Promote to a hard gate when v1.0.0 ships. Bundles the v1.0 changeset: - .changeset/marketplace-action.md describes the full Slice 1b-4 surface (--ci flag, action.yml, pr-comment, dogfood) as one minor release. Slice 5 (Marketplace publish + listing metadata) is post-merge once a v1.0.0 tag exists. * chore: slim new comments + sweep docs staleness Per .agents/rules/concise-comments.md — re-read every comment authored in this PR and slimmed the ones the code itself already conveys. Net: +68 / -163 lines of comments / docstrings; same information density, fewer words. Cuts/slims (in order of file): - output-formatters.ts: AuditSarifDelta + formatAuditSarif JSDoc; the message-fallback inline comment. - cmd-audit.ts: AUDIT_OUTPUT_FORMATS doc-block; jsonShortcut / ci / Reconcile / --ci-exit / emitAuditError comments. - cmd-query.ts: --ci variable + resolveFormat doc; printFormattedQuery's ci option doc; "no-locatable-rows warning" inline; runQueryCmd's ci option JSDoc; parseQueryRest's ci field JSDoc. - pr-comment-engine.ts: file header; RenderedComment JSDoc; "kind" field doc; detectCommentInputShape doc; renderAuditComment + renderSarifComment JSDocs; "Header summary line" / "Group by ruleId" / "Prefer the most-specific identity columns first." / "Unknown shape — fall through to JSON." inline comments. - cmd-pr-comment.ts: PrCommentOpts.shape doc; printPrCommentCmdHelp JSDoc; runPrCommentCmd JSDoc; "detected here is …" inline; readStdinSync gotcha doc. - scripts/detect-pm.mjs: file header; "Step 1/2/3:" labels; "Local / non-Actions invocation —" comment; codemapInDevDependencies JSDoc. - action.yml: "Build args based on inputs" inline. Docs staleness sweep — fact-checked against the codebase: - docs/architecture.md § "Audit wiring" listed audit's flags but pre-dated --format sarif + --ci. Added both + the formatAuditSarif shape note. - docs/architecture.md § "Output formatters" missed formatAuditSarif. Added (one rule per delta key, severity warning, removed-rows excluded, location-only fallback). - docs/architecture.md § "Query wiring" missed --ci. Added. - docs/architecture.md added a new "PR-comment wiring" section (mirrors the cmd ↔ engine seam pattern). - docs/glossary.md added `--ci` (under C) and `pr-comment` (under P) entries per Rule 9. - README.md CLI examples updated with `audit --format sarif`, `audit --ci`, `query --ci`, and the `pr-comment` pipe-to-`gh pr comment` idiom. Pre-existing comments (preserve-comments rule) untouched. * fix(action): empty defaults for github-context inputs (composite actions reject expressions in input defaults) CI failure on PR #74's Action smoke job: TemplateValidationException: Unrecognized named-value: 'github'. Located at position 1 within expression: github.base_ref / github.token GitHub composite actions do NOT allow ${{ github.* }} expressions in input defaults. Only `runs:` step expressions can reference the github context. Two inputs were affected: - `audit-base`: now defaults to "". The existing run step already does `BASE="${AUDIT_BASE:-$BASE_REF}"` where `BASE_REF: ${{ github.base_ref }}` is set as an env var (legal in step env blocks), so empty-input → PR base_ref behavior is preserved. - `token`: now defaults to "". Two call sites (`upload-sarif` step's `token:` arg + `pr-comment` step's `GH_TOKEN`) now use `${{ inputs.token != '' && inputs.token || github.token }}` to fall back to `github.token` when unset. Both inputs' descriptions updated to document the empty-falls-back behavior so consumers know what to expect. * fix: address PR #74 CodeRabbit review (6 findings, all real) Fact-checked each finding against the codebase; all valid. Two were critical correctness bugs (T2, T3, T5) that would have shipped silently broken behavior. T2 (Major) — `scripts/detect-pm.mjs` wrong package-name keys `codemapInDevDependencies` checked `manifest?.dependencies?.codemap` but the published package is `@stainless-code/codemap` — the project-installed branch was dead code for all real consumers; `dlx-pinned` / `dlx-latest` also pulled the unscoped `codemap` package (a different npm namespace entirely). Fixed: - Lookup checks both scoped (`@stainless-code/codemap`) and bare (`codemap`) keys — the latter for monorepos that alias via `"codemap": "workspace:*"`. - `'execute'` (dlx) commandArgs now use the scoped published name so npm/pnpm/yarn/bun resolve the right registry entry. - `'execute-local'` keeps `["codemap"]` because that's the bin alias per `package.json#bin`, regardless of the scoped name. - Tests updated: scoped-dev-dep / bare-dev-dep / scoped-dlx-pinned cases. Old tests that asserted dlx-with-unscoped-`codemap@<v>` were themselves testing a bug. T3 (Critical) — no-op `expect()` in formatAuditSarif test `expect(run.results.every(...))` without a chained matcher creates and discards the expectation. If formatAuditSarif reverted to severity `note`, the test would still pass. Added `.toBe(true)`. T5 (Major) — `require()` in ESM module `readStdinSync` in `cmd-pr-comment.ts` used `require("node:fs").readSync` but the file loads via `await import()` (ESM); `require` is undefined at runtime. `codemap pr-comment -` would have crashed for every user. My unit tests passed file paths, not stdin — caught nothing. Imported `readSync` from `node:fs` at the top, used directly. T4 (Major) — pr-comment SARIF renderer dropped runs[1+] `renderSarifComment` only read `doc.runs?.[0]?.results`. Valid SARIF allows multiple runs (merged / multi-tool docs). Now flattens via `(doc.runs ?? []).flatMap(run => run.results ?? [])`. New test `aggregates results across multi-run SARIF docs (not just runs[0])`. T1 (Minor) — `mode: command` without `command:` input falls through The validate step accepted `mode=command` but didn't guard against empty `command:`. Run step's if/elif only handled `audit` + `recipe`, so `$EXEC` would invoke with empty `$ARGS`. Added an explicit guard mirroring the `mode=recipe` pattern. T6 (Minor) — `--ci` doc said "no-findings warning"; actual is "no-locatable-rows" Both `.agents/rules/codemap.md` and `templates/agents/rules/codemap.md` described the suppressed warning incorrectly. Aligned wording with the implementation in `printFormattedQuery`. * docs(research): mark § 1.5 boundary-violations as shipped (PR #72) `non-goals-reassessment-2026-05.md` § 1 Shipped table + § 7 Lifted-to table contradicted each other on § 1.5 — § 1 didn't list it (lifted table was last edited before PR #72), § 7 still said "(pending)". Boundary-violations shipped 2026-05 as PR #72; both tables now reflect that, plus the "Pending picks" enumeration drops § 1.5. Per Rule 8, research notes get closed/lifted but factual state may still be corrected when the codebase moves under them. This is hygiene, not extension of the analysis. * docs(plan): consolidate Slice 5 runbook for later pickup Slice 5 (publish + Marketplace listing) is post-merge work — anyone should be able to pick it up without triangulating across the plan's Q7 release-workflow + Q10 listing-metadata + Slice-5 one-liner. Added a "Slice 5 runbook (post-merge)" subsection that: - Surfaces the CLI / Action version stream decoupling explicitly: Action ships at v1 against CLI 0.5.0 (the version PR #74's changeset bumps to). CLI v1.0.0 is not required. - Lists 7 sequenced executable steps from "merge PR #74" through to "delete this plan per Rule 3" (the canonical close-out per docs-governance). - Calls out which docs already hold the durable design decisions (architecture.md / glossary.md / agent rules / MARKETPLACE.md) so the deletion in step 7 is safe. - Documents subsequent-release cadence (changesets-driven; force-push the floating v<major> tag) and the major-bump policy.
1 parent f1a28c1 commit 7889fed

26 files changed

Lines changed: 2000 additions & 100 deletions

.agents/rules/codemap.md

Lines changed: 30 additions & 28 deletions
Large diffs are not rendered by default.

.changeset/audit-format-sarif.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@stainless-code/codemap": minor
3+
---
4+
5+
`codemap audit --format <text|json|sarif>` — emit a SARIF 2.1.0 doc directly from the audit envelope, no JSON→SARIF transform step needed. One rule per delta key (`codemap.audit.files-added`, `codemap.audit.dependencies-added`, `codemap.audit.deprecated-added`); one result per `added` row; severity = `warning` (audit deltas are more actionable than per-recipe `note`). Locations auto-detected via the same `file_path` / `path` / `to_path` / `from_path` priority list that `query --format sarif` uses; line ranges (`line_start` / `line_end`) populate the SARIF `region`. Pure output-formatter addition on top of the existing audit envelope; no schema impact.
6+
7+
`--json` stays as the shortcut for `--format json` (backward-compatible). `--json` + `--format <other>` rejected as a contradiction. `--summary` is a no-op with `--format sarif` (SARIF results are per-row, not counts) and surfaces a stderr warning.
8+
9+
`removed` rows are intentionally excluded from SARIF output — SARIF surfaces findings to act on, not cleanups. Location-only rows (e.g. files-added has only `path`) get a "new files: src/foo.ts" message instead of the generic "(no message)" fallback.
10+
11+
This is the first half of Slice 1 from the [GitHub Marketplace Action plan](../docs/plans/github-marketplace-action.md) — independently useful for any CI consumer running `codemap audit` who wants Code Scanning surface without a translation layer; required for the upcoming Marketplace Action's headline default command.

.changeset/marketplace-action.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"@stainless-code/codemap": minor
3+
---
4+
5+
GitHub Marketplace Action — Slices 1b-4 of [`docs/plans/github-marketplace-action.md`](../docs/plans/github-marketplace-action.md). v1.0 readiness; `action.yml` is now installable via `- uses: stainless-code/codemap@v1` once the corresponding tag is published.
6+
7+
**`--ci` aggregate flag (Slice 1b)** on `query` + `audit`. Aliases `--format sarif` + `process.exitCode = 1` on findings/additions + suppresses no-locatable-rows stderr warning. Mutually exclusive with `--json` and `--format <other>`. Parser rejects contradictions with helpful errors.
8+
9+
**`action.yml` + `scripts/detect-pm.mjs` (Slice 2).** Composite Action wrapping the codemap CLI. ~16 declarative inputs across 3 categories (where to run / what to run / what to do with output); Q1 resolution. Default α command on `pull_request` events: `audit --base ${{ github.base_ref }} --ci`; no-op on other events unless an explicit `command:` input is passed. Package-manager autodetection delegates to [`package-manager-detector`](https://github.com/antfu-collective/package-manager-detector) (antfu/userquin, MIT, 0 transitive deps); CLI invocation resolution via the library's `'execute-local'` / `'execute'` intents.
10+
11+
**`codemap pr-comment` (Slice 3).** New CLI verb that renders a markdown PR-summary comment from a codemap-audit-JSON envelope or a SARIF doc. Auto-detects input shape; `--shape audit|sarif` overrides. Reads from a file or stdin (`-`). `--json` envelope emits `{ markdown, findings_count, kind }` for action.yml steps. Closes the SARIF→Code-Scanning gap for: private repos without GHAS, repos that haven't enabled Code Scanning, aggregate audit deltas without a single file:line anchor, trend / delta narratives, and bot-context seeding (review bots read PR conversation, not workflow artifacts). v1.0 ships the (b) summary-comment shape per Q4 resolution; (c) inline-review comments deferred to v1.x.
12+
13+
**Dogfood (Slice 4).** New `action-smoke` job in `.github/workflows/ci.yml` runs `uses: ./` on every PR with `command: --version` to validate the composite-step flow + npm-pulled codemap binary. Non-blocking until v1.0.0 ships (at which point the smoke gates the build).
14+
15+
**Engine + CLI separation discipline preserved:** `pr-comment-engine.ts` is pure; `cmd-pr-comment.ts` wraps it. Tests cover the engine (12 cases) and the CLI parser (4 audit + 4 query tests for `--ci`).
16+
17+
**Lockstep agent updates** (per `docs/README.md` Rule 10): `.agents/rules/codemap.md` + `templates/agents/rules/codemap.md` gain rows for `--ci` and `pr-comment` so installed agents and this clone's session view stay in lockstep.
18+
19+
Slice 5 (Marketplace publish + listing metadata) is post-merge — gated on a v1.0.0 tag.

.github/workflows/ci.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,35 @@ jobs:
159159
- name: bun audit
160160
run: bun audit
161161

162+
action-smoke:
163+
# Dogfood Slice 4 of plan PR #73: invoke `uses: ./` from this very repo
164+
# so the action.yml + scripts/detect-pm.mjs are exercised on every PR.
165+
# Smoke uses `command: --version` to avoid the real-audit dependency
166+
# chain (audit baselines etc.) — this validates the composite-step
167+
# flow + npm-pulled codemap binary, not the audit logic itself
168+
# (which is covered by the unit-test suite).
169+
name: 🤖 Action smoke (dogfood)
170+
needs: skip-ci
171+
if: needs['skip-ci'].outputs.skip != 'true'
172+
runs-on: ubuntu-latest
173+
# Non-blocking until we've published codemap@<v1> matching the Action.
174+
# Today the Action pulls codemap@latest from npm (0.4.0), which works
175+
# for `--version` but doesn't validate v1.x flags. Promote to a hard
176+
# gate when v1.0.0 ships.
177+
continue-on-error: true
178+
steps:
179+
- name: Checkout
180+
uses: actions/checkout@v4
181+
182+
- name: Run the local-path action
183+
uses: ./
184+
with:
185+
command: "--version"
186+
format: "json"
187+
upload-sarif: "false"
188+
pr-comment: "false"
189+
fail-on: "never"
190+
162191
ci-complete:
163192
name: CI complete
164193
needs: [skip-ci, format, lint, typecheck, test, build, benchmark]

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ codemap audit --files-baseline base-files # explicit per-d
112112
codemap audit --baseline base --files-baseline hotfix-files # mixed — auto-resolve deps + deprecated; override files
113113
codemap audit --baseline base --no-index # skip the auto-incremental-index prelude (frozen-DB CI)
114114
codemap audit --base origin/main --json # ad-hoc — worktree+reindex against any committish; no --save-baseline needed
115+
codemap audit --base origin/main --format sarif # emit SARIF 2.1.0 directly (Code Scanning); also: --ci alias
116+
codemap audit --base origin/main --ci # CI shortcut: --format sarif + non-zero exit on additions + quiet
115117
codemap audit --base v1.0.0 --files-baseline pre-release-files # mix --base with per-delta override
116118
# --base materialises <ref> via `git worktree add` to .codemap/audit-cache/<sha>/, reindexes into
117119
# a temp DB, then diffs. Cache hit on second run against same sha is sub-100ms. Requires git;
@@ -127,7 +129,11 @@ codemap audit --base v1.0.0 --files-baseline pre-release-files # mix --base wit
127129
# requires {from, to, label?, kind?} rows and rejects unbounded inputs (>50 edges) with a
128130
# scope-suggestion error — alias columns via SELECT col AS "from", col2 AS "to".
129131
codemap query --recipe deprecated-symbols --format sarif > findings.sarif
132+
codemap query --recipe deprecated-symbols --ci # CI shortcut: --format sarif + non-zero exit + quiet
130133
codemap query --recipe deprecated-symbols --format annotations # one ::notice per row
134+
# Render any audit/SARIF output as a markdown PR-summary comment (for repos without
135+
# Code Scanning / aggregate audit deltas / bot-context seeding):
136+
codemap audit --base origin/main --json | codemap pr-comment - | gh pr comment <PR> -F -
131137
codemap query --format mermaid 'SELECT from_path AS "from", to_path AS "to" FROM dependencies LIMIT 50'
132138
codemap query --format diff 'SELECT "README.md" AS file_path, 1 AS line_start, "# Codemap" AS before_pattern, "# Codemap Preview" AS after_pattern'
133139
codemap query --format diff-json 'SELECT "README.md" AS file_path, 1 AS line_start, "# Codemap" AS before_pattern, "# Codemap Preview" AS after_pattern' | jq '.summary'

action.yml

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
name: "Codemap"
2+
description: "SQL-queryable structural index of your codebase. Run any predicate as a recipe; CI gating via SARIF → Code Scanning."
3+
author: "stainless-code"
4+
branding:
5+
icon: "database"
6+
color: "blue"
7+
8+
inputs:
9+
# WHERE TO RUN
10+
working-directory:
11+
description: "Subdirectory to run codemap in (for monorepos). Defaults to the repository root."
12+
required: false
13+
default: "."
14+
package-manager:
15+
description: "Override package-manager autodetect. Accepts npm | pnpm | yarn | yarn@berry | bun. Empty = autodetect via package-manager-detector (lockfile + packageManager field + devEngines.packageManager + install-metadata + parent-dir walk)."
16+
required: false
17+
default: ""
18+
version:
19+
description: "Pin codemap CLI version (e.g. 1.2.3). Empty = use the project's devDependency if present, else fall back to <pm> dlx codemap@latest."
20+
required: false
21+
default: ""
22+
state-dir:
23+
description: "Override codemap state directory location. Empty = .codemap/ at the working-directory root (codemap default)."
24+
required: false
25+
default: ""
26+
27+
# WHAT TO RUN — high-level (mutually exclusive; precedence: command > mode > defaults)
28+
mode:
29+
description: "Run shape: 'audit' | 'recipe' | 'aggregate' | 'command'. Default: 'audit' on pull_request events; ignored on other events (Action no-ops). 'aggregate' is reserved for v1.x — currently rejected."
30+
required: false
31+
default: "audit"
32+
recipe:
33+
description: "Recipe id (when mode=recipe). Use --recipes-json on the CLI to list known recipes."
34+
required: false
35+
default: ""
36+
params:
37+
description: "Recipe params for parametrised recipes (when mode=recipe). Multiline `key=value` pairs, one per line."
38+
required: false
39+
default: ""
40+
baseline:
41+
description: "Saved baseline name to diff against (when mode=recipe + a baseline was previously saved with --save-baseline)."
42+
required: false
43+
default: ""
44+
audit-base:
45+
description: "Git ref to audit against (when mode=audit). Empty (default) → falls back to `github.base_ref` on `pull_request` events; on other events the action no-ops unless an explicit `command:` is set."
46+
required: false
47+
default: ""
48+
changed-since:
49+
description: "Filter results to files changed since the given git ref (e.g. 'origin/main')."
50+
required: false
51+
default: ""
52+
group-by:
53+
description: "Bucket results by 'owner' (CODEOWNERS) | 'directory' | 'package' (workspace package). Empty = no bucketing."
54+
required: false
55+
default: ""
56+
command:
57+
description: "Raw CLI args to invoke codemap with (escape hatch). When set, overrides mode / recipe / params / baseline / audit-base / changed-since / group-by — those are silently ignored with a warning."
58+
required: false
59+
default: ""
60+
61+
# WHAT TO DO WITH OUTPUT
62+
format:
63+
description: "Output format: 'sarif' | 'json' | 'annotations' | 'mermaid' | 'diff' (per-mode availability varies; audit supports text/json/sarif). Default: 'sarif' (SARIF 2.1.0 → Code Scanning)."
64+
required: false
65+
default: "sarif"
66+
output-path:
67+
description: "Where to write the output file. Used as the artifact-upload source when format=sarif and upload-sarif=true."
68+
required: false
69+
default: "codemap.sarif"
70+
upload-sarif:
71+
description: "Upload the SARIF artifact to GitHub Code Scanning. Requires GitHub Advanced Security on private repos. Set 'false' if your repo can't use Code Scanning (still produces the artifact for manual download / pr-comment writer)."
72+
required: false
73+
default: "true"
74+
pr-comment:
75+
description: "Post a markdown summary comment on the PR (Slice 3 — opt-in for v1.0). Set 'true' to enable. Useful when SARIF→Code-Scanning isn't available (private repos without GHAS, or repos that haven't enabled Code Scanning)."
76+
required: false
77+
default: "false"
78+
fail-on:
79+
description: "Exit-code policy: 'any' | 'error' | 'warning' | 'never'. v1.0 ships only 'any' (fails when any finding) and 'never' (no exit code). 'error' / 'warning' deferred until per-recipe severity overrides ship."
80+
required: false
81+
default: "any"
82+
token:
83+
description: "GitHub token for SARIF upload + PR comment posting. Empty (default) → falls back to `github.token` automatically. Pass an explicit fine-grained PAT only if you need elevated permissions."
84+
required: false
85+
default: ""
86+
87+
outputs:
88+
agent:
89+
description: "Resolved package manager (npm / pnpm / yarn / bun)."
90+
value: ${{ steps.detect-pm.outputs.agent }}
91+
exec:
92+
description: "Shell-ready command used to invoke codemap."
93+
value: ${{ steps.detect-pm.outputs.exec }}
94+
install_method:
95+
description: "How codemap was located: 'project-installed' | 'dlx-pinned' | 'dlx-latest'."
96+
value: ${{ steps.detect-pm.outputs.install_method }}
97+
output-file:
98+
description: "Path to the written output file (echoes inputs.output-path)."
99+
value: ${{ inputs.output-path }}
100+
101+
runs:
102+
using: "composite"
103+
steps:
104+
- name: Skip on non-PR events when defaulting to audit
105+
id: gate
106+
shell: bash
107+
env:
108+
EVENT_NAME: ${{ github.event_name }}
109+
BASE_REF: ${{ github.base_ref }}
110+
MODE: ${{ inputs.mode }}
111+
COMMAND: ${{ inputs.command }}
112+
AUDIT_BASE_INPUT: ${{ inputs.audit-base }}
113+
run: |
114+
if [ -n "$COMMAND" ]; then
115+
echo "skip=false" >> "$GITHUB_OUTPUT"
116+
exit 0
117+
fi
118+
if [ "$MODE" != "audit" ]; then
119+
echo "skip=false" >> "$GITHUB_OUTPUT"
120+
exit 0
121+
fi
122+
BASE="${AUDIT_BASE_INPUT:-$BASE_REF}"
123+
if [ -z "$BASE" ]; then
124+
echo "codemap action: no PR context (event_name=$EVENT_NAME, base_ref empty), skipping. Pass an explicit 'command:' input to run on non-PR events."
125+
echo "skip=true" >> "$GITHUB_OUTPUT"
126+
exit 0
127+
fi
128+
echo "skip=false" >> "$GITHUB_OUTPUT"
129+
130+
- name: Setup Node.js (for the package-manager-detector wrapper)
131+
if: steps.gate.outputs.skip != 'true'
132+
uses: actions/setup-node@v4
133+
with:
134+
node-version: "20"
135+
136+
- name: Detect package manager + resolve CLI invocation
137+
if: steps.gate.outputs.skip != 'true'
138+
id: detect-pm
139+
shell: bash
140+
env:
141+
PACKAGE_MANAGER: ${{ inputs.package-manager }}
142+
VERSION: ${{ inputs.version }}
143+
WORKING_DIRECTORY: ${{ inputs.working-directory }}
144+
run: |
145+
# Action runs without its own node_modules; install the detector lazily.
146+
# Pinned to a known version so consumers get reproducible builds.
147+
npm install --no-save --prefix "$RUNNER_TEMP/codemap-action" package-manager-detector@1.6.0 >/dev/null
148+
cp "${{ github.action_path }}/scripts/detect-pm.mjs" "$RUNNER_TEMP/codemap-action/detect-pm.mjs"
149+
cd "$RUNNER_TEMP/codemap-action"
150+
node detect-pm.mjs
151+
152+
- name: Validate inputs (mode + flag interactions)
153+
if: steps.gate.outputs.skip != 'true'
154+
shell: bash
155+
env:
156+
MODE: ${{ inputs.mode }}
157+
RECIPE: ${{ inputs.recipe }}
158+
COMMAND: ${{ inputs.command }}
159+
run: |
160+
# Precedence: command > mode. If command is set, mode/recipe are silently
161+
# ignored but we surface a single warning so consumers notice.
162+
if [ -n "$COMMAND" ] && [ -n "$RECIPE" ]; then
163+
echo "::warning::codemap action: 'command' input takes precedence; 'recipe' (and other mode-* inputs) are ignored."
164+
fi
165+
166+
case "$MODE" in
167+
audit | recipe | command) ;;
168+
aggregate)
169+
echo "::error::codemap action: mode='aggregate' is reserved for v1.x and not yet implemented. Use mode='audit' or pass a 'command:' input."
170+
exit 1
171+
;;
172+
*)
173+
echo "::error::codemap action: unknown mode '$MODE'. Expected: audit | recipe | aggregate | command."
174+
exit 1
175+
;;
176+
esac
177+
178+
if [ "$MODE" = "recipe" ] && [ -z "$RECIPE" ] && [ -z "$COMMAND" ]; then
179+
echo "::error::codemap action: mode='recipe' requires the 'recipe' input (a recipe id). Run codemap query --recipes-json to list known recipes."
180+
exit 1
181+
fi
182+
183+
if [ "$MODE" = "command" ] && [ -z "$COMMAND" ]; then
184+
echo "::error::codemap action: mode='command' requires the 'command' input (raw CLI args)."
185+
exit 1
186+
fi
187+
188+
- name: Run codemap
189+
if: steps.gate.outputs.skip != 'true'
190+
id: run
191+
shell: bash
192+
working-directory: ${{ inputs.working-directory }}
193+
env:
194+
EXEC: ${{ steps.detect-pm.outputs.exec }}
195+
MODE: ${{ inputs.mode }}
196+
RECIPE: ${{ inputs.recipe }}
197+
PARAMS: ${{ inputs.params }}
198+
BASELINE: ${{ inputs.baseline }}
199+
AUDIT_BASE: ${{ inputs.audit-base }}
200+
CHANGED_SINCE: ${{ inputs.changed-since }}
201+
GROUP_BY: ${{ inputs.group-by }}
202+
COMMAND: ${{ inputs.command }}
203+
FORMAT: ${{ inputs.format }}
204+
OUTPUT_PATH: ${{ inputs.output-path }}
205+
FAIL_ON: ${{ inputs.fail-on }}
206+
STATE_DIR: ${{ inputs.state-dir }}
207+
BASE_REF: ${{ github.base_ref }}
208+
run: |
209+
set +e
210+
211+
if [ -n "$COMMAND" ]; then
212+
ARGS="$COMMAND"
213+
elif [ "$MODE" = "audit" ]; then
214+
BASE="${AUDIT_BASE:-$BASE_REF}"
215+
ARGS="audit --base $BASE --format $FORMAT"
216+
elif [ "$MODE" = "recipe" ]; then
217+
ARGS="query --recipe $RECIPE --format $FORMAT"
218+
[ -n "$PARAMS" ] && while IFS= read -r line; do
219+
[ -n "$line" ] && ARGS="$ARGS --params $line"
220+
done <<< "$PARAMS"
221+
[ -n "$BASELINE" ] && ARGS="$ARGS --baseline $BASELINE"
222+
fi
223+
224+
[ -n "$CHANGED_SINCE" ] && ARGS="$ARGS --changed-since $CHANGED_SINCE"
225+
[ -n "$GROUP_BY" ] && ARGS="$ARGS --group-by $GROUP_BY"
226+
[ -n "$STATE_DIR" ] && ARGS="--state-dir $STATE_DIR $ARGS"
227+
228+
echo "+ $EXEC $ARGS"
229+
$EXEC $ARGS > "$OUTPUT_PATH"
230+
EXIT=$?
231+
232+
echo "exit_code=$EXIT" >> "$GITHUB_OUTPUT"
233+
234+
# `fail-on` policy. v1.0 supports 'any' (default) and 'never'. Other
235+
# values fall back to passing the underlying exit code through.
236+
case "$FAIL_ON" in
237+
never) exit 0 ;;
238+
any | *) exit "$EXIT" ;;
239+
esac
240+
241+
- name: Upload SARIF to Code Scanning
242+
if: steps.gate.outputs.skip != 'true' && inputs.upload-sarif == 'true' && inputs.format == 'sarif' && always()
243+
uses: github/codeql-action/upload-sarif@v3
244+
with:
245+
sarif_file: ${{ inputs.working-directory }}/${{ inputs.output-path }}
246+
token: ${{ inputs.token != '' && inputs.token || github.token }}
247+
248+
- name: Post PR summary comment
249+
if: steps.gate.outputs.skip != 'true' && inputs.pr-comment == 'true' && github.event_name == 'pull_request' && always()
250+
shell: bash
251+
working-directory: ${{ inputs.working-directory }}
252+
env:
253+
EXEC: ${{ steps.detect-pm.outputs.exec }}
254+
OUTPUT_PATH: ${{ inputs.output-path }}
255+
PR_NUMBER: ${{ github.event.pull_request.number }}
256+
GH_TOKEN: ${{ inputs.token != '' && inputs.token || github.token }}
257+
run: |
258+
# Render the markdown body via `codemap pr-comment`, then post via
259+
# `gh pr comment`. The same binary that produced the SARIF / JSON
260+
# output renders the comment — keeps the version stream coherent.
261+
BODY=$($EXEC pr-comment "$OUTPUT_PATH" 2>/dev/null) || {
262+
echo "::warning::codemap action: pr-comment renderer failed; skipping PR comment."
263+
exit 0
264+
}
265+
if [ -z "$BODY" ]; then
266+
echo "::notice::codemap action: pr-comment produced empty body; skipping."
267+
exit 0
268+
fi
269+
echo "$BODY" | gh pr comment "$PR_NUMBER" -F -

0 commit comments

Comments
 (0)