# Using Digestabot with Chainguard Containers

URL: https://deploy-preview-3930--ornate-narwhal-088216.netlify.app/chainguard/containers/staying-secure/updating-images/digestabot.md
Last Modified: September 3, 2026
Tags: Chainguard Containers

How to configure Digestabot to keep digest-pinned references to Chainguard Containers current, including authenticating to a private cgr.dev registry

Digestabot is a GitHub Action that keeps digest-pinned container image references current. It reads the references in your repository, looks up the current digest for each tag in the registry, and opens a pull request when a digest has changed.
Chainguard rebuilds container images daily, so the digest behind a tag such as latest or 3.14 changes often. Digestabot lets you pin a reference to an exact digest for reproducibility and still pick up each rebuild, with a pull request as the place to test the change before it ships.
This guide explains how Digestabot matches references, how to authenticate it to your organization&rsquo;s private registry at cgr.dev, and how to control which files it scans.
Choosing between Digestabot, Dependabot, and Renovate These three tools solve different parts of the same problem:
Digestabot opens a pull request when the digest behind a fixed tag changes. Use it to pick up Chainguard&rsquo;s daily rebuilds without changing the tag you depend on. Dependabot opens a pull request when a newer tag is available, moving a reference from go:1.22 to go:1.26. Use it to move between version streams. Renovate does both, and it runs outside GitHub as well as inside it. Many teams run Digestabot alongside one of the others: Dependabot or Renovate moves the tag when a new version stream appears, and Digestabot keeps the digest current in between. Digestabot is also the better fit for a reference pinned to a mutable tag, where the tag string never changes and Dependabot has nothing to act on.
How Digestabot matches references Digestabot updates a reference only when it contains both a tag and a digest:
cgr.dev/example.com/go:1.22@sha256:0d0e0f5e9b7f8ee0dbcbb6d1c40ad1bbd0da5b2bb56de2b26dfd8ceb8ed69dbbBoth parts are required:
Digestabot never matches a reference with a tag and no digest. It updates a digest that&rsquo;s already there; it doesn&rsquo;t add one. Digestabot skips a reference with a digest and no tag, because it has no tag to look up. The job log records Image &lt;name&gt; in file &lt;file&gt; does not have a tag, ignoring.... This is the most common reason a Digestabot run reports nothing to do.
To add digests to references that carry only a tag, use Frizbee, as shown in Reproducible Dockerfiles with Frizbee and Digestabot. Renovate&rsquo;s pinDigests option does the same job; refer to Pin digests.
Digestabot also skips two categories of reference regardless of their form:
Any reference in a file whose path contains testdata. Any reference whose registry address contains .local:, such as registry.local:5000/example. Prerequisites To follow this guide, you need:
A GitHub repository with at least one reference to a Chainguard container image pinned in the tag@digest form described in the previous section. Access to a Chainguard organization, with permission to create identities, if the images come from a private repository in cgr.dev. chainctl installed on your local machine. Refer to How to install chainctl if you haven&rsquo;t set this up. Digestabot opens its pull requests with the workflow&rsquo;s GITHUB_TOKEN. For that to work, go to Settings &gt; Actions &gt; General in your repository and confirm that Allow GitHub Actions to create and approve pull requests is selected. If your organization turns this setting off, pass a token from a GitHub App or a repo-scoped personal access token through the token input instead.
Authenticate to a private registry Digestabot reads digests with crane, which uses the Docker credential store on the runner. Digestabot has no registry credential input of its own, so it inherits whatever credentials an earlier step configured.
For a private repository in cgr.dev, add a setup-chainctl step before the Digestabot step. That action authenticates as a Chainguard assumable identity and registers a Docker credential helper for cgr.dev, which crane then uses without further configuration.
Digestabot needs no static pull token, which is where it differs most from Dependabot. The identity mints a short-lived token for each run, so there is no credential to store or rotate.
Create an identity for your repository. The --github-repo value embeds GitHub&rsquo;s immutable numeric owner and repository IDs; refer to Finding your repository&rsquo;s numeric identifiers for how to retrieve them and when the format applies.
chainctl iam identities create github digestabot \ --github-repo=&lt;github-org&gt;@&lt;owner-id&gt;/&lt;github-repo-name&gt;@&lt;repo-id&gt; \ --github-ref=refs/heads/main \ --role=registry.pullThe command returns an identity ID. Record it for the workflow in the next section.
Digestabot only reads image digests, so the registry.pull role covers everything it does on its own. Annotating pull requests with chainctl images diff needs a broader role.
Create the workflow Create a file named .github/workflows/digestabot.yaml with the following content, replacing &lt;identity-id&gt; with the ID from the previous section:
name: Digestabot on: workflow_dispatch: schedule: - cron: &#34;0 1 * * *&#34; permissions: contents: read jobs: digestabot: name: Digestabot runs-on: ubuntu-latest permissions: contents: write # Push the branch holding the digest updates pull-requests: write # Open the pull request id-token: write # Assume the Chainguard identity and sign commits steps: - uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 with: egress-policy: audit - uses: chainguard-dev/setup-chainctl@2cddd35a2f120d9973e58094dc6878c93cf58c28 # v0.5.1 with: identity: &#34;&lt;identity-id&gt;&#34; - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: chainguard-dev/digestabot@33d0b78e580aa0c83fe188eb3dfad6611b662479 # v1.3.2 with: token: ${{ secrets.GITHUB_TOKEN }} labels-for-pr: &#39;&#39;Commit this file to your repository&rsquo;s default branch.
This workflow runs at 1:00 a.m. every day and on demand from Actions &gt; Digestabot &gt; Run workflow. Each run authenticates to cgr.dev as the identity you created, scans the repository, and opens a pull request titled Update images digests on a branch named update-digests if any digest has changed.
The example clears labels-for-pr, because the default value applies Chainguard&rsquo;s own repository labels — automated pr, kind/cleanup, and release-note-none — which probably don&rsquo;t exist in your repository. Set it to labels you use, or leave it empty.
If your images come from a public repository such as cgr.dev/chainguard, drop the setup-chainctl step. Digestabot reads public digests without credentials.
How Digestabot handles repeat runs Digestabot maintains one pull request rather than opening a new one per run. Each run resets the update-digests branch to the base branch, applies the current set of updates, and force-pushes, so an open pull request always reflects the latest digests. If you merge or close that pull request, the next run with pending updates opens a fresh one.
This is worth knowing if you also use Dependabot, which leaves an open pull request in place as newer digests are published.
Commit signing Digestabot signs its commits with gitsign by default, which is why the job needs the id-token: write permission. GitHub labels these commits as unverified. Refer to Digestabot&rsquo;s commits show as unverified if a branch protection rule in your repository requires signed commits.
Choose which files to scan By default, Digestabot scans files matching *.yaml, *.yml, Dockerfile*, Makefile*, *.sh, *.tf, and *.tfvars. It updates digests in Terraform configurations, Makefiles, shell scripts, ko configurations, and Kubernetes manifests, not only in Dockerfiles. Any file in the repository that holds a tag@digest reference and matches one of these patterns is in scope.
Patterns match file names rather than paths. Digestabot passes each pattern to find -name, which compares against the base name of each file, so a pattern such as manifests/*.yaml matches nothing. To narrow Digestabot to part of a repository, set working-dir to a path relative to the repository root:
- uses: chainguard-dev/digestabot@33d0b78e580aa0c83fe188eb3dfad6611b662479 # v1.3.2 with: token: ${{ secrets.GITHUB_TOKEN }} working-dir: manifests include-files: &#39;*.yaml,Dockerfile*&#39; Resolve digests through a registry proxy If you pull Chainguard Containers through a registry proxy, such as a remote repository in Artifact Registry, the proxy can return a stale digest for a tag. The registry-map input maps a proxy prefix to the registry behind it, so Digestabot resolves digests against cgr.dev while leaving the proxy address in your files:
- uses: chainguard-dev/digestabot@33d0b78e580aa0c83fe188eb3dfad6611b662479 # v1.3.2 with: token: ${{ secrets.GITHUB_TOKEN }} registry-map: &#39;us-docker.pkg.dev/my-project/cgr/=cgr.dev/&#39;Provide mappings as comma-separated proxy=upstream pairs. Digestabot rewrites the address it looks up, not the reference it writes back, so your files keep pulling through the proxy.
Because the lookups go to cgr.dev, the credentials on the runner must grant access there. Keep the setup-chainctl step in the workflow even when every reference in your files points at the proxy.
Extend Digestabot with its outputs Digestabot reports what it changed in two outputs: json describes each update, and changed_files lists the files it modified. Combined with create-pr: false, which applies the changes in the workspace without opening a pull request, these let you act on an update before it reaches a reviewer:
- uses: chainguard-dev/digestabot@33d0b78e580aa0c83fe188eb3dfad6611b662479 # v1.3.2 id: digestabot with: token: ${{ secrets.GITHUB_TOKEN }} - shell: bash run: | while read -r update; do updated_image=$(jq -r &#39;.image &#43; &#34;@&#34; &#43; .updated_digest&#39; &lt;&lt;&lt;&#34;${update}&#34;) echo &#34;Do something with ${updated_image} here.&#34; done &lt; &lt;(jq -c &#39;.updates // [] | .[]&#39; &lt;&lt;&lt;&#39;${{ steps.digestabot.outputs.json }}&#39;) Annotate pull requests with chainctl images diff chainctl images diff compares two container images and reports the packages and vulnerabilities that differ between them. Running it over Digestabot&rsquo;s json output turns &ldquo;test the update before merging&rdquo; into a summary in the pull request itself: which vulnerabilities the rebuild resolves, which packages changed, and whether the update is worth merging at all.
The digestabot-examples repository holds a complete workflow for this in .github/workflows/chainctl-image-diff.yaml. It runs Digestabot with create-pr: false, verifies that each image is signed by Chainguard, diffs the old and new digests, and discards updates that resolve no vulnerabilities. The same repository has a second workflow that scans each updated image with Grype and comments the results on the pull request.
chainctl images diff needs grype available on the runner and an identity with a broader role than registry.pull; the example workflow uses viewer.
Configuration reference Inputs Input Description Default working-dir Directory to scan, relative to the repository root. . include-files Comma-separated file name patterns to scan. Patterns match base names, not paths. *.yaml,*.yml,Dockerfile*,Makefile*,*.sh,*.tf,*.tfvars token Token used to push the branch and open the pull request. ${{ github.token }} registry-map Comma-separated proxy=upstream prefix mappings for digest lookups. None create-pr Whether to open a pull request. Set to false to leave the changes in the workspace. true use-gitsign Whether to sign commits with gitsign. true signoff Whether to add a Signed-off-by line to the commit message. false author Commit author, in the form Display Name &lt;email@address.com&gt;. The user who triggered the run committer Committer, in the form Display Name &lt;email@address.com&gt;. github-actions[bot] &lt;41898282+github-actions[bot]@users.noreply.github.com&gt; branch-for-pr Branch that carries the updates. update-digests title-for-pr Pull request title. Update images digests description-for-pr Pull request body. Digestabot appends the diff to whatever you set. Update images digests labels-for-pr Comma- or newline-separated labels to apply to the pull request. automated pr, kind/cleanup, release-note-none commit-message Commit message. Update images digests Outputs Output Description json The updates Digestabot made. Each entry in updates has a file, image, digest, and updated_digest. changed_files Newline-separated list of the files Digestabot modified. pull_request_number Number of the pull request Digestabot opened or updated. Troubleshooting Digestabot&rsquo;s commits show as unverified Digestabot signs its commits with gitsign, which uses Sigstore&rsquo;s certificate authority. GitHub doesn&rsquo;t recognize that authority, so it marks the commits Unverified. The signature is valid; the badge reflects GitHub&rsquo;s trust configuration rather than a problem with the commit.
This matters when a branch protection rule requires signed commits, because the rule then blocks Digestabot&rsquo;s pull requests from merging. You have two options:
Sign the commits with a GPG key that GitHub recognizes. Set create-pr: false so that Digestabot only edits the files, then import a key and create the pull request in later steps. Chainguard recommends keyless signing where you have the choice, so treat this as a way to satisfy the branch protection rule rather than an improvement. Set use-gitsign: false to leave the commits unsigned, if your repository doesn&rsquo;t require signed commits and you&rsquo;d rather not carry signatures GitHub can&rsquo;t verify. Digestabot opens no pull requests If the job succeeds but no pull request appears, check the following:
The form of the reference. Digestabot updates only references carrying both a tag and a digest. Refer to How Digestabot matches references. The file name. include-files patterns match base names, so Digestabot never reads a reference in a file the patterns don&rsquo;t cover. Refer to Choose which files to scan. The repository setting. Confirm that Allow GitHub Actions to create and approve pull requests is selected, as described in Prerequisites. An existing pull request. Digestabot updates an open update-digests pull request in place instead of opening a second one. Digest lookups fail The job summary records Failed to retrieve digest info for &lt;image&gt; for each reference Digestabot couldn&rsquo;t resolve, followed by the error from the registry. Check the following, in order:
Confirm that the setup-chainctl step runs before the Digestabot step and completes successfully. Confirm the identity has the registry.pull role and access to the repository holding the image. Run chainctl iam identities list to review the identities in your organization. Test the reference outside the workflow. Run chainctl auth configure-docker, then crane digest &lt;reference&gt; or docker pull &lt;reference&gt;. If you pull through a registry proxy, confirm whether the lookups need registry-map. Video overview This 2024 video introduces the problem Digestabot solves, using a digest-pinned multi-stage Python build as the example. The configuration in this guide has moved on since the recording, but the reasoning holds.
TranscriptToday, I&rsquo;d like to talk about a common question I get asked.
How can you keep images up to date while avoiding breaking changes?
The basic issue is that we&rsquo;d like to make sure we&rsquo;re getting the latest security updates and features for our software.
But we really don&rsquo;t want our applications and infrastructure to break unexpectedly.
So there&rsquo;s a tension between updating all the time, which gives you the latest code and limits unexpected breakages.
In this example, we have a multi-stage Python build using Chainguard Container which are pinned to Digest.
Now, Digests are content-based hashes of images.
So if you reference an image by Digest, you will always get exactly the same image every time.
Now, this is fantastic for reproducibility.
As I know, if anybody uses this Dockerfile, they will get exactly the same images that I was using.
And this is especially important in Python, where if the version changes, so we go from Python 3.12 to Python 3.13, you might find that various libraries don&rsquo;t work until they&rsquo;re updated.
Now, how do you do updates then?
Well, you could manually go in and change, bump this Digest yourself.
But we&rsquo;ve got a better solution for you that I want to talk about briefly today, and it&rsquo;s called Digestabot.
Digestabot is a GitHub action that can be set to run on a cron job and will open a PR when it detects there&rsquo;s a newer version of the image available.
You can then test the image to make sure it works with your application before merging the PR.
So for my example, it would check the Chainguard registry for the current digest of the latest tag and open a PR if it doesn&rsquo;t match the digest in the file.
We use Digestabot internally at Chainguard, and this pattern nicely balances the tension between keeping images up to date and vulnerability-free with the need to test and verify changes before shipping to production.
So please try it out and let me know if you have any questions.
Learn more Using Dependabot with Chainguard Containers covers the tool to pair with Digestabot for moving between version streams. Using Renovate with Chainguard Containers covers the equivalent setup for teams outside GitHub. Strategies and tooling for updating containers compares the wider range of update tools. Considerations for keeping containers up to date covers the tradeoffs behind an update policy. Container image digests explains what a digest is and why pinning to one matters. 
