Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
274 changes: 274 additions & 0 deletions .github/workflows/dependabot-auto-vet.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
name: Dependabot Cargo Vet

on:
pull_request:
types: [opened, synchronize, reopened]
branches:
- "*"
workflow_dispatch:

jobs:
vet-dependabot:
if: github.actor == 'bronzelle-cw' || github.actor == 'dependabot[bot]'
name: Vet Dependabot Updates
runs-on: ubuntu-22.04
permissions:
contents: write
pull-requests: write
env:
CARGO_VET_VERSION: 0.10.0

steps:
- name: Checkout PR head
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: true

- name: Set up Rust
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain 1.89
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"

- name: Install cargo-vet
run: |
source "$HOME/.cargo/env"
cargo install --locked --version $CARGO_VET_VERSION cargo-vet

- name: Initial cargo vet --locked
id: vet_locked_initial
continue-on-error: true
run: |
set +e
cargo vet --locked > vet-locked.log 2>&1
status=$?
echo "status=$status" >> "$GITHUB_OUTPUT"
exit 0

- name: Try importing audits
if: steps.vet_locked_initial.outputs.status != '0'
run: |
cargo vet > vet-import.log 2>&1 || true

# Grab the first recommended diff line and keep the first 6 tokens:
# cargo vet diff <crate> <old> <new>
line="$(grep -m1 -E '^\s*cargo vet diff ' vet-import.log | sed -E 's/^\s+//')"
echo "Recommended line: $line"
echo "$line" | awk '{print $1,$2,$3,$4,$5,$6}' > recommended.diff.cmd || true

echo "recommended.diff.cmd:"
cat recommended.diff.cmd || true

- name: Derive vet status
id: vet_status
run: |
status="${{ steps.vet_locked_initial.outputs.status }}"
echo "status=$status" >> "$GITHUB_OUTPUT"
echo "vet-locked.log:"
cat vet-locked.log || true
echo "vet-import.log:"
cat vet-import.log || true

- name: Early exit when fully vetted
if: steps.vet_status.outputs.status == '0'
run: echo "Vetting complete; no audits needed."

- name: Collect unvetted dependency and crate diff
id: collect_unvetted_and_diff
if: steps.vet_status.outputs.status != '0'
run: |
set -euo pipefail

if [ ! -s recommended.diff.cmd ]; then
echo "No recommended diff found" >&2
exit 1
fi

diff_cmd="$(cat recommended.diff.cmd)"
echo "Using recommended diff: $diff_cmd"

# diff_cmd tokens: cargo vet diff <crate> <old> <new>
set -- $diff_cmd
crate="$4"
old="$5"
new="$6"

# Run diff and capture output, but don't let -e kill the step yet
set +e
PAGER=cat GIT_PAGER=cat cargo vet diff "$crate" "$old" "$new" --mode=local --output-format=human \
> crate-diff.txt 2> crate-diff.stderr
status=$?
set -e

echo "cargo vet diff exit status: $status"

# 0 = no diff, 1 = diff exists (expected), 2 = trouble (real failure)
if [ "$status" -gt 1 ]; then
echo "cargo vet diff failed (status=$status). stderr:"
sed -n '1,200p' crate-diff.stderr
exit "$status"
fi

cp VETTING_CONTEXT.md vetting-context.md
{
echo "crate=$crate"
echo "version=$new"
} >> "$GITHUB_OUTPUT"

- name: Build prompt for Codex agent
id: build_prompt
if: steps.vet_status.outputs.status != '0'
run: |
ctx="$(cat vetting-context.md)"
diff="$(head -c 180000 crate-diff.txt)"
crate="${{ steps.collect_unvetted_and_diff.outputs.crate }}"
version="${{ steps.collect_unvetted_and_diff.outputs.version }}"
delim="PROMPT_$(date +%s%N)"
{
echo "prompt<<$delim"
echo "You are a Rust supply-chain security auditor. Follow VETTING_CONTEXT strictly and assess the diff only."
echo
echo "VETTING_CONTEXT:"
echo "$ctx"
echo
echo "Unvetted dependency: $crate $version"
echo
echo "Diff between previous and bumped version (cargo vet diff):"
echo "$diff"
echo
echo "Respond ONLY with JSON (no prose, no code fences) matching exactly:"
echo '{"status":"vetted|unvetted","description":"THE DESCRIPTION OF ITS ASSESSTMENT"}'
echo "$delim"
} >> "$GITHUB_OUTPUT"

