diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 15c753252..99362e15c 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -111,6 +111,11 @@ that promotion metadata. Human-visible workflow inputs, summaries, and error messages must describe the path as a prerelease or preview flow rather than implying a near-ready release candidate, while machine-owned identifiers such as `rc`, `rc-to-ga-*`, and `v6.0.0-rc.1` remain the canonical internal keys. +That same prerelease framing requirement also applies to installer and update +runtime copy: `install.sh`, `scripts/pulse-auto-update.sh`, and +`internal/updates/manager.go` must present `rc`-tagged builds as prerelease or +preview paths in menus, operator diagnostics, and runtime logs rather than as +release-candidate promises. Those same workflows must also fetch and dispatch the governed release branch derived from release-control metadata instead of hardcoding `pulse/v6`, `pulse/v6-release`, or any later branch literal inline. @@ -190,6 +195,10 @@ That same managed browser proof pack must also keep the desktop Recovery page layout guard on the canonical entrypoint, so `dev:verify` catches right-edge history-table overflow regressions introduced by more human-readable subject labels instead of leaving that check as a hidden one-off Playwright command. +That same proof pack must also keep the Patrol blocked-runtime page contract on +the canonical entrypoint, so `dev:verify` catches stale healthy-summary +regressions where the real `/ai` route would otherwise look healthy even after +the backend reports `runtime_state=blocked`. That same launcher boundary now also owns the one-command verification entry point for that proof. `./scripts/hot-dev-bg.sh verify` must prepare a coherent managed runtime, run the canonical browser recovery proof with the managed dev @@ -257,9 +266,10 @@ and restart it when the owner process exits unexpectedly, so a killed or wedged foreground owner does not leave both `5173` and `7655` down until a human notices. That self-healing guarantee must be covered by the canonical managed browser -proof pack as well: `dev:verify` must prove both backend-bounce recovery and -owner-process-death recovery on the browser entrypoint, rather than leaving the -supervision contract to shell-only smoke tests. +proof pack as well: `dev:verify` must prove backend-bounce recovery, +owner-process-death recovery, and the Patrol blocked-runtime page contract on +the browser entrypoint, rather than leaving supervision and Patrol-shell drift +to shell-only smoke tests. The same wrapper-first rule applies to launcher help text: `hot-dev-bg` usage output must present the repo-root npm entrypoints first and reserve raw subcommands as secondary script-local controls for direct troubleshooting. diff --git a/install.sh b/install.sh index fc31e06d0..ef8d2e9e0 100755 --- a/install.sh +++ b/install.sh @@ -2133,13 +2133,13 @@ download_pulse() { CONFIGURED_CHANNEL=$(cat "$CONFIG_DIR/system.json" 2>/dev/null | grep -o '"updateChannel"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"\([^"]*\)"$/\1/' || true) if [[ "$CONFIGURED_CHANNEL" == "rc" ]]; then UPDATE_CHANNEL="rc" - print_info "RC channel detected in configuration" + print_info "Prerelease channel detected in configuration" fi fi fi # Get appropriate release based on channel (with timeout) - # Both stable and RC channels now use /releases endpoint to handle draft releases + # Both stable and prerelease channels now use /releases endpoint to handle draft releases if command -v timeout >/dev/null 2>&1; then RELEASES_JSON=$(timeout 15 curl -s --connect-timeout 10 --max-time 30 https://api.github.com/repos/$GITHUB_REPO/releases 2>/dev/null || true) else @@ -2148,7 +2148,7 @@ download_pulse() { if [[ -n "$RELEASES_JSON" ]]; then if [[ "$UPDATE_CHANNEL" == "rc" ]]; then - # RC channel: Get latest release (including pre-releases, but skip drafts) + # Prerelease channel: get latest release (including prereleases, but skip drafts) if command -v jq >/dev/null 2>&1; then LATEST_RELEASE=$(echo "$RELEASES_JSON" | jq -r '[.[] | select(.draft == false)][0].tag_name' 2>/dev/null || true) else @@ -3428,7 +3428,7 @@ main() { fi if [[ -n "$RC_VERSION" ]] && [[ "$RC_VERSION" != "$STABLE_VERSION" ]] && [[ "$RC_VERSION" != "$CURRENT_VERSION" ]]; then - echo "${menu_option}) Update to $RC_VERSION (release candidate)" + echo "${menu_option}) Update to $RC_VERSION (prerelease preview)" ((menu_option++)) fi diff --git a/internal/updates/manager.go b/internal/updates/manager.go index a067728bb..78dd5fef0 100644 --- a/internal/updates/manager.go +++ b/internal/updates/manager.go @@ -714,12 +714,12 @@ func (m *Manager) getLatestReleaseForChannel(ctx context.Context, channel string } // Find latest release based on channel - // RC channel: return newest release (RC or stable), even if not newer than current + // Prerelease channel: return newest release (prerelease or stable), even if not newer than current // Stable channel: return newest stable release, even if not newer than current // The caller will determine if it's actually an update by comparing versions if channel == "rc" { - // For RC channel: find newest release (RC or stable) - // RC users should see both RCs and stable releases + // For the prerelease channel: find newest release (prerelease or stable) + // Prerelease users should see both prereleases and stable releases var newestRC *ReleaseInfo var newestStable *ReleaseInfo @@ -737,7 +737,7 @@ func (m *Manager) getLatestReleaseForChannel(ctx context.Context, channel string } if releases[i].Prerelease { - // Track newest RC + // Track newest prerelease if newestRC == nil { newestRC = &releases[i] } else { @@ -767,7 +767,7 @@ func (m *Manager) getLatestReleaseForChannel(ctx context.Context, channel string if stableVer.IsNewerThan(rcVer) { isUpdate := stableVer.IsNewerThan(currentVer) if isUpdate { - log.Info().Str("version", newestStable.TagName).Msg("Found stable update for RC user") + log.Info().Str("version", newestStable.TagName).Msg("Found stable update for prerelease user") } else { log.Info().Str("version", newestStable.TagName).Msg("On latest stable version") } @@ -775,15 +775,15 @@ func (m *Manager) getLatestReleaseForChannel(ctx context.Context, channel string } isUpdate := rcVer.IsNewerThan(currentVer) if isUpdate { - log.Info().Str("version", newestRC.TagName).Msg("Found RC update") + log.Info().Str("version", newestRC.TagName).Msg("Found prerelease update") } else { - log.Info().Str("version", newestRC.TagName).Msg("On latest RC version") + log.Info().Str("version", newestRC.TagName).Msg("On latest prerelease version") } return newestRC, nil } else if newestStable != nil { isUpdate := newestStable.TagName != currentVer.String() if isUpdate { - log.Info().Str("version", newestStable.TagName).Msg("Found stable update for RC user") + log.Info().Str("version", newestStable.TagName).Msg("Found stable update for prerelease user") } else { log.Info().Str("version", newestStable.TagName).Msg("On latest stable version") } @@ -791,9 +791,9 @@ func (m *Manager) getLatestReleaseForChannel(ctx context.Context, channel string } else if newestRC != nil { isUpdate := newestRC.TagName != currentVer.String() if isUpdate { - log.Info().Str("version", newestRC.TagName).Msg("Found RC update") + log.Info().Str("version", newestRC.TagName).Msg("Found prerelease update") } else { - log.Info().Str("version", newestRC.TagName).Msg("On latest RC version") + log.Info().Str("version", newestRC.TagName).Msg("On latest prerelease version") } return newestRC, nil } diff --git a/internal/updates/manager_test.go b/internal/updates/manager_test.go index 1990116c4..58655525d 100644 --- a/internal/updates/manager_test.go +++ b/internal/updates/manager_test.go @@ -42,7 +42,7 @@ func mockGitHubReleases(releases []ReleaseInfo) *httptest.Server { })) } -func TestRCUpdateNotifications(t *testing.T) { +func TestPrereleaseUpdateNotifications(t *testing.T) { tests := []struct { name string currentVersion string @@ -52,7 +52,7 @@ func TestRCUpdateNotifications(t *testing.T) { description string }{ { - name: "RC user with newer RC available", + name: "prerelease user with newer prerelease available", currentVersion: "4.22.0-rc.1", releases: []ReleaseInfo{ {TagName: "v4.22.0-rc.3", Prerelease: true}, @@ -61,10 +61,10 @@ func TestRCUpdateNotifications(t *testing.T) { }, expectedVersion: "v4.22.0-rc.3", expectUpdate: true, - description: "RC users should see newer RC releases", + description: "Prerelease users should see newer prerelease releases", }, { - name: "RC user with newer stable available", + name: "prerelease user with newer stable available", currentVersion: "4.22.0-rc.3", releases: []ReleaseInfo{ {TagName: "v4.22.0", Prerelease: false}, @@ -73,10 +73,10 @@ func TestRCUpdateNotifications(t *testing.T) { }, expectedVersion: "v4.22.0", expectUpdate: true, - description: "RC users should see newer stable releases (stable > RC for same version)", + description: "Prerelease users should see newer stable releases (stable > prerelease for the same version)", }, { - name: "RC user with both newer RC and stable (stable wins)", + name: "prerelease user with both newer prerelease and stable (stable wins)", currentVersion: "4.22.0-rc.1", releases: []ReleaseInfo{ {TagName: "v4.23.0-rc.1", Prerelease: true}, @@ -86,10 +86,10 @@ func TestRCUpdateNotifications(t *testing.T) { }, expectedVersion: "v4.23.0-rc.1", expectUpdate: true, - description: "When both RC and stable are available, return the highest version", + description: "When both a prerelease and stable release are available, return the highest version", }, { - name: "RC user with only older releases", + name: "prerelease user with only older releases", currentVersion: "4.23.0-rc.1", releases: []ReleaseInfo{ {TagName: "v4.22.0", Prerelease: false}, @@ -97,10 +97,10 @@ func TestRCUpdateNotifications(t *testing.T) { }, expectedVersion: "v4.22.0", expectUpdate: true, // Returns latest version even if not newer (Available will be false) - description: "Returns latest stable version even when user is on newer RC", + description: "Returns the latest stable version even when the user is on a newer prerelease", }, { - name: "RC user already on latest stable", + name: "prerelease user already on latest stable", currentVersion: "4.22.0-rc.1", releases: []ReleaseInfo{ {TagName: "v4.22.0", Prerelease: false}, @@ -108,7 +108,7 @@ func TestRCUpdateNotifications(t *testing.T) { }, expectedVersion: "v4.22.0", expectUpdate: true, - description: "RC user should see stable release even if RC number is same (4.22.0 > 4.22.0-rc.1)", + description: "A prerelease user should see the stable release even when the prerelease number matches (4.22.0 > 4.22.0-rc.1)", }, } @@ -152,6 +152,36 @@ func TestRCUpdateNotifications(t *testing.T) { } } +func TestPrereleaseUpdateLogCopy(t *testing.T) { + content, err := os.ReadFile("manager.go") + if err != nil { + t.Fatalf("read manager.go: %v", err) + } + + source := string(content) + required := []string{ + `Found stable update for prerelease user`, + `Found prerelease update`, + `On latest prerelease version`, + } + for _, needle := range required { + if !strings.Contains(source, needle) { + t.Fatalf("manager.go missing prerelease runtime log fragment: %s", needle) + } + } + + forbidden := []string{ + `Found stable update for RC user`, + `Found RC update`, + `On latest RC version`, + } + for _, needle := range forbidden { + if strings.Contains(source, needle) { + t.Fatalf("manager.go preserved stale release-candidate runtime log fragment: %s", needle) + } + } +} + func TestStableUpdateNotifications(t *testing.T) { tests := []struct { name string diff --git a/scripts/installtests/root_install_sh_test.go b/scripts/installtests/root_install_sh_test.go index 55690d661..4804634e0 100644 --- a/scripts/installtests/root_install_sh_test.go +++ b/scripts/installtests/root_install_sh_test.go @@ -113,3 +113,44 @@ func TestRootInstallScriptAutoRegisterUsesSecureContractShape(t *testing.T) { } } } + +func TestPrereleaseUpdateCopyUsesPreviewFraming(t *testing.T) { + rootInstall, err := os.ReadFile(filepath.Join("..", "..", "install.sh")) + if err != nil { + t.Fatalf("read root install.sh: %v", err) + } + + installScript := string(rootInstall) + requiredInstall := []string{ + `Update to $RC_VERSION (prerelease preview)`, + `Prerelease channel detected in configuration`, + `Prerelease channel: get latest release (including prereleases, but skip drafts)`, + } + for _, needle := range requiredInstall { + if !strings.Contains(installScript, needle) { + t.Fatalf("root install.sh missing prerelease framing fragment: %s", needle) + } + } + forbiddenInstall := []string{ + `Update to $RC_VERSION (release candidate)`, + `RC channel detected in configuration`, + `RC channel: Get latest release (including pre-releases, but skip drafts)`, + } + for _, needle := range forbiddenInstall { + if strings.Contains(installScript, needle) { + t.Fatalf("root install.sh preserved stale release-candidate framing fragment: %s", needle) + } + } + + autoUpdate, err := os.ReadFile(filepath.Join("..", "..", "scripts", "pulse-auto-update.sh")) + if err != nil { + t.Fatalf("read pulse-auto-update.sh: %v", err) + } + autoUpdateScript := string(autoUpdate) + if !strings.Contains(autoUpdateScript, `Prerelease channel detected; unattended auto-updates run only on stable`) { + t.Fatalf("pulse-auto-update.sh missing prerelease channel log message") + } + if strings.Contains(autoUpdateScript, `RC channel detected; unattended auto-updates run only on stable`) { + t.Fatalf("pulse-auto-update.sh preserved stale release-candidate channel log message") + } +} diff --git a/scripts/pulse-auto-update.sh b/scripts/pulse-auto-update.sh index 0a95d2e7f..7fdf7980c 100755 --- a/scripts/pulse-auto-update.sh +++ b/scripts/pulse-auto-update.sh @@ -32,7 +32,7 @@ check_auto_updates_enabled() { exit 0 fi if [[ "$channel" == "rc" ]]; then - log info "RC channel detected; unattended auto-updates run only on stable" + log info "Prerelease channel detected; unattended auto-updates run only on stable" exit 0 fi fi