Skip to content
Open
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
20 changes: 20 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ updates:
interval: "daily"
cooldown:
default-days: 7
# Lockfile-only dependencies are updated too.
allow:
- dependency-type: "all"
labels:
- "dependencies"
- "rust"
Expand Down Expand Up @@ -35,6 +38,23 @@ updates:
- "github-actions"
commit-message:
prefix: "chore(ci)"
groups:
# The CodeQL actions share one bundle version and fail when only some of
# them move, so they always update together.
codeql-action:
patterns:
- "github/codeql-action*"
github-actions-safe-dependencies:
patterns:
- "*"
update-types:
- "minor"
- "patch"
github-actions-major-dependencies:
patterns:
- "*"
update-types:
- "major"

- package-ecosystem: "cargo"
directory: "/fuzz"
Expand Down
19 changes: 17 additions & 2 deletions .github/workflows/audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ name: Security & Dependency Audit
on:
branch_protection_rule:
workflow_dispatch:
inputs:
dependency_review_base:
description: >-
Base commit for Dependency Review when validating a repaired
dependency update branch. Empty for a mainline run.
required: false
type: string
schedule:
- cron: '0 0 * * *' # Run daily at midnight
push:
Expand Down Expand Up @@ -154,14 +161,22 @@ jobs:
dependency_review:
name: Dependency Review
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
# A repaired dependency update is validated by dispatch on its branch,
# because a branch update made with the Actions token leaves pull request
# workflows waiting for approval. The dispatch names the base commit.
if: >-
github.event_name == 'pull_request' ||
(github.event_name == 'workflow_dispatch' && inputs.dependency_review_base != '')
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Dependency Review
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
with:
base-ref: ${{ inputs.dependency_review_base || github.event.pull_request.base.sha }}
head-ref: ${{ github.event.pull_request.head.sha || github.sha }}

codeql:
name: CodeQL (${{ matrix.language }})
Expand Down Expand Up @@ -208,7 +223,7 @@ jobs:

ossf_scorecard:
name: OSSF Scorecard
if: github.event_name != 'pull_request'
if: github.event_name != 'pull_request' && inputs.dependency_review_base == ''
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ name: CI
branches: [main]
pull_request:
branches: [main]
# Raised for a main commit that the Actions token merged, because such a
# merge starts no push workflow.
workflow_dispatch:

permissions:
contents: read
Expand Down Expand Up @@ -78,6 +81,11 @@ jobs:
(cd /tmp/guard-release-test/verify && sha256sum --check SHA256SUMS)
rm -rf /tmp/guard-release-test

- name: Test dependency automation policy
if: runner.os == 'Linux'
shell: bash
run: ./scripts/test-dependabot-automerge.sh

- name: Exercise packaged systemd argument expansion
if: runner.os == 'Linux'
shell: bash
Expand Down
154 changes: 40 additions & 114 deletions .github/workflows/dependabot-automerge.yml
Original file line number Diff line number Diff line change
@@ -1,140 +1,66 @@
name: Dependabot review gate

# Gate Dependabot PRs that need human review while staying quiet for updates
# that normal CI can cover:
# Reports how the dependency automation classifies a Dependabot pull request.
# The classification policy is scripts/dependabot-automerge.sh, read from the
# default branch so the pull request cannot change it. The job reads GitHub API
# metadata only; it does not execute pull request code, post comments, approve
# reviews, or merge changes.
#
# - patch and minor cargo updates
# - all GitHub Actions updates (we already SHA-pin them, so the bump
# is functionally a vetted-tag → vetted-tag move)
# The job succeeds for a routine update, and for any update that a repository
# owner, member, or collaborator approved at its exact head commit. It fails
# for a major or unclassified update without that review, so the required
# check blocks the merge. A submitted or dismissed review runs the job again.
#
# Major and unclassified version bumps fail closed for human review. The gate
# does not execute pull request code, use external model credentials, post
# comments, approve reviews, or merge changes. Major security updates follow
# the same fail-closed policy because alert lookup requires a separate
# privileged token.
# Merging is the separate, privileged dependabot-reconcile.yml workflow, which
# repeats the classification itself. That workflow dispatches this one on a
# branch that it repaired or refreshed, because a branch update made with the
# Actions token leaves pull request workflows waiting for approval.

on:
"on":
pull_request:
types:
- opened
- reopened
- synchronize
pull_request_review:
types:
- submitted
- dismissed
workflow_dispatch:
inputs:
pr_number:
description: "Dependabot pull request number to process"
pull_request:
description: Number of the Dependabot pull request whose head is the dispatched commit.
required: true
type: number
type: string

