Skip to content
Open
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
147 changes: 116 additions & 31 deletions .github/workflows/dependabot-auto-vet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ jobs:
CODEX_MODEL: "gpt-5-codex"
CRITERIA: "safe-to-deploy"
CONTEXT_FILE: "supply-chain/vet/VETTING_POLICY.md"
RETENTION_DAYS: "90"

# Prompt size guards (avoid accidental huge contexts)

Expand Down Expand Up @@ -367,38 +368,64 @@ jobs:
exit 0

# -------------------------
# Commit & push (even if some crates unvetted)
# Patch artifact publication
# -------------------------
- name: Commit audit changes if any (signed)
id: signed_commit
if: github.event_name == 'pull_request'
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
sign-commits: true
commit-message: "chore(vet): apply automated audits"
add-paths: |
supply-chain
branch: ${{ github.event.pull_request.head.ref }}
base: ${{ github.event.pull_request.base.ref }}

- name: Expose commit outputs
id: commit
- name: Generate auto-vet patch
id: patch
if: always()
run: |
set -euo pipefail

op="${{ steps.signed_commit.outputs.pull-request-operation }}"
sha="${{ steps.signed_commit.outputs.pull-request-head-sha }}"
patch_path="vet/auto-vet.patch"

if [ "$op" = "none" ] || [ -z "$sha" ]; then
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "pushed=false" >> "$GITHUB_OUTPUT"
if git diff --quiet -- supply-chain; then
echo "has_patch=false" >> "$GITHUB_OUTPUT"
echo "patch_path=" >> "$GITHUB_OUTPUT"
echo "patch_bytes=0" >> "$GITHUB_OUTPUT"
exit 0
fi

echo "changed=true" >> "$GITHUB_OUTPUT"
echo "sha=$sha" >> "$GITHUB_OUTPUT"
echo "pushed=true" >> "$GITHUB_OUTPUT"
git diff --binary --patch -- supply-chain > "$patch_path"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mkdir -p vet still missing (flagged in previous review).

patch_path is set to vet/auto-vet.patch but the vet/ directory is never created. The shell redirect will fail with No such file or directory on a clean runner where vet/ doesn't already exist. Fix:

mkdir -p vet
git diff --binary --patch -- supply-chain > "$patch_path"


if [ ! -s "$patch_path" ]; then
echo "Expected patch file at $patch_path, but it was empty." >&2
exit 1
fi

patch_bytes="$(wc -c < "$patch_path" | tr -d '[:space:]')"
echo "has_patch=true" >> "$GITHUB_OUTPUT"
echo "patch_path=$patch_path" >> "$GITHUB_OUTPUT"
echo "patch_bytes=$patch_bytes" >> "$GITHUB_OUTPUT"

- name: Upload auto-vet patch artifact
id: upload_patch
if: github.event_name == 'pull_request' && steps.patch.outputs.has_patch == 'true'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: dependabot-auto-vet-patch-pr-${{ github.event.pull_request.number }}
path: ${{ steps.patch.outputs.patch_path }}
if-no-files-found: error
retention-days: ${{ env.RETENTION_DAYS }}

- name: Expose patch outputs
id: patch_meta
if: always()
run: |
set -euo pipefail

generated="${{ steps.patch.outputs.has_patch || 'false' }}"
uploaded="false"
if [ "${{ steps.upload_patch.outcome || 'skipped' }}" = "success" ]; then
uploaded="true"
fi

echo "generated=$generated" >> "$GITHUB_OUTPUT"
echo "uploaded=$uploaded" >> "$GITHUB_OUTPUT"
echo "artifact_name=dependabot-auto-vet-patch-pr-${{ github.event.pull_request.number || 'manual' }}" >> "$GITHUB_OUTPUT"
echo "artifact_id=${{ steps.upload_patch.outputs.artifact-id || '' }}" >> "$GITHUB_OUTPUT"
echo "artifact_url=${{ steps.upload_patch.outputs.artifact-url || '' }}" >> "$GITHUB_OUTPUT"
echo "run_url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> "$GITHUB_OUTPUT"
echo "retention_days=${{ env.RETENTION_DAYS }}" >> "$GITHUB_OUTPUT"

# -------------------------
# PR Comment (consolidated)
Expand All @@ -418,9 +445,15 @@ jobs:
const importChanged = `${{ steps.detect_import_changes.outputs.import_changed || 'false' }}` === 'true';
const codexInitOk = `${{ steps.codex_init_status.outputs.codex_init_ok || 'false' }}` === 'true';

const changed = `${{ steps.commit.outputs.changed || 'false' }}` === 'true';
const pushed = `${{ steps.commit.outputs.pushed || 'false' }}` === 'true';
const sha = `${{ steps.commit.outputs.sha || '' }}`.trim();
const patchGenerated = `${{ steps.patch_meta.outputs.generated || 'false' }}` === 'true';
const patchUploaded = `${{ steps.patch_meta.outputs.uploaded || 'false' }}` === 'true';
const artifactName = `${{ steps.patch_meta.outputs.artifact_name || '' }}`.trim();
const artifactId = `${{ steps.patch_meta.outputs.artifact_id || '' }}`.trim();
const artifactUrl = `${{ steps.patch_meta.outputs.artifact_url || '' }}`.trim();
const runUrl = `${{ steps.patch_meta.outputs.run_url || '' }}`.trim();
const retentionDays = `${{ steps.patch_meta.outputs.retention_days || '' }}`.trim();
const runId = `${{ github.run_id }}`.trim();
const prNumber = `${{ github.event.pull_request.number || '' }}`.trim();
const vetAfterStatus = `${{ steps.verify_after.outputs.status }}`.trim();

