Publish Bluefin dakota #604
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; } |