Pulse/.github/workflows/publish-docker.yml
rcourtman 4fcb90673b Allow stable patch artifact publishing
Reuse shared release-line validation for Docker, floating-tag, and Helm artifact workflows so stable patch tags can publish from the previous stable tag without a fabricated same-version RC.
2026-07-04 20:49:47 +01:00

211 lines
8.9 KiB
YAML

name: Publish Docker Images
run-name: Publish Docker Images ${{ inputs.tag }}
# Triggered by create-release.yml after staging images pass tests.
# Builds multi-arch images (amd64+arm64) from source and publishes to Docker Hub and GHCR.
on:
workflow_dispatch:
inputs:
tag:
description: 'Release tag (e.g., v4.34.0)'
required: true
type: string
concurrency:
group: docker-publish-${{ inputs.tag }}
cancel-in-progress: false
permissions:
contents: read
jobs:
publish:
runs-on: ubuntu-24.04
timeout-minutes: 60 # Increased for multi-arch builds with QEMU
permissions:
contents: read
packages: write
id-token: write
attestations: write
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
fetch-tags: true
- name: Extract version from release tag
id: version
run: |
TAG="${{ inputs.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 - will NOT update :latest tag"
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 }}
run: |
set -euo pipefail
python3 scripts/release_control/validate_artifact_release_line.py \
--tag "${TAG}" \
--purpose "Docker publish"
- name: Check out validated release tag
env:
TAG: ${{ steps.version.outputs.tag }}
run: |
set -euo pipefail
git checkout --detach "refs/tags/${TAG}"
- 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: Derive license public key Docker cache key
id: license_key_cache
env:
PULSE_LICENSE_PUBLIC_KEY: ${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}
run: |
set -euo pipefail
decoded_len="$(printf '%s' "${PULSE_LICENSE_PUBLIC_KEY}" | base64 -d | wc -c | tr -d ' ')"
if [ "${decoded_len}" != "32" ]; then
echo "PULSE_LICENSE_PUBLIC_KEY must decode to 32 bytes." >&2
exit 1
fi
key_sha256="$(printf '%s' "${PULSE_LICENSE_PUBLIC_KEY}" | base64 -d | sha256sum | awk '{print $1}')"
echo "sha256=${key_sha256}" >> "${GITHUB_OUTPUT}"
- name: Build and push Pulse server image (multi-arch)
id: build_server_image
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
target: runtime
platforms: linux/amd64,linux/arm64
push: true
provenance: mode=max
sbom: true
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse:buildcache
build-args: |
VERSION=${{ steps.version.outputs.tag }}
PULSE_LICENSE_PUBLIC_KEY_SHA256=${{ steps.license_key_cache.outputs.sha256 }}
PULSE_UPDATE_SIGNING_PUBLIC_KEY=${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}
secrets: |
pulse_license_public_key=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}
pulse_update_signing_key=${{ secrets.PULSE_UPDATE_SIGNING_KEY }}
tags: |
rcourtman/pulse:${{ steps.version.outputs.tag }}
rcourtman/pulse:${{ steps.version.outputs.version }}
${{ steps.version.outputs.is_prerelease != 'true' && 'rcourtman/pulse:latest' || '' }}
ghcr.io/${{ github.repository_owner }}/pulse:${{ steps.version.outputs.tag }}
ghcr.io/${{ github.repository_owner }}/pulse:${{ steps.version.outputs.version }}
${{ steps.version.outputs.is_prerelease != 'true' && format('ghcr.io/{0}/pulse:latest', github.repository_owner) || '' }}
- name: Attest Pulse server image on Docker Hub
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
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: Build and push Pulse control-plane image (multi-arch)
id: build_control_plane_image
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
file: deploy/provider-msp/Dockerfile.control-plane
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
build-args: |
VERSION=${{ steps.version.outputs.tag }}
PULSE_LICENSE_PUBLIC_KEY_SHA256=${{ steps.license_key_cache.outputs.sha256 }}
secrets: |
pulse_license_public_key=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}
tags: |
rcourtman/pulse-control-plane:${{ steps.version.outputs.tag }}
rcourtman/pulse-control-plane:${{ steps.version.outputs.version }}
${{ steps.version.outputs.is_prerelease != 'true' && 'rcourtman/pulse-control-plane:latest' || '' }}
ghcr.io/${{ github.repository_owner }}/pulse-control-plane:${{ steps.version.outputs.tag }}
ghcr.io/${{ github.repository_owner }}/pulse-control-plane:${{ steps.version.outputs.version }}
${{ steps.version.outputs.is_prerelease != 'true' && format('ghcr.io/{0}/pulse-control-plane:latest', github.repository_owner) || '' }}
- name: Attest Pulse control-plane image on Docker Hub
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
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
run: |
IS_PRERELEASE="${{ steps.version.outputs.is_prerelease }}"
echo "✅ Docker images published successfully!"
echo ""
echo "Server images (linux/amd64, linux/arm64):"
echo " - rcourtman/pulse:${{ steps.version.outputs.tag }}"
echo " - rcourtman/pulse:${{ steps.version.outputs.version }}"
if [ "$IS_PRERELEASE" != "true" ]; then
echo " - rcourtman/pulse:latest"
fi
echo ""
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 }}"
if [ "$IS_PRERELEASE" != "true" ]; then
echo " - rcourtman/pulse-control-plane:latest"
fi
echo ""
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,...}."
echo ""
if [ "$IS_PRERELEASE" = "true" ]; then
echo "Note: :latest tags were NOT updated (this is a prerelease)"
fi