Skip to content

Claude Dependabot Alert Remediation #8

Claude Dependabot Alert Remediation

Claude Dependabot Alert Remediation #8

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