Skip to content
Merged
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
24 changes: 24 additions & 0 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,30 @@
language: en-US

reviews:
# Approve once CodeRabbit's own comments are resolved and no pre-merge check
# is failing, and request changes until then.
#
# This is here for the dependency bots. Their pull requests merge without a
# person looking at them, so CodeRabbit is the reviewer that does, and a
# changes-requested review blocks the merge under branch protection exactly
# like an unresolved conversation does. Nothing else in the pipeline reads a
# bump for intent: the mechanical linters check form, the suite checks that
# the stack still works, and neither would object to a plausible looking line
# that does something else as well.
#
# Its reach is narrower than it looks, and worth being honest about. A version
# or digest bump has no reviewable content, so on most of these pull requests
# CodeRabbit has nothing to say and cannot possibly detect a backdoored
# upstream image. What it does cover is the case the diff assertion in
# bot-auto-merge.yml is also aimed at, a bump that carries a change to
# workflow or hook logic alongside it. Two independent readers of the same
# risk, one mechanical and one not.
#
# The cost, accepted deliberately: the setting is global, so human pull
# requests are gated on CodeRabbit's satisfaction too. Dismissing its review
# is a click when it is wrong, and a bot bump it dislikes waits for the
# maintainer rather than merging, which is the direction to fail in.
request_changes_workflow: true
auto_review:
# CodeRabbit is a GitHub App posting a check, not a workflow job, so it
# cannot be ordered after the linters with `needs:` the way a workflow job
Expand Down
1 change: 1 addition & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
"autoupdate",
"autouse",
"autowant",
"backdoored",
"Badass",
"badasstorrents",
"bandit",
Expand Down
85 changes: 79 additions & 6 deletions .github/renovate.json5
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,77 @@
// Keep PR volume manageable so CI is not overwhelmed.
prConcurrentLimit: 5,
prHourlyLimit: 2,

// The default, overridden per update type in packageRules below. Kept as the
// default rather than inverted, so a datasource or update type nobody thought
// about arrives needing a person.
automerge: false,

// GitHub's own auto-merge, armed by Renovate when it opens the pull request
// and completed by GitHub once branch protection is satisfied. The default,
// stated because it is load-bearing: the alternative is Renovate merging
// through the API on its own judgement of whether the checks passed, and
// Renovate cannot see a required status context that is *absent*, which is
// exactly the state a pull request is in until the integration suite
// publishes `Tests Verified`. Handing the decision to GitHub means the check
// it cannot see is the one thing it cannot skip.
platformAutomerge: true,
automergeStrategy: "squash",

// A cooling window before an update is offered at all. This is the main
// defense against a compromised upstream release, and it is a defense the
// scanners do not provide: Trivy runs `fs` against the working tree and never
// inspects the images a bump points at, and every scanner matches published
// advisories, which a fresh supply chain attack has none of. Most compromised
// releases are spotted and pulled within hours to days, so waiting is what
// turns "we merged it first" into "it was already yanked". See
// docs/HARDENING.md.
minimumReleaseAge: "7 days",

// Hold the pull request until the window is satisfied rather than opening one
// that is not allowed to merge yet. Without this the window would still be
// honoured, but a week of pull requests would sit there looking blocked for
// no visible reason.
internalChecksFilter: "strict",

// The exception, and the reason the window can be as long as a week: a fix
// for a known vulnerability is not made safer by aging, and this stack faces
// the internet. null clears the inherited window for these updates only.
vulnerabilityAlerts: {
minimumReleaseAge: null,
},

// Required status checks are strict on `main`, so a pull request that falls
// behind cannot merge until it is updated. Renovate doing the rebase keeps
// the push attributed to renovate[bot]: the approval on these pull requests
// is github-actions[bot]'s, and `requireLastPushApproval` needs the approver
// and the pusher to be different accounts, which would stop being true if
// GitHub's auto-merge did the update instead.
rebaseWhen: "behind-base-branch",

