mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
285 lines
13 KiB
YAML
285 lines
13 KiB
YAML
name: install.sh Smoke (Published Release)
|
|
|
|
# End-to-end smoke that exercises the documented Proxmox-LXC / systemd
|
|
# install flow against the published GitHub Release. Across v6 rc.1 → rc.5
|
|
# the published install.sh asset was actually the agent installer (rejecting
|
|
# --version) and the README's pinned signature key did not match the
|
|
# pipeline's actual signing key, so anyone running the secure-install
|
|
# snippet from the README hit one of two silent failure modes for ~30 days.
|
|
# The validate-release.sh content checks catch the asset-identity drift at
|
|
# build time; this workflow proves the documented commands actually install
|
|
# and boot Pulse end-to-end in a clean Linux environment.
|
|
#
|
|
# Triggers:
|
|
# - workflow_dispatch: run manually against any tag for verification.
|
|
# - workflow_call: callable from create-release.yml so future releases get
|
|
# the gate automatically once it's stable.
|
|
#
|
|
# What it does:
|
|
# 1. Downloads install.sh + install.sh.sshsig + the linux-amd64 tarball
|
|
# from the published release URL (not the local release/ dir).
|
|
# 2. Extracts the README's pinned ed25519 key and runs the README's exact
|
|
# ssh-keygen -Y verify command. Catches README/key drift.
|
|
# 3. Boots a privileged systemd Debian 12 container, runs
|
|
# `bash install.sh --archive <tarball> --enable-auto-updates` inside it
|
|
# (avoids the install.sh self-refetch loop that --version triggers; the
|
|
# re-fetched bytes are the same as what we already validated).
|
|
# 4. Waits for systemd pulse.service to be active and hits /api/health
|
|
# from inside the container.
|
|
# 5. Confirms /api/health reports the expected version.
|
|
|
|
on:
|
|
workflow_call:
|
|
inputs:
|
|
tag:
|
|
description: 'Release tag (e.g., v6.0.0-rc.6)'
|
|
required: true
|
|
type: string
|
|
version:
|
|
description: 'Version without v prefix (e.g., 6.0.0-rc.6)'
|
|
required: true
|
|
type: string
|
|
repository:
|
|
description: 'owner/repo to pull the published release from. Defaults to the workflow repository.'
|
|
required: false
|
|
type: string
|
|
default: ''
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag:
|
|
description: 'Release tag (e.g., v6.0.0-rc.6)'
|
|
required: true
|
|
type: string
|
|
version:
|
|
description: 'Version without v prefix (e.g., 6.0.0-rc.6)'
|
|
required: true
|
|
type: string
|
|
repository:
|
|
description: 'owner/repo to pull from (leave blank for current repo)'
|
|
required: false
|
|
type: string
|
|
default: ''
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
group: install-sh-smoke-${{ inputs.tag }}
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
smoke:
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 15
|
|
steps:
|
|
- name: Checkout repository (for README key extraction)
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
|
|
- name: Resolve smoke inputs
|
|
id: inputs
|
|
env:
|
|
INPUT_TAG: ${{ inputs.tag }}
|
|
INPUT_VERSION: ${{ inputs.version }}
|
|
INPUT_REPO: ${{ inputs.repository }}
|
|
DEFAULT_REPO: ${{ github.repository }}
|
|
run: |
|
|
# workflow_call and workflow_dispatch both require tag + version,
|
|
# so these should always be present when this job runs.
|
|
tag="${INPUT_TAG}"
|
|
version="${INPUT_VERSION}"
|
|
if [ -z "$tag" ] || [ -z "$version" ]; then
|
|
echo "::error::install-sh-smoke requires both tag and version inputs"
|
|
exit 1
|
|
fi
|
|
repo="${INPUT_REPO:-$DEFAULT_REPO}"
|
|
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
|
echo "repo=$repo" >> "$GITHUB_OUTPUT"
|
|
echo "Resolved: tag=$tag version=$version repo=$repo"
|
|
|
|
- name: Download published install.sh + sshsig + linux-amd64 tarball
|
|
env:
|
|
TAG: ${{ steps.inputs.outputs.tag }}
|
|
REPO: ${{ steps.inputs.outputs.repo }}
|
|
run: |
|
|
set -euo pipefail
|
|
mkdir -p smoke-workspace
|
|
cd smoke-workspace
|
|
base="https://github.com/${REPO}/releases/download/${TAG}"
|
|
echo "Pulling from ${base}/"
|
|
|
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
|
|
-o install.sh "${base}/install.sh"
|
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
|
|
-o install.sh.sshsig "${base}/install.sh.sshsig"
|
|
|
|
tarball="pulse-${TAG}-linux-amd64.tar.gz"
|
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
|
|
-o "${tarball}" "${base}/${tarball}"
|
|
# The .sshsig sidecar must travel with the tarball: install.sh
|
|
# --archive now requires it for signature verification before
|
|
# extraction, closing the local-tarball-swap window.
|
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
|
|
-o "${tarball}.sshsig" "${base}/${tarball}.sshsig"
|
|
|
|
echo "Downloaded:"
|
|
ls -la
|
|
|
|
- name: Verify install.sh signature with README's pinned key
|
|
env:
|
|
TAG: ${{ steps.inputs.outputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
readme_key=$(grep -oE 'ssh-ed25519 [A-Za-z0-9+/=]+ pulse-installer' README.md | head -1)
|
|
if [ -z "$readme_key" ]; then
|
|
echo "::error::Could not extract pulse-installer key from README.md"
|
|
exit 1
|
|
fi
|
|
echo "README pins: $readme_key"
|
|
|
|
allowed_signers=$(mktemp)
|
|
printf 'pulse-installer %s\n' "$readme_key" > "$allowed_signers"
|
|
|
|
cd smoke-workspace
|
|
if ! ssh-keygen -Y verify \
|
|
-f "$allowed_signers" \
|
|
-I pulse-installer \
|
|
-n pulse-install \
|
|
-s install.sh.sshsig < install.sh; then
|
|
echo "::error::Published install.sh.sshsig does not verify against the README's pinned key."
|
|
echo "::error::Either README.md is pinning the wrong key or the pipeline signed with a different key."
|
|
rm -f "$allowed_signers"
|
|
exit 1
|
|
fi
|
|
rm -f "$allowed_signers"
|
|
echo "✓ install.sh signature verifies against README's pinned key"
|
|
|
|
- name: Assert install.sh is the Pulse server installer
|
|
run: |
|
|
set -euo pipefail
|
|
cd smoke-workspace
|
|
if ! grep -qE '^# Pulse Installer Script' install.sh; then
|
|
echo "::error::install.sh banner is not the Pulse server installer"
|
|
exit 1
|
|
fi
|
|
if grep -q 'Pulse Unified Agent Installer' install.sh; then
|
|
echo "::error::install.sh is the agent installer, not the server installer"
|
|
exit 1
|
|
fi
|
|
if ! grep -qE '^[[:space:]]*--version\)' install.sh; then
|
|
echo "::error::install.sh does not handle --version"
|
|
exit 1
|
|
fi
|
|
echo "✓ install.sh is the server installer with --version support"
|
|
|
|
- name: Run install.sh end-to-end in a privileged systemd container
|
|
env:
|
|
TAG: ${{ steps.inputs.outputs.tag }}
|
|
VERSION: ${{ steps.inputs.outputs.version }}
|
|
run: |
|
|
set -euo pipefail
|
|
tarball="pulse-${TAG}-linux-amd64.tar.gz"
|
|
container_name="pulse-install-smoke-$$"
|
|
|
|
# Cleanup on exit no matter what.
|
|
trap 'docker rm -f "${container_name}" >/dev/null 2>&1 || true' EXIT
|
|
|
|
# jrei/systemd-debian:12 is a community systemd-in-Docker image used
|
|
# for Ansible / Molecule testing — small, no Pulse-specific assumptions.
|
|
# GHA ubuntu-24.04 runners use cgroup v2 unified hierarchy; without
|
|
# --cgroupns=host the container gets its own cgroup namespace and
|
|
# systemd PID 1 exits before it can mount the cgroup tree, causing
|
|
# the container to disappear mid-boot. /run/lock must also be tmpfs
|
|
# for systemd-tmpfiles. We drop --rm so a failed boot leaves logs
|
|
# behind for diagnosis; the trap removes the container on exit.
|
|
docker run -d \
|
|
--name "${container_name}" \
|
|
--privileged \
|
|
--cgroupns=host \
|
|
--tmpfs /tmp --tmpfs /run --tmpfs /run/lock \
|
|
-v /sys/fs/cgroup:/sys/fs/cgroup:rw \
|
|
-v "$(pwd)/smoke-workspace:/smoke" \
|
|
-p 7655:7655 \
|
|
jrei/systemd-debian:12
|
|
|
|
echo "Waiting for systemd to be ready inside the container..."
|
|
for i in $(seq 1 30); do
|
|
if ! docker inspect -f '{{.State.Running}}' "${container_name}" 2>/dev/null | grep -q true; then
|
|
echo "::error::Container ${container_name} is no longer running."
|
|
docker inspect -f 'ExitCode={{.State.ExitCode}} Error={{.State.Error}}' "${container_name}" || true
|
|
docker logs "${container_name}" || true
|
|
exit 1
|
|
fi
|
|
if docker exec "${container_name}" systemctl is-system-running --wait 2>/dev/null | grep -qE '^(running|degraded)$'; then
|
|
break
|
|
fi
|
|
if [ "$i" -eq 30 ]; then
|
|
docker logs "${container_name}" || true
|
|
docker exec "${container_name}" systemctl --no-pager status || true
|
|
docker exec "${container_name}" journalctl --no-pager --lines=120 || true
|
|
echo "::error::systemd did not become ready inside the container"
|
|
exit 1
|
|
fi
|
|
sleep 2
|
|
done
|
|
echo "✓ systemd is up"
|
|
|
|
echo "Installing prerequisites inside container..."
|
|
docker exec "${container_name}" bash -lc 'apt-get update -qq && apt-get install -y -qq curl ca-certificates jq sudo'
|
|
|
|
echo "Running install.sh --archive against the published tarball..."
|
|
# docker exec without -t leaves stdin without a TTY, which install.sh's
|
|
# safe_read helper detects and falls through to defaults on every prompt.
|
|
# PULSE_INSTALL_ALLOW_DOCKER=1 opts the smoke harness past install.sh's
|
|
# Docker-environment refusal — install.sh treats the test container as
|
|
# a normal systemd host, which is the exact contract this gate exists
|
|
# to validate.
|
|
docker exec -e PULSE_INSTALL_ALLOW_DOCKER=1 "${container_name}" \
|
|
bash -lc "cd /smoke && PULSE_INSTALL_ALLOW_DOCKER=1 bash install.sh --archive /smoke/${tarball} --disable-auto-updates"
|
|
|
|
echo "Waiting for pulse.service to become active..."
|
|
for i in $(seq 1 60); do
|
|
state=$(docker exec "${container_name}" systemctl is-active pulse 2>/dev/null || true)
|
|
if [ "$state" = "active" ]; then
|
|
break
|
|
fi
|
|
if [ "$i" -eq 60 ]; then
|
|
docker exec "${container_name}" systemctl status pulse --no-pager || true
|
|
docker exec "${container_name}" journalctl -u pulse --no-pager --lines=80 || true
|
|
echo "::error::pulse.service did not become active within 2 minutes"
|
|
exit 1
|
|
fi
|
|
sleep 2
|
|
done
|
|
echo "✓ pulse.service is active"
|
|
|
|
echo "Hitting /api/health (curl --retry handles the poll loop)..."
|
|
if ! docker exec "${container_name}" curl -fsS --retry 30 --retry-delay 2 --retry-connrefused --retry-all-errors http://127.0.0.1:7655/api/health >/dev/null; then
|
|
docker exec "${container_name}" systemctl status pulse --no-pager || true
|
|
docker exec "${container_name}" journalctl -u pulse --no-pager --lines=80 || true
|
|
echo "::error::/api/health did not respond within 60 seconds of service activation"
|
|
exit 1
|
|
fi
|
|
echo "✓ /api/health responded 200"
|
|
|
|
echo "Confirming installed version matches ${VERSION} via /api/version..."
|
|
# /api/health intentionally does not include version; /api/version is
|
|
# the authoritative endpoint and is one of the canonical post-upgrade
|
|
# checks documented in docs/UPGRADE_v6.md.
|
|
version_payload=$(docker exec "${container_name}" curl -fsS http://127.0.0.1:7655/api/version)
|
|
echo "Version payload: ${version_payload}"
|
|
installed_version=$(echo "${version_payload}" | jq -r '.version // empty')
|
|
if [ -z "${installed_version}" ]; then
|
|
echo "::error::/api/version did not include a version field"
|
|
exit 1
|
|
fi
|
|
# Normalize: VERSION input is "6.0.0-rc.6", installed_version may be "v6.0.0-rc.6".
|
|
installed_version="${installed_version#v}"
|
|
if [ "${installed_version}" != "${VERSION}" ]; then
|
|
echo "::error::Installed version mismatch. Expected ${VERSION}, got ${installed_version}"
|
|
exit 1
|
|
fi
|
|
echo "✓ Installed version matches ${VERSION}"
|
|
|
|
- name: Smoke result
|
|
run: |
|
|
echo "::notice::install.sh smoke passed for tag ${{ steps.inputs.outputs.tag }}"
|