|
| 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." |
0 commit comments