packageRules: [
// What may merge without anyone looking, and the label that says so.
//
// First in the list on purpose. Later rules win on any key they share, and
// the group rules below deliberately share none of these: they set names
// and labels, not policy. A rule that means to keep something manual has to
// come after this one.
//
// The label is not decoration. bot-auto-merge.yml approves a Renovate pull
// request only when it carries this label, so the decision about which
// bumps are eligible lives here, once, instead of being restated in a
// workflow expression free to drift from it. A major bump matches nothing
// here, arrives unlabelled, and waits for review.
//
// digest and pin are in the list because a digest bump is how a rebuilt tag
// reaches this repository at all: leaving them out would mean the pins that
// pinDigests maintains could only be refreshed by hand, which is how they
// rot.
{
matchUpdateTypes: ["patch", "minor", "digest", "pin", "pinDigest"],
automerge: true,
addLabels: ["automerge"],
},
// *arr suite — group together as they share config and often release together.
{
matchDatasources: ["docker"],
Expand Down Expand Up @@ -97,18 +165,23 @@
// request: they are the same pin, and letting them drift apart would mean
// the prerequisite job and the suite testing different tools.
//
// Left out of any automerge, and deliberately: podman-compose is what
// talks to podman, and a bump has to be proven by an actual suite run,
// which
// only starts when someone comments `/run-tests`. The `ci-runtime` label is
// there to make that obvious on the pull request.
// This used to be held out of automerge, on the grounds that podman-compose
// is what talks to podman and a bump had to be proven by an actual suite
// run, which only started when someone commented `/run-tests`. The suite now
// starts itself for these pull requests, so the condition that kept this
// manual is met by the thing it was waiting for. A podman-compose that
// cannot stand the stack up fails the suite loudly, which is the whole
// proof that was being asked for.
//
// The `ci-runtime` label stays. It marks the one bump that changes the
// tooling the suite itself runs on, which is worth being able to find after
// the fact when something starts behaving oddly in CI.
{
matchDatasources: ["pypi"],
matchPackageNames: ["podman-compose"],
groupName: "container runtime tooling",
groupSlug: "ci-runtime",
addLabels: ["ci-runtime"],
automerge: false,
},
],

Expand Down
177 changes: 177 additions & 0 deletions .github/workflows/bot-auto-merge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
---
name: Bot Auto Merge

# Supplies the approving review that lets a dependency bump merge unattended.
# Nothing else: the tests are proven by `Tests Verified`, which
# integration-tests.yml publishes on its own for these pull requests, and the
# merge itself is GitHub's, armed either by Renovate (platformAutomerge) or by
# the last step here on the Dependabot path, because Dependabot cannot arm it.
#
# Why an approval has to be supplied at all: branch protection requires one, and
# `main` no longer requires it to come from a code owner, because CODEOWNERS
# accepts only users and teams. No bot can ever satisfy a code owner rule, so
# with that rule on, a dependency bump could not merge without a person or a
# stored credential belonging to one. The rule was excluding nothing else, since
# an approving review only counts from an account with write access and this
# repository has exactly one. See docs/CONTRIBUTING.md for the consequence: if a
# second write collaborator is ever added, that stops being true and this
# arrangement has to be revisited.
#
# pull_request_target, not pull_request, and the difference is the whole
# defense. Under `pull_request` this file would be read from the pull request's
# own merge commit, so a bump that reached workflow files could delete the
# assertion below and approve itself, and Renovate does maintain pins in
# .github/workflows/. pull_request_target runs the default branch's copy
# instead. It is normally the more dangerous of the two because it hands a write
# token to a workflow a fork can trigger, which does not apply here: this job
# never checks out the pull request, and the author condition it is gated on
# comes from the immutable copy.
on:
# zizmor: ignore[dangerous-triggers]
#
# Flagged by default, and the reasoning above is why it stays. The checkout
# below is pinned to the base branch, and the only thing read from the pull
# request is its diff, as data, on stdin.
pull_request_target:
types: [opened, synchronize, reopened]
branches:
- main

# The floor. The job widens it for itself.
permissions:
contents: read

jobs:
approve:
name: Approve and enable auto-merge
# user.login is set by GitHub from the authenticated author, so a pull
# request cannot claim to be a bot. Exact equality, not startsWith or
# contains: `renovate[bot]-x` is a login somebody may register, and the
# non-fork condition is the second half of the same fence, since reaching
# this job any other way would take write access to this repository.
if: >-
github.event.pull_request.head.repo.fork == false && (
github.event.pull_request.user.login == 'renovate[bot]' ||
github.event.pull_request.user.login == 'dependabot[bot]'
)
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
# For `gh pr merge --auto` on the Dependabot path only. Arming auto-merge
# does not merge anything: every required check still has to pass, and
# `Tests Verified` is absent until the suite proves this commit.
contents: write
pull-requests: write
steps:
- name: Check out the default branch
uses: actions/checkout@v7
with:
# Explicitly the base branch, which is also what pull_request_target
# checks out by default. Stated anyway, because the assertion below is
# only worth running if it is this repository's copy of it and not the
# pull request's, and a default is a poor place to keep that.
ref: ${{ github.event.pull_request.base.ref }}
persist-credentials: false

- name: Refuse anything that is not a pin change
# The check that keeps "approve on the strength of the author" from
# meaning "the bot identity has write access to main". Two of the paths
# a bump is allowed to touch are executable, since the pip pins live in
# workflow run steps and the scanner images inside pre-commit hook
# entries, so the diff is compared line by line rather than only by
# path. See scripts/assert-pin-only-diff.py.
#
# The diff is fetched through the API rather than checked out, so
# nothing from the pull request is ever on disk here.
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
# pipefail, and it is load-bearing rather than habit. The default shell
# is `bash -e {0}`, where a pipeline's status is the last command's, so
# a `gh pr diff` that fails partway would hand the checker a truncated
# diff, and a truncation that happens to drop the offending lines would
# exit 0 and approve. This step is the gate for an unattended merge, so
# it fails closed or it is not a gate.
shell: bash
run: |
set -euo pipefail
gh pr diff "$PR_NUMBER" | python3 scripts/assert-pin-only-diff.py

- name: Fetch Dependabot metadata
id: metadata
if: github.event.pull_request.user.login == 'dependabot[bot]'
uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

- name: Decide whether this bump may merge unattended
# One decision, one place. The steps below read its outputs rather than
# restating the condition, which is what went wrong before: the same
# expression was written out twice, both copies compared the ecosystem
# to a hyphenated name that fetch-metadata never emits (it reports
# "pre_commit", confirmed live), and all ten pull requests from the
# first run silently skipped both steps, none flagged as a failure
# since a skipped step is not an error.
#
# Renovate is asked by label, not by update type. It already decides
# which of its own pull requests may automerge, in
# .github/renovate.json5, and the rule that grants it also adds the
# `automerge` label. Restating the update types here would be a second
# copy of that policy, free to drift from the one Renovate acts on.
# A major bump therefore arrives with no label, gets no approval, and
# waits.
#
# Dependabot has no such config, so its update type is read from the
# metadata action. All three of its ecosystems are eligible, test
# requirements included: they only reach the test virtualenv, and the
# suite is what clears them either way.
id: decide
env:
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
HAS_AUTOMERGE_LABEL: ${{ contains(github.event.pull_request.labels.*.name, 'automerge') }}
UPDATE_TYPE: ${{ steps.metadata.outputs.update-type }}
run: |
approve=false
arm=false

case "$PR_AUTHOR" in
"renovate[bot]")
if [ "$HAS_AUTOMERGE_LABEL" = "true" ]; then
approve=true
else
echo "::notice::No automerge label, so Renovate is not asking for this to merge on its own."
fi
;;
"dependabot[bot]")
case "$UPDATE_TYPE" in
version-update:semver-patch | version-update:semver-minor)
approve=true
# Dependabot cannot arm auto-merge itself, unlike Renovate.
arm=true
;;
*)
echo "::notice::Update type ${UPDATE_TYPE:-unknown} is not a patch or minor bump, so it waits for review."
;;
esac
;;
esac

echo "approve=$approve" >> "$GITHUB_OUTPUT"
echo "arm=$arm" >> "$GITHUB_OUTPUT"

- name: Approve
if: steps.decide.outputs.approve == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_URL: ${{ github.event.pull_request.html_url }}
# Re-approved on every push, which is required rather than wasteful:
# branch protection dismisses stale reviews, so a rebase drops the
# previous approval, and `synchronize` is above for that reason.
run: gh pr review --approve "$PR_URL"

- name: Enable auto-merge
if: steps.decide.outputs.arm == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_URL: ${{ github.event.pull_request.html_url }}
run: gh pr merge --auto --squash "$PR_URL"
58 changes: 0 additions & 58 deletions .github/workflows/dependabot-auto-merge.yml

This file was deleted.

Loading
Loading