Documents
reusable-workflow
reusable-workflow
Type
External
Status
Published
Created
Jun 13, 2026
Updated
Jun 13, 2026
Source
View

Reusable Workflows#

The repo provides two reusable workflows:

WorkflowPurpose
.github/workflows/reusable-build.ymlFull Fedora bootc image build pipeline (Path 1)
.github/workflows/reusable-release.ymlImage stable-release orchestration and Conventional Commits GitHub Release creation

Permissions hardening: default reusable workflows to permissions: {} at the workflow level, then grant the minimum required scopes per job. Do not rely on workflow-level packages: write/contents: write unless every job in the file truly needs that access.

Contents#


reusable-build.yml — calling from a consuming repo#

jobs:
  build:
    uses: projectbluefin/actions/.github/workflows/reusable-build.yml@v1
    secrets: inherit
    with:
      brand_name: bluefin
      stream_name: stable
      image_flavors: '["main", "nvidia-open"]'
      architecture: '["x86_64"]'

How action refs work inside the reusable workflow#

When a consuming repo calls the workflow:

  • github.repository = the caller's repo (e.g. projectbluefin/bluefin)
  • actions/checkout checks out the caller's code into GITHUB_WORKSPACE
  • just commands run against the caller's Justfile — this is intentional

Critical: cross-repo action refs
When the reusable workflow is called cross-repo (e.g. from projectbluefin/bluefin), uses: ./bootc-build/<name> resolves to the caller's checked-out workspace — not the actions repo. This causes Can't find action.yml errors.
Always use full SHA-pinned refs inside the reusable workflow:

uses: projectbluefin/actions/bootc-build/setup-runner@<SHA>

Never use ./bootc-build/... in .github/workflows/reusable-build.yml.

Inside the reusable workflow, cross-repo composite action calls must use fully qualified projectbluefin/actions/bootc-build/<name>@<SHA> refs, while the Justfile-driven build steps continue to run caller-specific logic from the checked-out consumer repo.

