Pinning every action to its commit SHA closes the door on moved tags. But a SHA also freezes the fixes: with no update mechanism, the good practice turns into debt. This page shows how to keep a pinned estate healthy over time, with Dependabot or Renovate, and why the critical point is not the tool but the triage of the queue.
What you will learn
- Measure the debt an unmaintained pinning creates
- Configure Dependabot for actions, images and dependencies
- Reduce the noise through grouping and a quarantine window
- Triage the queue without letting it pile up, and decide what to auto-merge
- Replay the digests of base images, which Dependabot does not always cover
The pinning debt, measured
Abstract reasoning convinces nobody. Here is the state of a reference repository that is exemplary in every other respect (minimal permissions, scanners in CI, SLSA provenance, Cosign signature, a carefully tended OpenSSF Scorecard), audited in September 2026, whose Dependabot queue had not been touched since 16 July, that is seven weeks.
| Pinned item | Version in place | Current version | Gap |
|---|---|---|---|
actions/checkout | v7.0.0 | v7.0.1 | 1 fix |
docker/login-action | v4.4.0 | v4.5.0 | 1 minor version |
getplumber/plumber | v0.4.6 | v0.4.60 | 54 versions |
python:3.11-slim | a July digest | the 2 September digest | an unpatched image |
Three consequences, all of them observed:
- The CI fell over by itself. The image scan job failed on every pull
request, including the ones touching neither the
Dockerfilenor the dependencies. Since the pinned digest receives no fix, it only took a CVE fixed upstream being published for theHIGH,CRITICALthreshold to be crossed. - The documentation drifted from the code. The guides quoted pinnings the repository no longer carried, while claiming to comment on its real files.
- Ten pull requests had piled up, to the point where the queue itself was discouraging to work through.
Configuring Dependabot
Dependabot reads the version comment placed after the SHA to know where you stand. That is why the comment is not decoration: without it, the tool cannot compare.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1The minimal configuration covers the three ecosystems of a containerised repository:
version: 2updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly"
- package-ecosystem: "docker" directory: "/" schedule: interval: "weekly"
- package-ecosystem: "pip" directory: "/" schedule: interval: "weekly"Note the docker declaration: without it, the base image stays on its
digest indefinitely, and that is exactly the scenario that brought down the CI
measured above.
Reducing the noise before it discourages
A queue of ten pull requests does not get worked through, it gets ignored. Two settings cut the volume without losing anything.
Grouping gathers several updates into a single pull request:
- package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" groups: actions-patch: update-types: ["patch"] actions-minor: update-types: ["minor"]The quarantine window (cooldown) delays the proposal of a freshly
published version. The point is security, not comfort: a compromised version is
usually pulled within hours or days, and waiting is enough never to see it
go by.
cooldown: default-days: 7The Renovate alternative
Renovate offers one setting Dependabot lacks, and it matters for a pinned
estate: pinDigests, which converts and then maintains references by
digest, images included.
{ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": ["config:recommended"], "github-actions": { "enabled": true, "pinDigests": true }, "docker": { "pinDigests": true }, "minimumReleaseAge": "7 days"}The choice between the two comes down to this: Dependabot is built in, with no third-party service to authorise, and it is enough when your actions already carry their version comments. Renovate configures more finely, knows how to pin what is not pinned yet, and groups better. On a single repository Dependabot is enough; across dozens of repositories Renovate starts paying for itself.
Triaging the queue: the part that actually fails
The tooling is never the problem. In the case measured above, Dependabot worked perfectly: it had proposed every missing update, on time. Nobody had looked at them.
A triage rule fits in three lines, and the difficulty is sticking to it.
| Kind of update | Decision | Condition |
|---|---|---|
| Patch on an action or an image | Merge, possibly automatically | Green CI, workflow scans included |
| Minor | Merge after reading the changelog | Green CI |
| Major | An explicit review, never in bulk | Green CI and changelog read |
Automating the patches, under conditions
Auto-merging patches is only defensible when your CI is genuinely blocking. A repository running actionlint, zizmor, poutine and a strict posture gate can afford it: the door is guarded by the scanners, not by human attention.
name: Dependabot auto-merge
on: pull_request_target
permissions: {}
jobs: automerge: if: github.actor == 'dependabot[bot]' runs-on: ubuntu-24.04 permissions: contents: write pull-requests: write steps: - name: Fetch the update metadata id: meta uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0 - name: Enable auto-merge for patches if: steps.meta.outputs.update-type == 'version-update:semver-patch' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_URL: ${{ github.event.pull_request.html_url }} run: gh pr merge --auto --squash "$PR_URL"Two points of vigilance on that example. The trigger is
pull_request_target, which is required for the token to hold write rights
on a Dependabot pull request; the workflow never checks out the branch code,
which keeps it inside the safe use case described in
Securing pull_request_target.
And gh pr merge --auto overrides nothing: the merge only happens once
every branch protection is satisfied.
Replaying image digests
Base images deserve a section of their own, because their failure mode is the most disconcerting: the CI falls over without a single line of the repository changing.
FROM python:3.11-slim@sha256:9534e5a8e315485d4061ed659af0fd78a284c015f9b73661b41d6bab25604534That digest is immutable, therefore reproducible, therefore never patched. Meanwhile, your scanner's vulnerability database is updated daily. A job configured like this will eventually fail:
with: severity: HIGH,CRITICAL ignore-unfixed: true exit-code: "1"And yet ignore-unfixed: true is the right setting: it only reports
vulnerabilities for which a fix exists. In other words, when that job turns
red, it is telling you something exact and actionable: a fix is available
upstream and you have not taken it.
The remediation is replaying the pinning:
# Read the current digest of the tag you followdocker buildx imagetools inspect python:3.11-slim --format '{{.Manifest.Digest}}'-
Read the current digest of the tag you follow.
-
Replace the digest in every stage of the
Dockerfile, the build stage included: one forgottenFROMleaves the vulnerability in the intermediate image. -
Let the CI decide. If the scan turns green again, the CVE did come from the base layer. If it stays red, it comes from your application dependencies, and that is a different job.
Cadence, the only thing that lasts
The mechanism matters less than the appointment. Three formats work, in increasing order of cost:
- Auto-merging patches costs no human time, provided the CI is a real guardrail.
- A weekly fifteen-minute slot is enough to empty a grouped queue. It is the format that fails most often, because it rests on discipline.
- A monthly review catches the majors and the deeper changes, with the changelogs read.
The warning signal is simple to watch, and it is objective:
# The number of Dependabot pull requests waitinggh pr list --repo OWNER/REPO --author "app/dependabot" --json number --jq 'length'Past five, the queue is no longer being worked, it is being endured. That is the moment to group more, to automate the patches, or to accept that some updates do not need proposing every week.
Key points
- A pinned SHA receives no fix: without an update mechanism, pinning turns a protection into debt.
- The version comment after the SHA is not decoration, it is what Dependabot reads to compare.
- Declare the
dockerecosystem alongsidegithub-actions: otherwise the base image stays frozen and eventually fails your scan. - Grouping and the quarantine window make the queue workable, and quarantine keeps you out of the first adoption wave of a compromised package.
- Renovate brings
pinDigests, useful to pin and maintain what is not pinned yet; Dependabot is enough on a single repository. - Auto-merging patches is only defensible when the CI genuinely blocks, and
gh pr merge --autooverrides no protection. - A Dependabot pull request closed by hand does not come back: it takes
@dependabot reopen. - An image scan turning red with no change in the repository signals a fix available upstream, not a false positive: replay the digest.
Next steps
- Hardening the runner with Harden-Runner: the layer that limits what a compromised dependency can exfiltrate when it slips past the quarantine.
- Threat modelling a workflow: the seven questions no scanner asks for you, which say whether a finding matters in your case.
- Security checklist: the systematic pass that verifies the update queue is actually worked, not merely configured.