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
876 lines
39 KiB
YAML
876 lines
39 KiB
YAML
name: Update Demo Server
|
|
|
|
on:
|
|
workflow_call:
|
|
inputs:
|
|
tag:
|
|
description: 'Stable release tag to deploy, or latest for verification-only checks'
|
|
required: true
|
|
type: string
|
|
target:
|
|
description: 'Demo target to deploy'
|
|
required: false
|
|
default: stable
|
|
type: string
|
|
verify_only:
|
|
description: 'Verify connectivity and the current stable demo without changing the host'
|
|
required: false
|
|
default: false
|
|
type: boolean
|
|
activation_convergence_run_id:
|
|
description: 'Exact Release Convergence owner. Required for every mutating call.'
|
|
required: false
|
|
default: ''
|
|
type: string
|
|
customer_promotion_lease_sha:
|
|
description: 'Exact active customer-promotion lease. Required for every mutating call.'
|
|
required: false
|
|
default: ''
|
|
type: string
|
|
convergence_owner_asset_name:
|
|
description: 'Owner record stored in the exact convergence lease commit.'
|
|
required: false
|
|
default: ''
|
|
type: string
|
|
convergence_owner_asset_sha256:
|
|
description: 'SHA-256 digest of the commit-bound convergence owner record.'
|
|
required: false
|
|
default: ''
|
|
type: string
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
# GitHub environments gate secret access and deployment admission, but do not
|
|
# serialize jobs. Share one resource lock with the recovery workflow so two
|
|
# release convergences, a verification run, and emergency recovery can never
|
|
# operate on the single stable demo host concurrently.
|
|
concurrency:
|
|
group: stable-demo-runtime
|
|
queue: max
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
resolve:
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 10
|
|
outputs:
|
|
tag: ${{ steps.target.outputs.tag }}
|
|
target: ${{ steps.target.outputs.target }}
|
|
environment_name: ${{ steps.target.outputs.environment_name }}
|
|
skip: ${{ steps.target.outputs.skip || 'false' }}
|
|
mock_nodes: ${{ steps.runtime_profile.outputs.mock_nodes }}
|
|
mock_vms_per_node: ${{ steps.runtime_profile.outputs.mock_vms_per_node }}
|
|
mock_lxcs_per_node: ${{ steps.runtime_profile.outputs.mock_lxcs_per_node }}
|
|
mock_docker_hosts: ${{ steps.runtime_profile.outputs.mock_docker_hosts }}
|
|
mock_docker_containers: ${{ steps.runtime_profile.outputs.mock_docker_containers }}
|
|
mock_generic_hosts: ${{ steps.runtime_profile.outputs.mock_generic_hosts }}
|
|
mock_k8s_clusters: ${{ steps.runtime_profile.outputs.mock_k8s_clusters }}
|
|
mock_k8s_nodes: ${{ steps.runtime_profile.outputs.mock_k8s_nodes }}
|
|
mock_k8s_pods: ${{ steps.runtime_profile.outputs.mock_k8s_pods }}
|
|
mock_k8s_deployments: ${{ steps.runtime_profile.outputs.mock_k8s_deployments }}
|
|
mock_seed_duration: ${{ steps.runtime_profile.outputs.mock_seed_duration }}
|
|
mock_sample_interval: ${{ steps.runtime_profile.outputs.mock_sample_interval }}
|
|
mock_update_interval: ${{ steps.runtime_profile.outputs.mock_update_interval }}
|
|
|
|
steps:
|
|
- name: Resolve target tag and demo environment
|
|
id: target
|
|
env:
|
|
INPUT_TAG: ${{ inputs.tag }}
|
|
INPUT_TARGET: ${{ inputs.target }}
|
|
VERIFY_ONLY: ${{ inputs.verify_only }}
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
TAG="$INPUT_TAG"
|
|
REQUESTED_TARGET="$INPUT_TARGET"
|
|
if [ -z "$TAG" ]; then
|
|
echo "::error::A stable release tag is required."
|
|
exit 1
|
|
fi
|
|
if [ "$TAG" = "latest" ]; then
|
|
if [ "${VERIFY_ONLY:-false}" != "true" ]; then
|
|
echo "::error::The latest alias is allowed only for verification-only checks."
|
|
exit 1
|
|
fi
|
|
TAG="$(gh api "repos/${{ github.repository }}/releases/latest" --jq '.tag_name')"
|
|
echo "Resolved verification-only target to latest stable release ${TAG}."
|
|
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
|
|
python3 scripts/write_github_output.py tag "$TAG"
|
|
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
|
|
|
|
python3 scripts/write_github_output.py tag "$TAG"
|
|
python3 scripts/write_github_output.py target "$TARGET"
|
|
echo "environment_name=$ENVIRONMENT_NAME" >> "$GITHUB_OUTPUT"
|
|
echo "Resolved demo deployment: tag=${TAG}, target=${TARGET}, environment=${ENVIRONMENT_NAME}"
|
|
|
|
- name: Checkout repository
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
fetch-depth: 0
|
|
fetch-tags: true
|
|
|
|
- name: Validate governed release line for selected tag
|
|
env:
|
|
WORKFLOW_OUTPUT_1: ${{ steps.target.outputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
TAG="${WORKFLOW_OUTPUT_1}"
|
|
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: Resolve target-compatible demo runtime profile
|
|
id: runtime_profile
|
|
env:
|
|
WORKFLOW_OUTPUT_1: ${{ steps.target.outputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
TAG="${WORKFLOW_OUTPUT_1}"
|
|
TAG_REF="refs/tags/${TAG}"
|
|
PROFILE_OUTPUT="$(bash .github/scripts/resolve-demo-runtime-profile.sh "${TAG_REF}")"
|
|
while IFS='=' read -r output_name output_value; do
|
|
python3 scripts/write_github_output.py "$output_name" "$output_value"
|
|
done <<< "$PROFILE_OUTPUT"
|
|
printf 'Resolved target-compatible demo runtime profile for %s:\n%s\n' "$TAG" "$PROFILE_OUTPUT"
|
|
|
|
update-demo:
|
|
needs: resolve
|
|
if: needs.resolve.outputs.skip != 'true'
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 60
|
|
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
|
|
env:
|
|
VERIFY_ONLY: ${{ inputs.verify_only }}
|
|
WORKFLOW_OUTPUT_1: ${{ needs.resolve.outputs.tag }}
|
|
WORKFLOW_OUTPUT_2: ${{ needs.resolve.outputs.target }}
|
|
WORKFLOW_OUTPUT_3: ${{ needs.resolve.outputs.environment_name }}
|
|
run: |
|
|
echo "Tag: ${WORKFLOW_OUTPUT_1}"
|
|
echo "Target: ${WORKFLOW_OUTPUT_2}"
|
|
echo "Environment: ${WORKFLOW_OUTPUT_3}"
|
|
echo "Verification only: ${VERIFY_ONLY}"
|
|
|
|
- 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: Require exact committed activation marker for mutation
|
|
if: inputs.verify_only != true
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
TAG: ${{ needs.resolve.outputs.tag }}
|
|
CONVERGENCE_RUN_ID: ${{ inputs.activation_convergence_run_id }}
|
|
LEASE_SHA: ${{ inputs.customer_promotion_lease_sha }}
|
|
OWNER_ASSET_NAME: ${{ inputs.convergence_owner_asset_name }}
|
|
OWNER_ASSET_SHA256: ${{ inputs.convergence_owner_asset_sha256 }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [[ ! "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || \
|
|
[[ ! "${CONVERGENCE_RUN_ID}" =~ ^[0-9]+$ ]] || \
|
|
[[ ! "${LEASE_SHA}" =~ ^[0-9a-f]{40}$ ]] || \
|
|
[[ ! "${OWNER_ASSET_NAME}" =~ ^release-convergence-owner-${CONVERGENCE_RUN_ID}-[1-9][0-9]*\.json$ ]] || \
|
|
[[ ! "${OWNER_ASSET_SHA256}" =~ ^[0-9a-f]{64}$ ]]; then
|
|
echo "::error::Stable demo mutation requires an exact stable tag, convergence owner, and lease."
|
|
exit 1
|
|
fi
|
|
release_state="$(
|
|
gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}" \
|
|
--jq '[.tag_name, (.draft | tostring), (.prerelease | tostring), (.immutable // false | tostring), (.published_at // "")] | @tsv'
|
|
)"
|
|
if [ "$(awk -F '\t' '{print $1}' <<<"${release_state}")" != "${TAG}" ] || \
|
|
[ "$(awk -F '\t' '{print $2}' <<<"${release_state}")" != "false" ] || \
|
|
[ "$(awk -F '\t' '{print $3}' <<<"${release_state}")" != "false" ] || \
|
|
[ "$(awk -F '\t' '{print $4}' <<<"${release_state}")" != "true" ] || \
|
|
[ -z "$(awk -F '\t' '{print $5}' <<<"${release_state}")" ]; then
|
|
echo "::error::Stable demo mutation refuses mutable, inactive, or prerelease tag ${TAG}."
|
|
exit 1
|
|
fi
|
|
marker="$(mktemp)"
|
|
owner_record="$(mktemp)"
|
|
curl -fsSL --retry 4 --retry-delay 2 --retry-all-errors \
|
|
-o "${marker}" \
|
|
"https://github.com/${{ github.repository }}/releases/download/${TAG}/release-activation.json"
|
|
jq -e \
|
|
--arg tag "${TAG}" \
|
|
'.schema_version == 1 and .tag == $tag and (.convergence_run_id | test("^[0-9]+$"))' \
|
|
"${marker}" >/dev/null
|
|
activation_owner_run_id="$(jq -r '.convergence_run_id' "${marker}")"
|
|
activation_marker_sha256="$(sha256sum "${marker}" | awk '{print $1}')"
|
|
observed_lease_sha="$(
|
|
gh api \
|
|
"repos/${{ github.repository }}/git/ref/heads/release-customer-promotion-lock" \
|
|
--jq '.object.sha'
|
|
)"
|
|
if [ "${observed_lease_sha}" != "${LEASE_SHA}" ]; then
|
|
echo "::error::Stable demo lease is ${observed_lease_sha:-absent}, expected ${LEASE_SHA}."
|
|
exit 1
|
|
fi
|
|
curl -fsSL --retry 4 --retry-delay 2 --retry-all-errors \
|
|
-o "${owner_record}" \
|
|
"https://raw.githubusercontent.com/${{ github.repository }}/${LEASE_SHA}/${OWNER_ASSET_NAME}"
|
|
printf '%s %s\n' "${OWNER_ASSET_SHA256}" "${owner_record}" | sha256sum --check --
|
|
jq -e \
|
|
--arg tag "${TAG}" \
|
|
--arg activation_owner_run_id "${activation_owner_run_id}" \
|
|
--arg activation_marker_sha256 "${activation_marker_sha256}" \
|
|
--arg convergence_run_id "${CONVERGENCE_RUN_ID}" \
|
|
--arg owner_asset_name "${OWNER_ASSET_NAME}" \
|
|
'.schema_version == 2 and .tag == $tag and .activation_owner_run_id == $activation_owner_run_id and .activation_marker_sha256 == $activation_marker_sha256 and .convergence_run_id == $convergence_run_id and .owner_asset_name == $owner_asset_name' \
|
|
"${owner_record}" >/dev/null
|
|
rm -f "${marker}" "${owner_record}"
|
|
echo "[OK] Stable demo mutation is bound to committed ${TAG} convergence run ${CONVERGENCE_RUN_ID}."
|
|
|
|
- name: Checkout repository
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
fetch-depth: 0
|
|
fetch-tags: true
|
|
|
|
- name: Set up Go
|
|
if: inputs.verify_only != true
|
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
|
with:
|
|
go-version-file: go.mod
|
|
cache: false
|
|
|
|
- name: Wait for release assets
|
|
if: inputs.verify_only != true
|
|
env:
|
|
WORKFLOW_OUTPUT_1: ${{ needs.resolve.outputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
TAG="${WORKFLOW_OUTPUT_1}"
|
|
echo "Waiting for activated 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
|
|
if: inputs.verify_only != true
|
|
env:
|
|
PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}
|
|
WORKFLOW_OUTPUT_1: ${{ needs.resolve.outputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
TAG="${WORKFLOW_OUTPUT_1}"
|
|
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
|
|
id: tailscale
|
|
uses: tailscale/github-action@780049a30b6ff5c378a9e7b389d15ece7a204888 # v4.1.3
|
|
with:
|
|
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
|
|
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
|
|
tags: tag:infra
|
|
version: '1.94.2'
|
|
ping: ${{ secrets.DEMO_SERVER_HOST }}
|
|
|
|
- name: Diagnose Tailscale setup failure
|
|
if: failure() && steps.tailscale.outcome == 'failure'
|
|
env:
|
|
DEMO_SERVER_HOST: ${{ secrets.DEMO_SERVER_HOST }}
|
|
run: bash .github/scripts/check-demo-reachability.sh diagnose
|
|
|
|
- name: Verify demo network path
|
|
env:
|
|
DEMO_SERVER_HOST: ${{ secrets.DEMO_SERVER_HOST }}
|
|
run: bash .github/scripts/check-demo-reachability.sh
|
|
|
|
- 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
|
|
env:
|
|
WORKFLOW_OUTPUT_1: ${{ needs.resolve.outputs.target }}
|
|
run: |
|
|
set -euo pipefail
|
|
TARGET="${WORKFLOW_OUTPUT_1}"
|
|
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 }}
|
|
WORKFLOW_OUTPUT_1: ${{ needs.resolve.outputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
TARGET="${WORKFLOW_OUTPUT_1}"
|
|
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: Refuse mutation during verification-only checks
|
|
if: inputs.verify_only == true && steps.current.outputs.skip_current != 'true'
|
|
run: |
|
|
echo "::error::Verification-only check found a demo version mismatch; refusing to update the host."
|
|
exit 1
|
|
|
|
- name: Prepare demo host storage
|
|
if: inputs.verify_only != true && steps.current.outputs.skip_current != 'true'
|
|
env:
|
|
DEMO_SERVER_HOST: ${{ secrets.DEMO_SERVER_HOST }}
|
|
DEMO_SERVER_USER: ${{ secrets.DEMO_SERVER_USER }}
|
|
WORKFLOW_OUTPUT_1: ${{ steps.config.outputs.service_name }}
|
|
run: |
|
|
set -euo pipefail
|
|
SERVICE_NAME="${WORKFLOW_OUTPUT_1}"
|
|
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: inputs.verify_only != true && 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: inputs.verify_only != true && steps.current.outputs.skip_current != 'true'
|
|
env:
|
|
DEMO_SERVER_HOST: ${{ secrets.DEMO_SERVER_HOST }}
|
|
DEMO_SERVER_USER: ${{ secrets.DEMO_SERVER_USER }}
|
|
WORKFLOW_OUTPUT_1: ${{ needs.resolve.outputs.tag }}
|
|
WORKFLOW_OUTPUT_2: ${{ steps.config.outputs.service_name }}
|
|
run: |
|
|
set -euo pipefail
|
|
TAG="${WORKFLOW_OUTPUT_1}"
|
|
SERVICE_NAME="${WORKFLOW_OUTPUT_2}"
|
|
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
|
|
if: inputs.verify_only != true
|
|
env:
|
|
DEMO_SERVER_HOST: ${{ secrets.DEMO_SERVER_HOST }}
|
|
DEMO_SERVER_USER: ${{ secrets.DEMO_SERVER_USER }}
|
|
WORKFLOW_OUTPUT_1: ${{ steps.config.outputs.service_name }}
|
|
WORKFLOW_OUTPUT_2: ${{ needs.resolve.outputs.mock_nodes }}
|
|
WORKFLOW_OUTPUT_3: ${{ needs.resolve.outputs.mock_vms_per_node }}
|
|
WORKFLOW_OUTPUT_4: ${{ needs.resolve.outputs.mock_lxcs_per_node }}
|
|
WORKFLOW_OUTPUT_5: ${{ needs.resolve.outputs.mock_docker_hosts }}
|
|
WORKFLOW_OUTPUT_6: ${{ needs.resolve.outputs.mock_docker_containers }}
|
|
WORKFLOW_OUTPUT_7: ${{ needs.resolve.outputs.mock_generic_hosts }}
|
|
WORKFLOW_OUTPUT_8: ${{ needs.resolve.outputs.mock_k8s_clusters }}
|
|
WORKFLOW_OUTPUT_9: ${{ needs.resolve.outputs.mock_k8s_nodes }}
|
|
WORKFLOW_OUTPUT_10: ${{ needs.resolve.outputs.mock_k8s_pods }}
|
|
WORKFLOW_OUTPUT_11: ${{ needs.resolve.outputs.mock_k8s_deployments }}
|
|
WORKFLOW_OUTPUT_12: ${{ needs.resolve.outputs.mock_seed_duration }}
|
|
WORKFLOW_OUTPUT_13: ${{ needs.resolve.outputs.mock_sample_interval }}
|
|
WORKFLOW_OUTPUT_14: ${{ needs.resolve.outputs.mock_update_interval }}
|
|
run: |
|
|
set -euo pipefail
|
|
SERVICE_NAME="${WORKFLOW_OUTPUT_1}"
|
|
MOCK_NODES="${WORKFLOW_OUTPUT_2}"
|
|
MOCK_VMS_PER_NODE="${WORKFLOW_OUTPUT_3}"
|
|
MOCK_LXCS_PER_NODE="${WORKFLOW_OUTPUT_4}"
|
|
MOCK_DOCKER_HOSTS="${WORKFLOW_OUTPUT_5}"
|
|
MOCK_DOCKER_CONTAINERS="${WORKFLOW_OUTPUT_6}"
|
|
MOCK_GENERIC_HOSTS="${WORKFLOW_OUTPUT_7}"
|
|
MOCK_K8S_CLUSTERS="${WORKFLOW_OUTPUT_8}"
|
|
MOCK_K8S_NODES="${WORKFLOW_OUTPUT_9}"
|
|
MOCK_K8S_PODS="${WORKFLOW_OUTPUT_10}"
|
|
MOCK_K8S_DEPLOYMENTS="${WORKFLOW_OUTPUT_11}"
|
|
MOCK_SEED_DURATION="${WORKFLOW_OUTPUT_12}"
|
|
MOCK_SAMPLE_INTERVAL="${WORKFLOW_OUTPUT_13}"
|
|
MOCK_UPDATE_INTERVAL="${WORKFLOW_OUTPUT_14}"
|
|
REMOTE_SCRIPT=$(cat <<'EOF'
|
|
set -euo pipefail
|
|
SERVICE_NAME="${1:-pulse}"
|
|
DEMO_LOCAL_BASE_URL="$2"
|
|
MOCK_NODES="$3"
|
|
MOCK_VMS_PER_NODE="$4"
|
|
MOCK_LXCS_PER_NODE="$5"
|
|
MOCK_DOCKER_HOSTS="$6"
|
|
MOCK_DOCKER_CONTAINERS="$7"
|
|
MOCK_GENERIC_HOSTS="$8"
|
|
MOCK_K8S_CLUSTERS="$9"
|
|
MOCK_K8S_NODES="${10}"
|
|
MOCK_K8S_PODS="${11}"
|
|
MOCK_K8S_DEPLOYMENTS="${12}"
|
|
MOCK_SEED_DURATION="${13}"
|
|
MOCK_SAMPLE_INTERVAL="${14}"
|
|
MOCK_UPDATE_INTERVAL="${15}"
|
|
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 "$MOCK_NODES"
|
|
set_env_value PULSE_MOCK_VMS_PER_NODE "$MOCK_VMS_PER_NODE"
|
|
set_env_value PULSE_MOCK_LXCS_PER_NODE "$MOCK_LXCS_PER_NODE"
|
|
set_env_value PULSE_MOCK_DOCKER_HOSTS "$MOCK_DOCKER_HOSTS"
|
|
set_env_value PULSE_MOCK_DOCKER_CONTAINERS "$MOCK_DOCKER_CONTAINERS"
|
|
set_env_value PULSE_MOCK_GENERIC_HOSTS "$MOCK_GENERIC_HOSTS"
|
|
set_env_value PULSE_MOCK_K8S_CLUSTERS "$MOCK_K8S_CLUSTERS"
|
|
set_env_value PULSE_MOCK_K8S_NODES "$MOCK_K8S_NODES"
|
|
set_env_value PULSE_MOCK_K8S_PODS "$MOCK_K8S_PODS"
|
|
set_env_value PULSE_MOCK_K8S_DEPLOYMENTS "$MOCK_K8S_DEPLOYMENTS"
|
|
set_env_value PULSE_MOCK_RANDOM_METRICS true
|
|
set_env_value PULSE_MOCK_STOPPED_PERCENT 6
|
|
# Never carry the isolated local-dev metrics-store opt-in onto the
|
|
# persistent public demo data directory.
|
|
set_env_value PULSE_MOCK_SEED_METRICS_STORE false
|
|
set_env_value PULSE_MOCK_TRENDS_SEED_DURATION "$MOCK_SEED_DURATION"
|
|
set_env_value PULSE_MOCK_TRENDS_SAMPLE_INTERVAL "$MOCK_SAMPLE_INTERVAL"
|
|
set_env_value PULSE_MOCK_UPDATE_INTERVAL "$MOCK_UPDATE_INTERVAL"
|
|
|
|
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" "$MOCK_NODES" "$MOCK_VMS_PER_NODE" "$MOCK_LXCS_PER_NODE" "$MOCK_DOCKER_HOSTS" "$MOCK_DOCKER_CONTAINERS" "$MOCK_GENERIC_HOSTS" "$MOCK_K8S_CLUSTERS" "$MOCK_K8S_NODES" "$MOCK_K8S_PODS" "$MOCK_K8S_DEPLOYMENTS" "$MOCK_SEED_DURATION" "$MOCK_SAMPLE_INTERVAL" "$MOCK_UPDATE_INTERVAL")" <<<"$REMOTE_SCRIPT"
|
|
|
|
- name: Verify demo runtime
|
|
env:
|
|
DEMO_SERVER_HOST: ${{ secrets.DEMO_SERVER_HOST }}
|
|
DEMO_SERVER_USER: ${{ secrets.DEMO_SERVER_USER }}
|
|
WORKFLOW_OUTPUT_1: ${{ needs.resolve.outputs.tag }}
|
|
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="${WORKFLOW_OUTPUT_1}"
|
|
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
|
|
export PULSE_DEMO_AUTH_USER="${DEMO_AUTH_USER:-demo}"
|
|
export PULSE_DEMO_AUTH_PASS="${DEMO_AUTH_PASS:-demo}"
|
|
./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
|