Keep self-refs in lockstep: when bumping reusable workflow self-references, update all projectbluefin/actions/bootc-build/*@<SHA> entries in that workflow family to the same tested commit. Mixing self-ref SHAs means one pipeline can execute different generations of this repo's actions in a single run.

Retry GitHub API polling in reusable workflows: wrap gh api polling for other workflow runs (for example, post-testing-e2e release-gate lookups) with projectbluefin/actions/actions/retry@<SHA> and write the API response to ${{ runner.temp }}. The retry action executes with.command via eval, so keep the command free of unescaped double quotes — prefer single-quoted headers plus escaped ? / & separators when redirecting API output to a file. If the retried helper lives in another reusable workflow such as reusable-release-gate.yml, bump every caller's pinned projectbluefin/actions/.github/workflows/...@<SHA> ref in the same PR so consumers execute the retried helper instead of the previous commit.

Pin GitHub-hosted Linux jobs to explicit runner labels (ubuntu-24.04 / ubuntu-24.04-arm) instead of ubuntu-latest, and set timeout-minutes on every lightweight helper job (preflight, check, collect-digests, release/validation/report jobs). The build matrix itself gets the longer explicit timeout because it can otherwise hold a runner indefinitely when podman or registry operations hang.


Tag generation and manifest scope#

reusable-build.yml intentionally keeps tag generation in the caller repo by running just generate-build-tags instead of bootc-build/generate-tags. That is part of the Path 1 Justfile contract, alongside image_name, generate-default-tag, build-ghcr, and tag-images.

bootc-build/generate-tags exists for Path 2 / à la carte pipelines that want the shared default tag policy without adopting the full reusable workflow contract.

bootc-build/create-manifest is also a Path 2 building block today. The reusable workflow builds and pushes per-architecture images and emits digests, but it does not assemble or push a multi-arch manifest index; callers that need a manifest job should add an explicit follow-on create-manifest step in their own workflow.


Digest output shape (multi-arch safe)#

The digests output is a nested JSON map: { "image-name": { "platform": "digest" } }. Platform keys use OCI names (amd64, arm64), mapped from runner architecture names (x86_64, aarch64) during artifact writing. Single-arch builds produce one platform key per image; multi-arch builds produce one per architecture.

This shape is directly compatible with create-manifest's digests-json input — callers can iterate the outer map and pass each inner object to create-manifest without reshaping.

The digest artifact files use pipe-delimited format (image_name|oci_platform|digest) so that the collect-digests job can build the nested structure without key collisions across architectures.


JSON array inputs#

Any input consumed via fromJson() must be valid JSON. That means string items inside the array must use double quotes.

Always use single outer quotes with double-quoted inner strings:

# ✅ correct
image_flavors: '["main", "nvidia-open"]'
architecture: '["x86_64", "aarch64"]'
install-tools: '["just", "cosign", "oras", "syft"]'

Wrong:

# ❌ wrong — invalid JSON, fromJson() will fail
architecture: "['x86_64']"

The reusable workflow's architecture input is the concrete pattern to follow because the matrix parses it with fromJson(inputs.architecture). Use architecture: '["x86_64"]' or architecture: '["x86_64", "aarch64"]', never single-quoted strings inside the JSON array.


SBOM artifact shape#

The workflow stages SBOMs as IMAGE_NAME.sbom.json (flat rename from sbom_out/IMAGE_NAME/sbom.json) before upload. The generate-release.yml workflow expects this *.sbom.json glob shape.

SBOM generation and upload should run for every non-PR build, including the testing stream. Weekly promotions retag testing digests directly to production tags, so skipping SBOM on testing leaves promoted images without signed SBOM referrers.


Promotion gate retries and stale-e2e recovery#

reusable-release-gate.yml treats the e2e lookup as a two-layer retry boundary:

  • short GitHub API hiccups (502/503/504, rate limits, timeouts) retry up to 3 times with a 30 second backoff
  • stale or still-pending e2e coverage re-checks the gate up to 4 times total with 10m / 20m / 30m waits between checks

When the latest relevant post-testing-e2e / post-merge-e2e run is older than the stale threshold (default 120 minutes), the gate attempts a workflow_dispatch re-run before waiting again. If the gate still cannot clear after the final check, it auto-files or updates a priority/p1 issue titled promotion blocked for >2h on <branch> in the caller repo and keeps the workflow failed.


reusable-release.yml — calling from a consuming repo#

Image stable-release mode#

jobs:
  release:
    uses: projectbluefin/actions/.github/workflows/reusable-release.yml@v1
    secrets:
      github_token: ${{ secrets.GITHUB_TOKEN }}
    with:
      stream_name: stable
      build_workflow: build-image-stable.yml
      build_branch: stable
      image: ghcr.io/projectbluefin/bluefin
      project_name: Bluefin
      cert_identity_regexp: ^https://github\.com/projectbluefin/(bluefin|actions)/\.github/workflows/

This mode finds the latest successful build run for the requested stream, downloads the uploaded SBOM artifact, resolves the current image digest, and calls bootc-build/create-release to publish the GitHub Release. The reusable workflow owns the production environment gate and grants only contents: write plus actions: read to the image release job.

Legacy semver mode#

jobs:
  release:
    uses: projectbluefin/actions/.github/workflows/reusable-release.yml@v1
    secrets:
      github_token: ${{ secrets.GITHUB_TOKEN }}
    with:
      tag: ${{ github.ref_name }} # e.g. v1.2.3
      # draft: false # optional
      # prerelease: false # optional
      # cliff-config: cliff.toml # optional; defaults to repo root

The legacy semver mode checks out with fetch-depth: 0 (required by git-cliff), runs generate-release-notes, and creates a GitHub Release with the generated body.

cliff.toml requirement: a cliff.toml must exist in the caller's repo root (or override via cliff-config input). A factory-wide config is available at the root of this repo and can be copied verbatim.