Skip to content

Commit 9383a95

Browse files
committed
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).
1 parent 7a81b88 commit 9383a95

5 files changed

Lines changed: 536 additions & 1 deletion

File tree

action.yml

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
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). Default: ${{ github.base_ref }} on pull_request events."
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. Default: ${{ github.token }}."
84+
required: false
85+
default: ${{ github.token }}
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+
- name: Run codemap
184+
if: steps.gate.outputs.skip != 'true'
185+
id: run
186+
shell: bash
187+
working-directory: ${{ inputs.working-directory }}
188+
env:
189+
EXEC: ${{ steps.detect-pm.outputs.exec }}
190+
MODE: ${{ inputs.mode }}
191+
RECIPE: ${{ inputs.recipe }}
192+
PARAMS: ${{ inputs.params }}
193+
BASELINE: ${{ inputs.baseline }}
194+
AUDIT_BASE: ${{ inputs.audit-base }}
195+
CHANGED_SINCE: ${{ inputs.changed-since }}
196+
GROUP_BY: ${{ inputs.group-by }}
197+
COMMAND: ${{ inputs.command }}
198+
FORMAT: ${{ inputs.format }}
199+
OUTPUT_PATH: ${{ inputs.output-path }}
200+
FAIL_ON: ${{ inputs.fail-on }}
201+
STATE_DIR: ${{ inputs.state-dir }}
202+
BASE_REF: ${{ github.base_ref }}
203+
run: |
204+
set +e
205+
206+
# Build args based on inputs (or use raw command).
207+
if [ -n "$COMMAND" ]; then
208+
ARGS="$COMMAND"
209+
elif [ "$MODE" = "audit" ]; then
210+
BASE="${AUDIT_BASE:-$BASE_REF}"
211+
ARGS="audit --base $BASE --format $FORMAT"
212+
elif [ "$MODE" = "recipe" ]; then
213+
ARGS="query --recipe $RECIPE --format $FORMAT"
214+
[ -n "$PARAMS" ] && while IFS= read -r line; do
215+
[ -n "$line" ] && ARGS="$ARGS --params $line"
216+
done <<< "$PARAMS"
217+
[ -n "$BASELINE" ] && ARGS="$ARGS --baseline $BASELINE"
218+
fi
219+
220+
[ -n "$CHANGED_SINCE" ] && ARGS="$ARGS --changed-since $CHANGED_SINCE"
221+
[ -n "$GROUP_BY" ] && ARGS="$ARGS --group-by $GROUP_BY"
222+
[ -n "$STATE_DIR" ] && ARGS="--state-dir $STATE_DIR $ARGS"
223+
224+
echo "+ $EXEC $ARGS"
225+
$EXEC $ARGS > "$OUTPUT_PATH"
226+
EXIT=$?
227+
228+
echo "exit_code=$EXIT" >> "$GITHUB_OUTPUT"
229+
230+
# `fail-on` policy. v1.0 supports 'any' (default) and 'never'. Other
231+
# values fall back to passing the underlying exit code through.
232+
case "$FAIL_ON" in
233+
never) exit 0 ;;
234+
any | *) exit "$EXIT" ;;
235+
esac
236+
237+
- name: Upload SARIF to Code Scanning
238+
if: steps.gate.outputs.skip != 'true' && inputs.upload-sarif == 'true' && inputs.format == 'sarif' && always()
239+
uses: github/codeql-action/upload-sarif@v3
240+
with:
241+
sarif_file: ${{ inputs.working-directory }}/${{ inputs.output-path }}
242+
token: ${{ inputs.token }}
243+
244+
- name: Post PR summary comment
245+
if: steps.gate.outputs.skip != 'true' && inputs.pr-comment == 'true' && github.event_name == 'pull_request' && always()
246+
shell: bash
247+
env:
248+
EXEC: ${{ steps.detect-pm.outputs.exec }}
249+
OUTPUT_PATH: ${{ inputs.working-directory }}/${{ inputs.output-path }}
250+
FORMAT: ${{ inputs.format }}
251+
PR_NUMBER: ${{ github.event.pull_request.number }}
252+
GH_TOKEN: ${{ inputs.token }}
253+
run: |
254+
# Slice 3 lands `codemap pr-comment` later; until then, just stub a
255+
# short marker comment so downstream consumers can see the toggle works.
256+
echo "::warning::codemap action: pr-comment writer (Slice 3) not yet implemented; toggle was set but no comment was posted."

bun.lock

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
"lightningcss": "^1.32.0",
7979
"oxc-parser": "^0.127.0",
8080
"oxc-resolver": "^11.19.1",
81+
"package-manager-detector": "^1.6.0",
8182
"tinyglobby": "^0.2.16",
8283
"zod": "^4.3.6"
8384
},

0 commit comments

Comments
 (0)