Skip to content

Merge pull request #125 from Coding-Moves/codex/fix-quarantine-path-l… #60

Merge pull request #125 from Coding-Moves/codex/fix-quarantine-path-l…

Merge pull request #125 from Coding-Moves/codex/fix-quarantine-path-l… #60

Workflow file for this run

# Slow-moving checks that shouldn't block every PR: dependency
# vulnerability audit and external-link rot. Run weekly, on manual
# dispatch, on every push to a release branch, and on PRs that move the
# Rust lockfile. New RUSTSEC advisories land against code that never
# changed, so the schedule matters as much as the lockfile trigger.
name: Audit
on:
schedule:
- cron: "0 6 * * 1" # Mondays 06:00 UTC
workflow_dispatch:
push:
branches: [main, master]
pull_request:
# The ignore list is part of what the audit means, so editing it has to
# re-run the audit just as moving the lockfile does.
paths: [Cargo.lock, .cargo/audit.toml]
jobs:
# PR-time advisory check: annotates the changed lockfile inline so the
# author sees the advisory in the diff. PRs only — off-PR runs are handled
# by `advisory-issue` below, which files a labelled tracking issue instead.
cargo-audit:
if: github.event_name == 'pull_request'
runs-on: ubuntu-22.04
permissions:
contents: read
checks: write # the action reports advisories as a check run
steps:
- uses: actions/checkout@v7
# `rustsec/audit-check` does not read .cargo/audit.toml — it only
# takes an `ignore` input — while the plain `cargo audit` in the job
# below does. Reading the ids out of the file keeps one source of
# truth instead of a second list that drifts from the first.
- name: Read the ignored advisories
id: ignored
run: |
ids=$(sed -n '/ignore *= *\[/,/^]/p' .cargo/audit.toml \
| grep -oE 'RUSTSEC-[0-9]{4}-[0-9]{4}' | paste -sd,)
echo "ids=$ids" >> "$GITHUB_OUTPUT"
echo "Ignoring: ${ids:-nothing}"
- uses: rustsec/audit-check@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
ignore: ${{ steps.ignored.outputs.ids }}
# Off-PR runs own a single tracking issue for the whole lockfile: one
# issue, kept current, rather than a fresh one every Monday.
advisory-issue:
if: github.event_name != 'pull_request'
runs-on: ubuntu-22.04
permissions:
contents: read
issues: write
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ISSUE_TITLE: RUSTSEC advisories in Cargo.lock
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@v2
with:
tool: cargo-audit
# gh refuses to attach a label the repo doesn't have, and a fresh
# clone of this repo has neither. --force makes this idempotent.
- name: Ensure the issue labels exist
run: |
gh label create security --force \
--color B60205 --description "Security advisory or hardening"
gh label create automated-issue --force \
--color C5DEF5 --description "Filed by a workflow, not a human"
# Visible, not silent: an ignore list nobody ever reads is how a
# suppressed advisory outlives the reason it was suppressed.
- name: Report the ignored advisories
run: |
{
echo "### Ignored advisories"
echo
sed -n '/ignore *= *\[/,/^]/p' .cargo/audit.toml \
| grep -oE 'RUSTSEC-[0-9]{4}-[0-9]{4}' | sed 's/^/- /'
echo
echo "Rationale and review cadence: docs/DEPENDENCY-AUTOMATION.md"
} >> "$GITHUB_STEP_SUMMARY"
# Reads .cargo/audit.toml from the working directory, so the ids
# above are already excluded from these counts.
- name: Run cargo audit
id: audit
run: |
# A non-zero exit only means "found something" — the case we most
# need to keep running for — so read the report, not the status.
cargo audit --json > audit.json || true
jq -e . audit.json > /dev/null || {
echo "::error::cargo audit produced no parsable JSON report"
exit 1
}
vulns=$(jq '.vulnerabilities.count // 0' audit.json)
# `warnings` is an object keyed by kind (unmaintained, unsound,
# yanked); flatten every bucket to get one count.
warns=$(jq '[(.warnings // {})[][]] | length' audit.json)
{
echo "vulnerabilities=$vulns"
echo "warnings=$warns"
echo "total=$((vulns + warns))"
} >> "$GITHUB_OUTPUT"
echo "$vulns vulnerabilities, $warns warnings"
# printf with single quotes throughout: the body is full of markdown
# backticks, which bash would otherwise read as command substitution.
- name: Render the advisory summary
if: steps.audit.outputs.total != '0'
env:
VULNS: ${{ steps.audit.outputs.vulnerabilities }}
WARNS: ${{ steps.audit.outputs.warnings }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
{
printf '`cargo audit` found **%s** vulnerabilities and **%s** warnings in `Cargo.lock`.\n\n' \
"$VULNS" "$WARNS"
jq -r '
(.vulnerabilities.list[]? |
"- **\(.advisory.id)** — `\(.package.name) \(.package.version)`: \(.advisory.title)"),
((.warnings // {}) | to_entries[] | .key as $kind | .value[] |
"- _\($kind)_ **\(.advisory.id // "no id")** — `\(.package.name) \(.package.version)`: \(.advisory.title // "no advisory metadata")")
' audit.json
printf '\nReproduce with `cargo audit` locally. The **Dependency auto-fix**\n'
printf 'workflow runs an hour after this one and opens an update PR when a\n'
printf 'newer version clears the advisory.\n\n'
printf '<sub>Filed automatically by [this run](%s). Updated in place while the\n' "$RUN_URL"
printf 'advisories stand, and closed once the audit comes back clean.</sub>\n'
} > issue-body.md
cat issue-body.md
- name: File or refresh the tracking issue
if: steps.audit.outputs.total != '0'
run: |
number=$(gh issue list --state open --label automated-issue \
--json number,title \
--jq '[.[] | select(.title == $ENV.ISSUE_TITLE) | .number] | first // empty')
if [ -n "$number" ]; then
gh issue edit "$number" --body-file issue-body.md
echo "Refreshed #$number"
else
gh issue create --title "$ISSUE_TITLE" --body-file issue-body.md \
--label security --label automated-issue
fi
# Symmetry matters: an issue nobody closes stops being a signal.
- name: Close the tracking issue when the audit is clean
if: steps.audit.outputs.total == '0'
run: |
number=$(gh issue list --state open --label automated-issue \
--json number,title \
--jq '[.[] | select(.title == $ENV.ISSUE_TITLE) | .number] | first // empty')
if [ -z "$number" ]; then
echo "No open advisory issue — nothing to close."
exit 0
fi
gh issue close "$number" \
--comment 'cargo audit is clean as of this run — closing automatically.'
# Red run, not just a filed issue: this is what auto-fix.yml watches
# for, and what makes a vulnerable main visible on the branch list.
- name: Fail the run while advisories stand
if: steps.audit.outputs.total != '0'
run: |
echo "::error::cargo audit reported ${{ steps.audit.outputs.total }} advisories — see the tracking issue"
exit 1
# External links in docs — checked here (not per-PR) so a flaky or
# rate-limiting third-party site can't fail unrelated PRs.
external-links:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
# ubuntu-latest, not 22.04: the prebuilt lychee binary needs glibc >= 2.38
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: lycheeverse/lychee-action@v2
with:
args: --no-progress --accept '200..=299,429' './**/*.md'
fail: true