mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
676 lines
27 KiB
YAML
676 lines
27 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:
|
|
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
|
|
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
|
|
tags: tag:infra
|
|
|
|
- name: Setup SSH
|
|
env:
|
|
DEMO_SERVER_HOST: ${{ secrets.DEMO_SERVER_HOST }}
|
|
DEMO_SERVER_SSH_KEY: ${{ secrets.DEMO_SERVER_SSH_KEY }}
|
|
run: bash .github/scripts/setup-demo-ssh.sh
|
|
|
|
- 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
|