Files
pulse/.github/workflows/publish-docker.yml
T
2026-08-31 13:21:25 +01:00

279 lines
11 KiB
YAML

name: Publish Docker Images
run-name: Publish Docker Images ${{ inputs.tag }}
# Called by create-release.yml as soon as the immutable candidate exists.
# Exact-version images are inert staging surfaces, so assembly and publication
# overlap container qualification and draft creation. The release-readiness
# barrier still joins both paths before any customer-facing alias can move.
on:
workflow_call:
inputs:
tag:
description: 'Release tag (e.g., v4.34.0)'
required: true
type: string
container_artifact:
description: 'Exact-candidate container payload artifact from this release run'
required: true
type: string
source_sha:
description: 'Exact source commit bound to the candidate payload'
required: true
type: string
outputs:
server_digest:
description: 'Verified multi-registry digest for the Pulse server image'
value: ${{ jobs.verify.outputs.server_digest }}
control_plane_digest:
description: 'Verified multi-registry digest for the Pulse control-plane image'
value: ${{ jobs.verify.outputs.control_plane_digest }}
concurrency:
group: docker-publish-${{ inputs.tag }}
cancel-in-progress: false
permissions:
contents: read
jobs:
publish:
name: Publish ${{ matrix.image }} image
runs-on: ubuntu-24.04
timeout-minutes: 60 # Increased for multi-arch builds with QEMU
strategy:
fail-fast: false
matrix:
image:
- server
- control-plane
permissions:
contents: read
packages: write
id-token: write
attestations: write
steps:
- name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
persist-credentials: false
fetch-depth: 0
fetch-tags: true
- name: Extract version from release tag
id: version
env:
INPUT_TAG: ${{ inputs.tag }}
run: |
TAG="${INPUT_TAG}"
VERSION="${TAG#v}"
# Detect if this is a prerelease (RC, alpha, beta)
IS_PRERELEASE="false"
if [[ "$VERSION" =~ -rc\.[0-9]+$ ]] || [[ "$VERSION" =~ -alpha\.[0-9]+$ ]] || [[ "$VERSION" =~ -beta\.[0-9]+$ ]]; then
IS_PRERELEASE="true"
echo "Detected prerelease version"
fi
echo "tag=${TAG}" >> $GITHUB_OUTPUT
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "is_prerelease=${IS_PRERELEASE}" >> $GITHUB_OUTPUT
echo "Publishing Docker images for ${TAG} (prerelease: ${IS_PRERELEASE})"
- name: Validate release line policy
env:
TAG: ${{ steps.version.outputs.tag }}
SOURCE_SHA: ${{ inputs.source_sha }}
run: |
set -euo pipefail
python3 scripts/release_control/validate_artifact_release_line.py \
--tag "${TAG}" \
--anticipated-source-sha "${SOURCE_SHA}" \
--purpose "Docker publish"
- name: Verify exact source checkout
env:
EXPECTED_SOURCE_SHA: ${{ inputs.source_sha }}
EXPECTED_VERSION: ${{ steps.version.outputs.version }}
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "${EXPECTED_SOURCE_SHA}"
test "$(tr -d '\n\r[:space:]' < VERSION)" = "${EXPECTED_VERSION}"
- name: Download exact-candidate container payload
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ inputs.container_artifact }}
path: ${{ runner.temp }}/release-container-payload
- name: Verify exact-candidate container payload
env:
EXPECTED_SOURCE_SHA: ${{ inputs.source_sha }}
VERSION: ${{ steps.version.outputs.version }}
run: |
python3 scripts/release_candidate_manifest.py verify-local \
--release-dir "$RUNNER_TEMP/release-container-payload/payload" \
--manifest "$RUNNER_TEMP/release-container-payload/release-container-payload.json" \
--version "${VERSION}" \
--source-sha "${EXPECTED_SOURCE_SHA}"
- name: Set up QEMU
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Log in to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to GHCR
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Assemble and push Pulse server image (multi-arch)
if: matrix.image == 'server'
id: build_server_image
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
target: runtime_prebuilt
build-contexts: |
release_payload=${{ runner.temp }}/release-container-payload/payload/release
platforms: linux/amd64,linux/arm64
push: true
provenance: mode=max
sbom: true
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse:buildcache
tags: |
rcourtman/pulse:${{ steps.version.outputs.tag }}
rcourtman/pulse:${{ steps.version.outputs.version }}
ghcr.io/${{ github.repository_owner }}/pulse:${{ steps.version.outputs.tag }}
ghcr.io/${{ github.repository_owner }}/pulse:${{ steps.version.outputs.version }}
- name: Attest Pulse server image on Docker Hub
if: matrix.image == 'server'
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
with:
subject-name: docker.io/rcourtman/pulse
subject-digest: ${{ steps.build_server_image.outputs.digest }}
push-to-registry: true
create-storage-record: false
- name: Attest Pulse server image on GHCR
if: matrix.image == 'server'
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
with:
subject-name: ghcr.io/${{ github.repository_owner }}/pulse
subject-digest: ${{ steps.build_server_image.outputs.digest }}
push-to-registry: true
create-storage-record: false
- name: Assemble and push Pulse control-plane image (multi-arch)
if: matrix.image == 'control-plane'
id: build_control_plane_image
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
file: deploy/provider-msp/Dockerfile.control-plane
target: control_plane_prebuilt
build-contexts: |
compiled_payload=${{ runner.temp }}/release-container-payload/payload/compiled
platforms: linux/amd64,linux/arm64
push: true
provenance: mode=max
sbom: true
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse-control-plane:buildcache
tags: |
rcourtman/pulse-control-plane:${{ steps.version.outputs.tag }}
rcourtman/pulse-control-plane:${{ steps.version.outputs.version }}
ghcr.io/${{ github.repository_owner }}/pulse-control-plane:${{ steps.version.outputs.tag }}
ghcr.io/${{ github.repository_owner }}/pulse-control-plane:${{ steps.version.outputs.version }}
- name: Attest Pulse control-plane image on Docker Hub
if: matrix.image == 'control-plane'
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
with:
subject-name: docker.io/rcourtman/pulse-control-plane
subject-digest: ${{ steps.build_control_plane_image.outputs.digest }}
push-to-registry: true
create-storage-record: false
- name: Attest Pulse control-plane image on GHCR
if: matrix.image == 'control-plane'
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
with:
subject-name: ghcr.io/${{ github.repository_owner }}/pulse-control-plane
subject-digest: ${{ steps.build_control_plane_image.outputs.digest }}
push-to-registry: true
create-storage-record: false
- name: Output image information
env:
IMAGE_KIND: ${{ matrix.image }}
run: |
echo "✅ Exact-version ${IMAGE_KIND} Docker image staged successfully!"
if [[ "${IMAGE_KIND}" == "server" ]]; then
echo "Server images (linux/amd64, linux/arm64):"
echo " - rcourtman/pulse:${{ steps.version.outputs.tag }}"
echo " - rcourtman/pulse:${{ steps.version.outputs.version }}"
echo "Pulse Agent binaries ship as release assets, not as a Docker image."
echo "See the GitHub release page for pulse-agent-{darwin,freebsd,linux,windows}-{amd64,arm64,...}."
else
echo "Control-plane images (linux/amd64, linux/arm64):"
echo " - rcourtman/pulse-control-plane:${{ steps.version.outputs.tag }}"
echo " - rcourtman/pulse-control-plane:${{ steps.version.outputs.version }}"
fi
echo "Floating aliases are promoted separately at the activation barrier."
verify:
name: Verify exact image identities and provenance
needs: publish
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: read
packages: read
outputs:
server_digest: ${{ steps.proof.outputs.server_digest }}
control_plane_digest: ${{ steps.proof.outputs.control_plane_digest }}
steps:
- name: Checkout release verification control
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
persist-credentials: false
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Log in to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to GHCR
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Verify exact tags and signed provenance
id: proof
env:
GH_TOKEN: ${{ github.token }}
REPOSITORY: ${{ github.repository }}
SOURCE_SHA: ${{ inputs.source_sha }}
TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
./scripts/verify-release-container-images.sh \
"${TAG}" "${SOURCE_SHA}" \
"${REPOSITORY}" >> "$GITHUB_OUTPUT"