- name: Debug OPENAI key presence
if: steps.vet_status.outputs.status != '0'
run: |
if [ -z "$OPENAI_API_KEY" ]; then
echo "OPENAI_API_KEY is empty/missing"
exit 1
fi
echo "OPENAI_API_KEY present (length: ${#OPENAI_API_KEY})"
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}

- name: 🤖 Analyze and Fix Issue with Codex
id: codex
if: steps.vet_status.outputs.status != '0'
uses: openai/codex-action@v1
with:
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
model: gpt-5-codex
prompt: ${{ steps.build_prompt.outputs.prompt }}

- name: Comment when agent step failed or missing
if: steps.vet_status.outputs.status != '0' && (steps.codex.outcome == 'failure' || steps.codex.outputs.response == '')
uses: actions/github-script@v7
with:
script: |
const crate = '${{ steps.collect_unvetted_and_diff.outputs.crate }}';
const version = '${{ steps.collect_unvetted_and_diff.outputs.version }}';
const msg = [
'Cargo vet still needs audits and no agent result was applied.',
'',
`Unvetted dependency: ${crate} ${version}`,
'',
'Codex agent was not configured or did not return a response. Ensure OPENAI_API_KEY is set and the prompt is valid.'
].join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: msg
});
core.setFailed('Agent step failed or was not configured.')

- name: Apply agent decision
id: apply_agent_decision
if: steps.vet_status.outputs.status != '0' && steps.codex.outcome == 'success' && steps.codex.outputs.response != ''
run: |
set -euo pipefail

crate="${{ steps.collect_unvetted_and_diff.outputs.crate }}"
version="${{ steps.collect_unvetted_and_diff.outputs.version }}"

echo '${{ steps.codex.outputs.response }}' > agent-decision.json

# Validate JSON shape
jq -e '
(type=="object")
and (.status | type=="string")
and (.description | type=="string")
and (.status=="vetted" or .status=="unvetted")
' agent-decision.json > /dev/null

status="$(jq -r '.status' agent-decision.json)"
desc="$(jq -r '.description' agent-decision.json)"

# Export for later steps (commenting)
delim="DESC_$(date +%s%N)"
{
echo "status=$status"
echo "description<<$delim"
echo "$desc"
echo "$delim"
} >> "$GITHUB_OUTPUT"

if [ "$status" = "unvetted" ]; then
echo "Agent marked as unvetted; will not certify."
exit 0
fi

who="dependabot-vet-bot <actions@github.com>"
notes_one_line="$(printf "%s" "$desc" | tr '\n' ' ' | tr -s ' ')"

cargo vet certify "$crate" "$version" \
--criteria "safe-to-deploy" \
--who "$who" \
--notes "$notes_one_line" \
--accept-all

echo "$who" > .agent-who

- name: Comment when agent refuses to vet
if: steps.vet_status.outputs.status != '0' && steps.apply_agent_decision.outputs.status == 'unvetted'
uses: actions/github-script@v7
with:
script: |
const crate = '${{ steps.collect_unvetted_and_diff.outputs.crate }}';
const version = '${{ steps.collect_unvetted_and_diff.outputs.version }}';
const desc = `${{ steps.apply_agent_decision.outputs.description }}`;
const msg = [
'Cargo vet still needs audits, and the automated diff review **did not approve** this update.',
'',
`**Dependency:** \`${crate} ${version}\``,
'',
'**Assessment:**',
desc
].join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: msg
});

- name: Verify cargo vet after agent decision
if: steps.vet_status.outputs.status != '0' && steps.apply_agent_decision.outputs.status == 'vetted'
run: cargo vet --locked

- name: Commit audit changes
if: steps.vet_status.outputs.status != '0' && steps.apply_agent_decision.outputs.status == 'vetted'
run: |
set -eo pipefail
if git diff --quiet; then
echo "No changes to commit."
exit 0
fi

who="$(cat .agent-who || true)"
git config user.name "${who:-dependabot-vet-bot}"
git config user.email "actions@github.com"
git add supply-chain/audits.toml supply-chain/imports.lock || true
git commit -m "chore(vet): apply agent audits"
git push origin "HEAD:${{ github.event.pull_request.head.ref }}"
Loading
Loading