Skip to content

Latest commit

 

History

History

builder

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 

README.md

Conditional Container Builder with Fallback, Attestations and SBOMs (Software Bill of Materials)

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.

Permissions

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.

Fork builds

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.

Usage

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

Private Repository Support

This action supports building from and pushing to private repositories.

Authentication

  • Same Repository/Organization: By default, the action uses the automatic GITHUB_TOKEN. Ensure your workflow has contents: read and packages: write permissions.
  • Cross-Organization: To build from a private repository in a different organization, provide a Personal Access Token (PAT) with repo scope via the token input. Registry login always uses github.actor as the user name.

Security

To prevent credential leakage, this action:

  • Uses persist-credentials: false during all checkout steps.
  • Performs a clean registry login as github.actor with the provided token before any manifest or build operations.
  • Follows the principle of least privilege by not automatically injecting GITHUB_TOKEN into the Docker build process. If your build needs a token (e.g., to pull private packages), you must explicitly provide it via the secrets input.

Example, Single Build

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/

Example, Single Build with context, file and multiple tags

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/

Example, Matrix Build

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

ARM images

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

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

Security Features

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.

Container Metadata 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.

Example OCI Labels

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

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 attestations

If these permissions are not granted, the action will still build and push images but skip the attestation step.

Software Bill of Materials (SBOM)

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

Outputs

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

Image Naming Convention

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