feat: add announcement banner - #3189
Conversation
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 69 |
| Duplication | -30 |
🟢 Coverage 7.72% diff coverage · -0.26% coverage variation
Metric Results Coverage variation ✅ -0.26% coverage variation (-1.00%) Diff coverage ✅ 7.72% diff coverage Coverage variation details
Coverable lines Covered lines Coverage Common ancestor commit (d9e406c) 82331 19145 23.25% Head commit (341ebdf) 83754 (+1423) 19256 (+111) 22.99% (-0.26%) Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch:
<coverage of head commit> - <coverage of common ancestor commit>Diff coverage details
Coverable lines Covered lines Diff coverage Pull request (#3189) 1438 111 7.72% Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified:
<covered lines added or modified>/<coverable lines added or modified> * 100%
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
e339568 to
00ec8a2
Compare
00ec8a2 to
501ab78
Compare
501ab78 to
51dd107
Compare
There was a problem hiding this comment.
Pull request overview
Adds a runtime-managed “announcement banner” feature to the web service, allowing admins to publish a message that is surfaced via the public config.json (so it can display pre-auth, including on the login page) and persisted in a configurable key-value store (defaulting to NATS).
Changes:
- Introduces an
announcementpackage with a persistent store abstraction and HTTP handlers guarded by a newAnnouncement.Writepermission. - Injects the stored (enabled) announcement into the web
config.jsonresponse at request time. - Adds configuration/defaults and proxy/settings wiring for the new store-backed settings and permission.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| services/web/pkg/service/v0/service.go | Wires announcement routes and injects announcement into config.json payload. |
| services/web/pkg/service/v0/service_test.go | Adds unit tests for the web service’s announcement selection helper. |
| services/web/pkg/service/v0/option.go | Adds AnnouncementStore option plumbing into the web service constructor. |
| services/web/pkg/server/http/server.go | Instantiates the persistent store and passes it into the web service. |
| services/web/pkg/config/options.go | Extends web options with an announcement object and defines its schema. |
| services/web/pkg/config/defaults/defaultconfig.go | Adds default store configuration for the web service. |
| services/web/pkg/config/config.go | Adds Store configuration section to the web service config. |
| services/web/pkg/announcement/service.go | Implements announcement management HTTP handlers with permission checks and body-size limit. |
| services/web/pkg/announcement/announcement.go | Implements the store-backed persistence for a single announcement record. |
| services/web/pkg/announcement/announcement_test.go | Adds Ginkgo/Gomega tests for store behavior and service authorization/body handling. |
| services/web/pkg/announcement/announcement_suite_test.go | Adds Ginkgo test suite bootstrap for the announcement package. |
| services/settings/pkg/store/defaults/permissions.go | Defines the new Announcement.Write permission. |
| services/settings/pkg/store/defaults/defaults.go | Grants Announcement.Write to relevant default bundles/roles (incl. admin). |
| services/proxy/pkg/config/defaults/defaultconfig.go | Routes /announcement to the web service via proxy defaults. |
Comments suppressed due to low confidence (1)
services/web/pkg/service/v0/service.go:173
currentAnnouncement()currently returns nil for both “no stored announcement” and “stored but disabled”, which makes it impossible to distinguish between “fall back to static config” and “explicitly clear/disable any static announcement”. Returning the static config value as a fallback when the store is missing/empty/unavailable, and returning nil only when the stored announcement explicitly disables it, avoids that ambiguity.
func (p Web) currentAnnouncement() *config.Announcement {
if p.announcementStore == nil {
return nil
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Always reflect the stored announcement in config.json: expose it while live and clear it otherwise. A disabled announcement now clears the config output instead of falling back to a statically configured one, which removes the precedence ambiguity raised in review.
Make the store authoritative only once it manages an announcement: an enabled record is exposed, a disabled record clears any static config, and an empty store falls back to a statically configured web.config.options.announcement. This matches the review request and stops a static announcement from being silently dropped.
The permission gates reading the full announcement state (including disabled ones) as well as writing it, so name it ReadWrite to match the other management permissions (Accounts.ReadWrite, Settings.ReadWrite, ...) and use the READWRITE operation.
Distinguish the http.MaxBytesReader limit from a malformed body: an oversized payload now returns 413 Request Entity Too Large instead of a generic 400.
Add a logger to the announcement service and log server-side errors before returning a 5xx, tagged with the request id so they correlate with the request log line from the logging middleware.
Replace the go-micro store with a NATS JetStream key-value bucket, created via reva's cache.NewNatsKeyValue, so the web service no longer depends on go-micro stores. The store methods now take a context, threaded through the config.json render path and the management handlers. Tests use a mockery mock of jetstream.KeyValue.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 17 changed files in this pull request and generated 1 comment.
Files not reviewed (2)
- services/web/mocks/key_value.go: Generated file
- services/web/mocks/key_value_entry.go: Generated file
Comments suppressed due to low confidence (4)
services/web/pkg/service/v0/service.go:95
- The /announcement routes are mounted unconditionally. If the announcement service/store is optional, this should be guarded (otherwise the handler will be nil / not initialized).
r.Route("/announcement", func(r chi.Router) {
r.Use(middleware.ExtractAccountUUID(
account.Logger(options.Logger),
account.JWTSecret(options.Config.TokenManager.JWTSecret),
))
services/web/pkg/service/v0/service.go:80
- announcement.NewService currently runs unconditionally and fails when AnnouncementStore isn't provided (options.AnnouncementStore is nil by default). If the runtime store is intended to be optional, service creation should be conditional so the web service can still start and serve static config.json without runtime announcement management.
This issue also appears on line 91 of the same file.
announcementService, err := announcement.NewService(
announcement.ServiceOptions{}.
WithLogger(options.Logger).
WithStore(options.AnnouncementStore).
WithGatewaySelector(options.GatewaySelector),
services/web/pkg/announcement/service.go:20
- PR description mentions an
Announcement.Writepermission for these endpoints, but the implementation checks forAnnouncement.ReadWrite(and settings defaults also registerAnnouncement.ReadWrite). Please align the PR description/API docs with the implemented permission name (or rename the permission consistently across code + defaults ifAnnouncement.Writeis the intended contract).
// _permission is the settings permission required to read and manage the announcement.
const _permission = "Announcement.ReadWrite"
services/web/pkg/config/config.go:41
- The new Store config fields use introductionVersion:"%%NEXT%%" placeholders. This placeholder isn’t used elsewhere in the repo and will likely leak into generated configuration docs. Please replace with the actual introduction version (or the project’s standard placeholder token, if any).
// Store configures the NATS JetStream key-value store used to keep runtime managed web settings,
// e.g. the announcement banner.
type Store struct {
Nodes []string `yaml:"nodes" env:"OC_PERSISTENT_STORE_NODES;WEB_STORE_NODES" desc:"A list of nodes to access the NATS JetStream store. See the Environment Variable Types description for more details." introductionVersion:"%%NEXT%%"`
Database string `yaml:"database" env:"WEB_STORE_DATABASE" desc:"The bucket name the store should use." introductionVersion:"%%NEXT%%"`
AuthUsername string `yaml:"username" env:"OC_PERSISTENT_STORE_AUTH_USERNAME;WEB_STORE_AUTH_USERNAME" desc:"The username to authenticate with the store." introductionVersion:"%%NEXT%%"`
AuthPassword string `yaml:"password" env:"OC_PERSISTENT_STORE_AUTH_PASSWORD;WEB_STORE_AUTH_PASSWORD" desc:"The password to authenticate with the store." introductionVersion:"%%NEXT%%"`
EnableTLS bool `yaml:"enable_tls" env:"OC_PERSISTENT_STORE_ENABLE_TLS;WEB_STORE_ENABLE_TLS" desc:"Enable TLS for the connection to the store." introductionVersion:"%%NEXT%%"`
TLSInsecure bool `yaml:"tls_insecure" env:"OC_INSECURE;OC_PERSISTENT_STORE_TLS_INSECURE;WEB_STORE_TLS_INSECURE" desc:"Whether to verify the server TLS certificates." introductionVersion:"%%NEXT%%"`
TLSRootCACertificate string `yaml:"tls_root_ca_certificate" env:"OC_PERSISTENT_STORE_TLS_ROOT_CA_CERTIFICATE;WEB_STORE_TLS_ROOT_CA_CERTIFICATE" desc:"The root CA certificate used to validate the server's TLS certificate. If provided WEB_STORE_TLS_INSECURE will be seen as false." introductionVersion:"%%NEXT%%"`
A statically configured announcement is not supported: it cannot be removed via the runtime API and disappears from the running session once the admin settings load (they read the store, not config.json). Make the store the single source of truth, config.json always reflects it and a statically configured options.announcement is ignored.
Replace reva's cache.NewNatsKeyValue (which retries with backoff and can hang the web service startup) with a direct nats.Connect + jetstream setup like the graph service, so an unreachable or misconfigured store surfaces as a clear startup error instead of a hang.
The announcement is runtime-managed and injected into config.json; it is not a static yaml config option. Exclude it from yaml (yaml:"-") so it is not presented as a configurable knob, keeping the json tag only for the config.json output.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 17 changed files in this pull request and generated no new comments.
Files not reviewed (2)
- services/web/mocks/key_value.go: Generated file
- services/web/mocks/key_value_entry.go: Generated file
Comments suppressed due to low confidence (3)
services/web/pkg/service/v0/service.go:163
- The PR description says a statically configured
web.config.options.announcementis kept when the runtime store is empty/unavailable, but this code always overwritesoptions.announcementwith the runtime value (andAnnouncementisyaml:"-", so static config is effectively unsupported). Please align the PR description/contract with the implemented runtime-only behavior.
// the runtime store is the single source of truth for the announcement banner: expose it
// when live, clear it otherwise. A statically configured value is not supported.
webConfig.Options.Announcement = p.currentAnnouncement(ctx)
services/web/pkg/announcement/service.go:20
- The PR description states the endpoint is protected by an
Announcement.Writepermission, but the implementation checks forAnnouncement.ReadWrite(and the settings defaults added the same). Please update the PR description (or rename the permission) so operators/admin UI know which permission to grant.
// _permission is the settings permission required to read and manage the announcement.
const _permission = "Announcement.ReadWrite"
services/web/pkg/server/http/server.go:95
- The server establishes a long-lived NATS connection for the announcement store but never drains/closes it on shutdown. This can leave subscriptions/IO hanging and makes graceful stop/restart noisier in operations and tests. Register an AfterStop hook to drain the connection when the service stops.
natsConn, err := nats.Connect(
strings.Join(options.Config.Store.Nodes, ","),
natspkg.Secure(options.Config.Store.EnableTLS, options.Config.Store.TLSInsecure, options.Config.Store.TLSRootCACertificate),
nats.UserInfo(options.Config.Store.AuthUsername, options.Config.Store.AuthPassword),
)
if err != nil {
return http.Service{}, fmt.Errorf("could not connect to nats for the announcement store: %w", err)
}
Runtime managed announcement banner shown above the top bar to all users, with a dialog showing Markdown detail text.
Model:
{ enabled, bannerText, infoText }, persisted in a NATS JetStream key-value store.GET /announcement(authenticated,Announcement.ReadWritepermission): the full stored state, for the admin UI.PUT /announcement: upsert; an empty banner text removes the record. There is no DELETE endpoint (an empty banner text is the implicit delete, andenabledhides/shows without deleting).config.json:options.announcement.{bannerText, infoText}is injected only whileenabledand a banner text are set, so prepared/disabled announcements stay private. The store is the single source of truth: config.json always reflects it, and a statically configuredweb.config.options.announcementis ignored (the field isyaml:"-", output only).Public by design
The message is part of the unauthenticated
config.json(loaded before login), so the banner also shows on the login and other pre-auth pages, e.g. to announce that a login provider is unavailable. Admins must not put sensitive information into it.Web frontend: opencloud-eu/web#2967