This action builds Docker/Podman containers conditionally using a set of directories. If any files were changed matching that, then build a container. If those files were not changed, retag an existing build.
To run this action, the calling workflow job must have the following minimum permissions:
permissions:
contents: read
packages: write
id-token: write # (optional, required for build provenance attestations)
attestations: write # (optional, required for build provenance attestations)This is useful in CI/CD pipelines where not every package/app needs to be rebuilt.
Use the same workflow on upstream and fork (on: [push, pull_request]). The action adapts — no if: fork in your YAML.
| Event | Where it runs | Image coordinates | Writes to GHCR |
|---|---|---|---|
push on fork |
Fork | ghcr.io/<fork-owner>/... |
Yes |
pull_request (same repo) |
Upstream | ghcr.io/<upstream>/... |
Yes |
pull_request (fork → upstream) |
Upstream | ghcr.io/<fork-owner>/... |
No — validates locally; image is published on fork push |
There is no pushed output. Do not add one, and do not gate deploy on builder. Use build → image-tracker → deploy. image-tracker fails the job when the image is missing.
org.opencontainers.image.revision is always the source SHA (PR head.sha, or github.sha on push) — never the pull_request merge ref. That is what lets merge workflows resolve the PR image with image-tracker without rebuilding.
On a fork pull request, the source SHA tag names the fork image (ghcr.io/<fork-owner>/...:<head.sha>) and image is not pullable until it publishes. Images publish on push to the fork.
Do not use pull_request_target for builds or deploys. That event runs the workflow file from the upstream default branch, not from the contributor's PR. This action refuses fork pull_request_target outright.
on:
push:
branches-ignore: [main]
pull_request:
jobs:
build:
runs-on: ubuntu-24.04
permissions:
contents: read
packages: write
strategy:
matrix:
package: [backend, frontend]
steps:
- uses: bcgov/actions/builder@vX.Y.Z
with:
package: ${{ matrix.package }}
tag_fallback: test
triggers: |
${{ matrix.package }}/GHCR visibility: images pushed to a fork's GHCR are private by default. The action emits this warning after a fork push:
WARNING: Images built from forks require that the fork set package visibility to public. See the fork builds guide for details.
This tool is currently strongly opinionated and generates images with a rigid structure below. This is intended to become more flexible in future.
Package name: <organization>/<repository>/<package>:<tag>
Pull with: docker pull ghcr.io/<organization>/<repository>/<package>:<tag>
Only GitHub Container Registry (ghcr.io) is supported.
- uses: bcgov/actions/builder@vX.Y.Z
with:
### Required
# Package name
package: frontend
### Typical / recommended
# Sets the build context/directory, which contains the build files
# Optional, defaults to package name
context: ./frontend
# Sets the Dockerfile with path
# Optional, defaults to {package}/Dockerfile or {context}/Dockerfile
file: ./frontend/Dockerfile
# Fallback tag, used if no build was generated
# Optional. Omit to always build fresh if triggers do not match
# Non-matching or malformed tags are rejected, which forces a build
tag_fallback: test
# Tags to apply to the image
# Defaults to pull request number in pull_request workflows
# Required for non-PR events (push, workflow_dispatch, schedule)
# The source commit SHA is always added in addition to these tags.
# Note: All tags are normalized to lowercase and stripped of spaces before use.
tags: |
pr123
demo
# Default amd64. quickstart-openshift `builds` omits this and runs on
# ubuntu-24.04, which publishes `package`. See "ARM images".
# architecture: arm64
# Paths to diff for build triggering (multiline recommended)
# Optional, defaults to nothing, which forces a build
triggers: |
frontend/
backend/
database/
### Usually a bad idea / not recommended
# Sets a list of [build-time variables](https://docs.docker.com/engine/reference/commandline/buildx_build/#build-arg)
# Optional, defaults to sample content
build_args: |
ENV=build
# Overrides the default reference/branch to diff against
# Defaults to the default branch, usually `main`
base: main # Optional; default is the PR base branch, or HEAD^ on push
# Specify token (GH or PAT), instead of inheriting one from the calling workflow
token: ${{ secrets.GITHUB_TOKEN }}
# Multiline input for secrets to mount.
# Note: GITHUB_TOKEN is not injected by default. If your build needs it
# (e.g. to pull private packages), you must explicitly pass it here.
# https://docs.docker.com/build/ci/github-actions/secrets/#secret-mounts
secrets: |
GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}
MY_SECRET=${{ secrets.MY_SECRET }}
ANOTHER_SECRET=${{ secrets.ANOTHER_SECRET }}This action supports building from and pushing to private repositories.
- Same Repository/Organization: By default, the action uses the automatic
GITHUB_TOKEN. Ensure your workflow hascontents: readandpackages: writepermissions. - Cross-Organization: To build from a private repository in a different organization, provide a Personal Access Token (PAT) with
reposcope via thetokeninput. Registry login always usesgithub.actoras the user name.
To prevent credential leakage, this action:
- Uses
persist-credentials: falseduring all checkout steps. - Performs a clean registry login as
github.actorwith the providedtokenbefore any manifest or build operations. - Follows the principle of least privilege by not automatically injecting
GITHUB_TOKENinto the Docker build process. If your build needs a token (e.g., to pull private packages), you must explicitly provide it via thesecretsinput.
Build a single subfolder with a Dockerfile in it. Runs on pull requests (PRs).
builds:
runs-on: ubuntu-24.04
steps:
- name: Builds
uses: bcgov/actions/builder@vX.Y.Z
with:
package: frontend
tag_fallback: test
triggers: |
frontend/Same as previous, but specifying build folder and Dockerfile.
builds:
runs-on: ubuntu-24.04
steps:
- name: Builds
uses: bcgov/actions/builder@vX.Y.Z
with:
package: frontend
context: ./
file: subdir/Dockerfile
tags: |
${{ github.event.number }}
${{ github.sha }}
test
tag_fallback: test
token: ${{ secrets.GITHUB_TOKEN }}
triggers: |
frontend/Build from multiple subfolders with Dockerfile in them. This time an outside repository is used. Runs on pull requests (PRs).
builds:
runs-on: ubuntu-24.04
strategy:
matrix:
package: [backend, frontend]
include:
- package: backend
triggers: |
backend/
- package: frontend
triggers: |
frontend/
steps:
- uses: actions/checkout@v7
- name: Test Builds
uses: bcgov/actions/builder@vX.Y.Z
with:
package: ${{ matrix.package }}
tags: ${{ github.event.number }}
tag_fallback: test
token: ${{ secrets.GITHUB_TOKEN }}
triggers: ${{ matrix.triggers }}
quickstart-openshift .github/workflows/pr-open.yml is the workflow most consumers run. Its builds job is a matrix of backend, frontend, and migrations on ubuntu-24.04. It does not set architecture. Tags are the pull request number and the head SHA, and tag_fallback is latest. deploys needs only builds and passes the head SHA through reusable-deploy.yml as IMAGE_TAG.
Leave that builds job as it is. Add a second job for ARM builds. Do not add it to deploys.needs, or an ARM failure blocks the OpenShift deploy.
build-arm64:
name: ARM builds
runs-on: ubuntu-24.04-arm
strategy:
matrix:
package: [backend, frontend, migrations]
permissions:
contents: read
packages: write
steps:
- uses: bcgov/actions/builder@vX.Y.Z
with:
package: ${{ matrix.package }}
architecture: arm64
tags: |
${{ github.event.number }}
${{ github.event.pull_request.head.sha }}
tag_fallback: latest
triggers: |
${{ matrix.package }}/
.github/workflows/pr-open.ymlA Mac pulls ghcr.io/<organization>/<repository>/backend-arm64:<pull request number>. The cluster still pulls backend at the head SHA on a pull request, and at the pull request number after merge.
pr-open.yml's results job says every new job must be listed in its needs. This job is not a deploy gate. Leave it out of results and an unavailable ARM runner does not fail the pull request. Add it there and it does.
tag_fallback: latest on the ARM job looks for <package>-arm64:latest. Promotion writes prod on backend, frontend, and migrations only, so that tag is never created and the ARM job builds on every pull request.
builds omits architecture, so it stays amd64 and publishes backend, frontend, and migrations. An ARM runner that omits it, or sets amd64, fails. That keeps an ARM image off the package OpenShift deploys. architecture: arm64 is the Mac and Podman package, and it fails on ubuntu-24.04.
This action provides two key security features: Container Attestations and Software Bill of Materials (SBOM) generation. Additionally, it leverages the docker/metadata-action for best-practice container image tagging and labeling.
This action uses the docker/metadata-action to automatically generate OCI-compliant labels and annotations for container images. This ensures that images are tagged and labeled following Open Container Initiative (OCI) specifications and Docker best practices.
The following OCI Image Format Specification labels are automatically added to all container images:
- org.opencontainers.image.created - Image creation timestamp (ISO 8601 format)
- org.opencontainers.image.url - URL to the source repository (e.g., https://github.com/bcgov/repo-name)
- org.opencontainers.image.source - URL to the source repository
- org.opencontainers.image.version - Version/tag of the image (e.g., main, pr-123)
- org.opencontainers.image.revision - Git commit SHA that triggered the build
- org.opencontainers.image.title - Human-readable title (repository name)
- org.opencontainers.image.description - Description from the repository
- org.opencontainers.image.licenses - License information from the repository's LICENSE file
These labels provide standardized metadata for source provenance, attestation, and help ensure baseline security practices for container deployments.
Here's an example of the OCI labels that would be applied to a container image built with this action:
{
"org.opencontainers.image.created": "2025-05-29T19:09:03.374Z",
"org.opencontainers.image.url": "https://github.com/bcgov/nr-peach",
"org.opencontainers.image.source": "https://github.com/bcgov/nr-peach",
"org.opencontainers.image.version": "main",
"org.opencontainers.image.revision": "fadf03ce2db919752ada03af3f8fb4895fe96fcf",
"org.opencontainers.image.title": "nr-peach",
"org.opencontainers.image.description": "NR Permitting Exchange, Aggregation and Collection Hub",
"org.opencontainers.image.licenses": "Apache-2.0"
}You can inspect these labels on any built image using:
docker inspect ghcr.io/org/repo/package:tag | jq '.[0].Config.Labels'Container attestations use GitHub's OIDC token to provide cryptographic proof of:
- Where the container was built (GitHub Actions)
- When it was built (timestamp)
- What repository and workflow built it
- What inputs and environment were used
Attestations require the following permissions:
permissions:
packages: write # Required for pushing images
id-token: write # Required for OIDC token generation
attestations: write # Required for creating attestationsIf these permissions are not granted, the action will still build and push images but skip the attestation step.
This action automatically generates SBOMs for all container builds using Syft. SBOMs provide a detailed inventory that includes:
- All installed packages and their versions
- Dependencies and their relationships
- License information
- Known vulnerabilities
Two SBOM formats are generated and uploaded as workflow artifacts:
- CycloneDX JSON
- SPDX JSON
| Output | Description |
|---|---|
digest |
Immutable digest of the built or retagged image |
triggered |
Whether a build was triggered (true/false) |
image |
Fully-qualified digest reference, ghcr.io/owner/repo/package@sha256:... (or ghcr.io/owner/repo@sha256:... when the package matches the repository name); same format as image-tracker's image. Empty when there is no digest |
New image digest (SHA). This applies to build and retags.
- id: digest
uses: bcgov/actions/builder@vX.Y.Z
...
- name: Echo digest
run: |
echo "Digest: ${{ steps.digest.outputs.digest }}"
...Has an image been built? [true|false]
- id: trigger
uses: bcgov/actions/builder@vX.Y.Z
...
- name: Echo build trigger
run: |
echo "Trigger result: ${{ steps.trigger.outputs.triggered }}"
...- Multi package repos:
ghcr.io/org/repo/package:tag - Single package repos:
ghcr.io/org/repo:tag
Single package naming is only triggered when package=repository.