From b2f4b1f41dcce94332ada46de31d2b1d112a8094 Mon Sep 17 00:00:00 2001 From: Chase Date: Thu, 29 Oct 2026 15:28:55 +0100 Subject: [PATCH] ci: publish through the shared Blockstream/ci-components workflow The build/publish/sign jobs now live in one place for every repository that publishes this way. Same behaviour: build without credentials, publish without repository code, keyless cosign, - tags. The repository passes its own workload identity provider, publisher SA and image from repository variables. --- .github/workflows/publish.yml | 200 +++------------------------------- 1 file changed, 14 insertions(+), 186 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c558338..640c446 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,11 +1,12 @@ name: Publish image -# Builds the relay image from master and publishes it to the staging registry. -# The build job runs repository code without registry credentials. The publish -# job runs no repository code: it only re-uploads the OCI data produced by the -# build, then signs the pushed digest with keyless cosign. +# Builds the relay image from master and publishes it to the staging registry +# through the shared Blockstream/ci-components publish workflow. Its build job +# runs repository code without registry credentials; its publish job runs no +# repository code, pushes only the OCI data produced by the build and signs the +# pushed digest with keyless cosign. # -# Registry settings come from variables of the `staging` environment: +# This repository passes its own registry identity from repository variables: # GCP_WORKLOAD_IDENTITY_PROVIDER, GCP_SERVICE_ACCOUNT, IMAGE_NAME on: @@ -18,189 +19,16 @@ concurrency: group: publish-master cancel-in-progress: false -defaults: - run: - shell: bash - jobs: - build: - if: github.repository == 'BlockstreamResearch/wallet-session' - runs-on: ubuntu-24.04 - timeout-minutes: 45 - permissions: - contents: read - outputs: - artifact-id: ${{ steps.upload.outputs.artifact-id }} - steps: - - name: Check out the triggering commit - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ github.sha }} - persist-credentials: false - - - name: Set up Buildx - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 - - - name: Build OCI archive - run: | - set -euo pipefail - docker buildx build \ - --file deployment/Dockerfile.relay \ - --output "type=oci,dest=$RUNNER_TEMP/image.oci.tar" \ - --provenance=false \ - . - - - name: Upload OCI archive - id: upload - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: oci-${{ github.run_id }}-${{ github.run_attempt }} - path: ${{ runner.temp }}/image.oci.tar - if-no-files-found: error - retention-days: 1 - compression-level: 0 - publish: - needs: build - if: github.repository == 'BlockstreamResearch/wallet-session' && github.ref == 'refs/heads/master' - runs-on: ubuntu-24.04 - timeout-minutes: 20 - environment: staging + if: github.repository == 'BlockstreamResearch/wallet-session' permissions: contents: read id-token: write - outputs: - image: ${{ steps.push.outputs.image }} - # No checkout, docker load or container runs in this job. The only - # build-produced input is the OCI data artifact. - steps: - - name: Validate settings and derive the tag - id: destination - env: - BUILD_ARTIFACT_ID: ${{ needs.build.outputs.artifact-id }} - CALLER_SHA: ${{ github.sha }} - PROVIDER: ${{ vars.GCP_WORKLOAD_IDENTITY_PROVIDER }} - SERVICE_ACCOUNT: ${{ vars.GCP_SERVICE_ACCOUNT }} - IMAGE_NAME: ${{ vars.IMAGE_NAME }} - run: | - set -euo pipefail - [[ "$BUILD_ARTIFACT_ID" =~ ^[0-9]+$ ]] || exit 1 - [[ "$CALLER_SHA" =~ ^[0-9a-f]{40}$ ]] || exit 1 - [[ "$PROVIDER" =~ ^projects/[0-9]+/locations/global/workloadIdentityPools/[a-z0-9-]+/providers/[a-z0-9-]+$ ]] \ - || { echo 'GCP_WORKLOAD_IDENTITY_PROVIDER is missing or invalid' >&2; exit 1; } - [[ "$SERVICE_ACCOUNT" =~ ^[a-z0-9-]+@[a-z0-9-]+\.iam\.gserviceaccount\.com$ ]] \ - || { echo 'GCP_SERVICE_ACCOUNT is missing or invalid' >&2; exit 1; } - [[ "$IMAGE_NAME" =~ ^([a-z0-9-]+)-docker\.pkg\.dev/[a-z0-9-]+/[a-z0-9-]+/[a-z0-9-]+$ ]] \ - || { echo 'IMAGE_NAME is missing or invalid' >&2; exit 1; } - printf 'registry=%s-docker.pkg.dev\n' "${BASH_REMATCH[1]}" >> "$GITHUB_OUTPUT" - printf 'tag=%s-%s\n' "$CALLER_SHA" "$(date -u +%Y%m%d%H%M%S)" >> "$GITHUB_OUTPUT" - - - name: Download only this build's artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - artifact-ids: ${{ needs.build.outputs.artifact-id }} - path: ${{ runner.temp }}/incoming-image - merge-multiple: true - - - name: Unpack OCI data without executing build content - run: | - set -euo pipefail - python3 -I - <<'PY' - import hashlib - import os - from pathlib import Path - import re - import tarfile - - archive = Path(os.environ["RUNNER_TEMP"]) / "incoming-image/image.oci.tar" - root = Path(os.environ["RUNNER_TEMP"]) / "publish-oci" - root.mkdir(mode=0o700) - total = 0 - entries = 0 - # crane expects an OCI directory. Copy only the OCI layout's data - # files; never extract links, paths or file modes. - with tarfile.open(archive, mode="r|") as source: - for member in source: - entries += 1 - if entries > 100000: - raise SystemExit("Too many OCI entries") - name = member.name.removeprefix("./") - if member.isdir() and name.rstrip("/") in (".", "blobs", "blobs/sha256"): - continue - is_blob = re.fullmatch(r"blobs/sha256/[0-9a-f]{64}", name) - if not member.isfile() or (name not in ("index.json", "oci-layout") and not is_blob): - raise SystemExit("Unexpected OCI entry") - total += member.size - if member.size < 0 or total > 10 * 1024**3: - raise SystemExit("OCI archive exceeds 10 GiB limit") - target = root / name - target.parent.mkdir(mode=0o700, parents=True, exist_ok=True) - digest = hashlib.sha256() - # Exclusive creation rejects duplicate entries. - with source.extractfile(member) as incoming, target.open("xb") as output: - os.chmod(target, 0o600) - while chunk := incoming.read(1024 * 1024): - digest.update(chunk) - output.write(chunk) - if is_blob and digest.hexdigest() != target.name: - raise SystemExit("OCI blob digest mismatch") - if not (root / "index.json").is_file() or not (root / "oci-layout").is_file(): - raise SystemExit("Missing OCI layout metadata") - PY - - - name: Install crane with a pinned archive checksum - uses: jaxxstorm/action-install-gh-release@25e24d2d23ae098373794ef1d6faecb48ee52da8 # v3.0.0 - with: - repo: google/go-containerregistry - tag: v0.22.1 - platform: linux - arch: x86_64 - digest: 0ab7a1d6932a213aed964ce97666c3077fe691c8606413674a8b3e0b9ec4cda0 - cache: disable - - - name: Install cosign - uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - with: - cosign-release: v3.1.3 - - - name: Authenticate to the registry project - id: auth - uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0 - with: - workload_identity_provider: ${{ vars.GCP_WORKLOAD_IDENTITY_PROVIDER }} - service_account: ${{ vars.GCP_SERVICE_ACCOUNT }} - token_format: access_token - create_credentials_file: false - export_environment_variables: false - - - name: Log in to the registry - env: - REGISTRY: ${{ steps.destination.outputs.registry }} - ACCESS_TOKEN: ${{ steps.auth.outputs.access_token }} - run: | - set -euo pipefail - printf '%s' "$ACCESS_TOKEN" | crane auth login "$REGISTRY" \ - --username oauth2accesstoken --password-stdin - - - name: Push image and record the registry digest - id: push - env: - IMAGE_NAME: ${{ vars.IMAGE_NAME }} - IMAGE_TAG: ${{ steps.destination.outputs.tag }} - run: | - set -euo pipefail - crane push "$RUNNER_TEMP/publish-oci" "$IMAGE_NAME:$IMAGE_TAG" - digest="$(crane digest "$IMAGE_NAME:$IMAGE_TAG")" - [[ "$digest" =~ ^sha256:[0-9a-f]{64}$ ]] || exit 1 - printf 'image=%s@%s\n' "$IMAGE_NAME" "$digest" >> "$GITHUB_OUTPUT" - - - name: Sign image by digest using GitHub OIDC - env: - IMAGE: ${{ steps.push.outputs.image }} - run: cosign sign --yes "$IMAGE" - - - name: Remove registry login - if: ${{ always() && steps.auth.outcome == 'success' }} - env: - REGISTRY: ${{ steps.destination.outputs.registry }} - run: crane auth logout "$REGISTRY" + uses: Blockstream/ci-components/.github/workflows/publish.yml@v1 + with: + workload-identity-provider: ${{ vars.GCP_WORKLOAD_IDENTITY_PROVIDER }} + service-account: ${{ vars.GCP_SERVICE_ACCOUNT }} + image: ${{ vars.IMAGE_NAME }} + environment: staging + dockerfile: deployment/Dockerfile.relay