permissions:
contents: read
permissions: {}

concurrency:
group: dependabot-review-${{ github.event.pull_request.number || inputs.pr_number }}
group: dependabot-review-${{ github.event.pull_request.number || inputs.pull_request }}
cancel-in-progress: true

jobs:
review-gate:
name: Classify dependency update
if: github.event_name == 'workflow_dispatch' || github.event.pull_request.user.login == 'dependabot[bot]'
if: >-
github.event_name == 'workflow_dispatch' ||
github.event.pull_request.user.login == 'dependabot[bot]'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
pull-requests: read # Reads Dependabot PR metadata and changed-file names.
pull-requests: read # Reads pull request, commit, review, and changed-file metadata.
steps:
- name: Resolve pull request
id: pr
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
EVENT_PR_NUMBER: ${{ github.event.pull_request.number }}
DISPATCH_PR_NUMBER: ${{ inputs.pr_number }}
run: |
set -euo pipefail

pr_number="${DISPATCH_PR_NUMBER:-${EVENT_PR_NUMBER:-}}"
if [ -z "$pr_number" ]; then
echo "No pull request number was provided."
exit 1
fi

author="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${pr_number}" --jq '.user.login')"
if [ "$author" != "dependabot[bot]" ]; then
echo "PR #${pr_number} was opened by ${author}, not dependabot[bot]."
exit 1
fi

{
echo "number=${pr_number}"
echo "url=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${pr_number}" --jq '.html_url')"
echo "head_sha=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${pr_number}" --jq '.head.sha')"
echo "head_ref=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${pr_number}" --jq '.head.ref')"
} >> "$GITHUB_OUTPUT"

- name: Resolve update metadata
id: fetch_meta
if: github.event_name == 'pull_request'
uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0

- name: Resolve dispatched update metadata
id: meta
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ steps.pr.outputs.number }}
FETCH_UPDATE_TYPE: ${{ steps.fetch_meta.outputs.update-type }}
FETCH_PACKAGE_ECOSYSTEM: ${{ steps.fetch_meta.outputs.package-ecosystem }}
HEAD_REF: ${{ steps.pr.outputs.head_ref }}
run: |
set -euo pipefail

update_type="$FETCH_UPDATE_TYPE"
package_ecosystem="$FETCH_PACKAGE_ECOSYSTEM"

if [ -z "$update_type" ]; then
body="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" --jq '.body // ""')"
update_type="$(printf '%s\n' "$body" | sed -n 's/^[[:space:]]*-[[:space:]]*update-type:[[:space:]]*//p' | head -1)"
fi
if [ -z "$update_type" ]; then
case "$HEAD_REF" in
*major*) update_type="version-update:semver-major" ;;
*minor*) update_type="version-update:semver-minor" ;;
*patch*) update_type="version-update:semver-patch" ;;
esac
fi

if [ -z "$package_ecosystem" ]; then
files="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files" --paginate --jq '.[].filename')"
if printf '%s\n' "$files" | grep -Eq '^\.github/workflows/'; then
package_ecosystem="github_actions"
elif printf '%s\n' "$files" | grep -Eq '(^|/)Cargo\.(toml|lock)$'; then
package_ecosystem="cargo"
else
package_ecosystem="unknown"
fi
fi

{
echo "update-type=${update_type}"
echo "package-ecosystem=${package_ecosystem}"
} >> "$GITHUB_OUTPUT"

- name: Enforce review policy
- name: Check out the default-branch policy
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.repository.default_branch }}
persist-credentials: false
sparse-checkout: scripts

- name: Classify pull request
env:
UPDATE_TYPE: ${{ steps.meta.outputs.update-type }}
PACKAGE_ECOSYSTEM: ${{ steps.meta.outputs.package-ecosystem }}
run: |
set -euo pipefail

case "$UPDATE_TYPE" in
version-update:semver-patch|version-update:semver-minor)
echo "Routine update; normal CI is sufficient."
exit 0
;;
esac

if [ "$PACKAGE_ECOSYSTEM" = "github_actions" ]; then
echo "SHA-pinned GitHub Actions update; normal CI is sufficient."
exit 0
fi

echo "Major or unclassified dependency update requires human review."
exit 1
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number || inputs.pull_request }}
EXPECTED_HEAD: ${{ github.event_name == 'workflow_dispatch' && github.sha || '' }}
run: scripts/dependabot-automerge.sh gate "$PR_NUMBER"
Loading
Loading