Files
pulse/.github/workflows/qualify-release-containers.yml
T
2026-08-31 13:21:25 +01:00

218 lines
9.1 KiB
YAML

name: Qualify Exact-Candidate Containers
on:
workflow_call:
inputs:
version:
description: 'Version number without the leading v'
required: true
type: string
container_artifact:
description: 'Exact-candidate container payload artifact from this run'
required: true
type: string
permissions:
contents: read
jobs:
qualify:
name: Exact-Candidate Container and Helm Smoke
# Stable qualification must be independent of persistent runner disk state.
# Prereleases keep the faster PVE lane, where a later clean rehearsal can
# expose and repair runner capacity without blocking a stable patch.
runs-on: ${{ !contains(inputs.version, '-') && 'ubuntu-24.04' || fromJSON('["self-hosted","Linux","X64","pulse-pve-build"]') }}
timeout-minutes: 15
env:
VERSION: ${{ inputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
# A reusable workflow inherits github.sha from its caller. Do not
# accept a caller-controlled checkout ref: the caller event commit is
# the immutable source identity for both code and candidate payload.
ref: ${{ github.sha }}
persist-credentials: false
- name: Verify exact caller source
env:
EXPECTED_SOURCE_SHA: ${{ github.sha }}
run: test "$(git rev-parse HEAD)" = "${EXPECTED_SOURCE_SHA}"
- 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
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 "${{ github.sha }}"
- name: Assemble exact-candidate runtime and agent images
run: |
set -euo pipefail
docker buildx build \
--target runtime_prebuilt \
--platform linux/amd64 \
--build-context "release_payload=${RUNNER_TEMP}/release-container-payload/payload/release" \
--load \
--tag "pulse-helm-smoke:${VERSION}" \
.
docker buildx build \
--target agent_runtime_prebuilt \
--platform linux/amd64 \
--build-context "release_payload=${RUNNER_TEMP}/release-container-payload/payload/release" \
--load \
--tag "pulse-agent-candidate:${VERSION}" \
.
docker buildx build \
--file deploy/provider-msp/Dockerfile.control-plane \
--target control_plane_prebuilt \
--platform linux/amd64 \
--build-context "compiled_payload=${RUNNER_TEMP}/release-container-payload/payload/compiled" \
--load \
--tag "pulse-control-plane-candidate:${VERSION}" \
.
- name: Verify container binaries match immutable candidate
run: |
set -euo pipefail
expected_server="$(sha256sum "$RUNNER_TEMP/release-container-payload/payload/release/amd64/bin/pulse" | awk '{print $1}')"
actual_server="$(docker run --rm --entrypoint /bin/sh "pulse-helm-smoke:${VERSION}" -c 'sha256sum /app/pulse' | awk '{print $1}')"
expected_agent="$(sha256sum "$RUNNER_TEMP/release-container-payload/payload/release/amd64/bin/pulse-agent-linux-amd64" | awk '{print $1}')"
actual_embedded_agent="$(docker run --rm --entrypoint /bin/sh "pulse-helm-smoke:${VERSION}" -c 'sha256sum /usr/local/bin/pulse-agent' | awk '{print $1}')"
actual_agent="$(docker run --rm --entrypoint /bin/sh "pulse-agent-candidate:${VERSION}" -c 'sha256sum /usr/local/bin/pulse-agent' | awk '{print $1}')"
expected_control_plane="$(sha256sum "$RUNNER_TEMP/release-container-payload/payload/compiled/binaries/pulse-control-plane-linux-amd64" | awk '{print $1}')"
actual_control_plane="$(docker run --rm --entrypoint /bin/sh "pulse-control-plane-candidate:${VERSION}" -c 'sha256sum /usr/local/bin/pulse-control-plane' | awk '{print $1}')"
test "${actual_server}" = "${expected_server}"
test "${actual_embedded_agent}" = "${expected_agent}"
test "${actual_agent}" = "${expected_agent}"
test "${actual_control_plane}" = "${expected_control_plane}"
docker run --rm --entrypoint /bin/sh "pulse-helm-smoke:${VERSION}" -c '
set -euo pipefail
test "$(readlink /usr/local/bin/pulse-agent)" = "/opt/pulse/bin/pulse-agent-linux-amd64"
test -x /usr/local/bin/pulse-agent
for sidecar in /opt/pulse/bin/*.sig /opt/pulse/bin/*.sshsig; do
test ! -x "$sidecar"
done
'
- name: Set up Helm
uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0
with:
version: v3.15.2
- name: Helm smoke test with local release-line image
env:
SMOKE_IMAGE_REPOSITORY: pulse-helm-smoke
SMOKE_IMAGE_TAG: ${{ inputs.version }}
run: |
set -euo pipefail
cleanup() {
kind delete cluster --name pulse-test >/dev/null 2>&1 || true
}
diagnose() {
echo "::group::helm status"
helm status pulse || true
echo "::endgroup::"
echo "::group::kubectl get all"
kubectl get all -A || true
echo "::endgroup::"
echo "::group::kubectl describe pods"
kubectl describe pods -A || true
echo "::endgroup::"
echo "::group::pod logs"
pods=$(kubectl get pods -A -o name 2>/dev/null || true)
for pod in $pods; do
echo "### ${pod}"
kubectl logs --all-containers=true --tail=200 "$pod" || true
done
echo "::endgroup::"
echo "::group::events"
kubectl get events -A --sort-by=.lastTimestamp || kubectl get events -A || true
echo "::endgroup::"
cleanup
}
trap 'diagnose' ERR
prepare_cluster_tool() {
local name="$1"
local url="$2"
local expected_sha="$3"
local destination="$RUNNER_TEMP/$name"
local download="$destination.download"
local installed_path=""
installed_path="$(command -v "$name" 2>/dev/null || true)"
if [ -n "$installed_path" ] && printf '%s %s\n' "$expected_sha" "$installed_path" | sha256sum --check --status; then
install -m 0755 "$installed_path" "$destination"
else
rm -f "$download"
curl -fsSL -o "$download" "$url"
printf '%s %s\n' "$expected_sha" "$download" | sha256sum --check
chmod 0755 "$download"
mv "$download" "$destination"
fi
printf '%s %s\n' "$expected_sha" "$destination" | sha256sum --check
test -x "$destination"
}
prepare_cluster_tool \
kind \
https://kind.sigs.k8s.io/dl/v0.20.0/kind-linux-amd64 \
513a7213d6d3332dd9ef27c24dab35e5ef10a04fa27274fe1c14d8a246493ded
prepare_cluster_tool \
kubectl \
https://dl.k8s.io/release/v1.27.3/bin/linux/amd64/kubectl \
fba6c062e754a120bc8105cde1344de200452fe014a8759e06e4eec7ed258a09
export PATH="$RUNNER_TEMP:$PATH"
test "$(command -v kind)" = "$RUNNER_TEMP/kind"
test "$(command -v kubectl)" = "$RUNNER_TEMP/kubectl"
kind version
kubectl version --client=true
cleanup
kind create cluster --name pulse-test --wait 5m
kind load docker-image "${SMOKE_IMAGE_REPOSITORY}:${SMOKE_IMAGE_TAG}" --name pulse-test
helm install pulse deploy/helm/pulse \
--set persistence.enabled=false \
--set server.secretEnv.create=true \
--set server.secretEnv.data.API_TOKENS=test-token \
--set image.repository="${SMOKE_IMAGE_REPOSITORY}" \
--set image.tag="${SMOKE_IMAGE_TAG}" \
--set image.pullPolicy=Never \
--wait --timeout 5m --debug
kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=pulse --timeout=180s || (kubectl describe pods -l app.kubernetes.io/name=pulse && exit 1)
kubectl get pods -l app.kubernetes.io/name=pulse
helm upgrade pulse deploy/helm/pulse \
--set persistence.enabled=false \
--set server.secretEnv.create=true \
--set server.secretEnv.data.API_TOKENS=test-token \
--set image.repository="${SMOKE_IMAGE_REPOSITORY}" \
--set image.tag="${SMOKE_IMAGE_TAG}" \
--set image.pullPolicy=Never \
--wait --timeout 5m --debug
trap - ERR
cleanup
echo "✓ Helm smoke test passed"