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_call: inputs: tag: description: 'Release tag (e.g., v4.34.0)' required: true type: string 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