From 74ba4a61e940ba8417a0d94699ce18b9c04bf562 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 14:13:33 +0000 Subject: [PATCH 1/2] ci: only fail pull requests on advisories they introduce A RustSec advisory published against main's Cargo.lock failed every open pull request, including Dependabot updates that do not touch Rust. Pull requests now compare against the base revision and warn about advisories already present there; main pushes, local runs and dependency candidates stay strict, and a daily workflow reports advisories on main. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_016FyuSKkineBeVKbubYFsqd --- .github/workflows/advisories.yml | 32 ++++++++++++++++ .github/workflows/validate.yml | 2 + README.md | 4 +- tools/check-advisories.sh | 63 ++++++++++++++++++++++++++++++++ tools/validate.sh | 3 +- 5 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/advisories.yml create mode 100755 tools/check-advisories.sh diff --git a/.github/workflows/advisories.yml b/.github/workflows/advisories.yml new file mode 100644 index 0000000..d49703a --- /dev/null +++ b/.github/workflows/advisories.yml @@ -0,0 +1,32 @@ +name: Dependency advisories +on: + schedule: + - cron: '41 6 * * *' + workflow_dispatch: +permissions: + contents: read +concurrency: + group: client-dependency-advisories + cancel-in-progress: true +jobs: + advisories: + if: github.ref == 'refs/heads/main' + name: Main branch advisories + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install pinned cargo-deny + run: | + install -d "${RUNNER_TEMP}/tools/cargo-deny" "${RUNNER_TEMP}/bin" + curl --fail --silent --show-error --location https://github.com/EmbarkStudios/cargo-deny/releases/download/0.20.2/cargo-deny-0.20.2-x86_64-unknown-linux-musl.tar.gz --output "${RUNNER_TEMP}/cargo-deny.tar.gz" + printf '%s %s\n' 9f12ed4c49936e09b48bf862b595cde2fe64fcbd9d74dfacac6131ca824c8d5f "${RUNNER_TEMP}/cargo-deny.tar.gz" | sha256sum --check --strict + tar -xzf "${RUNNER_TEMP}/cargo-deny.tar.gz" -C "${RUNNER_TEMP}/tools/cargo-deny" --strip-components=1 + install "${RUNNER_TEMP}/tools/cargo-deny/cargo-deny" "${RUNNER_TEMP}/bin/cargo-deny" + echo "${RUNNER_TEMP}/bin" >>"${GITHUB_PATH}" + - run: rustup toolchain install 1.97.1 --profile minimal + # Pull requests only fail on advisories they introduce, so this is where + # a newly published advisory against main's Cargo.lock shows up. + - run: tools/check-advisories.sh diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index d9a4e15..75e42a8 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -40,6 +40,8 @@ jobs: tar -xzf "${RUNNER_TEMP}/syft.tar.gz" -C "${RUNNER_TEMP}/bin" syft echo "${RUNNER_TEMP}/bin" >>"${GITHUB_PATH}" - run: tools/validate.sh + env: + ADVISORY_BASE_REVISION: ${{ github.event.pull_request.base.sha }} windows: name: Windows build and tests diff --git a/README.md b/README.md index 375f3a9..ffefc3e 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,9 @@ SDL_VIDEODRIVER=dummy cargo run --locked --package atrinik-client -- window Run `tools/validate.sh` for formatting, Clippy-as-errors, tests, architecture, provenance/parity, dependency/license/advisory, native-library, release, SBOM, -and reproducibility gates. +and reproducibility gates. On pull requests, `ADVISORY_BASE_REVISION` limits +the advisory gate to advisories the change introduces; ones already on `main` +are warnings there and fail the daily `Dependency advisories` workflow instead. Semantic-release creates an immutable version tag from `main` without exposing a partial GitHub release. A completion-triggered workflow resolves that exact diff --git a/tools/check-advisories.sh b/tools/check-advisories.sh new file mode 100755 index 0000000..52f73d6 --- /dev/null +++ b/tools/check-advisories.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Copyright (c) 2026 Atrinik contributors. SPDX-License-Identifier: MIT +# +# Fail on RustSec advisories that this change introduces. With +# ADVISORY_BASE_REVISION set (pull requests), advisories already present at +# that revision are reported as warnings so an advisory published against +# main does not fail unrelated pull requests. Without it (main, local runs, +# dependency candidates) every advisory fails. +set -euo pipefail +repository=$(git rev-parse --show-toplevel) +cd "${repository}" + +advisory_ids() { + local directory=$1 output + output=$(mktemp) + # cargo-deny exits non-zero when it finds advisories; the IDs decide below. + (cd "${directory}" && cargo deny --locked --format json check advisories 2>"${output}" >/dev/null) || true + python3 - "${output}" <<'PY' +import json, sys +ids = set() +for line in open(sys.argv[1], encoding='utf-8'): + try: + entry = json.loads(line) + except json.JSONDecodeError: + continue + fields = entry.get('fields', {}) + if entry.get('type') == 'diagnostic' and fields.get('severity') == 'error': + advisory = fields.get('advisory') or {} + ids.add(advisory.get('id') or fields.get('code') or 'unknown') +print('\n'.join(sorted(ids))) +PY + rm -f -- "${output}" +} + +head_ids=$(advisory_ids .) +if [ -z "${head_ids}" ]; then + echo "advisories ok" + exit 0 +fi + +base=${ADVISORY_BASE_REVISION:-} +if [ -z "${base}" ]; then + cargo deny --locked check advisories + exit 1 +fi + +worktree=$(mktemp -d /tmp/atrinik-client-advisory-base.XXXXXX) +trap 'git worktree remove --force "${worktree}" >/dev/null 2>&1 || rm -rf -- "${worktree}"' EXIT +git worktree add --detach --quiet "${worktree}" "${base}" +base_ids=$(advisory_ids "${worktree}") + +introduced=$(comm -23 <(printf '%s\n' "${head_ids}") <(printf '%s\n' "${base_ids}" | sed '/^$/d')) +if [ -n "${introduced}" ]; then + echo "advisories introduced by this change:" >&2 + printf ' %s\n' ${introduced} >&2 + cargo deny --locked check advisories + exit 1 +fi + +for id in ${head_ids}; do + echo "::warning title=Pre-existing dependency advisory::${id} is already present on the base revision; fix it on main (see tools/check-advisories.sh)." +done +echo "advisories present on base only: $(echo ${head_ids})" diff --git a/tools/validate.sh b/tools/validate.sh index a2a1cdb..983922a 100755 --- a/tools/validate.sh +++ b/tools/validate.sh @@ -12,7 +12,8 @@ cargo fmt --all --check cargo clippy --locked --workspace --all-targets -- -D warnings cargo test --locked --workspace --all-targets cargo test --locked --workspace --doc -cargo deny --locked check +cargo deny --locked check bans licenses sources +tools/check-advisories.sh tools/check-foundations.sh cargo run --locked --quiet --package atrinik-client -- version cargo run --locked --quiet --package atrinik-client -- headless From d306e5d4283acd0d35d7ae314398a9bb4b295984 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 14:24:03 +0000 Subject: [PATCH 2/2] ci: fail on cargo-deny errors and key advisories by crate A cargo-deny run that failed without reporting advisories (a database fetch or metadata error) was read as no advisories. Treat a non-zero exit without a completed summary and findings as a failure, and key findings by crate coordinates so distinct yanked crates are not merged into one entry. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_016FyuSKkineBeVKbubYFsqd --- tools/check-advisories.sh | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/tools/check-advisories.sh b/tools/check-advisories.sh index 52f73d6..91f7787 100755 --- a/tools/check-advisories.sh +++ b/tools/check-advisories.sh @@ -10,29 +10,39 @@ set -euo pipefail repository=$(git rev-parse --show-toplevel) cd "${repository}" +# Prints one "ID:crate@version" key per denied finding. Yanked crates carry no +# advisory ID, so the crate coordinates keep distinct findings distinct. advisory_ids() { - local directory=$1 output + local directory=$1 output status=0 output=$(mktemp) - # cargo-deny exits non-zero when it finds advisories; the IDs decide below. - (cd "${directory}" && cargo deny --locked --format json check advisories 2>"${output}" >/dev/null) || true - python3 - "${output}" <<'PY' + (cd "${directory}" && cargo deny --locked --format json check advisories 2>"${output}" >/dev/null) || status=$? + python3 - "${output}" "${status}" <<'PY' || { rm -f -- "${output}"; return 1; } import json, sys -ids = set() +keys, summary = set(), False for line in open(sys.argv[1], encoding='utf-8'): try: entry = json.loads(line) except json.JSONDecodeError: continue fields = entry.get('fields', {}) - if entry.get('type') == 'diagnostic' and fields.get('severity') == 'error': + if entry.get('type') == 'summary': + summary = True + elif entry.get('type') == 'diagnostic' and fields.get('severity') == 'error': advisory = fields.get('advisory') or {} - ids.add(advisory.get('id') or fields.get('code') or 'unknown') -print('\n'.join(sorted(ids))) + crates = [graph.get('Krate') or {} for graph in fields.get('graphs') or []] + coordinates = ','.join(sorted(f"{krate.get('name')}@{krate.get('version')}" for krate in crates)) or 'unknown' + keys.add(f"{advisory.get('id') or fields.get('code') or 'unknown'}:{coordinates}") +# A non-zero exit is only an advisory result when cargo-deny completed its +# check and reported the findings; anything else is an operational failure. +if int(sys.argv[2]) != 0 and not (summary and keys): + sys.exit(f'cargo-deny advisories check failed (exit {sys.argv[2]}) without reporting advisories:\n' + + open(sys.argv[1], encoding='utf-8').read()[-4000:]) +print('\n'.join(sorted(keys))) PY rm -f -- "${output}" } -head_ids=$(advisory_ids .) +head_ids=$(advisory_ids .) || exit 1 if [ -z "${head_ids}" ]; then echo "advisories ok" exit 0 @@ -47,7 +57,7 @@ fi worktree=$(mktemp -d /tmp/atrinik-client-advisory-base.XXXXXX) trap 'git worktree remove --force "${worktree}" >/dev/null 2>&1 || rm -rf -- "${worktree}"' EXIT git worktree add --detach --quiet "${worktree}" "${base}" -base_ids=$(advisory_ids "${worktree}") +base_ids=$(advisory_ids "${worktree}") || exit 1 introduced=$(comm -23 <(printf '%s\n' "${head_ids}") <(printf '%s\n' "${base_ids}" | sed '/^$/d')) if [ -n "${introduced}" ]; then