Skip to content

✨ Added channel-aware dispatch to update-check notifications#28293

Draft
rob-ghost wants to merge 3 commits into
mainfrom
feat/notification-channels-dispatch
Draft

✨ Added channel-aware dispatch to update-check notifications#28293
rob-ghost wants to merge 3 commits into
mainfrom
feat/notification-channels-dispatch

Conversation

@rob-ghost
Copy link
Copy Markdown
Contributor

Problem

When UpdateChecker sends a critical security advisory, the dispatcher today renders the same content as both a banner in admin and an email to Owners and Administrators. The shared content is wrong on at least one surface: either the banner overflows with email-shaped body, or the email is a one-liner that does not carry the substance an admin needs. There is no path for an advisory to be tersely banner-worthy and substantively email-worthy at the same time.

Solution

The update-check dispatcher now reads channels from the upstream response and routes per surface. When the response carries channels, the banner channel goes through the existing notifications API as a banner record, and the email channel goes through the notification email service to Owner and Administrator users. The two surfaces no longer share a single body, so a security advisory can be a short banner and a substantive email at the same time, with content tailored to each.

When channels is absent the dispatcher falls back to the existing messages handling, so pre-channels UpdateChecker responses and the legacy updates.ghost.org service continue to work unchanged. When both channels and messages are present the dispatcher prefers channels and ignores messages, so installs never receive duplicate banners or duplicate emails.

A notification.dispatched log event names the notification id, the shape used (channels or messages), and the channels actually delivered, so production has a structured signal for what was sent and how.

Stacked on

rob-ghost added 2 commits May 29, 2026 16:54
The notification stack needs a shared, source-neutral way to send a
notification email. That's three responsibilities introduced here:

- A styled email shell template every sender wraps its semantic content
  in, so notification emails look like the rest of Ghost's transactional
  mail without each source duplicating presentation markup.
- A sanitiser that strips scripts, inline handlers and unsafe links from
  notification content, so an untrusted upstream source cannot ship a
  phishing or injection payload through this path.
- A small service that takes a recipient list, sanitises the content,
  renders the shell per recipient, and sends. It is intentionally
  recipient-agnostic: who receives a notification is a notification
  concern that callers decide for themselves.

Nothing wires it in yet; a follow-up commit adopts the primitive for
update-check's alert email.
update-check's alert branch had its own inline email assembly: it looked
up administrator users, built the subject, called the mailer per
recipient, and shipped the raw notification HTML unstyled. It owned no
concept beyond "call out to a mailer."

It now composes the notification email primitive with an admin lookup
via the users API and sends the alert through the shared, sanitised,
shell-rendered path. The admin lookup is pushed to a SQL filter (NQL:
status:active and roles.name in Owner/Administrator), so the database
returns only the relevant users instead of fetching all active users to
filter in JS.

The same Owner/Administrator users receive the same email, just through
the shared primitive that future sources (security advisories, etc.) can
compose the same way.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 1, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cf1cb775-02fb-4ed0-ae51-a4e6f3e6abbf

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/notification-channels-dispatch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@rob-ghost rob-ghost force-pushed the feat/notification-channels-dispatch branch from d8afba4 to 97d3fac Compare June 1, 2026 12:06
UpdateChecker now serves a channels-first response: each notification
carries a banner channel and/or an email channel with content tailored
to the surface. The legacy messages array continues to ride alongside
for pre-channels Ghost installs, but for installs running this code,
channels is the first-party shape and messages is the shim.

The update-check dispatcher routes per channel. When the response
carries channels, banner.content is added through the existing
notifications API as a banner record, and email.content is sent
through the notification email service to Owner/Administrator users.
The two surfaces no longer share a single body, so a critical
security advisory can be a short banner and a substantive email at
the same time.

When channels is absent the dispatcher falls back to the existing
messages handling, so pre-channels UpdateChecker responses (and the
legacy updates.ghost.org service) continue to work unchanged. When
both channels and messages are present the dispatcher picks channels
and ignores messages so installs never get double-delivered.

A notification.dispatched log event names the notification id, the
shape used (channels or messages), and the channels actually
delivered, so production can see at a glance what was sent and how.
@rob-ghost rob-ghost force-pushed the feat/notification-channels-dispatch branch from 97d3fac to 0189ab6 Compare June 1, 2026 12:31
Base automatically changed from feat/notification-email-shell to main June 4, 2026 11:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant