Skip to content

Commit a54fee6

Browse files
committed
fix: make update banner dynamic instead of static at startup
The update check was only evaluated once at server startup and injected as static values into the HTML template. Long-running servers would never show the update banner even if new versions were released. Now the template functions read from the cache store on every page load, and a periodic background goroutine refreshes the cache every 24 hours.
1 parent e9ffc5c commit a54fee6

2 files changed

Lines changed: 56 additions & 17 deletions

File tree

internal/service/frontend/server.go

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ type Server struct {
9393
dagStore exec.DAGStore
9494
licenseManager *license.Manager
9595
remoteNodeResolver *remotenode.Resolver
96+
upgradeStore upgrade.CacheStore
9697
}
9798

9899
// ServerOption is a functional option for configuring the Server.
@@ -312,13 +313,6 @@ func NewServer(ctx context.Context, cfg *config.Config, dr exec.DAGStore, drs ex
312313
logger.Warn(ctx, "Failed to create upgrade check store", tag.Error(err))
313314
}
314315

315-
// Check for updates asynchronously (populates cache for next startup)
316-
if upgradeStore != nil {
317-
go func() { _, _ = upgrade.CheckAndUpdateCache(upgradeStore, config.Version) }()
318-
}
319-
320-
updateAvailable, latestVersion := getUpdateInfo(upgradeStore)
321-
322316
// Note: SSO/OIDC gating is applied after opts are processed (see below)
323317

324318
srv := &Server{
@@ -333,6 +327,7 @@ func NewServer(ctx context.Context, cfg *config.Config, dr exec.DAGStore, drs ex
333327
metricsRegistry: mr,
334328
dagStore: dr,
335329
remoteNodeResolver: remoteNodeResolver,
330+
upgradeStore: upgradeStore,
336331
funcsConfig: funcsConfig{
337332
NavbarColor: cfg.UI.NavbarColor,
338333
NavbarTitle: cfg.UI.NavbarTitle,
@@ -350,8 +345,7 @@ func NewServer(ctx context.Context, cfg *config.Config, dr exec.DAGStore, drs ex
350345
TerminalEnabled: cfg.Server.Terminal.Enabled && authSvc != nil,
351346
GitSyncEnabled: cfg.GitSync.Enabled,
352347
SetupRequiredChecker: &setupChecker{authSvc: authSvc, fallback: setupRequired},
353-
UpdateAvailable: updateAvailable,
354-
LatestVersion: latestVersion,
348+
UpdateChecker: &updateChecker{store: upgradeStore},
355349
AgentEnabledChecker: agentConfigStore,
356350
},
357351
}
@@ -420,12 +414,16 @@ func NewServer(ctx context.Context, cfg *config.Config, dr exec.DAGStore, drs ex
420414
return srv, nil
421415
}
422416

423-
// getUpdateInfo returns update availability and latest version from cache.
424-
func getUpdateInfo(store upgrade.CacheStore) (updateAvailable bool, latestVersion string) {
425-
if store == nil {
417+
// updateChecker implements UpdateChecker by reading from the upgrade cache store.
418+
type updateChecker struct {
419+
store upgrade.CacheStore
420+
}
421+
422+
func (u *updateChecker) GetUpdateInfo() (bool, string) {
423+
if u.store == nil {
426424
return false, ""
427425
}
428-
cache := upgrade.GetCachedUpdateInfo(store)
426+
cache := upgrade.GetCachedUpdateInfo(u.store)
429427
if cache == nil {
430428
return false, ""
431429
}
@@ -842,12 +840,36 @@ func (srv *Server) Serve(ctx context.Context) error {
842840
metrics.StartUptime(ctx)
843841
logger.Info(ctx, "Server is starting", tag.Addr(addr))
844842

843+
srv.startPeriodicUpdateCheck(ctx)
844+
845845
go srv.startServer(ctx)
846846
srv.setupGracefulShutdown(ctx)
847847

848848
return nil
849849
}
850850

851+
// startPeriodicUpdateCheck runs an initial update check and then repeats
852+
// every CacheTTL interval so that long-running servers pick up new releases.
853+
func (srv *Server) startPeriodicUpdateCheck(ctx context.Context) {
854+
if srv.upgradeStore == nil {
855+
return
856+
}
857+
go func() {
858+
_, _ = upgrade.CheckAndUpdateCache(srv.upgradeStore, config.Version)
859+
860+
ticker := time.NewTicker(upgrade.CacheTTL)
861+
defer ticker.Stop()
862+
for {
863+
select {
864+
case <-ctx.Done():
865+
return
866+
case <-ticker.C:
867+
_, _ = upgrade.CheckAndUpdateCache(srv.upgradeStore, config.Version)
868+
}
869+
}
870+
}()
871+
}
872+
851873
func (srv *Server) configureAPIPath() string {
852874
apiV1BasePath := path.Join(srv.config.Server.BasePath, "api/v1")
853875
return ensureLeadingSlash(apiV1BasePath)

internal/service/frontend/templates.go

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ type SetupRequiredChecker interface {
6262
IsSetupRequired(ctx context.Context) bool
6363
}
6464

65+
// UpdateChecker provides dynamic update availability for template rendering.
66+
// Called on every HTML page render so the banner reflects the latest cached check.
67+
type UpdateChecker interface {
68+
GetUpdateInfo() (updateAvailable bool, latestVersion string)
69+
}
70+
6571
// AgentEnabledChecker provides the agent enabled status for template rendering.
6672
type AgentEnabledChecker interface {
6773
IsEnabled(ctx context.Context) bool
@@ -85,8 +91,7 @@ type funcsConfig struct {
8591
GitSyncEnabled bool
8692

8793
SetupRequiredChecker SetupRequiredChecker
88-
UpdateAvailable bool
89-
LatestVersion string
94+
UpdateChecker UpdateChecker
9095
AgentEnabledChecker AgentEnabledChecker
9196
LicenseChecker license.Checker
9297
LicenseManager *license.Manager
@@ -130,8 +135,20 @@ func defaultFunctions(cfg *funcsConfig) template.FuncMap {
130135
}
131136
return boolStr(cfg.SetupRequiredChecker.IsSetupRequired(context.Background()))
132137
},
133-
"updateAvailable": func() string { return boolStr(cfg.UpdateAvailable) },
134-
"latestVersion": func() string { return cfg.LatestVersion },
138+
"updateAvailable": func() string {
139+
if cfg.UpdateChecker == nil {
140+
return "false"
141+
}
142+
available, _ := cfg.UpdateChecker.GetUpdateInfo()
143+
return boolStr(available)
144+
},
145+
"latestVersion": func() string {
146+
if cfg.UpdateChecker == nil {
147+
return ""
148+
}
149+
_, version := cfg.UpdateChecker.GetUpdateInfo()
150+
return version
151+
},
135152

136153
// License functions
137154
"licenseValid": func() string {

0 commit comments

Comments
 (0)