name: Manual relay image run-name: Manual relay image ยท ${{ inputs.source_ref }} # Operator-only publishing. This deliberately does not qualify the source via CI. # Publish manual-* tags only; normal release publishing remains in docker.yml. on: workflow_dispatch: inputs: source_ref: description: block/buzz branch, tag, or full SHA reachable from a repository branch required: true type: string permissions: {} env: IMAGE_NAME: ${{ vars.GHCR_IMAGE != '' && vars.GHCR_IMAGE || 'ghcr.io/block/buzz' }} jobs: resolve: name: Resolve the selected source runs-on: ubuntu-24.04 timeout-minutes: 5 permissions: contents: read outputs: sha: ${{ steps.source.outputs.sha }} tag: ${{ steps.source.outputs.tag }} steps: - name: Require block/buzz repository env: REPOSITORY: ${{ github.repository }} shell: bash run: | set -euo pipefail [[ "$REPOSITORY" == block/buzz ]] || { echo '::error::Run this workflow in block/buzz; use source_ref to select repository code.' exit 1 } - name: Resolve a source reachable from a repository branch id: source env: SOURCE_REF: ${{ inputs.source_ref }} shell: bash run: | set -euo pipefail if [[ -z "$SOURCE_REF" || ${#SOURCE_REF} -gt 255 || ! "$SOURCE_REF" =~ ^[A-Za-z0-9._/@-]+$ ]]; then echo '::error::Supply a repository branch, tag, or full commit SHA.' exit 1 fi case "$SOURCE_REF" in -*|*..*|*.lock|refs/pull/*|pull/*|*/pull/*) echo '::error::PR refs and malformed refs are not allowed.' exit 1 ;; esac workdir=$(mktemp -d) trap 'rm -rf "$workdir"' EXIT git -C "$workdir" init --quiet git -C "$workdir" remote add origin https://github.com/block/buzz.git # Fetch only canonical branch histories, never refs/pull or arbitrary SHAs. # Full history is needed to prove ancestry; blobs are fetched only by build jobs. git -C "$workdir" fetch --filter=blob:none --no-tags origin \ '+refs/heads/*:refs/remotes/origin/*' if [[ "$SOURCE_REF" =~ ^[0-9a-fA-F]{40}$ ]]; then sha=$(git -C "$workdir" rev-parse --verify "${SOURCE_REF}^{commit}") elif [[ "$SOURCE_REF" == refs/heads/* ]]; then sha=$(git -C "$workdir" rev-parse --verify "refs/remotes/origin/${SOURCE_REF#refs/heads/}^{commit}") else tag_ref="$SOURCE_REF" if [[ "$SOURCE_REF" != refs/* ]] && \ sha=$(git -C "$workdir" rev-parse --verify "refs/remotes/origin/${SOURCE_REF}^{commit}" 2>/dev/null); then : else if [[ "$SOURCE_REF" != refs/* ]]; then tag_ref="refs/tags/$SOURCE_REF" fi [[ "$tag_ref" == refs/tags/* ]] || { echo '::error::Only repository branches and tags are allowed.' exit 1 } git -C "$workdir" fetch --no-tags origin "$tag_ref:refs/tags/manual-source" sha=$(git -C "$workdir" rev-parse --verify 'refs/tags/manual-source^{commit}') fi fi [[ "$sha" =~ ^[0-9a-f]{40}$ ]] branches=$(git -C "$workdir" for-each-ref --format='%(refname)' --contains="$sha" refs/remotes/origin/) [[ -n "$branches" ]] || { echo '::error::Selected commit must be reachable from a block/buzz branch; PR-only commits are not allowed.' exit 1 } echo "sha=$sha" >> "$GITHUB_OUTPUT" echo "tag=manual-${sha:0:12}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" >> "$GITHUB_OUTPUT" echo "Selected source: $sha. CI qualification is skipped." build: name: Build (${{ matrix.platform }}) needs: resolve runs-on: ${{ matrix.runner }} timeout-minutes: 90 permissions: contents: read packages: write strategy: fail-fast: false matrix: include: - platform: linux/amd64 runner: ubuntu-24.04 arch: amd64 - platform: linux/arm64 runner: ubuntu-24.04-arm arch: arm64 steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ needs.resolve.outputs.sha }} persist-credentials: false - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 with: buildkitd-config-inline: | [worker.oci] max-parallelism = 2 - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push the runtime by digest id: build uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . file: ./Dockerfile target: runtime platforms: ${{ matrix.platform }} build-args: | BUZZ_SOURCE_SHA=${{ needs.resolve.outputs.sha }} BUZZ_BUILD_ID=manual-actions:${{ github.run_id }}:${{ github.run_attempt }} BUZZ_BUILD_URL=https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }} labels: | org.opencontainers.image.source=https://github.com/${{ github.repository }} org.opencontainers.image.revision=${{ needs.resolve.outputs.sha }} xyz.buzz.manual-build=true outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true cache-from: | type=registry,ref=${{ env.IMAGE_NAME }}-buildcache:${{ matrix.arch }} type=registry,ref=${{ env.IMAGE_NAME }}-buildcache:manual-${{ matrix.arch }} cache-to: type=registry,ref=${{ env.IMAGE_NAME }}-buildcache:manual-${{ matrix.arch }},mode=max,compression=zstd - name: Record this architecture's published digest env: IMAGE_DIGEST: ${{ steps.build.outputs.digest }} shell: bash run: | set -euo pipefail [[ "$IMAGE_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]] mkdir -p "$RUNNER_TEMP/manual-digest" printf '%s\n' "$IMAGE_DIGEST" > "$RUNNER_TEMP/manual-digest/digest" - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: manual-digest-${{ matrix.arch }} path: ${{ runner.temp }}/manual-digest/digest if-no-files-found: error overwrite: true retention-days: 7 publish: name: Publish and verify the combined image needs: [resolve, build] runs-on: ubuntu-24.04 timeout-minutes: 10 permissions: contents: read packages: write steps: - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: manual-digest-* path: ${{ runner.temp }}/manual-digests - name: Assemble and verify the published image env: SOURCE_SHA: ${{ needs.resolve.outputs.sha }} MANUAL_TAG: ${{ needs.resolve.outputs.tag }} shell: bash run: | set -euo pipefail digests=() for arch in amd64 arm64; do digest=$(cat "$RUNNER_TEMP/manual-digests/manual-digest-$arch/digest") [[ "$digest" =~ ^sha256:[0-9a-f]{64}$ ]] digests+=("$IMAGE_NAME@$digest") done image="$IMAGE_NAME:$MANUAL_TAG" docker buildx imagetools create --tag "$image" \ --annotation "index:org.opencontainers.image.revision=$SOURCE_SHA" \ --annotation "index:org.opencontainers.image.source=https://github.com/$GITHUB_REPOSITORY" \ --annotation "index:xyz.buzz.manual-build=true" "${digests[@]}" # Verify the published bytes using this workflow, not scripts from the selected source. merged_digest=$(docker buildx imagetools inspect "$image" --format '{{json .Manifest}}' | jq -er '.digest | select(test("^sha256:[0-9a-f]{64}$"))') pinned="$IMAGE_NAME@$merged_digest" source_url="https://github.com/$GITHUB_REPOSITORY" docker buildx imagetools inspect "$pinned" --raw > "$RUNNER_TEMP/index.json" jq -e '.manifests | map(select((.platform.os == "unknown" and .platform.architecture == "unknown") | not)) | map(.platform.os + "/" + .platform.architecture) | sort == ["linux/amd64", "linux/arm64"]' \ "$RUNNER_TEMP/index.json" >/dev/null runtime_digests='{}' for arch in amd64 arm64; do child=$(jq -er --arg arch "$arch" '.manifests[] | select(.platform.os == "linux" and .platform.architecture == $arch) | .digest | select(test("^sha256:[0-9a-f]{64}$"))' "$RUNNER_TEMP/index.json") docker buildx imagetools inspect "$IMAGE_NAME@$child" --format '{{json .Image}}' | jq -e --arg arch "$arch" --arg sha "$SOURCE_SHA" --arg source "$source_url" ' .os == "linux" and .architecture == $arch and .config.Labels["org.opencontainers.image.revision"] == $sha and .config.Labels["org.opencontainers.image.source"] == $source' >/dev/null runtime_digests=$(jq -cn --argjson current "$runtime_digests" \ --arg platform "linux/$arch" --arg digest "$child" '$current + {($platform): $digest}') done jq -n --arg image "$pinned" --arg tag "$image" --arg digest "$merged_digest" \ --arg sha "$SOURCE_SHA" --arg source "$source_url" --argjson platforms "$runtime_digests" \ '{image:$image, tag:$tag, digest:$digest, source_sha:$sha, source_url:$source, platforms:$platforms, ci_qualification:"skipped", status:"registry-verified"}' \ > "$RUNNER_TEMP/manual-image.json" { echo '## Manual relay image published to GHCR' echo echo "Source commit: $SOURCE_SHA" echo "Registry tag: $image" echo "Digest: $(jq -r .digest "$RUNNER_TEMP/manual-image.json")" echo 'Platforms: linux/amd64, linux/arm64' echo 'CI qualification was skipped. No deployment was started.' echo 'Any downstream registry import must be verified separately.' } >> "$GITHUB_STEP_SUMMARY" - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: manual-image-${{ github.run_id }}-${{ github.run_attempt }} path: ${{ runner.temp }}/manual-image.json if-no-files-found: error retention-days: 30