mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
ad1cfd33c3
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
227 lines
9.5 KiB
YAML
227 lines
9.5 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
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 arch in amd64 arm64 386; do
|
|
for suffix in "" .sig .sshsig; do
|
|
alias="/opt/pulse/bin/pulse-agent-windows-${arch}${suffix}"
|
|
target="pulse-agent-windows-${arch}.exe${suffix}"
|
|
test -L "${alias}"
|
|
test "$(readlink "${alias}")" = "${target}"
|
|
test -s "${alias}"
|
|
done
|
|
done
|
|
for sidecar in /opt/pulse/bin/*.sig /opt/pulse/bin/*.sshsig; do
|
|
test ! -x "$sidecar"
|
|
done
|
|
'
|
|
|
|
- name: Set up Helm
|
|
uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1
|
|
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"
|