mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
d029e4dc42
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.
681 lines
28 KiB
YAML
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
|