Claude Dependabot Alert Remediation #8
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: Claude Dependabot Alert Remediation | |
| on: | |
| schedule: | |
| - cron: "0 9 1 * *" # 1st of each month at 9am UTC | |
| workflow_dispatch: | |
| jobs: | |
| maintenance: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| issues: read | |
| id-token: write | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Generate GitHub App token for Dependabot alerts | |
| id: app-token | |
| uses: actions/create-github-app-token@v1 | |
| with: | |
| app-id: ${{ vars.DEPENDABOT_APP_ID }} | |
| private-key: ${{ secrets.DEPENDABOT_APP_PRIVATE_KEY }} | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v5 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version-file: "pyproject.toml" | |
| - name: Install Graphite CLI | |
| run: npm install -g @withgraphite/graphite-cli@stable | |
| - name: Configure git and Graphite | |
| env: | |
| GRAPHITE_AUTH_TOKEN: ${{ secrets.GRAPHITE_AUTH_TOKEN }} | |
| run: | | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git config user.name "github-actions[bot]" | |
| gt auth --token "$GRAPHITE_AUTH_TOKEN" | |
| - name: Fetch Dependabot alerts and repo context | |
| id: ctx | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| run: | | |
| # Fetch all open Dependabot alerts as newline-delimited JSON objects. | |
| # Note: GITHUB_TOKEN cannot access Dependabot alerts; uses a GitHub App | |
| # installation token (org-owned, no individual user dependency). | |
| if ! RAW_ALERTS=$(gh api "repos/${{ github.repository }}/dependabot/alerts" \ | |
| --paginate \ | |
| -q '.[] | select(.state=="open")' 2>&1); then | |
| echo "has_alerts=false" >> "$GITHUB_OUTPUT" | |
| echo "::error::Failed to fetch Dependabot alerts. Ensure security-events: read is permitted for GITHUB_TOKEN in this org. API response: $RAW_ALERTS" | |
| exit 0 | |
| fi | |
| if [ -z "$RAW_ALERTS" ]; then | |
| echo "has_alerts=false" >> "$GITHUB_OUTPUT" | |
| echo "No open Dependabot alerts found. Skipping Claude step." | |
| exit 0 | |
| fi | |
| # Pre-process: extract only the fields Claude needs, filtering out | |
| # development-scoped dependencies (not in production). | |
| ALERTS=$(echo "$RAW_ALERTS" | jq -c 'select( | |
| .dependency.scope != "development" | |
| ) | { | |
| number: .number, | |
| severity: .security_vulnerability.severity, | |
| package: .dependency.package.name, | |
| ecosystem: .dependency.package.ecosystem, | |
| manifest: .dependency.manifest_path, | |
| current_version: .security_vulnerability.vulnerable_version_range, | |
| patched_version: .security_vulnerability.first_patched_version.identifier | |
| }') | |
| if [ -z "$ALERTS" ]; then | |
| echo "has_alerts=false" >> "$GITHUB_OUTPUT" | |
| echo "No actionable Dependabot alerts after filtering. Skipping Claude step." | |
| exit 0 | |
| fi | |
| ALERT_COUNT=$(echo "$ALERTS" | wc -l | tr -d ' ') | |
| echo "has_alerts=true" >> "$GITHUB_OUTPUT" | |
| echo "alert_count=$ALERT_COUNT" >> "$GITHUB_OUTPUT" | |
| echo "Found $ALERT_COUNT actionable alert(s)." | |
| # Collect Python manifest paths (excluding virtual envs and hidden dirs) | |
| MANIFESTS=$(find . \( -name pyproject.toml -o -name 'requirements*.txt' \) \ | |
| -not -path '*/.venv/*' \ | |
| -not -path '*/node_modules/*' \ | |
| -not -path '*/__pycache__/*' \ | |
| | sort | tr '\n' ' ') | |
| # Write both to files in the workspace so Claude Code can access them. | |
| echo "$ALERTS" > dependabot-alerts.txt | |
| echo "$MANIFESTS" > manifests.txt | |
| - name: Find existing remediation PR branch to stack on | |
| id: find-base | |
| if: steps.ctx.outputs.has_alerts == 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| EXISTING_BRANCH=$(gh pr list \ | |
| --repo ${{ github.repository }} \ | |
| --search "dependabot alert remediation in:title" \ | |
| --state open \ | |
| --json number,headRefName \ | |
| --jq 'sort_by(.number) | last | .headRefName // empty') | |
| if [ -n "$EXISTING_BRANCH" ]; then | |
| echo "base_branch=$EXISTING_BRANCH" >> "$GITHUB_OUTPUT" | |
| git fetch origin "$EXISTING_BRANCH":"$EXISTING_BRANCH" | |
| echo "Stacking on existing remediation branch: $EXISTING_BRANCH" | |
| else | |
| echo "base_branch=main" >> "$GITHUB_OUTPUT" | |
| echo "No existing remediation branch found, branching from main" | |
| fi | |
| - name: Claude Dependabot Alert Remediation | |
| if: steps.ctx.outputs.has_alerts == 'true' | |
| uses: anthropics/claude-code-action@v1 | |
| with: | |
| anthropic_api_key: ${{ secrets.SECURITY_REMEDIATOR_API_KEY }} | |
| show_full_output: true | |
| github_token: ${{ github.token }} | |
| allowed_bots: "claude,graphite-app" | |
| claude_args: '--max-turns 60 --allowedTools "Grep,Glob,Read,Edit,Write,Bash(cd *),Bash(git *),Bash(uv *),Bash(gt *),Bash(gh pr edit *),Bash(gh api *)"' | |
| prompt: | | |
| The repo is ${{ github.repository }}. | |
| There are ${{ steps.ctx.outputs.alert_count }} actionable Dependabot alerts in dependabot-alerts.txt (one JSON object per line, already filtered to production dependencies). | |
| The list of Python manifest files in this repo is in manifests.txt. | |
| Read both files (they are in the repo root working directory), then for each alert: | |
| 1. Identify the affected manifest path, package name, ecosystem, current version, and minimum patched version. | |
| 2. If the manifest path does not appear in manifests.txt, mark it as stale and skip it. | |
| 3. This repository uses Python with uv. Update vulnerable dependencies using the most appropriate manifest for the alert: | |
| - For `pyproject.toml` dependencies, use the Edit tool to add `"<package>>=<patched_version>"` directly to the `dependencies` list in the file. Do NOT use `uv add` β it requires shell characters that are blocked in this environment. | |
| - For `requirements.txt` style manifests, update the pinned requirement entry directly using the Edit tool. | |
| 4. After editing a manifest, `cd` into the directory containing it, then run `uv lock` as a separate Bash command. Never chain commands with `&&`, `;`, or pipes. | |
| 5. Create a stacked PR using Graphite β but ONLY if at least one manifest file was actually modified. Before creating any branch or PR, check whether any files were changed by running `git status --porcelain`. If the output is empty (no changes), stop here and print a summary of why each alert was skipped. Do NOT create a branch, commit, or PR when there are no file changes. | |
| a. Run `git checkout ${{ steps.find-base.outputs.base_branch }}` to switch to the base branch. | |
| b. Run `gt checkout -b gradient-labs-dependabot-remediation-<YYYY-MM-DD>` (using today's date) to create a new Graphite-tracked branch stacked on it. | |
| c. Make all dependency changes and run `uv lock` as described above. | |
| d. Run `git status --porcelain` to confirm there are actual file changes. If there are none, stop β do not commit or create a PR. | |
| e. Stage and commit all changes with `git add -A` then `git commit -m "gradient-labs: dependabot alert remediation <YYYY-MM-DD>"`. | |
| f. Run `gt submit --no-interactive` to create the PR via Graphite. | |
| g. Find the newly created PR number with `gh api "repos/${{ github.repository }}/pulls" -q '[.[] | select(.head.ref | startswith("gradient-labs-dependabot-remediation"))] | sort_by(.number) | last | .number'`. | |
| h. Update the PR body with `gh pr edit <number> --body "..."` β the body should include a markdown table with columns: alert number, severity, package, old version, new version, ecosystem, manifest updated. Note any stale or skipped alerts below the table. | |
| - name: Assign teams to remediation PR | |
| if: steps.ctx.outputs.has_alerts == 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| PR_NUMBER=$(gh pr list \ | |
| --repo ${{ github.repository }} \ | |
| --search "dependabot alert remediation in:title" \ | |
| --state open \ | |
| --json number \ | |
| --jq 'sort_by(.number) | last | .number // empty') | |
| if [ -n "$PR_NUMBER" ]; then | |
| gh pr edit "$PR_NUMBER" \ | |
| --repo ${{ github.repository }} \ | |
| --add-reviewer "gradientlabs-ai/backend-engineers,gradientlabs-ai/ai-engineers" | |
| fi |