Files
pulse-triage[bot] ad1cfd33c3 fix(ci): qualify grouped release action pin consumers
The grouped action upgrade leaves signing, network and publication consumer assertions on superseded pins. Align those contracts and check every consumer against reviewed immutable upstream manifests, retaining exact dispatch and release trust boundaries without claiming hosted execution.

Change-source: pulse-maintainer
2026-09-10 00:35:47 +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@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"