Audit NPM Claude Remediation #175
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Audit NPM Claude Remediation | |
| on: | |
| workflow_run: | |
| workflows: ["Audit NPM Packages"] | |
| types: | |
| - completed | |
| workflow_dispatch: | |
| permissions: | |
| actions: read | |
| contents: write | |
| id-token: write | |
| issues: write | |
| pull-requests: write | |
| concurrency: | |
| group: audit-npm-claude-remediation-${{ github.event.repository.default_branch }} | |
| cancel-in-progress: false | |
| jobs: | |
| remediate: | |
| if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'failure' }} | |
| runs-on: blacksmith-2vcpu-ubuntu-2404 | |
| timeout-minutes: 60 | |
| env: | |
| NPM_AUDIT_CLAUDE_SLACK_WEBHOOK_URL: ${{ secrets.NPM_AUDIT_CLAUDE_SLACK_WEBHOOK_URL }} | |
| steps: | |
| - name: Checkout default branch | |
| uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ github.event.repository.default_branch }} | |
| - name: Install pnpm | |
| uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4 | |
| with: | |
| version: 10 | |
| - name: Scan default branch audit failures | |
| id: scan | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: node .github/scripts/audit-ci-vuln-scan.mjs | |
| - name: Skip when all vulnerabilities are covered | |
| if: ${{ steps.scan.outputs.has_uncovered != 'true' }} | |
| run: | | |
| echo "No uncovered audit-ci vulnerabilities on ${GITHUB_REF_NAME}; skipping Claude." | |
| - name: Run Claude remediation | |
| id: claude | |
| if: ${{ steps.scan.outputs.has_uncovered == 'true' }} | |
| uses: anthropics/claude-code-action@2cc1ac1331eac7a6a96d716dd204dd2888d0fcd2 # v1 | |
| with: | |
| anthropic_api_key: ${{ secrets.NPM_AUDIT_CLAUDE_ANTHROPIC_API_KEY }} | |
| base_branch: ${{ github.event.repository.default_branch }} | |
| branch_prefix: claude/audit-ci/ | |
| prompt: ${{ steps.scan.outputs.prompt }} | |
| claude_args: | | |
| --max-turns 40 | |
| --allowedTools "Read,Edit,MultiEdit,Write,Glob,Grep,LS,WebFetch,WebSearch,TodoWrite,Bash(pnpm:*),Bash(npm:*),Bash(node:*),Bash(git:*),Bash(gh:*),Bash(jq:*),Bash(rg:*),Bash(curl:*),Bash(date:*)" | |
| - name: Ensure remediation PR marker | |
| id: remediation_pr | |
| if: ${{ steps.scan.outputs.has_uncovered == 'true' && steps.claude.outputs.branch_name != '' }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| BRANCH_NAME: ${{ steps.claude.outputs.branch_name }} | |
| MARKER: ${{ steps.scan.outputs.marker }} | |
| run: | | |
| set -euo pipefail | |
| PR_NUMBER="$(gh pr list --head "$BRANCH_NAME" --state open --json number --jq '.[0].number // empty')" | |
| if [ -z "$PR_NUMBER" ]; then | |
| echo "::warning::Claude did not leave an open PR for branch ${BRANCH_NAME}; marker could not be enforced." | |
| exit 0 | |
| fi | |
| PR_URL="$(gh pr view "$PR_NUMBER" --json url --jq '.url')" | |
| PR_TITLE="$(gh pr view "$PR_NUMBER" --json title --jq '.title')" | |
| PR_SUMMARY="$(node -e 'const title = process.argv[1] || ""; console.log(title.replace(/^[A-Za-z]+(?:\([^)]+\))?!?:\s*/, ""));' "$PR_TITLE")" | |
| echo "pr_url=${PR_URL}" >> "$GITHUB_OUTPUT" | |
| echo "summary=${PR_SUMMARY}" >> "$GITHUB_OUTPUT" | |
| gh label create audit-ci-remediation \ | |
| --description "Automated audit-ci vulnerability remediation" \ | |
| --color "B60205" \ | |
| 2>/dev/null || true | |
| gh pr edit "$PR_NUMBER" --add-label audit-ci-remediation | |
| BODY_FILE="$(mktemp)" | |
| UPDATED_BODY_FILE="$(mktemp)" | |
| gh pr view "$PR_NUMBER" --json body --jq '.body // ""' > "$BODY_FILE" | |
| node - "$BODY_FILE" "$UPDATED_BODY_FILE" "$MARKER" <<'NODE' | |
| const { readFileSync, writeFileSync } = require("node:fs"); | |
| const [, , inputPath, outputPath, marker] = process.argv; | |
| const markerRegex = /<!--\s*audit-ci-vuln-keys:\s*\[[\s\S]*?\]\s*-->/; | |
| let body = readFileSync(inputPath, "utf8"); | |
| if (!markerRegex.test(body)) { | |
| if (/^## Summary\b.*$/m.test(body)) { | |
| body = body.replace(/^## Summary\b.*$/m, (heading) => `${heading}\n\n${marker}`); | |
| } else { | |
| body = `## Summary\n\n${marker}\n\n${body.trim()}\n`; | |
| } | |
| } | |
| writeFileSync(outputPath, body); | |
| NODE | |
| gh pr edit "$PR_NUMBER" --body-file "$UPDATED_BODY_FILE" | |
| - name: Request code review | |
| if: ${{ steps.scan.outputs.has_uncovered == 'true' && steps.claude.outputs.branch_name != '' }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| BRANCH_NAME: ${{ steps.claude.outputs.branch_name }} | |
| run: | | |
| set -euo pipefail | |
| PR_NUMBER="$(gh pr list --head "$BRANCH_NAME" --state open --json number --jq '.[0].number // empty')" | |
| if [ -z "$PR_NUMBER" ]; then | |
| echo "::warning::No open PR for ${BRANCH_NAME}; cannot request reviewers." | |
| exit 0 | |
| fi | |
| gh pr edit "$PR_NUMBER" --add-reviewer abimaelmartell,mogery | |
| - name: Notify Slack about remediation PR | |
| if: ${{ steps.remediation_pr.outputs.pr_url != '' && env.NPM_AUDIT_CLAUDE_SLACK_WEBHOOK_URL != '' }} | |
| env: | |
| SLACK_WEBHOOK_URL: ${{ env.NPM_AUDIT_CLAUDE_SLACK_WEBHOOK_URL }} | |
| PR_URL: ${{ steps.remediation_pr.outputs.pr_url }} | |
| SUMMARY: ${{ steps.remediation_pr.outputs.summary }} | |
| run: | | |
| set -euo pipefail | |
| jq -n \ | |
| --arg pr_url "$PR_URL" \ | |
| --arg summary "$SUMMARY" \ | |
| '{pr_url: $pr_url, summary: $summary}' \ | |
| | curl --fail-with-body --show-error --silent \ | |
| --request POST \ | |
| --header "Content-Type: application/json" \ | |
| --data @- \ | |
| "$SLACK_WEBHOOK_URL" |