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
58 changes: 30 additions & 28 deletions .agents/rules/codemap.md

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions .changeset/audit-format-sarif.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@stainless-code/codemap": minor
---

`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.

`--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.

`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.

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.
19 changes: 19 additions & 0 deletions .changeset/marketplace-action.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
"@stainless-code/codemap": minor
---

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.

**`--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.

**`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.

**`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.

**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).

**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`).

**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.

Slice 5 (Marketplace publish + listing metadata) is post-merge — gated on a v1.0.0 tag.
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,35 @@ jobs:
- name: bun audit
run: bun audit

action-smoke:
# Dogfood Slice 4 of plan PR #73: invoke `uses: ./` from this very repo
# so the action.yml + scripts/detect-pm.mjs are exercised on every PR.
# Smoke uses `command: --version` to avoid the real-audit dependency
# chain (audit baselines etc.) — this validates the composite-step
# flow + npm-pulled codemap binary, not the audit logic itself
# (which is covered by the unit-test suite).
name: 🤖 Action smoke (dogfood)
needs: skip-ci
if: needs['skip-ci'].outputs.skip != 'true'
runs-on: ubuntu-latest
# Non-blocking until we've published codemap@<v1> matching the Action.
# 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.
continue-on-error: true
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Run the local-path action
uses: ./
with:
command: "--version"
format: "json"
upload-sarif: "false"
pr-comment: "false"
fail-on: "never"

ci-complete:
name: CI complete
needs: [skip-ci, format, lint, typecheck, test, build, benchmark]
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ codemap audit --files-baseline base-files # explicit per-d
codemap audit --baseline base --files-baseline hotfix-files # mixed — auto-resolve deps + deprecated; override files
codemap audit --baseline base --no-index # skip the auto-incremental-index prelude (frozen-DB CI)
codemap audit --base origin/main --json # ad-hoc — worktree+reindex against any committish; no --save-baseline needed
codemap audit --base origin/main --format sarif # emit SARIF 2.1.0 directly (Code Scanning); also: --ci alias
codemap audit --base origin/main --ci # CI shortcut: --format sarif + non-zero exit on additions + quiet
codemap audit --base v1.0.0 --files-baseline pre-release-files # mix --base with per-delta override
# --base materialises <ref> via `git worktree add` to .codemap/audit-cache/<sha>/, reindexes into
# a temp DB, then diffs. Cache hit on second run against same sha is sub-100ms. Requires git;
Expand All @@ -127,7 +129,11 @@ codemap audit --base v1.0.0 --files-baseline pre-release-files # mix --base wit
# requires {from, to, label?, kind?} rows and rejects unbounded inputs (>50 edges) with a
# scope-suggestion error — alias columns via SELECT col AS "from", col2 AS "to".
codemap query --recipe deprecated-symbols --format sarif > findings.sarif
codemap query --recipe deprecated-symbols --ci # CI shortcut: --format sarif + non-zero exit + quiet
codemap query --recipe deprecated-symbols --format annotations # one ::notice per row
# Render any audit/SARIF output as a markdown PR-summary comment (for repos without
# Code Scanning / aggregate audit deltas / bot-context seeding):
codemap audit --base origin/main --json | codemap pr-comment - | gh pr comment <PR> -F -
codemap query --format mermaid 'SELECT from_path AS "from", to_path AS "to" FROM dependencies LIMIT 50'
codemap query --format diff 'SELECT "README.md" AS file_path, 1 AS line_start, "# Codemap" AS before_pattern, "# Codemap Preview" AS after_pattern'
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'
Expand Down
269 changes: 269 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
name: "Codemap"
description: "SQL-queryable structural index of your codebase. Run any predicate as a recipe; CI gating via SARIF → Code Scanning."
author: "stainless-code"
branding:
icon: "database"
color: "blue"

inputs:
# WHERE TO RUN
working-directory:
description: "Subdirectory to run codemap in (for monorepos). Defaults to the repository root."
required: false
default: "."
package-manager:
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)."
required: false
default: ""
version:
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."
required: false
default: ""
state-dir:
description: "Override codemap state directory location. Empty = .codemap/ at the working-directory root (codemap default)."
required: false
default: ""

# WHAT TO RUN — high-level (mutually exclusive; precedence: command > mode > defaults)
mode:
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."
required: false
default: "audit"
recipe:
description: "Recipe id (when mode=recipe). Use --recipes-json on the CLI to list known recipes."
required: false
default: ""
params:
description: "Recipe params for parametrised recipes (when mode=recipe). Multiline `key=value` pairs, one per line."
required: false
default: ""
baseline:
description: "Saved baseline name to diff against (when mode=recipe + a baseline was previously saved with --save-baseline)."
required: false
default: ""
audit-base:
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."
required: false
Comment thread
coderabbitai[bot] marked this conversation as resolved.
default: ""
changed-since:
description: "Filter results to files changed since the given git ref (e.g. 'origin/main')."
required: false
default: ""
group-by:
description: "Bucket results by 'owner' (CODEOWNERS) | 'directory' | 'package' (workspace package). Empty = no bucketing."
required: false
default: ""
command:
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."
required: false
default: ""

# WHAT TO DO WITH OUTPUT
format:
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)."
required: false
default: "sarif"
output-path:
description: "Where to write the output file. Used as the artifact-upload source when format=sarif and upload-sarif=true."
required: false
default: "codemap.sarif"
upload-sarif:
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)."
required: false
default: "true"
pr-comment:
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)."
required: false
default: "false"
fail-on:
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."
required: false
default: "any"
token:
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."
required: false
default: ""

outputs:
agent:
description: "Resolved package manager (npm / pnpm / yarn / bun)."
value: ${{ steps.detect-pm.outputs.agent }}
exec:
description: "Shell-ready command used to invoke codemap."
value: ${{ steps.detect-pm.outputs.exec }}
install_method:
description: "How codemap was located: 'project-installed' | 'dlx-pinned' | 'dlx-latest'."
value: ${{ steps.detect-pm.outputs.install_method }}
output-file:
description: "Path to the written output file (echoes inputs.output-path)."
value: ${{ inputs.output-path }}

runs:
using: "composite"
steps:
- name: Skip on non-PR events when defaulting to audit
id: gate
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
BASE_REF: ${{ github.base_ref }}
MODE: ${{ inputs.mode }}
COMMAND: ${{ inputs.command }}
AUDIT_BASE_INPUT: ${{ inputs.audit-base }}
run: |
if [ -n "$COMMAND" ]; then
echo "skip=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ "$MODE" != "audit" ]; then
echo "skip=false" >> "$GITHUB_OUTPUT"
exit 0
fi
BASE="${AUDIT_BASE_INPUT:-$BASE_REF}"
if [ -z "$BASE" ]; then
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."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "skip=false" >> "$GITHUB_OUTPUT"

- name: Setup Node.js (for the package-manager-detector wrapper)
if: steps.gate.outputs.skip != 'true'
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Detect package manager + resolve CLI invocation
if: steps.gate.outputs.skip != 'true'
id: detect-pm
shell: bash
env:
PACKAGE_MANAGER: ${{ inputs.package-manager }}
VERSION: ${{ inputs.version }}
WORKING_DIRECTORY: ${{ inputs.working-directory }}
run: |
# Action runs without its own node_modules; install the detector lazily.
# Pinned to a known version so consumers get reproducible builds.
npm install --no-save --prefix "$RUNNER_TEMP/codemap-action" package-manager-detector@1.6.0 >/dev/null
cp "${{ github.action_path }}/scripts/detect-pm.mjs" "$RUNNER_TEMP/codemap-action/detect-pm.mjs"
cd "$RUNNER_TEMP/codemap-action"
node detect-pm.mjs

- name: Validate inputs (mode + flag interactions)
if: steps.gate.outputs.skip != 'true'
shell: bash
env:
MODE: ${{ inputs.mode }}
RECIPE: ${{ inputs.recipe }}
COMMAND: ${{ inputs.command }}
run: |
# Precedence: command > mode. If command is set, mode/recipe are silently
# ignored but we surface a single warning so consumers notice.
if [ -n "$COMMAND" ] && [ -n "$RECIPE" ]; then
echo "::warning::codemap action: 'command' input takes precedence; 'recipe' (and other mode-* inputs) are ignored."
fi

case "$MODE" in
audit | recipe | command) ;;
aggregate)
echo "::error::codemap action: mode='aggregate' is reserved for v1.x and not yet implemented. Use mode='audit' or pass a 'command:' input."
exit 1
;;
*)
echo "::error::codemap action: unknown mode '$MODE'. Expected: audit | recipe | aggregate | command."
exit 1
;;
esac

if [ "$MODE" = "recipe" ] && [ -z "$RECIPE" ] && [ -z "$COMMAND" ]; then
echo "::error::codemap action: mode='recipe' requires the 'recipe' input (a recipe id). Run codemap query --recipes-json to list known recipes."
exit 1
fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if [ "$MODE" = "command" ] && [ -z "$COMMAND" ]; then
echo "::error::codemap action: mode='command' requires the 'command' input (raw CLI args)."
exit 1
fi

- name: Run codemap
if: steps.gate.outputs.skip != 'true'
id: run
shell: bash
working-directory: ${{ inputs.working-directory }}
env:
EXEC: ${{ steps.detect-pm.outputs.exec }}
MODE: ${{ inputs.mode }}
RECIPE: ${{ inputs.recipe }}
PARAMS: ${{ inputs.params }}
BASELINE: ${{ inputs.baseline }}
AUDIT_BASE: ${{ inputs.audit-base }}
CHANGED_SINCE: ${{ inputs.changed-since }}
GROUP_BY: ${{ inputs.group-by }}
COMMAND: ${{ inputs.command }}
FORMAT: ${{ inputs.format }}
OUTPUT_PATH: ${{ inputs.output-path }}
FAIL_ON: ${{ inputs.fail-on }}
STATE_DIR: ${{ inputs.state-dir }}
BASE_REF: ${{ github.base_ref }}
run: |
set +e

if [ -n "$COMMAND" ]; then
ARGS="$COMMAND"
elif [ "$MODE" = "audit" ]; then
BASE="${AUDIT_BASE:-$BASE_REF}"
ARGS="audit --base $BASE --format $FORMAT"
elif [ "$MODE" = "recipe" ]; then
ARGS="query --recipe $RECIPE --format $FORMAT"
[ -n "$PARAMS" ] && while IFS= read -r line; do
[ -n "$line" ] && ARGS="$ARGS --params $line"
done <<< "$PARAMS"
[ -n "$BASELINE" ] && ARGS="$ARGS --baseline $BASELINE"
fi

[ -n "$CHANGED_SINCE" ] && ARGS="$ARGS --changed-since $CHANGED_SINCE"
[ -n "$GROUP_BY" ] && ARGS="$ARGS --group-by $GROUP_BY"
[ -n "$STATE_DIR" ] && ARGS="--state-dir $STATE_DIR $ARGS"

echo "+ $EXEC $ARGS"
$EXEC $ARGS > "$OUTPUT_PATH"
EXIT=$?

echo "exit_code=$EXIT" >> "$GITHUB_OUTPUT"

# `fail-on` policy. v1.0 supports 'any' (default) and 'never'. Other
# values fall back to passing the underlying exit code through.
case "$FAIL_ON" in
never) exit 0 ;;
any | *) exit "$EXIT" ;;
esac

- name: Upload SARIF to Code Scanning
if: steps.gate.outputs.skip != 'true' && inputs.upload-sarif == 'true' && inputs.format == 'sarif' && always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: ${{ inputs.working-directory }}/${{ inputs.output-path }}
token: ${{ inputs.token != '' && inputs.token || github.token }}

- name: Post PR summary comment
if: steps.gate.outputs.skip != 'true' && inputs.pr-comment == 'true' && github.event_name == 'pull_request' && always()
shell: bash
working-directory: ${{ inputs.working-directory }}
env:
EXEC: ${{ steps.detect-pm.outputs.exec }}
OUTPUT_PATH: ${{ inputs.output-path }}
PR_NUMBER: ${{ github.event.pull_request.number }}
GH_TOKEN: ${{ inputs.token != '' && inputs.token || github.token }}
run: |
# Render the markdown body via `codemap pr-comment`, then post via
# `gh pr comment`. The same binary that produced the SARIF / JSON
# output renders the comment — keeps the version stream coherent.
BODY=$($EXEC pr-comment "$OUTPUT_PATH" 2>/dev/null) || {
echo "::warning::codemap action: pr-comment renderer failed; skipping PR comment."
exit 0
}
if [ -z "$BODY" ]; then
echo "::notice::codemap action: pr-comment produced empty body; skipping."
exit 0
fi
echo "$BODY" | gh pr comment "$PR_NUMBER" -F -
Loading
Loading