Route demo updates over governed Tailscale path

This commit is contained in:
rcourtman
2026-04-11 13:35:37 +01:00
parent 1a0acd310a
commit 3a992a4fae
6 changed files with 53 additions and 14 deletions
+14 -4
View File
@@ -31,6 +31,13 @@ Required environment secrets:
3. **DEMO_SERVER_USER**
- The SSH username for the demo server (e.g. `root` or a deploy user with sudo access)
Required shared secret:
1. **TS_AUTHKEY**
- Tailscale auth key used by the governed demo deploy/update workflows before SSH
- Allows GitHub-hosted runners to reach private demo targets such as the stable `pulse-relay` Tailscale host
- May be stored as a repository secret or repeated in the selected environment if desired
Required environment variables:
1. **DEMO_EXPECTED_HOSTNAME**
@@ -68,10 +75,11 @@ Optional environment variables:
3. **Service identity guard**: Preview runs default to `pulse-v6-preview` and refuse to target the stable `pulse` service identity
4. **Governance check**: Validates the selected tag is reachable from the governed release branch for that version
5. **Latest check**: Refuses to update a target unless the published tag is the latest release for that target channel
6. **Update**: SSHs to the selected demo host and runs the tag-matched root installer from that exact git tag
7. **Host identity check**: Verifies the SSH target reports the governed expected hostname before running installer or deploy steps
8. **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
9. **Cleanup**: Removes SSH key from runner
6. **Network attach**: Joins Tailscale before any SSH step so governed demo targets can stay on private hostnames or Tailscale IPs
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
### Testing
@@ -103,6 +111,8 @@ environment without changing the governed release workflow.
- Uses the same `demo-stable` / `demo-preview-v6` environment contract as the
release-driven updater
- Joins Tailscale before SSH so governed demo targets can stay on private
addresses instead of requiring public runner reachability
- Requires `DEMO_EXPECTED_HOSTNAME`, `DEMO_LOCAL_BASE_URL`, and `DEMO_PUBLIC_HEALTH_URL`
- Supports optional `DEMO_SERVICE_NAME`, `DEMO_INSTALL_DIR`, `DEMO_TEST_PORT`,
`DEMO_AUTH_USER`, and `DEMO_AUTH_PASS`
+5
View File
@@ -226,6 +226,11 @@ jobs:
git show "refs/tags/${TAG}:install.sh" > /tmp/pulse-install.sh
chmod +x /tmp/pulse-install.sh
- name: Tailscale
uses: tailscale/github-action@v2
with:
authkey: ${{ secrets.TS_AUTHKEY }}
- name: Setup SSH
env:
DEMO_SERVER_HOST: ${{ secrets.DEMO_SERVER_HOST }}
@@ -7,26 +7,23 @@
## Blocking Facts
1. No Pulse v6 prerelease has shipped yet.
2. The selected remote ref `origin/pulse/v6-release` is still behind the current
local governed branch state, so `Release Dry Run` would exercise stale remote
control-plane metadata instead of the intended candidate.
3. The governed release profile in `docs/release-control/control_plane.json`
2. The governed release profile in `docs/release-control/control_plane.json`
currently declares both `prerelease_branch` and `stable_branch` as
`pulse/v6-release`.
4. The active control-plane target is still `v6-rc-stabilization`, not
3. The active control-plane target is still `v6-rc-stabilization`, not
`v6-ga-promotion`.
5. The active local `pulse/v6-release` branch currently reports `VERSION=6.0.0-rc.1`, so the
4. The active local `pulse/v6-release` branch currently reports `VERSION=6.0.0-rc.1`, so the
working line is still prerelease and there is not yet a governed local stable
`6.0.0` candidate.
6. There is still no governed `Prerelease-to-GA Rehearsal Record` proving a successful
5. There is still no governed `Prerelease-to-GA Rehearsal Record` proving a successful
non-publish `Release Dry Run` for the eventual stable `6.0.0` candidate.
7. `docs/releases/RELEASE_NOTES_v6.md` and
6. `docs/releases/RELEASE_NOTES_v6.md` and
`docs/release-control/v6/internal/V5_MAINTENANCE_SUPPORT_POLICY.md` still leave the
GA announcement dates as placeholders because no real prerelease lineage or GA-ready
rehearsal has locked them yet:
- `v6` GA date placeholder: `[v6-ga-date]`
- `v5` end-of-support placeholder: `[v5-eos-date]`
8. There is still no governed `Release Dry Run` artifact or rehearsal record
7. There is still no governed `Release Dry Run` artifact or rehearsal record
exercising stable inputs for:
- `version=6.0.0`
- no governed `promoted_from_tag` exists yet because no prerelease has shipped
@@ -191,6 +191,11 @@ shell actually updated. That proof
must use a deterministic HTML parser for the actual module entry script rather
than brittle escaped shell regex or a first-match asset scrape that can fail
differently over SSH or select the wrong preloaded chunk.
Those same governed demo deploy/update workflows also own the runner-to-host
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 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
@@ -139,6 +139,27 @@ func TestDeployDemoWorkflowFailsClosedForPreviewAndVerifiesFrontendParity(t *tes
}
}
func TestUpdateDemoWorkflowUsesGovernedNetworkPath(t *testing.T) {
workflowBytes, err := os.ReadFile(repoFile(".github", "workflows", "update-demo-server.yml"))
if err != nil {
t.Fatalf("read update-demo-server workflow: %v", err)
}
workflow := string(workflowBytes)
required := []string{
`- name: Tailscale`,
`uses: tailscale/github-action@v2`,
`authkey: ${{ secrets.TS_AUTHKEY }}`,
`Verify target host identity`,
`Demo environment points at host $REMOTE_HOSTNAME but expected $DEMO_EXPECTED_HOSTNAME.`,
}
for _, needle := range required {
if !strings.Contains(workflow, needle) {
t.Fatalf("update-demo-server workflow missing governed network path: %s", needle)
}
}
}
func TestDockerfileStagesShippedDocsForEmbeddedFrontendBuild(t *testing.T) {
dockerfileBytes, err := os.ReadFile(repoFile("Dockerfile"))
if err != nil {
@@ -231,6 +231,8 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
self.assertIn("latest published release for target", demo)
self.assertIn('SERVICE_NAME="pulse-v6-preview"', demo)
self.assertIn("Preview demo updates must not target the stable pulse service.", demo)
self.assertIn("tailscale/github-action@v2", demo)
self.assertIn("TS_AUTHKEY", demo)
self.assertIn("DEMO_EXPECTED_HOSTNAME", demo)
self.assertIn("Verify target host identity", demo)
self.assertIn("Demo environment points at host $REMOTE_HOSTNAME but expected $DEMO_EXPECTED_HOSTNAME.", demo)
@@ -282,7 +284,6 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
blocked = read("docs/release-control/v6/internal/records/rc-to-ga-promotion-readiness-blocked-2026-04-04.md")
current_version = read("VERSION").strip()
active_target_id = read_json("docs/release-control/control_plane.json")["active_target_id"]
self.assertIn("origin/pulse/v6-release", blocked)
self.assertIn(f"VERSION={current_version}", blocked)
self.assertIn("artifact-owned candidate stable tag", blocked)
self.assertIn("artifact-owned promotion channel", blocked)