-
-
Notifications
You must be signed in to change notification settings - Fork 3k
feat(updater): tier 1 — notify admin and pad users of available updates #7601
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
JohnMcLear
wants to merge
24
commits into
ether:develop
Choose a base branch
from
JohnMcLear:feat/auto-update-tier1
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
49ed39d
docs(updater): add four-tier auto-update design spec
JohnMcLear dcc5ba3
docs(updater): add PR 1 (Tier 1 notify) implementation plan
JohnMcLear babc4fc
feat(updater): add shared types for auto-update subsystem
JohnMcLear afb8f06
feat(updater): clarify OutdatedLevel and EMPTY_STATE doc, drop path h…
JohnMcLear ace84c1
feat(updater): add semver helpers and vulnerable-below parser
JohnMcLear 288b63e
fix(updater): tighten semver regex to reject four-part versions
JohnMcLear ade42e4
feat(updater): add state persistence with schema validation
JohnMcLear 9abb899
fix(updater): reject null email and array latest in state validation
JohnMcLear bc70f1b
feat(updater): add install-method detector with override
JohnMcLear a892b6e
feat(updater): add policy evaluator
JohnMcLear 8ddf14c
feat(updater): add GitHub Releases checker with ETag support
JohnMcLear dac75fd
fix(updater): validate release fields and preserve ETag on prerelease
JohnMcLear 7798653
feat(updater): add email cadence decider
JohnMcLear a448875
fix(updater): tagChanged email fires regardless of cadence; drop unus…
JohnMcLear 66919b0
feat(settings): add updates.* and adminEmail settings
JohnMcLear eeb81d2
feat(updater): wire boot hook and periodic checker
JohnMcLear dd841ee
feat(updater): add /admin/update/status and /api/version-status endpo…
JohnMcLear cf65dd7
i18n(updater): add english strings for update banner, page, and pad b…
JohnMcLear 7415d23
feat(updater): add pad footer badge for severe/vulnerable status
JohnMcLear 04a15e8
feat(admin-ui): add update banner, page, and nav link
JohnMcLear 190584c
test(updater): add Playwright specs for admin banner/page and pad badge
JohnMcLear 26d7a0b
docs(updater): document tier 1 settings, badge, email cadence
JohnMcLear b196bf7
refactor(updater): dedupe helpers, fix misleading log, add banner sty…
JohnMcLear 110bddd
fix(updater): address review feedback — async wrap, tier=off skip, po…
JohnMcLear File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import {useEffect} from 'react'; | ||
| import {Link} from 'react-router-dom'; | ||
| import {Trans, useTranslation} from 'react-i18next'; | ||
| import {useStore} from '../store/store'; | ||
|
|
||
| export const UpdateBanner = () => { | ||
| const {t} = useTranslation(); | ||
| const updateStatus = useStore((s) => s.updateStatus); | ||
| const setUpdateStatus = useStore((s) => s.setUpdateStatus); | ||
|
|
||
| useEffect(() => { | ||
| let cancelled = false; | ||
| fetch('/admin/update/status', {credentials: 'same-origin'}) | ||
| .then((r) => r.ok ? r.json() : null) | ||
| .then((data) => { if (data && !cancelled) setUpdateStatus(data); }) | ||
| .catch(() => {}); | ||
| return () => { cancelled = true; }; | ||
| }, [setUpdateStatus]); | ||
|
|
||
| if (!updateStatus || !updateStatus.latest) return null; | ||
| if (updateStatus.currentVersion === updateStatus.latest.version) return null; | ||
|
|
||
| return ( | ||
| <div className="update-banner" role="status"> | ||
| <strong><Trans i18nKey="update.banner.title"/></strong>{' '} | ||
| <span> | ||
| <Trans | ||
| i18nKey="update.banner.body" | ||
| values={{latest: updateStatus.latest.version, current: updateStatus.currentVersion}} | ||
| /> | ||
| </span>{' '} | ||
| <Link to="/update">{t('update.banner.cta')}</Link> | ||
| </div> | ||
| ); | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import {Trans, useTranslation} from 'react-i18next'; | ||
| import {useStore} from '../store/store'; | ||
|
|
||
| export const UpdatePage = () => { | ||
| const {t} = useTranslation(); | ||
| const us = useStore((s) => s.updateStatus); | ||
|
|
||
| if (!us) return <div>{t('admin.loading', {defaultValue: 'Loading...'})}</div>; | ||
|
|
||
| const upToDate = !us.latest || us.currentVersion === us.latest.version; | ||
|
|
||
| return ( | ||
| <div className="update-page"> | ||
| <h1><Trans i18nKey="update.page.title"/></h1> | ||
| <dl> | ||
| <dt><Trans i18nKey="update.page.current"/></dt> | ||
| <dd>{us.currentVersion}</dd> | ||
| <dt><Trans i18nKey="update.page.latest"/></dt> | ||
| <dd>{us.latest ? us.latest.version : '—'}</dd> | ||
| <dt><Trans i18nKey="update.page.last_check"/></dt> | ||
| <dd>{us.lastCheckAt ?? '—'}</dd> | ||
| <dt><Trans i18nKey="update.page.install_method"/></dt> | ||
| <dd>{us.installMethod}</dd> | ||
| <dt><Trans i18nKey="update.page.tier"/></dt> | ||
| <dd>{us.tier}</dd> | ||
| </dl> | ||
| {upToDate ? ( | ||
| <p><Trans i18nKey="update.page.up_to_date"/></p> | ||
| ) : us.latest ? ( | ||
| <> | ||
| <h2><Trans i18nKey="update.page.changelog"/></h2> | ||
| <pre style={{whiteSpace: 'pre-wrap'}}>{us.latest.body}</pre> | ||
| <p><a href={us.latest.htmlUrl} rel="noreferrer noopener" target="_blank">{us.latest.htmlUrl}</a></p> | ||
| </> | ||
| ) : null} | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default UpdatePage; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| # Etherpad updates | ||
|
|
||
| Etherpad ships with a built-in update subsystem. **Tier 1 (notify)** is enabled by default: a banner appears in the admin UI when a new release is available, and pad users see a discreet badge if the running version is severely outdated or flagged as vulnerable. No automatic execution happens at this tier — admins are simply informed. | ||
|
|
||
| Tiers 2 (manual click), 3 (auto with grace window), and 4 (autonomous in maintenance window) are designed but not yet implemented. They will land in subsequent releases. | ||
|
|
||
| ## Settings | ||
|
|
||
| In `settings.json`: | ||
|
|
||
| ```jsonc | ||
| { | ||
| "updates": { | ||
| "tier": "notify", | ||
| "source": "github", | ||
| "channel": "stable", | ||
| "installMethod": "auto", | ||
| "checkIntervalHours": 6, | ||
| "githubRepo": "ether/etherpad", | ||
| "requireAdminForStatus": false | ||
| }, | ||
| "adminEmail": null | ||
| } | ||
| ``` | ||
|
|
||
| | Setting | Default | Notes | | ||
| | --- | --- | --- | | ||
| | `updates.tier` | `"notify"` | One of `"off"`, `"notify"`, `"manual"`, `"auto"`, `"autonomous"`. Higher tiers are silently downgraded if the install method does not allow them. PR 1 only honors `"notify"` and `"off"`. | | ||
| | `updates.source` | `"github"` | Reserved for future alternative sources. Only `"github"` is implemented. | | ||
| | `updates.channel` | `"stable"` | Reserved. Stable releases only. | | ||
| | `updates.installMethod` | `"auto"` | One of `"auto"`, `"git"`, `"docker"`, `"npm"`, `"managed"`. Auto-detects via filesystem heuristics. Set explicitly to override. | | ||
| | `updates.checkIntervalHours` | `6` | How often to poll GitHub Releases. | | ||
| | `updates.githubRepo` | `"ether/etherpad"` | Override for forks. | | ||
| | `updates.requireAdminForStatus` | `false` | Lock the `/admin/update/status` endpoint to authenticated admin sessions. Default `false` matches existing Etherpad behavior — `/health` already exposes `releaseId` publicly, and changelog data comes from a public GitHub release. Set `true` to hide the full update payload from non-admins without disabling the updater (`tier: "off"` is the heavier opt-out that removes the endpoints entirely). | | ||
| | `adminEmail` | `null` | Top-level. Contact for admin notifications. Setting it enables the email nudges below. | | ||
|
|
||
| ## What "outdated" means | ||
|
|
||
| - **`severe`** — running at least one major version behind the latest release. | ||
| - **`vulnerable`** — the running version is below a `vulnerable-below` threshold announced in a recent release. Releases declare these via a `<!-- updater: vulnerable-below X.Y.Z -->` HTML comment in their body. The newest such directive wins. | ||
|
|
||
| ## Email cadence (when `adminEmail` is set) | ||
|
|
||
| | Trigger | First send | Repeat | | ||
| | --- | --- | --- | | ||
| | Vulnerable status detected | Immediate | Weekly while still vulnerable | | ||
| | New release announced while still vulnerable | Immediate | n/a (one event per tag change) | | ||
| | Severely outdated detected | Immediate | Monthly while still severely outdated | | ||
| | Up to date | No email | — | | ||
|
|
||
| If `adminEmail` is unset, the updater never sends mail. The admin UI banner and the pad-side badge still work without it. | ||
|
|
||
| PR 1 ships the cadence machinery but does not yet wire a real SMTP transport — emails are logged with `(would send email)` until a future PR adds the transport. The dedupe state still advances correctly so admins are not bombarded once SMTP is wired. | ||
|
|
||
| ## Pad-side badge | ||
|
|
||
| Pad users see no version information by default. A small badge appears in the bottom-right corner only when: | ||
|
|
||
| - The instance is `severe` (one or more major versions behind), or | ||
| - The instance is `vulnerable` (running below an announced threshold). | ||
|
|
||
| The public endpoint `/api/version-status` returns only `{outdated: null|"severe"|"vulnerable"}` — it never leaks the running version, so attackers do not gain a fingerprint vector. | ||
|
|
||
| ## Disabling everything | ||
|
|
||
| Set `updates.tier` to `"off"`. No HTTP request will leave the instance and no banner or badge will render. | ||
|
|
||
| ## Privacy | ||
|
|
||
| The version check sends no telemetry. Etherpad fetches the public GitHub Releases API (`api.github.com/repos/<repo>/releases/latest`) with `If-None-Match` to be cache-friendly. The only metadata GitHub sees is the same as any other GitHub API client — your IP and a `User-Agent: etherpad-self-update` header. No instance ID, no version, no identifiers travel upstream. | ||
|
|
||
| ## How install method is detected | ||
|
|
||
| `updates.installMethod` defaults to `"auto"`, which uses these heuristics in order: | ||
|
|
||
| 1. `/.dockerenv` exists → `"docker"`. | ||
| 2. `.git/` directory present and the install root is writable → `"git"`. | ||
| 3. `package-lock.json` present and writable → `"npm"`. | ||
| 4. Otherwise → `"managed"`. | ||
|
|
||
| Set the value explicitly if the heuristics get it wrong (e.g., a docker container that bind-mounts a writable git checkout). | ||
|
|
||
| In PR 1 (notify only) the install method does not change behavior — every install method gets the banner. From PR 2 onward the install method gates whether the manual-click and automatic tiers can run; only `"git"` is initially supported for write tiers. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2. Updates off hangs admin page
🐞 Bug≡ CorrectnessAgent Prompt
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools