Resolve immutable GHCR image digests for a given git commit by reading the
org.opencontainers.image.revision OCI label embedded in each image's config.
To run this action, the calling workflow job must have the following minimum permissions:
permissions:
contents: read
pull-requests: read
packages: readSetting the tags input writes to the registry, so that job needs
packages: write instead of packages: read.
Every OCI-compliant image has a config blob. Builds that use
docker/metadata-action (or raw
buildx --label) embed standard OCI labels including
org.opencontainers.image.revision — the git commit SHA that produced the
image.
config, and returns the manifest digest (sha256:...) of the image whose
revision label matches your target commit.
Tag names (sha-<7>, pr-123, latest, etc.) are used as search hints, but the label is the sole authority. Even if a tag matches your SHA, the tracker will verify the internal OCI label before returning the digest.
The returned digest is immutable and cryptographically verified on pull, making it the recommended form for deployment references.
Promotion contract: build → image-tracker → deploy that digest. Do not
promote by mutable PR tag (:<pr> / retag :test). A squash merge SHA was
never built, so merge/promote must walk (max_depth large enough to find
the last image for each package, e.g. 100). max_depth: 1 only asks “was
this git SHA built?” — that fails ordinary merges. A miss is always exit 1.
Fork does not change that: hit or stop. Do not copy quickstart-openshift
merge.yml as the image spec until it uses this contract.
The target images must be built with OCI labels populated. The easiest way
is docker/metadata-action, which
sets the labels by default. At bcgov, the
bcgov/actions/builder
wrapper does this for you when metadata_tags: true (the default from v4.3.0).
Images that lack the org.opencontainers.image.revision label cannot be
resolved — there is no workaround short of rebuilding them with proper labels.
When package resolution fails, image-tracker automatically outputs a diagnostic report to the workflow run logs and the GitHub Step Summary. The diagnostic report includes:
- Candidate Commits Table: Summarizes commits walked up to
max_depth, associated PR numbers, and commit messages. - Probed Candidate Tags Table: Lists candidate tags probed in the registry (
sha-<commit>,pr-<number>, etc.), HTTP response status, and exact rejection reasons (e.g. 404 Not Found, revision label mismatch, or source repository mismatch). - Targeted Guidance: Actionable recommendations tailored to the specific failure reasons observed (e.g. verifying builder workflows, adjusting
max_depth, or verifying permissions).
- name: Resolve image for HEAD
id: tracker
uses: bcgov/actions/image-tracker@vX.Y.Z
with:
package: frontend, backend
max_depth: 100
- name: Deploy
run: ./deploy.sh ${{ steps.tracker.outputs.digest }}Multiple packages:
- id: tracker
uses: bcgov/actions/image-tracker@vX.Y.Z
with:
package: frontend, backend, migrations
- run: |
echo "frontend: $(echo '${{ steps.tracker.outputs.images }}' | jq -r '.frontend')"Resolve a non-HEAD revision (tag, branch, or SHA):
- uses: bcgov/actions/image-tracker@vX.Y.Z
with:
package: frontend
revision: v1.2.3External repository:
- uses: actions/checkout@v6
with:
repository: bcgov/some-other-repo
path: target
fetch-depth: 0
- uses: bcgov/actions/image-tracker@vX.Y.Z
with:
package: frontend
repository: bcgov/some-other-repo
dir: targetDownstream workflows previously using get-pr to pick a mutable :<pr> tag
must deploy the digest instead. outputs.pr is metadata only.
- name: Track Images & PR
id: tracker
uses: bcgov/actions/image-tracker@vX.Y.Z
with:
package: frontend
max_depth: 100
- name: Deploy
run: |
echo "PR #${{ steps.tracker.outputs.pr }} digest ${{ steps.tracker.outputs.digest }}"
./deploy.sh --image "${{ steps.tracker.outputs.image }}"Fork pull requests: a shallow checkout of refs/pull/N/merge often does not
contain head.sha. Three repositories are distinct:
- Image repository — GHCR owner/repo (
repositoryinput; fork GHCR on a fork PR). Used only to pull manifests. - Source repository —
git remote originofdir(the checkout). Commit SHAs and/commits/{sha}/pullslookups always use this repo. - Workflow repository —
GITHUB_REPOSITORY. Used only to fetchpull/<N>/headwhen origin is that repo andrevisionis the workflow PR's head SHA.
The action fetches the SHA from origin first, then the workflow PR ref when it
applies, then the GitHub API against the source repo. If the revision cannot
be resolved, or no image exists for that revision, the action fails (exit 1).
| Input | Required | Default | Description |
|---|---|---|---|
package |
✔ | — | One or more package names (comma/space/newline separated). |
revision |
HEAD |
Git revision (SHA, branch, or tag) to resolve against. | |
repository |
current repo | Repository owning the images. | |
dir |
. |
Working directory containing the git repository. | |
token |
github.token |
GitHub token used to mint a GHCR bearer token. | |
max_tags |
500 |
Upper bound on tags inspected per package before failing. | |
max_depth |
1 |
Max commits of git ancestry to search. Default is this SHA only. Merge/promote must raise this (e.g. 100) so a squash can resolve the last built image. |
|
tags |
— | Tags (one per line) applied to every resolved digest. Omit to stay read-only. Needs packages: write. See Tagging. |
Package-to-image-path convention:
- If package name == repository name →
ghcr.io/<owner>/<repo> - Otherwise →
ghcr.io/<owner>/<repo>/<package>
With tags unset the action only reads. There is no default: latest (or any
tag) is never applied unless the caller sets tags explicitly. When set, each tag is pointed at every
resolved digest through the registry API (the manifest is fetched by digest and
re-PUT under the tag, so no image is rebuilt or copied).
- Tags are written only after every package resolves. A miss still exits 1 and nothing is tagged.
latestis a human convenience: "the imagemainuses right now". It moves on every merge to the default branch, never on PR builds, and moves even for packages that were not rebuilt (their resolved digest is re-tagged). Whenlatestdiffers fromprod, something is merged but not yet in production.latestonly moves when the resolvedrevisionequals the current tip of the default branch (checked through the GitHub API). Otherwise it is skipped with a warning, so an older or re-run workflow cannot move it backwards. It is also always skipped (with a warning) onpull_request*events. Other tags are still applied.- Deploy by digest, not by these tags.
permissions:
contents: read
packages: write
pull-requests: read
steps:
- uses: actions/checkout@v6
- uses: bcgov/actions/image-tracker@vX.Y.Z
with:
package: backend frontend
max_depth: 100
tags: |
${{ github.sha }}
latest| Output | Description |
|---|---|
images |
JSON object: {"<pkg>": "ghcr.io/<owner>/<repo>/<pkg>@sha256:..."}. Fully pullable references. |
digests |
JSON object: {"<pkg>": "sha256:..."}. Bare digests only. |
image |
Convenience — the fully-qualified digest reference for the first package. Empty on failure. |
digest |
Convenience — the bare digest for the first package. Empty on failure. |
pr |
Convenience — resolved PR number (e.g. 123) for the first resolved package/commit. Empty if unassociated. |
Using a digest in a Dockerfile:
FROM ghcr.io/owner/repo@sha256:3fa4...Or via the action's output directly:
- run: docker pull ${{ steps.tracker.outputs.image }}Tags are mutable — anyone with push access can move them. Digests are content addresses — they are computed from the image bytes and cannot point to anything else. Using digests for deployment references gives you:
- Reproducibility — the same commit always yields the same digest.
- Tamper evidence — Docker/containerd validate the pulled layers hash up to the digest on pull.
- Format independence — the tagging scheme the publisher uses (or changes) never breaks your resolution.