Skip to content

Publish Bluefin dakota #605

Publish Bluefin dakota

Publish Bluefin dakota #605

Workflow file for this run

name: Publish Bluefin dakota
on:
workflow_dispatch:
inputs:
source_sha:
description: 'Existing BuildStream artifact SHA to republish (recovery only)'
required: false
type: string
default: ''
workflow_run:
workflows: ["Build Bluefin dakota"]
types: [completed]
branches:
- next
- 'gh-readonly-queue/next/**'
- testing
- 'gh-readonly-queue/testing/**'
env:
IMAGE_NAME: dakota
IMAGE_REGISTRY: ghcr.io/${{ github.repository_owner }}
permissions:
contents: write
packages: write
id-token: write
attestations: write
actions: read
concurrency:
# Separate publish queues per branch so next and testing don't collide.
group: publish-${{ github.event.workflow_run.head_branch || github.ref_name }}
cancel-in-progress: false
jobs:
# ── Resolve context ───────────────────────────────────────────────────────
# Single source of truth for SHA and trigger event, shared by all downstream jobs.
setup:
runs-on: ubuntu-24.04
outputs:
sha: ${{ steps.context.outputs.sha }}
event: ${{ steps.context.outputs.event }}
branch: ${{ steps.context.outputs.branch }}
testing_tag: ${{ steps.context.outputs.testing_tag }}
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event != 'pull_request' &&
(github.event.workflow_run.head_branch == 'next' ||
startsWith(github.event.workflow_run.head_branch, 'gh-readonly-queue/next/') ||
github.event.workflow_run.head_branch == 'testing' ||
startsWith(github.event.workflow_run.head_branch, 'gh-readonly-queue/testing/')))
steps:
- name: Resolve build SHA and trigger event
id: context
run: |
if [[ "${{ github.event_name }}" == "workflow_run" ]]; then
echo "sha=${{ github.event.workflow_run.head_sha }}" >> "$GITHUB_OUTPUT"
echo "event=${{ github.event.workflow_run.event }}" >> "$GITHUB_OUTPUT"
BRANCH="${{ github.event.workflow_run.head_branch }}"
elif [[ -n "${{ inputs.source_sha }}" ]]; then
echo "sha=${{ inputs.source_sha }}" >> "$GITHUB_OUTPUT"
echo "event=${{ github.event_name }}" >> "$GITHUB_OUTPUT"
BRANCH="${{ github.ref_name }}"
else
echo "sha=${{ github.sha }}" >> "$GITHUB_OUTPUT"
echo "event=${{ github.event_name }}" >> "$GITHUB_OUTPUT"
BRANCH="${{ github.ref_name }}"
fi
echo "branch=${BRANCH}" >> "$GITHUB_OUTPUT"
# Derive the :testing tag from the branch name.
# Only trunk branches (direct pushes / schedule) advance stream tags.
# Merge-queue builds (gh-readonly-queue/**) publish an immutable :SHA
# tag only — they must NOT move public stream tags before the commit lands.
# testing → :testing (published daily on schedule)
# next → :next (rolling GNOME master / bleeding edge)
# queue/** → (empty) :SHA only, no stream tag
if [[ "$BRANCH" == "next" ]]; then
echo "testing_tag=next" >> "$GITHUB_OUTPUT"
elif [[ "$BRANCH" == "testing" ]]; then
echo "testing_tag=testing" >> "$GITHUB_OUTPUT"
else
# merge-queue or other refs: publish :SHA tag only, do NOT move stream tags
echo "testing_tag=" >> "$GITHUB_OUTPUT"
fi
# ── Export, sign, attest ─────────────────────────────────────────────────
# Pulls artifact from remote CAS, pushes :$sha, signs and attests.
# SBOM generation is split to a separate job (publish-sbom) so it runs
# in parallel with promote rather than blocking it.
publish-image:
needs: setup
runs-on: ubuntu-24.04
timeout-minutes: 90
# Variant matrix: default and NVIDIA publish in parallel from the shared
# BST artifact cache. NVIDIA is `continue-on-error: true` so its failure
# does not block default publication.
strategy:
fail-fast: false
matrix:
include:
- variant: default
element: oci/bluefin.bst
image_suffix: ''
sbom_filename: dakota.spdx.json
continue: false
- variant: nvidia
element: oci/bluefin-nvidia.bst
image_suffix: '-nvidia'
sbom_filename: dakota-nvidia.spdx.json
continue: true
# Gaming variants: same elements built with `-o gaming true`
# (BUILD_GAMING). No SBOM entries yet; publish-sbom stays two-variant.
- variant: gaming
export_variant: default
element: oci/bluefin.bst
image_suffix: '-gaming'
gaming: 'true'
continue: true
- variant: nvidia-gaming
export_variant: nvidia
element: oci/bluefin-nvidia.bst
image_suffix: '-nvidia-gaming'
gaming: 'true'
continue: true
continue-on-error: ${{ matrix.continue }}
permissions:
contents: read
packages: write
id-token: write
attestations: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ needs.setup.outputs.sha }}
- name: Capture build timestamp
id: timestamp
run: echo "created=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
# Use the runner's Podman stack with native rootful overlay storage.
# FUSE-backed storage below chunkify's kernel overlay can return ESTALE.
- name: Setup runner
uses: projectbluefin/actions/bootc-build/setup-runner@v1
with:
storage-backend: btrfs
update-podman: true
native-overlay: true
install-tools: '["just"]'
# Fetch-only BST config — pull artifact from remote CAS, no build/push
- name: Generate BST fetch config
env:
CASD_CLIENT_CERT: ${{ vars.CASD_CLIENT_CERT }}
CASD_CLIENT_KEY: ${{ secrets.CASD_CLIENT_KEY }}
uses: ./.github/actions/generate-bst-ci-config
with:
enable-remote-execution: 'false'
enable-push: 'false'
- name: Export OCI image from BuildStream
id: export
env:
BST_FLAGS: -o x86_64_v3 false --no-interactive --config /src/buildstream-ci.conf
BUILD_IMAGE_NAME: ${{ env.IMAGE_NAME }}
BUILD_GAMING: ${{ matrix.gaming || 'false' }}
OCI_IMAGE_CREATED: ${{ steps.timestamp.outputs.created }}
OCI_IMAGE_REVISION: ${{ needs.setup.outputs.sha }}
OCI_IMAGE_VERSION: latest
run: just export ${{ matrix.export_variant || matrix.variant }}
- name: Chunkify image layers
# Use the repository's compiled fakecap helper. The shared chunka action
# injects nearly one million manifest entries through Python, which makes
# this stage take hours for Dakota's 8–9 GiB images.
env:
BUILD_IMAGE_NAME: ${{ env.IMAGE_NAME }}${{ matrix.image_suffix }}
run: just chunkify "localhost/${BUILD_IMAGE_NAME}:latest"
- name: Validate with bootc container lint
env:
BUILD_IMAGE_NAME: ${{ env.IMAGE_NAME }}${{ matrix.image_suffix }}
run: just lint
- name: Audit swap/zram configuration
env:
BUILD_IMAGE_NAME: ${{ env.IMAGE_NAME }}${{ matrix.image_suffix }}
run: just swap-audit
- name: Login to GHCR
env:
GH_ACTOR: ${{ github.actor }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "$GH_TOKEN" | sudo podman login ghcr.io --username "$GH_ACTOR" --password-stdin
mkdir -p ~/.docker
echo "$GH_TOKEN" | podman login ghcr.io --username "$GH_ACTOR" --password-stdin \
--compat-auth-file ~/.docker/config.json
- name: Push :${{ needs.setup.outputs.sha }} to GHCR
id: push
env:
BUILD_SHA: ${{ needs.setup.outputs.sha }}
run: |
LOCAL_IMAGE="${{ env.IMAGE_NAME }}${{ matrix.image_suffix }}"
IMAGE="${{ env.IMAGE_REGISTRY }}/${LOCAL_IMAGE}"
sudo podman tag "localhost/${LOCAL_IMAGE}:latest" "${IMAGE}:${BUILD_SHA}"
PUSH_OK=false
for attempt in 1 2 3; do
sudo podman push --compression-format=zstd \
--digestfile /tmp/digest.txt \
"${IMAGE}:${BUILD_SHA}" && { PUSH_OK=true; break; }
echo "Push attempt ${attempt} failed for :${BUILD_SHA}, retrying in 5s..."
[ "$attempt" -lt 3 ] && sleep 5
done
$PUSH_OK || { echo "ERROR: all push attempts failed for :${BUILD_SHA}"; exit 1; }
DIGEST=$(cat /tmp/digest.txt)
if ! [[ "$DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]; then
echo "ERROR: malformed digest in digestfile: '${DIGEST}'"
exit 1
fi
# Advisory only; must never fail the job.
for attempt in $(seq 1 18); do
if skopeo inspect --no-tags \
"docker://${IMAGE}:${BUILD_SHA}" > /dev/null 2>&1; then
echo "Anonymous tag :${BUILD_SHA} visible (attempt ${attempt})"
break
fi
if [ "$attempt" -eq 18 ]; then
echo "::warning::GHCR has not exposed :${BUILD_SHA} anonymously after 3 min; continuing — the hard gate is the authenticated digest read"
fi
sleep 10
done
echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT"
- name: Install oras
uses: oras-project/setup-oras@1d808f7d7f6995cc68b7bf507bfe5c5446e1dc9d # v2.0.1
- name: Sign image and create attestation
# generate-sbom: false — dakota uses just sbom (BST-native provenance via
# bst artifact list). Syft post-build scan is less accurate for BST builds.
# SBOM attachment and SBOM signing are handled in the publish-sbom job.
uses: projectbluefin/actions/bootc-build/sign-and-publish@v1
with:
image: ${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.image_suffix }}
digest: ${{ steps.push.outputs.digest }}
generate-sbom: false
github-token: ${{ secrets.GITHUB_TOKEN }}
# Uploaded after signing: the artifact means pushed AND signed.
- name: Write digest artifact
env:
BUILD_SHA: ${{ needs.setup.outputs.sha }}
run: |
mkdir -p "${RUNNER_TEMP}/digest-artifact"
jq -n \
--arg variant "${{ matrix.variant }}" \
--arg image "${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.image_suffix }}" \
--arg sha "${BUILD_SHA}" \
--arg digest "${{ steps.push.outputs.digest }}" \
--arg created "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'{schema: 1, variant: $variant, image: $image, sha: $sha, digest: $digest, created: $created}' \
> "${RUNNER_TEMP}/digest-artifact/digest.json"
- name: Upload digest artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: digest-${{ matrix.variant }}
path: ${{ runner.temp }}/digest-artifact/digest.json
retention-days: 30
if-no-files-found: error
# ── Generate and attach SBOM ──────────────────────────────────────────────
# Runs outside the critical path to :testing. Failures here must not block
# publish/promotion; stable release notes already regenerate SBOM inline in
# execute-release.yml. A fresh runner fetches BST metadata from remote CAS
# (no full artifact download needed; bst show reads element graph only).
# P3: pip wheel cache keyed to the pinned buildstream-sbom commit SHA so
# repeated runs skip the GitLab git fetch entirely.
publish-sbom:
needs: [setup, publish-image]
runs-on: ubuntu-24.04
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- variant: default
element: oci/bluefin.bst
image_suffix: ''
sbom_filename: dakota.spdx.json
continue: true
- variant: nvidia
element: oci/bluefin-nvidia.bst
image_suffix: '-nvidia'
sbom_filename: dakota-nvidia.spdx.json
continue: true
continue-on-error: ${{ matrix.continue }}
permissions:
contents: read
packages: write
id-token: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ needs.setup.outputs.sha }}
- name: Setup runner
uses: projectbluefin/actions/bootc-build/setup-runner@v1
with:
storage-backend: btrfs
update-podman: true
native-overlay: true
install-tools: '["just"]'
# BST config for remote CAS — buildstream-sbom calls bst show --deps all
# internally which needs element graph resolution via the remote CAS.
- name: Generate BST fetch config
env:
CASD_CLIENT_CERT: ${{ vars.CASD_CLIENT_CERT }}
CASD_CLIENT_KEY: ${{ secrets.CASD_CLIENT_KEY }}
uses: ./.github/actions/generate-bst-ci-config
with:
enable-remote-execution: 'false'
enable-push: 'false'
# Cache the pip wheel for buildstream-sbom across runs.
# Key is the pinned GitLab commit SHA — auto-invalidates on pin bumps.
# The wheel lives at ~/.cache/pip on the host; mounted into the bst2
# container via -v in just sbom so pip finds it without a network fetch.
- name: Restore pip cache for buildstream-sbom
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: ~/.cache/pip
key: pip-sbom-0706fec3bedf6f73bd9d2fed32c2aed585feef8d
restore-keys: pip-sbom-
- name: Generate SBOM
env:
BST_FLAGS: -o x86_64_v3 false --no-interactive --config /src/buildstream-ci.conf
run: just sbom ${{ matrix.variant }}
- name: Upload SBOM artifact for release workflow
if: matrix.variant == 'default'
run: cp dakota.spdx.json dakota.sbom.json
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: matrix.variant == 'default'
with:
name: sbom-dakota
path: dakota.sbom.json
retention-days: 30
- name: Login to GHCR
env:
GH_ACTOR: ${{ github.actor }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mkdir -p ~/.docker
echo "$GH_TOKEN" | podman login ghcr.io --username "$GH_ACTOR" --password-stdin \
--compat-auth-file ~/.docker/config.json
- name: Install oras
uses: oras-project/setup-oras@1d808f7d7f6995cc68b7bf507bfe5c5446e1dc9d # v2.0.1
- name: Resolve image digest
id: digest
uses: ./.github/actions/resolve-image-digest
with:
artifact-name: digest-${{ matrix.variant }}
image: ${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.image_suffix }}
build-sha: ${{ needs.setup.outputs.sha }}
registry-creds: ${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}
- name: Attach SBOM
id: sbom-attach
env:
DIGEST: ${{ steps.digest.outputs.digest }}
run: |
set -o pipefail
SBOM_DIGEST=$(oras attach \
--artifact-type application/vnd.spdx+json \
--format json \
"${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.image_suffix }}@${DIGEST}" \
${{ matrix.sbom_filename }}:application/vnd.spdx+json \
| python3 -c "import json,sys; d=json.load(sys.stdin); print(d['digest'])")
if [[ -z "$SBOM_DIGEST" || "$SBOM_DIGEST" == "None" ]]; then
echo "ERROR: Failed to capture SBOM digest from oras attach"
exit 1
fi
echo "sbom_digest=${SBOM_DIGEST}" >> "$GITHUB_OUTPUT"
- name: Sign SBOM
env:
SBOM_DIGEST: ${{ steps.sbom-attach.outputs.sbom_digest }}
run: |
cosign sign -y \
"${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.image_suffix }}@${SBOM_DIGEST}"
# ── Promote :testing ──────────────────────────────────────────────────────
# Applies the stream tag to the digest recorded by publish-image.
# Uses skopeo copy for server-side re-tagging: layers never leave the registry,
# saving the 20–25 min round-trip of the previous podman pull→tag→push approach.
# --preserve-digests keeps the promoted tag pointing at the signed manifest digest.
# NVIDIA is continue-on-error so a failing NVIDIA promote does not
# prevent the default :testing from landing.
promote:
needs: [setup, publish-image]
if: >-
needs.publish-image.result == 'success' &&
needs.setup.outputs.testing_tag != '' &&
github.event_name == 'workflow_run'
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
include:
- variant: default
image_suffix: ''
continue: false
- variant: nvidia
image_suffix: '-nvidia'
continue: true
- variant: gaming
image_suffix: '-gaming'
continue: true
- variant: nvidia-gaming
image_suffix: '-nvidia-gaming'
continue: true
continue-on-error: ${{ matrix.continue }}
permissions:
contents: write
packages: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Resolve source digest
id: digest
uses: ./.github/actions/resolve-image-digest
with:
artifact-name: digest-${{ matrix.variant }}
image: ${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.image_suffix }}
build-sha: ${{ needs.setup.outputs.sha }}
registry-creds: ${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}
- name: Promote :${{ needs.setup.outputs.sha }} to :${{ needs.setup.outputs.testing_tag }}
env:
TESTING_TAG: ${{ needs.setup.outputs.testing_tag }}
GH_ACTOR: ${{ github.actor }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DIGEST: ${{ steps.digest.outputs.digest }}
run: |
IMAGE="${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.image_suffix }}"
PUSH_OK=false
for attempt in 1 2 3; do
skopeo copy \
--preserve-digests \
--src-creds "$GH_ACTOR:$GH_TOKEN" \
--dest-creds "$GH_ACTOR:$GH_TOKEN" \
"docker://${IMAGE}@${DIGEST}" \
"docker://${IMAGE}:${TESTING_TAG}" && { PUSH_OK=true; break; }
echo "skopeo copy attempt ${attempt} failed for :${TESTING_TAG}, retrying in 5s..."
[ "$attempt" -lt 3 ] && sleep 5
done
$PUSH_OK || { echo "ERROR: all copy attempts failed for :${TESTING_TAG}"; exit 1; }
PROMOTED=$(skopeo inspect --no-tags --creds "$GH_ACTOR:$GH_TOKEN" \
"docker://${IMAGE}:${TESTING_TAG}" | jq -r '.Digest')
if [ "$PROMOTED" != "$DIGEST" ]; then
echo "ERROR: :${TESTING_TAG} points at ${PROMOTED}, expected ${DIGEST}"
exit 1
fi
- name: Push :btw alias (next stream only)
if: needs.setup.outputs.testing_tag == 'next'
env:
GH_ACTOR: ${{ github.actor }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DIGEST: ${{ steps.digest.outputs.digest }}
run: |
IMAGE="${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.image_suffix }}"
PUSH_OK=false
for attempt in 1 2 3; do
skopeo copy \
--preserve-digests \
--src-creds "$GH_ACTOR:$GH_TOKEN" \
--dest-creds "$GH_ACTOR:$GH_TOKEN" \
"docker://${IMAGE}@${DIGEST}" \
"docker://${IMAGE}:btw" && { PUSH_OK=true; break; }
echo "skopeo copy attempt ${attempt} failed for :btw, retrying in 5s..."
[ "$attempt" -lt 3 ] && sleep 5
done
$PUSH_OK || { echo "ERROR: all copy attempts failed for :btw"; exit 1; }