Reframe prerelease update copy

This commit is contained in:
rcourtman
2026-03-25 11:25:41 +00:00
parent b204ed5d86
commit 2c1b90cc4e
6 changed files with 110 additions and 29 deletions
@@ -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.
+4 -4
View File
@@ -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
+10 -10
View File
@@ -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
}
+41 -11
View File
@@ -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
@@ -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")
}
}
+1 -1
View File
@@ -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