Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 

README.md

Image Tracker

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.

Permissions

To run this action, the calling workflow job must have the following minimum permissions:

permissions:
  contents: read
  pull-requests: read
  packages: read

Setting the tags input writes to the registry, so that job needs packages: write instead of packages: read.

How it works

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.

Requirements

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.

Diagnostics and Troubleshooting

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:

  1. Candidate Commits Table: Summarizes commits walked up to max_depth, associated PR numbers, and commit messages.
  2. 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).
  3. Targeted Guidance: Actionable recommendations tailored to the specific failure reasons observed (e.g. verifying builder workflows, adjusting max_depth, or verifying permissions).

Usage

- 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.3

External 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: target

Migrating from get-pr

Downstream 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 (repository input; fork GHCR on a fork PR). Used only to pull manifests.
  • Source repository — git remote origin of dir (the checkout). Commit SHAs and /commits/{sha}/pulls lookups always use this repo.
  • Workflow repository — GITHUB_REPOSITORY. Used only to fetch pull/<N>/head when origin is that repo and revision is 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).

Inputs

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>

Tagging

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.
  • latest is a human convenience: "the image main uses 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). When latest differs from prod, something is merged but not yet in production.
  • latest only moves when the resolved revision equals 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) on pull_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

Outputs

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 }}

Why digests, not tags?

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.