const lines = [];
Expand All @@ -435,18 +468,64 @@ jobs:
}
lines.push('');

if (changed) {
lines.push(`- **Audit files updated:** yes`);
if (sha) lines.push(`- **Commit:** ${sha}`);
lines.push(`- **Pushed to PR branch:** ${pushed ? 'yes' : 'no (push may be restricted for this actor/branch)'}`);
if (patchGenerated) {
lines.push(`- **Patch generated:** yes`);
lines.push(`- **Artifact uploaded:** ${patchUploaded ? 'yes' : 'no'}`);
if (artifactName) lines.push(`- **Artifact name:** ${artifactName}`);
if (artifactId) lines.push(`- **Artifact ID:** ${artifactId}`);
if (prNumber) lines.push(`- **PR number:** ${prNumber}`);
if (runId) lines.push(`- **Run ID:** ${runId}`);
if (retentionDays) lines.push(`- **Retention:** ${retentionDays} days`);
if (artifactUrl) {
lines.push(`- **Artifact download:** ${artifactUrl}`);
} else if (runUrl) {
lines.push(`- **Workflow run:** ${runUrl}`);
}
} else {
lines.push(`- **Audit files updated:** no changes to commit`);
lines.push(`- **Patch generated:** no audit files were produced`);
}

if (!hasCases) {
lines.push(`- **cargo vet import updates:** ${importChanged ? 'detected (no diffs required)' : 'none detected'}`);
}

lines.push('');
lines.push('CI did not commit anything. Review the patch locally and create the final signed commit yourself.');

if (patchUploaded) {
const downloadUrl = artifactUrl || '<artifact-download-url>';
lines.push('');
lines.push('### Apply the patch locally');
lines.push('The patch artifact is attached to this workflow run as a zip archive. Download it, extract `auto-vet.patch`, review the result, then create your signed commit.');
if (!artifactUrl && runUrl) {
lines.push(`If direct artifact download is unavailable here, open the workflow run and download the artifact from there: ${runUrl}`);
}
lines.push('');
lines.push('Preferred: GitHub CLI');
lines.push('```bash');
lines.push('git checkout <pr-branch>');
lines.push(`gh run download ${runId} -n ${artifactName}`);
lines.push(`git apply --index auto-vet.patch`);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug (carried over): wrong apply path after gh run download.

gh run download <run-id> -n <artifact-name> creates <artifactName>/ and preserves the uploaded directory structure inside it. Because the file was uploaded from vet/auto-vet.patch, it lands at <artifactName>/vet/auto-vet.patch — not auto-vet.patch. This instruction will fail with error: can't open patch file auto-vet.patch for anyone using the preferred CLI flow.

Fix:

git apply --index ${artifactName}/vet/auto-vet.patch

lines.push('git status');
lines.push('git commit -S -m "chore(vet): apply automated audits"');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-S still requires a GPG key (flagged in first review, present in both blocks).

git commit -S (uppercase) is GPG commit signing — it fails or produces an unverified commit for developers without a signing key configured. Use lowercase -s (DCO/Signed-off-by trailer) if a traceable attribution line is wanted, or drop the flag and let the developer sign per their own setup. Same issue at the fallback block (~line 524).

lines.push('git push');
lines.push('```');
lines.push('');
lines.push('Fallback: direct artifact download');
lines.push('```bash');
lines.push('git checkout <pr-branch>');
lines.push('curl -L \\');
lines.push(' -H "Authorization: Bearer <github-token>" \\');
lines.push(' -o auto-vet-artifact.zip \\');
lines.push(` ${downloadUrl}`);
lines.push('unzip -p auto-vet-artifact.zip vet/auto-vet.patch > auto-vet.patch');
lines.push('git apply --index auto-vet.patch');
lines.push('git status');
lines.push('git commit -S -m "chore(vet): apply automated audits"');
lines.push('git push');
lines.push('```');
}

if (vetted.length) {
lines.push('');
lines.push('### ✅ Auto-certified');
Expand All @@ -470,6 +549,12 @@ jobs:
body: lines.join('\n')
});

- name: Fail if patch publication did not complete
if: github.event_name == 'pull_request' && steps.patch_meta.outputs.generated == 'true' && steps.patch_meta.outputs.uploaded != 'true'
run: |
echo "Auto-vet patch generation succeeded, but the patch artifact was not uploaded."
exit 1

# Optional: fail the job if anything unvetted remain
- name: Fail if unvetted remain (optional gate)
if: steps.vet_import.outputs.has_cases == 'true' && steps.reason.outputs.any_unvetted == 'true'
Expand Down
Loading