Files
pulse/.github/workflows/publish-docker.yml
T
courtmanr@gmail.com 6ce41ae9d4 Fold the echo-only release joins into their public writers
candidate_qualification and release_readiness only restated other jobs'
results. publish_release_tag, publish_docker, publish_helm_chart and
activate_release now each carry the exact candidate predicate in their own
needs and if, guarded by !cancelled() with the same allowed integration
skip, and activation joins the tag and both registry publications directly.
The commit verdict restates every candidate result in place of the
readiness join.

recover-release-activation.yml accepts both shapes. A source run that has
release_readiness still has to prove only that join, as before. A run
without it has to prove publish_release_tag and at least one, and only
successful, jobs under the publish_docker and publish_helm_chart caller IDs.

Failure diagnostics and the inspection-only Helm chart artifact, which
nothing downloads, are kept for three days instead of 14 and 90.
2026-10-01 21:47:58 +01:00

278 lines
11 KiB
YAML

name: Publish Docker Images
run-name: Publish Docker Images ${{ inputs.tag }}
# Called only after exact candidate qualification and public tag creation.
# Versioned registry images are public release surfaces, not private staging.
# activate_release joins the published digests before activation.
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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
python3 scripts/write_github_output.py tag "${TAG}"
python3 scripts/write_github_output.py version "${VERSION}"
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@1f40c72289eff860ee54a304f1438e3cff362e0a # v4.3.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- name: Log in to Docker Hub
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to GHCR
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.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@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.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
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@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
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@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
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@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.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
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@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
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@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
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 }}
WORKFLOW_OUTPUT_1: ${{ steps.version.outputs.tag }}
WORKFLOW_OUTPUT_2: ${{ steps.version.outputs.version }}
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:${WORKFLOW_OUTPUT_1}"
echo " - rcourtman/pulse:${WORKFLOW_OUTPUT_2}"
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:${WORKFLOW_OUTPUT_1}"
echo " - rcourtman/pulse-control-plane:${WORKFLOW_OUTPUT_2}"
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- name: Log in to Docker Hub
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to GHCR
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.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"