Add public browser smoke proof to demo workflows

This commit is contained in:
rcourtman
2026-04-11 13:50:00 +01:00
parent b64782c083
commit be2b987cf0
9 changed files with 159 additions and 12 deletions
+4 -1
View File
@@ -79,7 +79,8 @@ Optional environment variables:
7. **Update**: SSHs to the selected demo host and runs the tag-matched root installer from that exact git tag
8. **Host identity check**: Verifies the SSH target reports the governed expected hostname before running installer or deploy steps
9. **Verify**: Checks that the new version is running, mock mode is active, and the public demo HTML serves the same frontend entry asset as the target service
10. **Cleanup**: Removes SSH key from runner
10. **Browser smoke**: Uses the governed Playwright helper to prove the public demo still renders the login shell in a real browser
11. **Cleanup**: Removes SSH key from runner
### Testing
@@ -122,6 +123,8 @@ environment without changing the governed release workflow.
- Verifies the SSH target reports the governed expected hostname before deploy
- Verifies that the public demo shell serves the same frontend entry asset that
was built and deployed
- Uses `scripts/run_demo_public_browser_smoke.sh` to prove the public demo
still renders the login shell in Chromium after deploy/update verification
## Helm CI
+14
View File
@@ -316,6 +316,20 @@ jobs:
exit 1
}
- 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
+14 -1
View File
@@ -385,7 +385,6 @@ jobs:
fi
- name: Verify frontend parity
if: steps.current.outputs.skip_current != 'true'
env:
DEMO_SERVER_HOST: ${{ secrets.DEMO_SERVER_HOST }}
DEMO_SERVER_USER: ${{ secrets.DEMO_SERVER_USER }}
@@ -426,6 +425,20 @@ jobs:
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
@@ -58,15 +58,17 @@ server-side update execution surfaces.
36. `scripts/pulse-auto-update.sh`
37. `scripts/release_control/resolve_release_promotion.py`
38. `scripts/release_ldflags.sh`
39. `scripts/trigger-release-dry-run.sh`
40. `scripts/trigger-release.sh`
41. `scripts/toggle-mock.sh`
42. `tests/integration/playwright.config.ts`
43. `tests/integration/QUICK_START.md`
44. `tests/integration/README.md`
45. `tests/integration/scripts/managed-dev-runtime.mjs`
46. `tests/integration/tests/helpers.ts`
47. `tests/integration/tests/runtime-defaults.ts`
39. `scripts/run_demo_public_browser_smoke.sh`
40. `scripts/demo_public_browser_smoke.cjs`
41. `scripts/trigger-release-dry-run.sh`
42. `scripts/trigger-release.sh`
43. `scripts/toggle-mock.sh`
44. `tests/integration/playwright.config.ts`
45. `tests/integration/QUICK_START.md`
46. `tests/integration/README.md`
47. `tests/integration/scripts/managed-dev-runtime.mjs`
48. `tests/integration/tests/helpers.ts`
49. `tests/integration/tests/runtime-defaults.ts`
## Shared Boundaries
@@ -81,7 +83,7 @@ server-side update execution surfaces.
2. Add or change release-build metadata injection, Docker build-context allowlists, release artifact assembly, or governed promotion metadata resolution through `scripts/build-release.sh`, `scripts/release_ldflags.sh`, `scripts/check-workflow-dispatch-inputs.py`, `scripts/release_control/resolve_release_promotion.py`, `.dockerignore`, `Dockerfile`, `docs/releases/V6_PRERELEASE_RUNBOOK.md`, the operator dispatch helpers `scripts/trigger-release.sh` and `scripts/trigger-release-dry-run.sh`, and the governed release workflows `.github/workflows/create-release.yml`, `.github/workflows/deploy-demo-server.yml`, `.github/workflows/helm-pages.yml`, `.github/workflows/publish-docker.yml`, `.github/workflows/publish-helm-chart.yml`, `.github/workflows/promote-floating-tags.yml`, `.github/workflows/release-dry-run.yml`, and `.github/workflows/update-demo-server.yml`
3. Add or change shell installer, Windows installer, container-agent installer, or auto-update script behavior through `scripts/install.sh`, `scripts/install.ps1`, `scripts/install-container-agent.sh`, and `scripts/pulse-auto-update.sh`
4. Add or change server update transport through `internal/api/updates.go` and `frontend-modern/src/api/updates.ts`
5. Add or change local dev-runtime orchestration, managed ownership, browser-runtime proof wiring, frontend/backend coherence diagnostics, canonical developer entry wrappers, dependency manifest floors, frontend build chunking, or dev-runtime helper control surfaces through `scripts/hot-dev.sh`, `scripts/hot-dev-bg.sh`, `Makefile`, `package.json`, `package-lock.json`, `frontend-modern/package.json`, `frontend-modern/package-lock.json`, `frontend-modern/vite.config.ts`, `go.mod`, `go.sum`, `scripts/dev-check.sh`, `scripts/toggle-mock.sh`, `scripts/clean-mock-alerts.sh`, `scripts/dev-launchd-setup.sh`, `scripts/dev-launchd-wrapper.sh`, `scripts/com.pulse.hot-dev.plist.template`, `tests/integration/scripts/managed-dev-runtime.mjs`, `tests/integration/playwright.config.ts`, `tests/integration/tests/helpers.ts`, `tests/integration/tests/runtime-defaults.ts`, `tests/integration/README.md`, and `tests/integration/QUICK_START.md`
5. Add or change local dev-runtime orchestration, managed ownership, browser-runtime proof wiring, frontend/backend coherence diagnostics, canonical developer entry wrappers, dependency manifest floors, frontend build chunking, or dev-runtime helper control surfaces through `scripts/hot-dev.sh`, `scripts/hot-dev-bg.sh`, `Makefile`, `package.json`, `package-lock.json`, `frontend-modern/package.json`, `frontend-modern/package-lock.json`, `frontend-modern/vite.config.ts`, `go.mod`, `go.sum`, `scripts/dev-check.sh`, `scripts/toggle-mock.sh`, `scripts/clean-mock-alerts.sh`, `scripts/dev-launchd-setup.sh`, `scripts/dev-launchd-wrapper.sh`, `scripts/run_demo_public_browser_smoke.sh`, `scripts/demo_public_browser_smoke.cjs`, `scripts/com.pulse.hot-dev.plist.template`, `tests/integration/scripts/managed-dev-runtime.mjs`, `tests/integration/playwright.config.ts`, `tests/integration/tests/helpers.ts`, `tests/integration/tests/runtime-defaults.ts`, `tests/integration/README.md`, and `tests/integration/QUICK_START.md`
6. Add or change governed release-promotion workflow inputs, operator-facing promotion metadata, artifact publication lineage enforcement, or stable-promotion rehearsal summaries through `.github/workflows/create-release.yml`, `.github/workflows/helm-pages.yml`, `.github/workflows/publish-docker.yml`, `.github/workflows/publish-helm-chart.yml`, `.github/workflows/promote-floating-tags.yml`, `.github/workflows/release-dry-run.yml`, `.github/workflows/update-demo-server.yml`, `docs/releases/V6_PRERELEASE_RUNBOOK.md`, `scripts/check-workflow-dispatch-inputs.py`, `scripts/trigger-release.sh`, and `scripts/trigger-release-dry-run.sh`
7. Preserve release-matched installer and Helm operator documentation links through `scripts/install.sh`, `.github/workflows/helm-pages.yml`, `.github/workflows/publish-helm-chart.yml`, and the chart metadata itself so deployment guidance and packaged chart metadata do not drift back to branch-tip `main` docs when a release line or promoted tag already exists.
8. Add or change operator-facing hosted tenant runtime canary rollout, batch runtime contract reconciliation, canonical hosted route/public URL generation, or control-plane runtime-registry reconciliation through `cmd/pulse-control-plane/main.go`, `internal/cloudcp/docker/manager.go`, `internal/cloudcp/docker/labels.go`, and `internal/cloudcp/tenant_runtime_rollout.go`
@@ -196,6 +198,13 @@ network path. They must establish the canonical Tailscale connectivity step
before SSH setup so stable or preview targets may stay on governed private
hostnames or Tailscale IPs, rather than silently depending on public SSH
reachability from GitHub-hosted runners.
Those same workflows also own customer-visible browser truth for the public
demo shell. Health checks and entry-asset parity are necessary but not
sufficient; after those checks pass, the governed helpers
`scripts/run_demo_public_browser_smoke.sh` and
`scripts/demo_public_browser_smoke.cjs` must exercise the public demo in a real
Chromium session and prove the login shell actually renders instead of failing
open on API-only reachability.
Those same governed release workflows also own the operator-facing wording for
that promotion metadata. Human-visible workflow inputs, summaries, and error
messages must describe the path as a prerelease or preview flow rather than
@@ -2355,6 +2355,7 @@
"scripts/check-workflow-dispatch-inputs.py",
"scripts/clean-mock-alerts.sh",
"scripts/com.pulse.hot-dev.plist.template",
"scripts/demo_public_browser_smoke.cjs",
"scripts/dev-check.sh",
"scripts/dev-launchd-setup.sh",
"scripts/dev-launchd-wrapper.sh",
@@ -2366,6 +2367,7 @@
"scripts/pulse-auto-update.sh",
"scripts/release_control/resolve_release_promotion.py",
"scripts/release_ldflags.sh",
"scripts/run_demo_public_browser_smoke.sh",
"scripts/toggle-mock.sh",
"scripts/trigger-release-dry-run.sh",
"scripts/trigger-release.sh",
@@ -2453,6 +2455,21 @@
"scripts/installtests/release_ldflags_test.go"
]
},
{
"id": "demo-public-browser-smoke-runtime",
"label": "demo public browser smoke proof",
"match_prefixes": [],
"match_files": [
"scripts/demo_public_browser_smoke.cjs",
"scripts/run_demo_public_browser_smoke.sh"
],
"allow_same_subsystem_tests": false,
"test_prefixes": [],
"exact_files": [
"scripts/installtests/build_release_assets_test.go",
"scripts/release_control/release_promotion_policy_test.py"
]
},
{
"id": "deployment-script-runtime",
"label": "deployment script runtime proof",
+52
View File
@@ -0,0 +1,52 @@
const { chromium } = require('playwright');
const siteUrl = String(process.env.PULSE_PUBLIC_SITE_URL || '').trim();
function assert(condition, message) {
if (!condition) {
throw new Error(message);
}
}
function normalizeText(value) {
return String(value || '')
.replace(/\s+/g, ' ')
.trim();
}
async function main() {
assert(siteUrl, 'PULSE_PUBLIC_SITE_URL is required');
const browser = await chromium.launch({ headless: true });
try {
const page = await browser.newPage({ viewport: { width: 1280, height: 900 } });
const response = await page.goto(siteUrl, {
waitUntil: 'networkidle',
timeout: 120000,
});
assert(response, `No response received for ${siteUrl}`);
assert(response.ok(), `Unexpected status ${response.status()} loading ${siteUrl}`);
await page.getByLabel('Username').waitFor();
await page.getByLabel('Password').waitFor();
await page.getByRole('button', { name: 'Sign in to Pulse' }).waitFor();
const title = normalizeText(await page.title());
assert(title === 'Pulse', `Unexpected page title: ${title}`);
const bodyText = normalizeText(await page.locator('body').innerText());
for (const expected of ['Welcome to Pulse', 'Username', 'Password', 'Sign in to Pulse']) {
assert(bodyText.includes(expected), `Public demo body missing ${JSON.stringify(expected)}`);
}
console.log(`public demo browser smoke passed for ${siteUrl}`);
} finally {
await browser.close();
}
}
main().catch((error) => {
const message = error instanceof Error ? error.stack || error.message : String(error);
console.error(message);
process.exit(1);
});
@@ -127,6 +127,8 @@ func TestDeployDemoWorkflowFailsClosedForPreviewAndVerifiesFrontendParity(t *tes
`Preview demo deployments must not target the stable pulse service.`,
`Demo environment points at host $REMOTE_HOSTNAME but expected $DEMO_EXPECTED_HOSTNAME.`,
`Verify frontend parity`,
`Verify public browser smoke`,
`./scripts/run_demo_public_browser_smoke.sh`,
`extract_entry_asset()`,
`<script\b[^>]*\bsrc=\"(/assets/index-[^\"]*\.js)\"`,
`Remote service is serving $REMOTE_ASSET but the build expected $EXPECTED_ASSET.`,
@@ -152,6 +154,8 @@ func TestUpdateDemoWorkflowUsesGovernedNetworkPath(t *testing.T) {
`authkey: ${{ secrets.TS_AUTHKEY }}`,
`Verify target host identity`,
`Demo environment points at host $REMOTE_HOSTNAME but expected $DEMO_EXPECTED_HOSTNAME.`,
`Verify public browser smoke`,
`./scripts/run_demo_public_browser_smoke.sh`,
}
for _, needle := range required {
if !strings.Contains(workflow, needle) {
@@ -237,6 +237,8 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
self.assertIn("Verify target host identity", demo)
self.assertIn("Demo environment points at host $REMOTE_HOSTNAME but expected $DEMO_EXPECTED_HOSTNAME.", demo)
self.assertIn("Verify frontend parity", demo)
self.assertIn("Verify public browser smoke", demo)
self.assertIn("./scripts/run_demo_public_browser_smoke.sh", demo)
self.assertIn("extract_entry_asset()", demo)
self.assertIn(r'<script\b[^>]*\bsrc=\"(/assets/index-[^\"]*\.js)\"', demo)
self.assertIn("Public demo is serving $PUBLIC_ASSET but the target service is serving $REMOTE_ASSET.", demo)
@@ -246,6 +248,8 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
self.assertIn("Verify target host identity", preview_deploy)
self.assertIn("Demo environment points at host $REMOTE_HOSTNAME but expected $DEMO_EXPECTED_HOSTNAME.", preview_deploy)
self.assertIn("Verify frontend parity", preview_deploy)
self.assertIn("Verify public browser smoke", preview_deploy)
self.assertIn("./scripts/run_demo_public_browser_smoke.sh", preview_deploy)
self.assertIn("extract_entry_asset()", preview_deploy)
self.assertIn(r'<script\b[^>]*\bsrc=\"(/assets/index-[^\"]*\.js)\"', preview_deploy)
self.assertIn("Public demo is serving $PUBLIC_ASSET but the build expected $EXPECTED_ASSET.", preview_deploy)
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
set -euo pipefail
if ! command -v node >/dev/null 2>&1; then
echo "node is required for demo browser smoke" >&2
exit 1
fi
if ! command -v npx >/dev/null 2>&1; then
echo "npx is required for demo browser smoke" >&2
exit 1
fi
if [ -z "${PULSE_PUBLIC_SITE_URL:-}" ]; then
echo "PULSE_PUBLIC_SITE_URL is required for demo browser smoke" >&2
exit 1
fi
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
BROWSERS_PATH="${PLAYWRIGHT_BROWSERS_PATH:-${RUNNER_TEMP:-/tmp}/pw-browsers}"
mkdir -p "$BROWSERS_PATH"
export PLAYWRIGHT_BROWSERS_PATH="$BROWSERS_PATH"
npx --yes -p playwright playwright install --with-deps chromium
playwright_bin="$(npx --yes -p playwright -c 'which playwright')"
playwright_node_path="$(cd "$(dirname "$playwright_bin")/.." && pwd)"
NODE_PATH="$playwright_node_path${NODE_PATH:+:$NODE_PATH}" \
node "$SCRIPT_DIR/demo_public_browser_smoke.cjs"