diff --git a/.github/workflows/README.md b/.github/workflows/README.md index d7b06e9d7..6f126d97a 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -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` diff --git a/.github/workflows/update-demo-server.yml b/.github/workflows/update-demo-server.yml index 1f2de3fe5..f63b193bf 100644 --- a/.github/workflows/update-demo-server.yml +++ b/.github/workflows/update-demo-server.yml @@ -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 }} diff --git a/docs/release-control/v6/internal/records/rc-to-ga-promotion-readiness-blocked-2026-04-04.md b/docs/release-control/v6/internal/records/rc-to-ga-promotion-readiness-blocked-2026-04-04.md index eb0872fd6..4a06863a6 100644 --- a/docs/release-control/v6/internal/records/rc-to-ga-promotion-readiness-blocked-2026-04-04.md +++ b/docs/release-control/v6/internal/records/rc-to-ga-promotion-readiness-blocked-2026-04-04.md @@ -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 diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index b73493a32..ba7275230 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -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 diff --git a/scripts/installtests/build_release_assets_test.go b/scripts/installtests/build_release_assets_test.go index 6845250eb..90b80979e 100644 --- a/scripts/installtests/build_release_assets_test.go +++ b/scripts/installtests/build_release_assets_test.go @@ -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 { diff --git a/scripts/release_control/release_promotion_policy_test.py b/scripts/release_control/release_promotion_policy_test.py index 75c990f2d..4989c76fe 100644 --- a/scripts/release_control/release_promotion_policy_test.py +++ b/scripts/release_control/release_promotion_policy_test.py @@ -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)