mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-21 18:46:08 +00:00
250 lines
10 KiB
YAML
250 lines
10 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@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
with:
|
|
ref: ${{ inputs.tag }}
|
|
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 }}
|
|
IS_PRERELEASE: ${{ steps.version.outputs.is_prerelease }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
VERSION="${TAG#v}"
|
|
REQUIRED_BRANCH="$(python3 scripts/release_control/control_plane.py --branch-for-version "${VERSION}")"
|
|
|
|
if [ "$(git rev-parse --is-shallow-repository)" = "true" ]; then
|
|
git fetch --prune --unshallow origin
|
|
fi
|
|
git fetch --prune origin "${REQUIRED_BRANCH}" --tags
|
|
|
|
if ! git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
|
|
echo "::error::Tag ${TAG} does not exist in repository tags."
|
|
exit 1
|
|
fi
|
|
|
|
TAG_COMMIT="$(git rev-list -n1 "refs/tags/${TAG}")"
|
|
if ! git merge-base --is-ancestor "$TAG_COMMIT" "origin/${REQUIRED_BRANCH}"; then
|
|
echo "::error::Tag ${TAG} is not reachable from origin/${REQUIRED_BRANCH}. Refusing cross-line Docker publish."
|
|
exit 1
|
|
fi
|
|
|
|
if [ "$IS_PRERELEASE" != "true" ]; then
|
|
BASE_VERSION="${TAG#v}"
|
|
BASE_VERSION="${BASE_VERSION%%-*}"
|
|
mapfile -t RC_TAGS < <(git tag -l "v${BASE_VERSION}-rc.*" --sort=-version:refname)
|
|
if [ "${#RC_TAGS[@]}" -eq 0 ]; then
|
|
echo "::error::Stable tag ${TAG} has no matching prerelease tags for base version ${BASE_VERSION}."
|
|
exit 1
|
|
fi
|
|
|
|
MATCHED_RC=""
|
|
for rc_tag in "${RC_TAGS[@]}"; do
|
|
rc_commit="$(git rev-list -n1 "refs/tags/${rc_tag}")"
|
|
if git merge-base --is-ancestor "$rc_commit" "$TAG_COMMIT"; then
|
|
MATCHED_RC="$rc_tag"
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [ -z "$MATCHED_RC" ]; then
|
|
echo "::error::Stable tag ${TAG} does not descend from any matching prerelease tag for base version ${BASE_VERSION}."
|
|
exit 1
|
|
fi
|
|
|
|
echo "[OK] ${TAG} descends from prerelease ${MATCHED_RC}"
|
|
fi
|
|
|
|
echo "[OK] ${TAG} validated against release line ${REQUIRED_BRANCH}"
|
|
|
|
- name: Set up QEMU
|
|
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
|
|
|
- name: Log in to Docker Hub
|
|
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
|
with:
|
|
username: ${{ secrets.DOCKER_USERNAME }}
|
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
|
|
|
- name: Log in to GHCR
|
|
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
|
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@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
|
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
|
|
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
|
|
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 agent image (multi-arch)
|
|
id: build_agent_image
|
|
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
|
with:
|
|
context: .
|
|
file: ./Dockerfile
|
|
target: agent_runtime
|
|
platforms: linux/amd64,linux/arm64
|
|
push: true
|
|
provenance: mode=max
|
|
sbom: true
|
|
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse-agent: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-agent:${{ steps.version.outputs.tag }}
|
|
rcourtman/pulse-agent:${{ steps.version.outputs.version }}
|
|
${{ steps.version.outputs.is_prerelease != 'true' && 'rcourtman/pulse-agent:latest' || '' }}
|
|
ghcr.io/${{ github.repository_owner }}/pulse-agent:${{ steps.version.outputs.tag }}
|
|
ghcr.io/${{ github.repository_owner }}/pulse-agent:${{ steps.version.outputs.version }}
|
|
${{ steps.version.outputs.is_prerelease != 'true' && format('ghcr.io/{0}/pulse-agent:latest', github.repository_owner) || '' }}
|
|
|
|
- name: Attest Pulse agent image on Docker Hub
|
|
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4
|
|
with:
|
|
subject-name: docker.io/rcourtman/pulse-agent
|
|
subject-digest: ${{ steps.build_agent_image.outputs.digest }}
|
|
push-to-registry: true
|
|
create-storage-record: false
|
|
|
|
- name: Attest Pulse agent image on GHCR
|
|
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4
|
|
with:
|
|
subject-name: ghcr.io/${{ github.repository_owner }}/pulse-agent
|
|
subject-digest: ${{ steps.build_agent_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 "Agent images (linux/amd64, linux/arm64):"
|
|
echo " - rcourtman/pulse-agent:${{ steps.version.outputs.tag }}"
|
|
echo " - rcourtman/pulse-agent:${{ steps.version.outputs.version }}"
|
|
if [ "$IS_PRERELEASE" != "true" ]; then
|
|
echo " - rcourtman/pulse-agent:latest"
|
|
fi
|
|
echo ""
|
|
if [ "$IS_PRERELEASE" = "true" ]; then
|
|
echo "Note: :latest tags were NOT updated (this is a prerelease)"
|
|
fi
|