* feat: add timeslider line numbers
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* perf: coalesce timeslider line-number updates
Addresses Qodo review: updateLineNumbers() was called synchronously
from applyChangeset() on every changeset, forcing full-document layout
reads/writes during timeslider scrubbing/playback. scheduleLineNumberUpdate()
also queued a fresh double-rAF pair for every resize tick. Add a pending
flag so only one rAF pair is in flight, and route applyChangeset() through
the scheduler.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: warn when a pending edit is not accepted
Show a gritter warning only when the pad disconnects while a local commit is still awaiting acceptance, leaving normal editing UI unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test: cover unaccepted-commit warning path
Addresses Qodo review: adds regression coverage for the two contract
changes this PR introduces — acceptCommit() must clear the pending
marker so hasUnacceptedCommit() returns false after a server ACK, and
the disconnect handler must surface the unsaved-edit gritter when a
commit is still pending.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: allow undo of clear authorship colors without disconnect (#2802)
When a user clears authorship colors and then undoes, the undo changeset
re-applies author attributes for all authors who contributed text. The
server was rejecting this because it treated any changeset containing
another author's ID as impersonation, disconnecting the user.
The fix distinguishes between:
- '+' ops (new text): still reject if attributed to another author
- '=' ops (attribute changes on existing text): allow restoring other
authors' attributes, which is needed for undo of clear authorship
Also removes the client-side workaround in undomodule.ts that prevented
clear authorship from being undone at all, and adds backend + frontend
tests covering the multi-author undo scenario.
Fixes: https://github.com/ether/etherpad-lite/issues/2802
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use robust Playwright assertions in authorship undo tests
- Use toHaveAttribute with regex instead of raw getAttribute + toContain
- Check div/span attributes within pad body instead of broad selectors
- Use Playwright auto-retry (expect with timeout) instead of toHaveCount(0)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: handle confirm dialog and sync timing in Playwright tests
- Add page.on('dialog') handler to accept the confirm dialog triggered
by clearAuthorship when no text is selected (clears whole pad)
- Use auto-retrying toHaveAttribute assertions instead of raw getAttribute
- Increase cross-user sync timeouts to 15s for CI reliability
- Add retries: 2 to multi-user test for CI flakiness
- Scope assertions to pad body spans instead of broad selectors
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use persistent socket listeners to avoid missing messages in CI
Replace sequential waitForSocketEvent loops with single persistent
listeners that filter messages inline. This prevents race conditions
where messages arrive between off/on listener cycles, causing timeouts
on slower CI runners.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: reject - ops with foreign author to prevent pool injection
The '-' op attribs are discarded from the document but still get added
to the pad's attribute pool by moveOpsToNewPool. Without this check, an
attacker could inject a fabricated author ID into the pool via a '-' op,
then use a '=' op to attribute text to that fabricated author (bypassing
the pool existence check).
Now all non-'=' ops (+, -) with foreign author IDs are rejected.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: use not.toHaveClass for cleared authorship spans
Addresses Qodo review: linestylefilter skips attribs with empty values,
so a span with author='' has no class attribute at all. The previous
negative-lookahead regex on the class attribute failed against a null
attribute and was flaky in CI. Switch to not.toHaveClass(/author-/),
which also passes when the attribute is missing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a core timeslider playback speed setting with an original-speed default, a realtime mode that uses revision timestamps, and frontend coverage for the new behavior.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat!: replace Abiword with LibreOffice and add DOCX export (#4805)
The Abiword converter is dropped. Abiword's DOCX export is weak and the
project is niche on modern platforms; LibreOffice (soffice) is the
common deployment path and now serves as the sole converter backend.
DOCX is added as an export format and becomes the new target for the
"Microsoft Word" UI button. The /export/doc URL still works for legacy
API consumers.
BREAKING CHANGE: The 'abiword' setting, the INSTALL_ABIWORD Dockerfile
build arg, the abiwordAvailable clientVar, and the
#importmessageabiword UI element (with locale key
pad.importExport.abiword.innerHTML) are removed. Deployments relying on
Abiword must configure 'soffice' instead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: add docxExport feature flag and abiword deprecation WARN
- Add `docxExport: true` setting to opt out of DOCX (use legacy DOC)
- Pass `docxExport` to client via clientVars
- Use `docxExport` flag in pad_impexp.ts for Word button format
- Emit a specific WARN when deprecated `abiword` config is detected
- Update settings.json.template and settings.json.docker with docxExport
- Add docxExport to ClientVarPayload type in SocketIOMessage.ts
Agent-Logs-Url: https://github.com/ether/etherpad/sessions/9afc5291-73b2-4b66-b028-feed39e7056f
Co-authored-by: JohnMcLear <220864+JohnMcLear@users.noreply.github.com>
* refactor: extract wordFormat variable and improve docxExport comment
Agent-Logs-Url: https://github.com/ether/etherpad/sessions/9afc5291-73b2-4b66-b028-feed39e7056f
Co-authored-by: JohnMcLear <220864+JohnMcLear@users.noreply.github.com>
* fix: restore import-limitation message when no converter is configured
The abiword removal dropped both the #importmessageabiword DOM element
and its locale key, but Copilot's refactor still expected the show()
call to surface a message when exportAvailable === 'no'. Result: users
with no soffice binary got silent failure instead of an explanation.
Add #importmessagenoconverter back with updated, LibreOffice-focused
copy (new locale key pad.importExport.noConverter.innerHTML) and flip
the hidden prop when the client knows no converter is available.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* i18n: inline English fallback for noConverter import message
The original abiword message existed in ~70 locale files and was
removed from all of them by this PR. The replacement key was only
added to en.json, so non-English users had an empty div until
translators localize. Follow the project's usual pad.html pattern
(e.g. line 146's "Font type:") and include the English text inside
the div as the fallback content; html10n replaces it when a
translation is available.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Revert "i18n: inline English fallback for noConverter import message"
This reverts commit f336f24d. Follow the project convention: add the
new locale key to en.json only and let translations catch up via the
translation system, rather than putting inline fallback in the template.
* i18n: leave non-English locale files untouched
The PR had removed pad.importExport.abiword.innerHTML from ~82 locale
files alongside its removal from en.json. The replacement message uses
a new key (pad.importExport.noConverter.innerHTML) in en.json only, so
churning every localisation file for a key that is no longer referenced
produces useless translation diffs. Restore every non-en locale file to
its pre-PR state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: JohnMcLear <220864+JohnMcLear@users.noreply.github.com>
* docs(openapi): document apikey auth in openapi.json (#7532)
The API accepts the key via ?apikey=, ?api_key=, or the apikey header, but
only ?apikey= was advertised in /api-docs.json. /api/{version}/openapi.json
was worse: it hardcoded an OAuth2 scheme even when Etherpad was started in
apikey auth mode.
Switch both generators on settings.authenticationMethod and publish apiKey
schemes for the query (apikey, api_key) and header (apikey) variants. The
openapi.ts definition is now regenerated per request so runtime settings
are reflected.
The raw authorization: <key> header still works in code but is deliberately
not documented — pinning it in the spec would ossify a quirk.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(openapi): add apiKeyAlias/apiKeyHeader conditionally in RestAPI.ts
In SSO mode, apiKeyAlias and apiKeyHeader were always present in
securitySchemes even though they're only relevant when
authenticationMethod is 'apikey'. Mirror the pattern used for the sso
scheme: add these two schemes dynamically inside the apikey branch, and
mark them optional in the TypeScript type annotation.
Agent-Logs-Url: https://github.com/ether/etherpad/sessions/1d440432-7389-462e-9aac-9a3c027640e8
Co-authored-by: JohnMcLear <220864+JohnMcLear@users.noreply.github.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: JohnMcLear <220864+JohnMcLear@users.noreply.github.com>
* checkPlugin: flag absolute /static/plugins/ paths in templates (#5203)
Plugin templates that reference assets as \`/static/plugins/...\`
(absolute) silently break any Etherpad instance hosted behind a reverse
proxy at a sub-path — the browser resolves the path against the domain
root instead of the proxy prefix and the asset 404s. The right form is
\`../static/plugins/...\` (relative), which ep_embedmedia PR #4 fixed
manually and which #5203 asked for as a mechanical check.
Walk \`templates/\` and \`static/\` of the plugin, scan every \`*.ejs\` /
\`*.html\` for \`/static/plugins/\` not preceded by a URL scheme, dot, or
word char (so \`https://host/static/plugins/...\` and already-correct
\`../static/plugins/...\` stay untouched). Warn normally; in \`autofix\`
mode rewrite to the relative form in place.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* checkPlugin: skip autofix for static/*.html
Addresses Qodo review: HTML served from a plugin's static/ directory
resolves against /static/plugins/<plugin>/static/..., so rewriting
/static/plugins/... to ../static/plugins/... yields a broken URL. Keep
scanning static/ for warnings but no longer rewrite, and clarify the
remediation guidance to point at the file's own location.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pnpm's default \`auto\` package-import-method eventually falls through
to \`copyfile\`, which uses \`copy_file_range\`. That syscall fails on
ZFS with \`EAGAIN: resource temporarily unavailable\` (see
https://github.com/pnpm/pnpm/issues/7024), so \`docker compose build\`
aborts inside the \`RUN pnpm install\` step on any host with a ZFS
root. Operators had to hand-patch every pnpm invocation in the
Dockerfile and install scripts.
Force \`package-import-method=hardlink\` in \`.npmrc\` so all pnpm
invocations (Docker build, \`bin/installDeps.sh\`,
\`bin/installLocalPlugins.sh\`, \`bin/updatePlugins.sh\`) pick up the
setting automatically. Hardlinks are fast, save disk, and work on
every filesystem Etherpad supports.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Rewrite title + About to lead with what Etherpad is for
(authorship, sovereignty, malleability) before features
- Rewrite Project Status to make the maintainer ask specific and
to situate the project's 16-year track record
- Add new "Who uses Etherpad" section with categorical adopter
profiles so institutional evaluators have proof points on-page
No code or behaviour changes.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
These files are stale: there's no CI/tooling left that reads them.
- .travis.yml — Etherpad moved to GitHub Actions years ago. The
workflows in .github/workflows/ are the source of truth.
- .lgtm.yml — LGTM was sunset by GitHub in late 2022.
- start.bat — README only documents the PowerShell installer for
Windows now (irm .../installer.ps1 | iex), no docs or scripts
reference start.bat.
- bin/installOnWindows.bat — same; not referenced by README, docs
or workflows.
Also drop the .travis.yml line from the plugin layout in
doc/plugins.md and replace it with a pointer at .github/workflows/.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: flush pending changesets immediately after reconnect
After reconnecting, setUpSocket() did not call handleUserChanges(),
so any edits made while disconnected were not sent to the server until
the user made another change. This caused divergent pad state between
users.
Now calls handleUserChanges() after reconnect to immediately transmit
any pending local changesets.
Fixes#5108
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: defer handleUserChanges on connect to avoid editor init race
Calling handleUserChanges() synchronously in setUpSocket() caused
"Cannot read properties of null (reading 'changeset')" because the
editor isn't fully initialized on first connect. Deferred with
setTimeout(500ms) to allow initialization to complete.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add test for pending changeset flush after reconnect
Verifies that edits made while disconnected are transmitted to the
server immediately upon reconnection.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: flush pending changesets on actual reconnect, not just initial connect
setUpSocket() only runs during initialization. Move handleUserChanges()
to the reconnect code path so pending edits are flushed when the
connection is re-established.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: flush pending changes when isPendingRevision clears after reconnect
The existing fix in setChannelState('CONNECTED') calls handleUserChanges(),
but at that point isPendingRevision is still true so changes are blocked.
The real trigger must be in setIsPendingRevision(): when it transitions
from true to false (after all CLIENT_RECONNECT messages are processed),
call handleUserChanges() to flush any locally-queued edits.
Also adds a targeted regression test that simulates the exact reconnect
state transitions and verifies pending edits reach the server.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: rewrite reconnect flush test for reliability
Replaced the fragile offline/online simulation with a direct test that
uses separate browser contexts. Simplified to a single test that
exercises the exact setIsPendingRevision(false) -> handleUserChanges()
codepath and verifies the flushed text is visible from another client.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: remove Playwright reconnect test (not feasible)
The reconnect test requires access to pad.collabClient internals which
are not exposed on window in the browser context. Playwright cannot
call setStateIdle/setIsPendingRevision/setChannelState. The backend
tests adequately cover the code fix.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: don't flush in setChannelState to avoid editor init race
Calling handleUserChanges() in setChannelState('CONNECTED') fires
synchronously on first connect before the editor is fully initialized,
breaking chat/user_name tests. The setIsPendingRevision(false) trigger
is sufficient for the reconnect path, and the existing
setTimeout(handleUserChanges, 500) in setUpSocket() already handles
the initial connect.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove setTimeout flush in setUpSocket — rely on setIsPendingRevision trigger
The setTimeout(handleUserChanges, 500) in setUpSocket was a timing hack
that:
- Only fires on initial connect (setUpSocket is called once at the end
of getCollabClient; reconnects go through pad.ts:248 which calls
setChannelState('CONNECTED') directly, bypassing setUpSocket).
- Doesn't actually fix issue #5108 (the reconnect-flush bug). That's
fixed deterministically by the wasPending && !value trigger in
setIsPendingRevision, which fires whenever the server's CLIENT_RECONNECT
message lands (both for noChanges and after replaying revisions).
- Introduced a 500ms race window on initial pad load.
The reconnect path now relies entirely on the deterministic event-based
trigger (setIsPendingRevision), with no timing assumptions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Plugin CI is still failing on ERR_PNPM_IGNORED_BUILDS even with the
build-script policy declared in both pnpm-workspace.yaml (#7523) and
package.json (#7525). pnpm's strict-dep-builds defaults to true in 10+,
so any transitive dep with an unrecognized postinstall fails the build.
For etherpad-lite — and especially for downstream plugin repos that
pull this codebase as their core install — that's a footgun: the moment
some new transitive ships a postinstall, every plugin's CI explodes.
Set strictDepBuilds: false in pnpm-workspace.yaml AND
strict-dep-builds=false in .npmrc as a defensive layer, so unknown
postinstalls become a warning instead of a hard failure. The
allow/ignore lists still control what actually runs.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Some pnpm versions don't read onlyBuiltDependencies / ignoredBuiltDependencies
from pnpm-workspace.yaml — leaving CI on plugin repos to fail with
ERR_PNPM_IGNORED_BUILDS even after #7523 added the workspace.yaml entries.
Mirror the same configuration into package.json's "pnpm" field, which is
the older (and more widely supported) location. The two files are kept in
sync; whichever pnpm version reads the values picks them up from one or
the other.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The test was failing intermittently in CI with "Element is not attached
to the DOM" at selectText(). The long text typed by writeToPad triggers
DOM re-renders in Etherpad, and on slower CI runners the div locator
could detach between resolve and action.
Switch to keyboard-based selection (Ctrl+A) via the existing
selectAllText helper, which doesn't depend on a specific DOM element
staying attached. Verified stable across 10 repeated runs on chromium.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: explicitly ignore scarf build scripts to fix bin install CI
My earlier commit removed @scarf/scarf from onlyBuiltDependencies, but
pnpm 10.7+ requires every encountered build script to be either allowed
(onlyBuiltDependencies) or explicitly ignored (ignoredBuiltDependencies),
otherwise it errors with ERR_PNPM_IGNORED_BUILDS.
The Update Plugins workflow runs pnpm install inside ./bin, which pulls
in ep_etherpad-lite -> swagger-ui-dist -> @scarf/scarf as a transitive
dep. Add scarf to ignoredBuiltDependencies so pnpm silently skips its
install-time telemetry script instead of failing the install.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(tests): serialize admin-spec tests to avoid shared-state races
Admin tests mutate global server state (install/uninstall plugins,
save settings, restart etherpad). Running them in parallel — both
across browsers (chromium + firefox) and with playwright's
fullyParallel mode — caused intermittent failures where one test's
install would leak into another test's assertions (e.g. seeing 3
installed plugins when expecting 2, or "ep_font_colormain" in the
hooks list when expecting only core hooks).
Two changes:
1. Drop firefox from test-admin — admin UI is browser-agnostic and
running two browsers concurrently was the main race source
2. Add test.describe.configure({ mode: 'serial' }) to each admin
spec file as a safety net in case the project list or worker
count changes in the future
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(tests): use ep_set_title_on_pad for admin plugin install test
ep_font_color depends on ep_plugin_helpers, so installing it via the
admin UI actually installs two plugins (ep_font_color + ep_plugin_helpers),
making the installed plugins table show 3 rows (including ep_etherpad-lite)
instead of the 2 the test asserts.
Switch to ep_set_title_on_pad which has no transitive deps, so install
produces exactly one new row as the test expects.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pnpm 10.7+ blocks dependency postinstall scripts unless explicitly
approved via onlyBuiltDependencies. Only esbuild genuinely needs its
postinstall (to download platform-specific native binaries).
Remove @scarf/scarf (install-time telemetry from swagger-ui-dist) and
@swc/core (not in the dependency tree) from the approved list.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Bump ueberdb2 to ^5.0.42 — adds production dependencies for db drivers
5.0.42 moves core database drivers (dirty-ts, rusty-store-kv) to
production dependencies so they're installed in production/Docker.
Optional drivers (cassandra, mongodb, etc.) are now optional
peerDependencies.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Update to ueberdb2 ^5.0.43 — fixes Node engine requirement
5.0.43 relaxes the Node engine from >=22.22.0 to >=18.0.0,
matching etherpad-lite's supported Node versions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add docs for aceRegisterLineAttributes hook
Documents the new hook in both .md and .adoc client-side hook references.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix: correct source file path from .js to .ts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The pnpm-workspace.yaml references admin, doc, and ui packages that
aren't copied into the production Docker image. This caused pnpm
install warnings and incomplete dependency resolution, which broke
ueberdb2's modular build (missing bin symlinks, broken module paths).
Fix: overwrite pnpm-workspace.yaml in the production stage with a
minimal version that only lists the packages present in the image
(src and bin).
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
5.0.41 includes the rolldown runtime fix (ether/ueberDB#925) needed
for the lazy-loaded database drivers to work at runtime.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ueberdb2 5.0.40 lazy-loads database drivers so only the configured
backend's dependencies need to be installed. Fixes the Docker production
build crash: "Cannot find module 'cassandra-driver'"
See ether/ueberDB#924
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When pressing Enter in a line with attributes like heading or align,
the attributes were lost on the new line. Lists had special-case code
in doReturnKey() but all other line attributes were ignored.
This adds a new client hook aceRegisterLineAttributes that plugins can
use to declare which attributes should be preserved when a line is
split by Enter:
- Enter at middle/end of line: attribute is copied to the new line
- Enter at start of line (col 0): attribute moves down with the text,
the now-empty line above gets the attribute removed
Plugins register by returning attribute names from the hook:
exports.aceRegisterLineAttributes = () => ['heading'];
Fixesether/ep_headings2#7
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The plugin publish workflow ran `git push --follow-tags` after `pnpm
version patch`. `--follow-tags` is non-atomic per ref: if a concurrent
publish run won the race, the branch fast-forward would be rejected
but the tag push would still land — leaving a dangling `vN+1` tag with
no matching version-bump commit on the branch. Every subsequent push
would then fail forever with `npm error fatal: tag 'vN+1' already
exists`, because `pnpm version patch` would re-derive the same tag
name from the unchanged `package.json`.
On 2026-04-08, a single churn day (badge fixes + Dependabot merges
firing back-to-back) put ~46 plugins into this state simultaneously.
Recovery required hand-bumping `package.json` past the dangling tag
on every affected repo, twice (a second wave appeared after the first
sweep finished, racing the next wave of publishes).
Fix: use `git push --atomic origin <branch> <tag>` so the branch
update and the tag update succeed or fail as a single server-side
transaction. A rejected branch push now also rejects the tag push,
the run aborts cleanly, and the next workflow tick can retry against
the up-to-date refs without leaving any orphaned tags.
Also derive the new tag name from `package.json` after the bump
(rather than parsing pnpm version's stdout, which has historically
varied) and pass it explicitly into the push.
Adds a backend regression test that asserts the workflow file uses
`--atomic`, does not contain a literal `git push --follow-tags`
command (ignoring the historical comment), and includes both the
branch ref and the freshly-bumped tag in the atomic push. The test
gates against accidental reverts.
This file is the source of truth that `bin/plugins/checkPlugin.ts`
propagates into every `ether/ep_*` plugin's `.github/workflows/`, so
the next `update-plugins` cron tick will roll the fix out across all
plugins automatically.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: setup-trusted-publishers.sh works with real npm trust CLI
Two issues found when running the script for the first time after #7490:
1. `npm trust github --file` wants ONLY the workflow filename basename
(e.g. `test-and-release.yml`), not the full
`.github/workflows/test-and-release.yml` path. npm errors out with
"GitHub Actions workflow must be just a file not a path" otherwise.
Constants updated.
2. `npm trust github` requires 2FA on accounts that have it enabled,
and there is no way to disable that requirement. Add a `--otp <code>`
pass-through flag and forward it to every call so a maintainer can
batch-process multiple packages within a single TOTP window.
Documented the limitation in the script header.
Also reword the call site so the npm command line is built without
shell-string round-tripping (passing $CMD through `$( $CMD )` was
unrelated to this bug but was bad practice).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: setup-trusted-publishers.sh recognizes 409 as already-configured
When --skip-existing is set, treat HTTP 409 Conflict from
POST /-/package/<name>/trust as 'already configured' so re-runs of
the bulk script don't fail on packages that were configured in a
previous run.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: cover setup-trusted-publishers.sh, harden against set -e, document --otp
Addresses qodo review on #7491:
- Add backend regression test that shims `npm` on PATH and asserts
`--file` is given the workflow basename (never a path), `--otp` is
forwarded to every `npm trust github` call when supplied, and the
loop survives a non-zero exit so `--skip-existing` can absorb 409
Conflict responses from the registry.
- Wrap the `npm trust github` invocation in `set +e` / `set -e`. The
`if configure_one` already shields the function from errexit in
practice, but a future refactor moving the call site out of an `if`
would silently reintroduce the bug — the explicit shim makes intent
obvious and survives such refactors.
- Document `--otp` and the 2FA / TOTP-expiry workflow in
doc/npm-trusted-publishing.md so maintainers don't follow the docs
and hit EOTP.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The daily update-plugins workflow already syncs boilerplate (workflows,
dependabot.yml, etc.) into every ether/ep_* repo via checkPlugin, but it
never closes the loop on the Dependabot PRs that config produces. With
plugin repos having no per-repo auto-merge wiring, those PRs sit green
indefinitely (e.g. ether/ep_loading_message#77).
Add a final step that, after the per-plugin updates run, walks every
ep_* repo and squash-merges any open Dependabot PR whose mergeStateStatus
is CLEAN — i.e. no conflicts, branch up to date, all required checks
green. Anything else (DIRTY, BLOCKED, BEHIND, UNSTABLE, …) is left alone
for a human.
No semver gating: trust each plugin's own CI to fail on a breaking
major bump rather than pre-filtering by version delta.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: capture head revision atomically with atext to prevent mismatched apply
When constructing CLIENT_VARS, pad.atext was captured at one point but
pad.getHeadRevisionNumber() was called later. If concurrent edits
advanced the revision between these two reads, the client received
initialAttributedText from rev N but rev=N+3, causing "mismatched apply"
errors when the next changeset arrived (expecting rev N+3 text).
Now captures headRev at the same time as atext and uses the captured
value consistently in CLIENT_VARS and sessionInfo.
Fixes#4040
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: flush missed revisions after socket joins pad room
During handleClientReady(), the server awaits the clientVars hook before
socket.join(). Any revisions appended during that await window are
broadcast to existing room members but the connecting socket misses them.
Call updatePadClients(pad) after joining to flush any such revisions.
Also adds a regression test that injects a slow clientVars hook and
verifies the connecting client receives catch-up changesets for edits
that occurred during the hook await window.
Fixes#4040
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: fix race condition in clientVars hook test
Listen for messages during handshake to avoid missing NEW_CHANGES that
arrive before the explicit waitForSocketEvent listener is attached.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: initialize sessionInfo.time before catch-up updatePadClients
The catch-up updatePadClients() call introduced in this PR could send
NEW_CHANGES with timeDelta=NaN because sessionInfo.time was never set
for new sessions. NaN poisons the client-side broadcast/timeslider
currentTime tracking.
Initialize sessionInfo.time to the timestamp of the snapshot revision
before the catch-up flush, with a fallback to Date.now() if the
revision date is unavailable.
Also strengthens the regression tests:
- Validate that initialAttributedText matches the pad AText at the
EXACT advertised rev (not just the latest pad text), using
pad.getInternalRevisionAText(rev).
- Add a load test that hammers the pad with concurrent edits while
multiple clients connect, asserting CLIENT_VARS consistency under
the exact race condition the fix is targeting.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: replace open-ended load loop with bounded mid-handshake edit
The previous load test ran 'while (!stopLoad) await pad.setText(...)'
in the background while the test connected clients. This saturated
ueberDB's write queue and on shutdown the queued writes never drained,
hanging the mocha process for the full 6h GitHub Actions job timeout.
Replace it with a bounded approach: a clientVars hook lands 3 edits
mid-handshake (deterministic, no background loop, no shutdown hang).
Still exercises the exact race the fix targets — an edit advancing
the rev after the atext snapshot but before CLIENT_VARS is sent —
and asserts AText / rev consistency via getInternalRevisionAText.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: address remaining Qodo concerns on PR #7480
Addresses Qodo review items 1, 2, 5 from
https://github.com/ether/etherpad-lite/issues/comments/4194702740 :
- Concern 1 (no loadTesting reproduction test): the suite now toggles
settings.loadTest = true in before(), restores in after(). The
middle test also pre-populates the pad with 20 revisions before
connecting so we genuinely exercise a busy/loaded pad rather than a
fresh one.
- Concern 2 (no CLIENT_VARS / NEW_CHANGES delay test): the slow
clientVars hook in the middle test now has explicit setTimeout
delays before AND after the mid-handshake edits, so the race window
between atext snapshot and CLIENT_VARS send is observably wide
rather than relying on async scheduling alone. The test also
collects post-handshake messages and asserts a NEW_CHANGES catch-up
arrives when the pad advanced past the advertised rev.
- Concern 5 (test doesn't validate rev): both rev-consistency tests
use pad.getInternalRevisionAText(advertisedRev) and assert text and
attribs match, not just `pad.text() === clientVars.text`.
Concerns 3 (connect can miss revisions) and 4 (NaN timeDelta) were
already addressed in earlier commits on this branch via the catch-up
updatePadClients() call and the sessionInfo.time initialization.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: migrate npm publish to OIDC trusted publishing (#7401)
Replaces NPM_TOKEN-based publishing with npm Trusted Publishing over
OIDC for both etherpad-lite core and the shared plugin publish
template. Tokens no longer expire every 90 days; each publish
authenticates via a short-lived OIDC token issued to the GitHub
Actions runner.
Changes:
- bin/plugins/lib/npmpublish.yml: the reusable workflow propagated to
every ether/ep_* plugin via the update-plugins cron. Now bumps Node
to 22, upgrades npm to >=11.5.1, declares id-token: write, drops
NODE_AUTH_TOKEN, and calls `npm publish --provenance --access public`
directly (not via pnpm/gnpm wrappers, which obscure the npm CLI
version requirement).
- bin/plugins/lib/test-and-release.yml: the parent workflow that calls
npmpublish.yml as a reusable workflow. Top-level and release-job
permissions now grant id-token: write so the OIDC token can flow
into the called workflow.
- .github/workflows/releaseEtherpad.yml: core's own publish workflow
for the ep_etherpad package. Same OIDC migration; keeps the gnpm
install + rename steps but switches the final publish to npm.
- doc/npm-trusted-publishing.md: explains how trusted publishing
works, the one-time per-package setup that has to happen on
npmjs.com, requirements (Node 22.14+, npm 11.5.1+, cloud runners),
and common errors.
The next update-plugins cron run will propagate the new template to
every plugin. Once that lands and the trusted publisher is configured
on npmjs.com per package, the NPM_TOKEN secret can be removed.
Closes#7401
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add bin/setup-trusted-publishers.sh for bulk OIDC config (#7401)
Adds a script that automates the per-package trusted-publisher setup
that previously had to be done by clicking through npmjs.com once for
each of the 80+ ep_* plugins. Uses the new `npm trust github` CLI
(npm >= 11.5.1) so the whole org can be configured in one shot:
npm login
bin/setup-trusted-publishers.sh
The script:
- Discovers every non-archived ether/ep_* repo via `gh repo list`
- Maps ep_etherpad to the etherpad-lite repo / releaseEtherpad.yml,
and every plugin to its same-named repo / test-and-release.yml
- Runs `npm trust github <pkg> --repository <org>/<repo> --file
<workflow> --yes` for each package
- Supports --dry-run, --packages <comma list>, and --skip-existing
- Verifies npm >= 11.5.1 and that the user is logged in before doing
anything destructive
Doc updated to feature the script as the recommended setup path,
with manual web-UI steps kept as a fallback.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: don't bump CI Node version to 22 for OIDC
npm 11.5.1 (the version that ships trusted publishing) actually
requires '^20.17.0 || >=22.9.0', not Node 22.14+. The npm docs
recommend Node 22 but only because that's what bundles a recent
enough npm — installing 'npm@latest' on top of Node 20.17+ works
just as well.
The repo already requires Node >= 20.0.0 in engines.node and the
setup-node@v6 'version: 20' input resolves to the latest 20.x
(currently 20.20+), which satisfies npm 11's range. Revert the CI
publish workflows from node-version: 22 back to 20 so this PR does
not raise the Node bar at all.
Doc updated to explain the actual constraint.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add one-line installer script (#7466)
Adds bin/installer.sh, a small POSIX shell script that:
- Verifies prerequisites (git, Node.js >= 18)
- Installs pnpm globally if missing (with sudo fallback)
- Clones etherpad-lite (configurable branch / dir)
- Runs `pnpm i` and `pnpm run build:etherpad`
- Optionally starts Etherpad if ETHERPAD_RUN=1
Users can now install Etherpad with a single command:
curl -fsSL https://raw.githubusercontent.com/ether/etherpad-lite/master/bin/installer.sh | sh
README updated to feature the one-liner above the existing
Docker-Compose / manual install instructions.
Closes#7466
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add installer-test workflow + Windows PowerShell installer
- bin/installer.ps1: PowerShell port of installer.sh so the one-liner
also works on Windows via 'irm ... | iex'.
- .github/workflows/installer-test.yml: end-to-end CI that runs each
installer against the PR's own commit (via ETHERPAD_REPO/BRANCH env
vars), verifies clone + node_modules + admin SPA artifacts, and
smoke-tests by starting Etherpad and curling /api. Runs on
ubuntu-latest, macos-latest, and windows-latest. Includes a
shellcheck job for installer.sh.
- README: feature the Windows one-liner alongside the POSIX one.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: fix windows smoke test - wrap pnpm in cmd /c
Start-Process can't run pnpm.cmd directly ("not a valid Win32 application").
Wrap it via cmd.exe /c instead, and bump the wait window to 90s for slower
Windows runners. Also dump stderr alongside stdout when the smoke test
fails for easier debugging.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address Qodo review on installer (#7485)
Two correctness issues caught by Qodo:
1. Node version mismatch: installer required Node >= 18, but the repo's
engines.node is >= 20. Bump REQUIRED_NODE_MAJOR to 20 in both shell
and PowerShell installers, and update the README's quick-install
prerequisite and Requirements section to match.
2. Branch ignored for existing checkouts: when ETHERPAD_DIR already
existed, the script ran 'git pull --ff-only' on whatever branch
happened to be checked out, ignoring ETHERPAD_BRANCH and never
verifying ETHERPAD_REPO. The existing-dir path now:
- validates the remote URL matches ETHERPAD_REPO
- refuses to clobber uncommitted changes (excluding pnpm-lock.yaml,
which pnpm i rewrites during install)
- fetches with --tags --prune
- checks out ETHERPAD_BRANCH as a branch or detaches at it as a tag
- prints the resulting commit short SHA for clarity
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>