Files
pulse/.github/workflows/update-demo-server.yml
T
rcourtman d029e4dc42 Fix demo fixture entitlement recovery
Seed the hidden demo fixture entitlement during stable demo updates so release builds can enable governed mock resources after runtime configuration is restored.

Keep the deployment contract and release policy checks aligned with the release-build entitlement gate.
2026-07-04 22:44:43 +01:00

681 lines
28 KiB
YAML

name: Update Demo Server
on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: 'Stable release tag to deploy (e.g., v6.0.0)'
required: true
type: string
target:
description: 'Demo target to deploy'
required: false
default: auto
type: choice
options:
- auto
- stable
permissions:
contents: read
jobs:
resolve:
runs-on: ubuntu-24.04
outputs:
tag: ${{ steps.target.outputs.tag }}
target: ${{ steps.target.outputs.target }}
environment_name: ${{ steps.target.outputs.environment_name }}
skip: ${{ steps.target.outputs.skip || steps.latest.outputs.skip || 'false' }}
steps:
- name: Resolve target tag and demo environment
id: target
env:
EVENT_NAME: ${{ github.event_name }}
INPUT_TAG: ${{ inputs.tag }}
INPUT_TARGET: ${{ inputs.target }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
set -euo pipefail
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
TAG="$INPUT_TAG"
REQUESTED_TARGET="$INPUT_TARGET"
else
TAG="$RELEASE_TAG"
REQUESTED_TARGET="auto"
fi
VERSION="${TAG#v}"
IS_PRERELEASE=false
if [[ "$VERSION" =~ -(rc|alpha|beta)\.[0-9]+$ ]]; then
IS_PRERELEASE=true
fi
TARGET="${REQUESTED_TARGET:-auto}"
if [ -z "$TARGET" ] || [ "$TARGET" = "auto" ]; then
if [ "$IS_PRERELEASE" = "true" ]; then
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "target=stable" >> "$GITHUB_OUTPUT"
echo "environment_name=demo-stable" >> "$GITHUB_OUTPUT"
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "Prerelease demo updates are retired after v6 GA; skipping public demo update for ${TAG}."
exit 0
fi
TARGET="stable"
fi
case "$TARGET" in
stable)
if [ "$IS_PRERELEASE" = "true" ]; then
echo "::error::Stable demo target only accepts stable tags. Refusing prerelease tag ${TAG}."
exit 1
fi
ENVIRONMENT_NAME="demo-stable"
;;
*)
echo "::error::Unsupported demo target: ${TARGET}"
exit 1
;;
esac
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "target=$TARGET" >> "$GITHUB_OUTPUT"
echo "environment_name=$ENVIRONMENT_NAME" >> "$GITHUB_OUTPUT"
echo "Resolved demo deployment: tag=${TAG}, target=${TARGET}, environment=${ENVIRONMENT_NAME}"
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
fetch-tags: true
- name: Validate governed release line for selected tag
run: |
set -euo pipefail
TAG="${{ steps.target.outputs.tag }}"
VERSION="${TAG#v}"
if [ "$(git rev-parse --is-shallow-repository)" = "true" ]; then
git fetch --prune --unshallow origin
fi
REQUIRED_BRANCH="$(python3 scripts/release_control/control_plane.py --branch-for-version "${VERSION}")"
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 demo deployment."
exit 1
fi
echo "[OK] ${TAG} validated for governed demo deployment on ${REQUIRED_BRANCH}"
- name: Skip if not latest published release for target
id: latest
if: github.event_name == 'release' && steps.target.outputs.skip != 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
TAG="${{ steps.target.outputs.tag }}"
LATEST=$(gh api "repos/${{ github.repository }}/releases/latest" --jq '.tag_name')
echo "Target tag: $TAG"
echo "Latest published stable release: $LATEST"
if [ -z "$LATEST" ] || [ "$LATEST" = "null" ]; then
echo "::error::Could not determine the latest published stable release for demo deployment."
exit 1
fi
if [ "$TAG" != "$LATEST" ]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "Release is not the latest published stable tag; skipping demo update."
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
update-demo:
needs: resolve
if: needs.resolve.outputs.skip != 'true'
runs-on: ubuntu-24.04
environment: ${{ needs.resolve.outputs.environment_name }}
env:
DEMO_EXPECTED_HOSTNAME: ${{ vars.DEMO_EXPECTED_HOSTNAME }}
DEMO_LOCAL_BASE_URL: ${{ vars.DEMO_LOCAL_BASE_URL }}
DEMO_PUBLIC_HEALTH_URL: ${{ vars.DEMO_PUBLIC_HEALTH_URL }}
DEMO_SERVICE_NAME: ${{ vars.DEMO_SERVICE_NAME }}
DEMO_AUTH_USER: ${{ vars.DEMO_AUTH_USER }}
DEMO_AUTH_PASS: ${{ vars.DEMO_AUTH_PASS }}
steps:
- name: Check release type
run: |
echo "Tag: ${{ needs.resolve.outputs.tag }}"
echo "Target: ${{ needs.resolve.outputs.target }}"
echo "Environment: ${{ needs.resolve.outputs.environment_name }}"
- name: Validate demo environment configuration
env:
DEMO_SERVER_HOST: ${{ secrets.DEMO_SERVER_HOST }}
DEMO_SERVER_USER: ${{ secrets.DEMO_SERVER_USER }}
DEMO_SERVER_SSH_KEY: ${{ secrets.DEMO_SERVER_SSH_KEY }}
run: |
set -euo pipefail
[ -n "$DEMO_SERVER_HOST" ] || { echo "::error::DEMO_SERVER_HOST is required in the selected demo environment."; exit 1; }
[ -n "$DEMO_SERVER_USER" ] || { echo "::error::DEMO_SERVER_USER is required in the selected demo environment."; exit 1; }
[ -n "$DEMO_SERVER_SSH_KEY" ] || { echo "::error::DEMO_SERVER_SSH_KEY is required in the selected demo environment."; exit 1; }
[ -n "$DEMO_EXPECTED_HOSTNAME" ] || { echo "::error::DEMO_EXPECTED_HOSTNAME is required in the selected demo environment."; exit 1; }
[ -n "$DEMO_LOCAL_BASE_URL" ] || { echo "::error::DEMO_LOCAL_BASE_URL is required in the selected demo environment."; exit 1; }
[ -n "$DEMO_PUBLIC_HEALTH_URL" ] || { echo "::error::DEMO_PUBLIC_HEALTH_URL is required in the selected demo environment."; exit 1; }
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
fetch-tags: true
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
- name: Wait for release assets
run: |
set -euo pipefail
TAG="${{ needs.resolve.outputs.tag }}"
echo "Waiting for release assets to be available..."
MAX_ATTEMPTS=30
ATTEMPT=0
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
echo "Checking for assets (attempt $((ATTEMPT + 1))/$MAX_ATTEMPTS)..."
CHECKSUMS_STATUS=$(curl -sL -o /dev/null -w "%{http_code}" \
"https://github.com/rcourtman/Pulse/releases/download/${TAG}/checksums.txt")
TARBALL_STATUS=$(curl -sL -o /dev/null -w "%{http_code}" \
"https://github.com/rcourtman/Pulse/releases/download/${TAG}/pulse-${TAG}-linux-amd64.tar.gz")
echo "checksums.txt: $CHECKSUMS_STATUS, tarball: $TARBALL_STATUS"
if [ "$CHECKSUMS_STATUS" = "200" ] && [ "$TARBALL_STATUS" = "200" ]; then
echo "Release assets are available."
exit 0
fi
ATTEMPT=$((ATTEMPT + 1))
if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then
echo "Assets not ready yet, waiting 10 seconds..."
sleep 10
fi
done
echo "::error::Timeout waiting for release assets"
exit 1
- name: Materialize tagged installer
env:
PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}
run: |
set -euo pipefail
TAG="${{ needs.resolve.outputs.tag }}"
TRUSTED_SSH_PUBLIC_KEY="$(
go run ./scripts/release_update_key.go public-key-ssh \
--public-key "${PULSE_UPDATE_SIGNING_PUBLIC_KEY}" \
--comment pulse-installer
)"
git show "refs/tags/${TAG}:install.sh" > /tmp/pulse-install.sh
sed -i "s|^PINNED_RELEASE_SSH_PUBLIC_KEY=.*|PINNED_RELEASE_SSH_PUBLIC_KEY=\"${TRUSTED_SSH_PUBLIC_KEY}\"|" /tmp/pulse-install.sh
grep -F "PINNED_RELEASE_SSH_PUBLIC_KEY=\"${TRUSTED_SSH_PUBLIC_KEY}\"" /tmp/pulse-install.sh
chmod +x /tmp/pulse-install.sh
- name: Tailscale
uses: tailscale/github-action@4e4c49acaa9818630ce0bd7a564372c17e33fb4d # v2
with:
authkey: ${{ secrets.TS_AUTHKEY }}
- name: Setup SSH
env:
DEMO_SERVER_HOST: ${{ secrets.DEMO_SERVER_HOST }}
DEMO_SERVER_SSH_KEY: ${{ secrets.DEMO_SERVER_SSH_KEY }}
run: |
set -euo pipefail
mkdir -p ~/.ssh
chmod 700 ~/.ssh
echo "$DEMO_SERVER_SSH_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -H "$DEMO_SERVER_HOST" >> ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts
- name: Verify target host identity
env:
DEMO_SERVER_HOST: ${{ secrets.DEMO_SERVER_HOST }}
DEMO_SERVER_USER: ${{ secrets.DEMO_SERVER_USER }}
run: |
set -euo pipefail
REMOTE_HOSTNAME="$(
ssh -i ~/.ssh/id_ed25519 \
-o IdentitiesOnly=yes \
-o StrictHostKeyChecking=yes \
-o UserKnownHostsFile=~/.ssh/known_hosts \
"$DEMO_SERVER_USER@$DEMO_SERVER_HOST" \
"hostname"
)"
[ "$REMOTE_HOSTNAME" = "$DEMO_EXPECTED_HOSTNAME" ] || {
echo "::error::Demo environment points at host $REMOTE_HOSTNAME but expected $DEMO_EXPECTED_HOSTNAME."
exit 1
}
- name: Resolve demo service identity
id: config
run: |
set -euo pipefail
TARGET="${{ needs.resolve.outputs.target }}"
SERVICE_NAME="${DEMO_SERVICE_NAME:-}"
if [ -z "$SERVICE_NAME" ]; then
SERVICE_NAME="pulse"
fi
if [ "$TARGET" != "stable" ]; then
echo "::error::Unsupported demo target: ${TARGET}"
exit 1
fi
echo "service_name=$SERVICE_NAME" >> "$GITHUB_OUTPUT"
- name: Check current demo version
id: current
env:
DEMO_SERVER_HOST: ${{ secrets.DEMO_SERVER_HOST }}
DEMO_SERVER_USER: ${{ secrets.DEMO_SERVER_USER }}
run: |
set -euo pipefail
TARGET="${{ needs.resolve.outputs.tag }}"
TARGET_STRIPPED="${TARGET#v}"
CURRENT=$(ssh -i ~/.ssh/id_ed25519 "$DEMO_SERVER_USER@$DEMO_SERVER_HOST" \
"curl -fsS ${DEMO_LOCAL_BASE_URL}/api/version | jq -r .version")
echo "Current demo version: ${CURRENT}"
if [ "$CURRENT" = "$TARGET" ] || [ "$CURRENT" = "$TARGET_STRIPPED" ] || [ "v${CURRENT}" = "$TARGET" ]; then
echo "skip_current=true" >> "$GITHUB_OUTPUT"
echo "Demo target already on the requested version; skipping update."
else
echo "skip_current=false" >> "$GITHUB_OUTPUT"
fi
- name: Prepare demo host storage
if: steps.current.outputs.skip_current != 'true'
env:
DEMO_SERVER_HOST: ${{ secrets.DEMO_SERVER_HOST }}
DEMO_SERVER_USER: ${{ secrets.DEMO_SERVER_USER }}
run: |
set -euo pipefail
SERVICE_NAME="${{ steps.config.outputs.service_name }}"
REMOTE_SCRIPT=$(cat <<'EOF'
set -euo pipefail
SERVICE_NAME="$1"
CONFIG_DIR="/etc/${SERVICE_NAME:-pulse}"
BACKUP_PARENT="$(dirname "$CONFIG_DIR")"
BACKUP_BASENAME="$(basename "$CONFIG_DIR")"
KEEP_BACKUPS=2
BACKUP_MARGIN_KB=$((64 * 1024))
echo "Demo host filesystem before cleanup:"
df -h "$BACKUP_PARENT" /tmp || true
mapfile -t old_backups < <(
sudo find "$BACKUP_PARENT" -maxdepth 1 -type d -name "${BACKUP_BASENAME}.backup.*" -printf '%T@ %p\n' 2>/dev/null \
| sort -nr \
| awk -v keep="$KEEP_BACKUPS" 'NR > keep {sub(/^[^ ]+ /, ""); print}'
)
if ((${#old_backups[@]} > 0)); then
printf 'Removing %d old demo backup directorie(s).\n' "${#old_backups[@]}"
printf '%s\0' "${old_backups[@]}" | sudo xargs -0 rm -rf --
fi
if [[ -d "$CONFIG_DIR" ]]; then
config_kb="$(sudo du -sk "$CONFIG_DIR" | awk 'NR == 1 {print $1}')"
free_kb="$(df -Pk "$BACKUP_PARENT" | awk 'NR == 2 {print $4}')"
required_kb=$((config_kb + BACKUP_MARGIN_KB))
if ((free_kb < required_kb)); then
mapfile -t remaining_backups < <(
sudo find "$BACKUP_PARENT" -maxdepth 1 -type d -name "${BACKUP_BASENAME}.backup.*" -printf '%T@ %p\n' 2>/dev/null \
| sort -n \
| awk '{sub(/^[^ ]+ /, ""); print}'
)
for backup_dir in "${remaining_backups[@]}"; do
printf 'Removing demo backup to restore install headroom: %s\n' "$backup_dir"
sudo rm -rf -- "$backup_dir"
free_kb="$(df -Pk "$BACKUP_PARENT" | awk 'NR == 2 {print $4}')"
if ((free_kb >= required_kb)); then
break
fi
done
fi
pruned_volatile=false
if ((free_kb < required_kb)); then
echo "Pruning demo volatile runtime stores to restore install headroom."
if command -v systemctl >/dev/null 2>&1; then
sudo systemctl stop "$SERVICE_NAME" || true
fi
mapfile -d '' volatile_paths < <(
sudo find "$CONFIG_DIR" -xdev -type f \( \
-name "metrics.db" -o \
-name "metrics.db-shm" -o \
-name "metrics.db-wal" -o \
-name "notification_queue.db" -o \
-name "notification_queue.db-shm" -o \
-name "notification_queue.db-wal" -o \
-name "deploy.db" -o \
-name "deploy.db-shm" -o \
-name "deploy.db-wal" \
\) -print0 2>/dev/null || true
)
for volatile_path in "${volatile_paths[@]}"; do
printf 'Removing demo volatile store: %s\n' "$(basename "$volatile_path")"
sudo rm -f -- "$volatile_path"
pruned_volatile=true
done
config_kb="$(sudo du -sk "$CONFIG_DIR" | awk 'NR == 1 {print $1}')"
free_kb="$(df -Pk "$BACKUP_PARENT" | awk 'NR == 2 {print $4}')"
required_kb=$((config_kb + BACKUP_MARGIN_KB))
fi
if ((free_kb < required_kb)); then
echo "::error::Demo host does not have enough free space to back up $CONFIG_DIR before install."
echo "Config: ${config_kb}KB, available: ${free_kb}KB, required: ${required_kb}KB"
sudo find "$BACKUP_PARENT" -maxdepth 1 -type d -name "${BACKUP_BASENAME}.backup.*" -printf '%TY-%Tm-%Td %TH:%TM %p\n' 2>/dev/null | sort || true
if [[ "$pruned_volatile" == "true" ]] && command -v systemctl >/dev/null 2>&1; then
sudo systemctl start "$SERVICE_NAME" || true
fi
exit 1
fi
fi
echo "Demo host filesystem after cleanup:"
df -h "$BACKUP_PARENT" /tmp || true
EOF
)
ssh -i ~/.ssh/id_ed25519 "$DEMO_SERVER_USER@$DEMO_SERVER_HOST" "bash -s -- $(printf '%q ' "$SERVICE_NAME")" <<<"$REMOTE_SCRIPT"
- name: Upload tagged installer
if: steps.current.outputs.skip_current != 'true'
env:
DEMO_SERVER_HOST: ${{ secrets.DEMO_SERVER_HOST }}
DEMO_SERVER_USER: ${{ secrets.DEMO_SERVER_USER }}
run: |
set -euo pipefail
scp -i ~/.ssh/id_ed25519 /tmp/pulse-install.sh "$DEMO_SERVER_USER@$DEMO_SERVER_HOST:/tmp/pulse-install.sh"
- name: Update demo server
if: steps.current.outputs.skip_current != 'true'
env:
DEMO_SERVER_HOST: ${{ secrets.DEMO_SERVER_HOST }}
DEMO_SERVER_USER: ${{ secrets.DEMO_SERVER_USER }}
run: |
set -euo pipefail
TAG="${{ needs.resolve.outputs.tag }}"
SERVICE_NAME="${{ steps.config.outputs.service_name }}"
REMOTE_SCRIPT=$(cat <<'EOF'
set -euo pipefail
TAG="$1"
SERVICE_NAME="$2"
INSTALLER_ENV=()
if [ -n "$SERVICE_NAME" ]; then
INSTALLER_ENV+=("PULSE_SERVICE_NAME=$SERVICE_NAME")
fi
sudo env "${INSTALLER_ENV[@]}" bash /tmp/pulse-install.sh --version "$TAG"
rm -f /tmp/pulse-install.sh
EOF
)
ssh -i ~/.ssh/id_ed25519 "$DEMO_SERVER_USER@$DEMO_SERVER_HOST" "bash -s -- $(printf '%q ' "$TAG" "$SERVICE_NAME")" <<<"$REMOTE_SCRIPT"
- name: Restore demo runtime configuration
env:
DEMO_SERVER_HOST: ${{ secrets.DEMO_SERVER_HOST }}
DEMO_SERVER_USER: ${{ secrets.DEMO_SERVER_USER }}
run: |
set -euo pipefail
SERVICE_NAME="${{ steps.config.outputs.service_name }}"
REMOTE_SCRIPT=$(cat <<'EOF'
set -euo pipefail
SERVICE_NAME="${1:-pulse}"
DEMO_LOCAL_BASE_URL="$2"
CONFIG_DIR="/etc/${SERVICE_NAME:-pulse}"
SERVICE_USER="$(systemctl show "$SERVICE_NAME" --property=User --value 2>/dev/null || true)"
SERVICE_GROUP="$(systemctl show "$SERVICE_NAME" --property=Group --value 2>/dev/null || true)"
SERVICE_USER="${SERVICE_USER:-pulse}"
SERVICE_GROUP="${SERVICE_GROUP:-$SERVICE_USER}"
resolve_config_dir() {
local fallback="$CONFIG_DIR"
local unit_env=""
local fragment=""
unit_env="$(systemctl show "$SERVICE_NAME" --property=Environment --value 2>/dev/null || true)"
for entry in $unit_env; do
case "$entry" in
PULSE_DATA_DIR=*)
printf '%s\n' "${entry#PULSE_DATA_DIR=}"
return 0
;;
esac
done
fragment="$(systemctl show "$SERVICE_NAME" --property=FragmentPath --value 2>/dev/null || true)"
if [ -n "$fragment" ] && sudo test -r "$fragment"; then
local unit_data_dir
unit_data_dir="$(sudo sed -n -E 's/^Environment="?PULSE_DATA_DIR=([^"]*)"?$/\1/p' "$fragment" | tail -1)"
if [ -n "$unit_data_dir" ]; then
printf '%s\n' "$unit_data_dir"
return 0
fi
fi
printf '%s\n' "$fallback"
}
CONFIG_DIR="$(resolve_config_dir)"
ENV_FILE="$CONFIG_DIR/.env"
set_env_value() {
local key="$1"
local value="$2"
sudo mkdir -p "$CONFIG_DIR"
if sudo test -f "$ENV_FILE"; then
if sudo grep -Eq "^[[:space:]]*${key}=" "$ENV_FILE"; then
sudo sed -i "s|^[[:space:]]*${key}=.*|${key}=${value}|" "$ENV_FILE"
else
printf '\n%s=%s\n' "$key" "$value" | sudo tee -a "$ENV_FILE" >/dev/null
fi
else
printf '# Pulse demo runtime configuration\n%s=%s\n' "$key" "$value" | sudo tee "$ENV_FILE" >/dev/null
fi
}
ensure_demo_fixture_entitlement() {
local billing_file="$CONFIG_DIR/billing.json"
local tmp_file
tmp_file="$(mktemp)"
sudo mkdir -p "$CONFIG_DIR"
if sudo test -s "$billing_file" && sudo jq '
.capabilities = (((.capabilities // []) + ["demo_fixtures"]) | unique)
| .limits = (.limits // {})
| .meters_enabled = (.meters_enabled // [])
| .plan_version = (if (.plan_version // "") == "" then "community" else .plan_version end)
| .subscription_state = (if (.subscription_state // "") == "" then "active" else .subscription_state end)
| del(.integrity)
' "$billing_file" > "$tmp_file"; then
:
else
cat > "$tmp_file" <<'JSON'
{"capabilities":["demo_fixtures"],"limits":{},"meters_enabled":[],"plan_version":"community","subscription_state":"active"}
JSON
fi
sudo tee "$billing_file" < "$tmp_file" >/dev/null
rm -f "$tmp_file"
sudo chown "$SERVICE_USER:$SERVICE_GROUP" "$billing_file" || sudo chown "$SERVICE_USER" "$billing_file" || true
sudo chmod 600 "$billing_file"
echo "Demo fixture entitlement ensured in governed demo billing state."
}
set_env_value DEMO_MODE true
set_env_value PULSE_MOCK_MODE true
set_env_value PULSE_MOCK_NODES 5
set_env_value PULSE_MOCK_VMS_PER_NODE 6
set_env_value PULSE_MOCK_LXCS_PER_NODE 8
set_env_value PULSE_MOCK_DOCKER_HOSTS 5
set_env_value PULSE_MOCK_DOCKER_CONTAINERS 14
set_env_value PULSE_MOCK_GENERIC_HOSTS 4
set_env_value PULSE_MOCK_K8S_CLUSTERS 3
set_env_value PULSE_MOCK_K8S_NODES 5
set_env_value PULSE_MOCK_K8S_PODS 40
set_env_value PULSE_MOCK_K8S_DEPLOYMENTS 14
set_env_value PULSE_MOCK_RANDOM_METRICS true
set_env_value PULSE_MOCK_STOPPED_PERCENT 6
ensure_demo_fixture_entitlement
sudo chown "$SERVICE_USER:$SERVICE_GROUP" "$ENV_FILE" || sudo chown "$SERVICE_USER" "$ENV_FILE" || true
sudo chmod 600 "$ENV_FILE"
sudo systemctl daemon-reload
sudo systemctl restart "$SERVICE_NAME"
for attempt in $(seq 1 30); do
if curl -fsS "${DEMO_LOCAL_BASE_URL}/api/health" >/dev/null; then
echo "Demo service restarted with governed demo runtime configuration."
exit 0
fi
sleep 2
done
echo "::error::Demo service did not become healthy after restoring demo runtime configuration."
exit 1
EOF
)
ssh -i ~/.ssh/id_ed25519 "$DEMO_SERVER_USER@$DEMO_SERVER_HOST" "bash -s -- $(printf '%q ' "$SERVICE_NAME" "$DEMO_LOCAL_BASE_URL")" <<<"$REMOTE_SCRIPT"
- name: Verify demo runtime
env:
DEMO_SERVER_HOST: ${{ secrets.DEMO_SERVER_HOST }}
DEMO_SERVER_USER: ${{ secrets.DEMO_SERVER_USER }}
run: |
set -euo pipefail
sleep 5
VERSION=$(ssh -i ~/.ssh/id_ed25519 "$DEMO_SERVER_USER@$DEMO_SERVER_HOST" \
"curl -fsS ${DEMO_LOCAL_BASE_URL}/api/version | jq -r .version")
echo "Demo server is now running version: $VERSION"
TAG="${{ needs.resolve.outputs.tag }}"
TAG_STRIPPED="${TAG#v}"
if [ "$VERSION" != "$TAG" ] && [ "$VERSION" != "$TAG_STRIPPED" ]; then
echo "::error::Version mismatch! Expected $TAG but got $VERSION"
exit 1
fi
AUTH_USER="${DEMO_AUTH_USER:-demo}"
AUTH_PASS="${DEMO_AUTH_PASS:-demo}"
ssh -i ~/.ssh/id_ed25519 "$DEMO_SERVER_USER@$DEMO_SERVER_HOST" \
'AUTH_USER='"$(printf '%q' "$AUTH_USER")"' AUTH_PASS='"$(printf '%q' "$AUTH_PASS")"' DEMO_LOCAL_BASE_URL='"$(printf '%q' "$DEMO_LOCAL_BASE_URL")"' bash -s' <<'EOF'
set -euo pipefail
COOKIE_FILE="$(mktemp)"
trap 'rm -f "$COOKIE_FILE"' EXIT
curl -fsS -c "$COOKIE_FILE" "${DEMO_LOCAL_BASE_URL}/api/login" -X POST \
-H "Content-Type: application/json" \
-d "{\"username\":\"${AUTH_USER}\",\"password\":\"${AUTH_PASS}\"}" > /dev/null
curl -fsS -b "$COOKIE_FILE" "${DEMO_LOCAL_BASE_URL}/api/license/runtime-capabilities" >/dev/null
MOCK_ENABLED="false"
RESOURCES="0"
for attempt in $(seq 1 30); do
MOCK_ENABLED=$(curl -fsS -b "$COOKIE_FILE" "${DEMO_LOCAL_BASE_URL}/api/system/mock-mode" | jq -r '.enabled // false')
RESOURCES=$(curl -fsS -b "$COOKIE_FILE" "${DEMO_LOCAL_BASE_URL}/api/state" | jq -r '.resources | if type == "array" then length else 0 end')
if [ "$MOCK_ENABLED" = "true" ] && [ "$RESOURCES" -ge 1 ]; then
break
fi
sleep 2
done
echo "Mock mode enabled: $MOCK_ENABLED"
echo "Mock resources detected: $RESOURCES"
if [ "$MOCK_ENABLED" != "true" ]; then
echo "::error::Demo server mock mode did not enable after entitlement sync"
exit 1
fi
if [ "$RESOURCES" -lt 1 ]; then
echo "::error::Demo server updated but canonical mock resources are missing"
exit 1
fi
EOF
echo "Demo server successfully updated and verified."
- name: Verify frontend parity
env:
DEMO_SERVER_HOST: ${{ secrets.DEMO_SERVER_HOST }}
DEMO_SERVER_USER: ${{ secrets.DEMO_SERVER_USER }}
run: |
set -euo pipefail
case "$DEMO_PUBLIC_HEALTH_URL" in
*/api/health)
PUBLIC_ROOT_URL="${DEMO_PUBLIC_HEALTH_URL%/api/health}/"
;;
*)
echo "::error::DEMO_PUBLIC_HEALTH_URL must end with /api/health for frontend verification."
exit 1
;;
esac
extract_entry_asset() {
python3 -c 'import re, sys; html = sys.stdin.read(); match = re.search(r"<script\b[^>]*\bsrc=\"(/assets/index-[^\"]*\.js)\"", html); sys.exit(1) if match is None else sys.stdout.write(match.group(1))'
}
if ! REMOTE_ASSET="$(
ssh -i ~/.ssh/id_ed25519 "$DEMO_SERVER_USER@$DEMO_SERVER_HOST" \
"curl -fsS ${DEMO_LOCAL_BASE_URL}/" | extract_entry_asset
)"; then
echo "::error::Failed to resolve the remote frontend entry asset."
exit 1
fi
if ! PUBLIC_ASSET="$(curl -fsS "$PUBLIC_ROOT_URL" | extract_entry_asset)"; then
echo "::error::Failed to resolve the public frontend entry asset."
exit 1
fi
[ "$PUBLIC_ASSET" = "$REMOTE_ASSET" ] || {
echo "::error::Public demo is serving $PUBLIC_ASSET but the target service is serving $REMOTE_ASSET."
exit 1
}
- name: Verify public health
run: |
set -euo pipefail
curl -fsS "$DEMO_PUBLIC_HEALTH_URL"
- name: Verify public browser smoke
run: |
set -euo pipefail
case "$DEMO_PUBLIC_HEALTH_URL" in
*/api/health)
export PULSE_PUBLIC_SITE_URL="${DEMO_PUBLIC_HEALTH_URL%/api/health}/"
;;
*)
echo "::error::DEMO_PUBLIC_HEALTH_URL must end with /api/health for browser verification."
exit 1
;;
esac
./scripts/run_demo_public_browser_smoke.sh
- name: Cleanup SSH material
if: always()
run: rm -f ~/.ssh/id_ed25519 ~/.ssh/known_hosts /tmp/pulse-install.sh