mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-07-17 16:47:05 +00:00
* feat(packaging): publish Etherpad as a Snap Adds first-class Snap packaging so Ubuntu / snapd users can install via `sudo snap install etherpad-lite`. - snap/snapcraft.yaml — core24, strict confinement, builds with pnpm against a pinned Node.js 22 runtime. Version is auto-derived from src/package.json so `snap info` tracks upstream release numbering. - snap/local/bin/etherpad-service — launch wrapper that seeds $SNAP_COMMON/etc/settings.json on first run (rewriting the default dirty-DB path to a writable $SNAP_COMMON location) and execs Etherpad via `node --import tsx/esm`. - snap/local/bin/etherpad-healthcheck-wrapper — HTTP probe for external supervisors, falling back to Node if curl isn't staged. - snap/local/bin/etherpad-cli — thin passthrough to Etherpad's bin/ scripts (importSqlFile, checkPad, etc.). - snap/hooks/configure — exposes `snap set etherpad-lite port=<n>` and `ip=<addr>` with validation, restarts the service when running. - snap/README.md — build / install / configure / publish instructions. - .github/workflows/snap-publish.yml — builds on every v* tag, uploads a short-lived artifact, publishes to `edge`, and then promotes to `stable` through a manually-approved GitHub Environment. Requires a one-time `snapcraft register etherpad-lite` plus provisioning of the `SNAPCRAFT_STORE_CREDENTIALS` repo secret (instructions inline). Pad data (dirty DB, logs) lives in /var/snap/etherpad-lite/common/ and survives snap refreshes. The read-only $SNAP squashfs is never written to at runtime. Refs #7529 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(snap): pass --settings flag, env-subst ip/port, 2-space indent Addresses Qodo review feedback on #7558: 1. Settings file ignored: Etherpad's Settings loader reads `argv.settings`, not the `EP_SETTINGS` env var. Without `--settings`, the launcher's seeded $SNAP_COMMON/etc/settings.json is never loaded; Etherpad falls back to <install-root>/settings.json, which lives on the read-only squashfs — so the default dirty-DB path ends up unwritable and the daemon fails to persist pads. Fix: pass `--settings "${SETTINGS}"` to node; drop the EP_SETTINGS export. 2. `snap set` overrides were no-ops: the seeded settings.json carries the template's literal `"ip": "0.0.0.0"` / `"port": 9001` values, which override the env-based defaults Etherpad exposes via ${…} substitution. Users following the README saw the listener stay put after `snap set etherpad-lite port=…`. Fix: after copying the template on first run, rewrite the top-level `ip` and `port` lines to `"${IP:0.0.0.0}"` / `"${PORT:9001}"`. Use `0,/…/` anchors so the `dbSettings.port` entry further down stays literal. 3. Indentation: reflow the new shell scripts from 4-space to 2-space to match the repo style rule. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(snap): default seeded settings to sqlite, not dirty settings.json.template's own comment says dirty is for testing only. A Snap install is the "not testing" case — shipping it by default means every `sudo snap install etherpad-lite` starts on a DB the project explicitly recommends against. Rewrite the postinstall sed to switch dbType: "dirty" → "sqlite" and point filename at $SNAP_COMMON/var/etherpad.db. sqlite is already shipped in-tree via ueberdb2 → rusty-store-kv (prebuilt napi-rs binary, no build deps), so this works under strict confinement with zero snap.yaml changes. Only affects first-run seeding; existing $SNAP_COMMON/etc/settings.json is never touched on refresh. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(snap): rename to "etherpad", glob tag filter, harden cli - Snap is registered as `etherpad` (the project's only name) — drops the legacy `etherpad-lite` from the name, app, paths, install dir, configure hook, README and workflow artifact. The daemon app shares the snap name, so `snap install etherpad` exposes a bare `etherpad` command; the bin/ passthrough is now `etherpad.cli`. - snap-publish.yml: GitHub Actions tag filters use globs, not regex. The prior `v?[0-9]+.[0-9]+.[0-9]+` pattern would never match a real release tag (Qodo review). Replace with two glob entries covering `vX.Y.Z` and `X.Y.Z`. - etherpad-cli: reject path-traversal in the `<bin-script>` arg (anything containing `/`, `..`, or empty) and add a default `*)` case so files with unsupported extensions fail loud instead of silently exiting 0 (Qodo review). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(snap): unbreak build — refresh corepack, drop pnpm prune Two issues hit on the first real `snapcraft pack` of this recipe: - `corepack prepare pnpm@10.33.0 --activate` failed with `Cannot find matching keyid` because Node 22.12's bundled corepack ships a stale signing-key list and rejects newer pnpm releases (nodejs/corepack#612). Refresh corepack itself via npm before preparing pnpm. - `pnpm prune --prod` is interactive on workspace projects: it asks "The modules directories will be removed and reinstalled from scratch. Proceed? (Y/n)" and deadlocks on stdin under sudo + tee. Replace it with the explicit "wipe node_modules + prod reinstall" pattern, which is non-interactive, faster (pnpm resolves the prod graph from its CAS cache), and byte-identical in result. Verified locally: `snapcraft pack --destructive-mode` produces `etherpad_2.6.1_amd64.snap` end-to-end in ~3 min. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(snap): unbreak runtime — tsx resolution, var/ writability, env Three runtime crashes surfaced when actually installing the built snap under strict confinement. Fixed each, plus a smoke-test script. - `tsx` is in the `src` workspace's node_modules under pnpm hoisting, not at the snap install root. The wrapper now `cd "${APP_DIR}/src"` and uses bare `--import tsx` (matching `bin/cleanRun.sh`); the prior `--import tsx/esm` triggered ERR_REQUIRE_CYCLE on Etherpad's mixed CJS/ESM source tree. - Etherpad's plugin installer writes `var/installed_plugins.json` via __dirname-relative paths, which resolve to absolute paths inside the read-only snap squashfs (EROFS). snap layouts can't intercept paths inside `$SNAP`, so replace the shipped `var/` dir with a symlink to `/var/snap/etherpad/common/etherpad-app-var/` (auto-created by the wrapper on first run). Persistent state survives `snap refresh`. - Drop the unused `EP_SETTINGS` and `EP_DATA_DIR` env vars from the app's `environment:` block. Etherpad's settings loader doesn't read them — it reads `argv.settings`, which the wrapper already passes via `--settings`. They were producing `[WARN] settings - Unknown Setting` noise on every start. Add `snap/tests/smoke.sh`: rebuild + install + configure test port 9003 + assert listener + curl /health + tail logs. Local verified output: HTTP 200, body {"status":"pass","releaseId":"2.6.1"}, server logs `Etherpad is running` on `http://0.0.0.0:9003/`. .gitignore now excludes destructive-mode build outputs (parts/, stage/, prime/, .craft/, *.snap). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(snap): wrapper unit tests, PR CI build, expanded docs Coverage in snap/tests/ (47 assertions, ~5s, no snapd/sudo/network): - test-snapcraft-yaml.sh: required keys, name validity, daemon-app matches snap name, no etherpad-lite regression, env-var whitelist. - test-cli.sh: path-traversal rejection, .ts/.sh dispatch, default-case rejection, no-args usage. - test-configure.sh: port (1-65535) and ip (v4/v6) validation via mocked snapctl. - test-service-bootstrap.sh: first-run seeding from settings.json.template, sed rewrite of dbType/filename/ip/port, writable-dir creation, snapctl override propagation to node env, idempotency on second run, default fallbacks. - run-all.sh: bash -n syntax check on every wrapper + hook, then sources each test file and reports totals. All assertions use port 9003 (project test convention). CI in .github/workflows/snap-build.yml: - Triggers on PR / push-to-develop touching snap/, settings.json.template, or the workflow itself. - Job 1 wrapper-tests: runs run-all.sh. - Job 2 snap-pack: snapcraft pack --destructive-mode, uploads .snap as PR artifact for sideload. - Stays separate from snap-publish.yml (tag-triggered, store-bound). snap/README.md fully rewritten: - User-facing usage, install, configure - Architecture: file layout, var/-symlink rationale, settings.json rewrite rationale, double-pnpm-install rationale, daemon-name-shares- snap-name rationale - Three test layers with exactly when/why to run each - Dev workflow loop - Publishing maintainer setup - Troubleshooting for every failure mode hit during this PR (EROFS, tsx not found, ERR_REQUIRE_CYCLE, snap-store-down, pnpm prune hang) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(snap): replace dead snapcraft.io/docs/releasing-to-the-snap-store link That URL now 404s. Point at the canonical documentation.ubuntu.com locations instead, broken out into the specific pages a maintainer actually needs: - Register a snap (to claim the name) - snapcraft export-login (to generate the SNAPCRAFT_STORE_CREDENTIALS secret) - Publishing how-to index (root index for everything else) Same fix in the snap-publish.yml header comment. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
266 lines
9.7 KiB
Markdown
266 lines
9.7 KiB
Markdown
# Etherpad snap
|
||
|
||
Packages Etherpad as a [Snap](https://snapcraft.io/) for publishing to the
|
||
Snap Store.
|
||
|
||
- [User-facing usage](#user-facing-usage)
|
||
- [Architecture](#architecture)
|
||
- [Testing](#testing)
|
||
- [Development workflow](#development-workflow)
|
||
- [Publishing](#publishing)
|
||
- [Troubleshooting](#troubleshooting)
|
||
|
||
## User-facing usage
|
||
|
||
### Install from the store
|
||
|
||
```
|
||
sudo snap install etherpad
|
||
```
|
||
|
||
The default listen port is **9001**. Pad data lives in
|
||
`/var/snap/etherpad/common/` and survives `snap refresh`.
|
||
|
||
### Configure
|
||
|
||
The snap seeds `$SNAP_COMMON/etc/settings.json` from the upstream
|
||
template on first run. Edit that file directly to customise Etherpad,
|
||
then:
|
||
|
||
```
|
||
sudo snap restart etherpad
|
||
```
|
||
|
||
A few values are exposed as snap config so users don't have to edit the
|
||
file by hand:
|
||
|
||
| Key | Default | Notes |
|
||
| ------------------------------ | --------- | --------------- |
|
||
| `snap set etherpad port=9001` | `9001` | Listen port |
|
||
| `snap set etherpad ip=0.0.0.0` | `0.0.0.0` | Bind address |
|
||
|
||
The configure hook validates these (`port` must be 1–65535 integer,
|
||
`ip` must be a valid v4/v6 address) and restarts the daemon on change.
|
||
|
||
### Build locally
|
||
|
||
```
|
||
sudo snap install --classic snapcraft
|
||
sudo snap install lxd && sudo lxd init --auto
|
||
snapcraft # from repo root; uses LXD by default
|
||
```
|
||
|
||
Output: `etherpad_<version>_<arch>.snap`.
|
||
|
||
### Install a local build
|
||
|
||
```
|
||
sudo snap install --dangerous ./etherpad_*.snap
|
||
sudo snap start etherpad
|
||
curl http://127.0.0.1:9001/health # → {"status":"pass","releaseId":"X.Y.Z"}
|
||
```
|
||
|
||
Logs: `sudo snap logs etherpad -f`.
|
||
|
||
## Architecture
|
||
|
||
### File layout inside the snap
|
||
|
||
```
|
||
$SNAP/ # = /snap/etherpad/current (read-only squashfs)
|
||
├── opt/
|
||
│ ├── node/bin/node # pinned Node.js 22.12.0
|
||
│ └── etherpad/
|
||
│ ├── src/ # ep_etherpad-lite workspace package (with node_modules incl. tsx)
|
||
│ ├── admin/, ui/, doc/ # other workspace packages (built artefacts)
|
||
│ ├── settings.json.template # template, copied to $SNAP_COMMON on first run
|
||
│ └── var → /var/snap/etherpad/common/etherpad-app-var/ # symlink (see below)
|
||
├── bin/
|
||
│ ├── etherpad-service # daemon launch wrapper
|
||
│ ├── etherpad-cli # passthrough to bin/ scripts
|
||
│ └── etherpad-healthcheck-wrapper # HTTP /health probe
|
||
└── meta/snap.yaml
|
||
|
||
$SNAP_COMMON/ # = /var/snap/etherpad/common (read-write, persists across refreshes)
|
||
├── etc/settings.json # seeded from template on first run, never overwritten
|
||
├── var/etherpad.db # sqlite database
|
||
├── etherpad-app-var/installed_plugins.json # plugin registry, written by Etherpad core
|
||
└── logs/ # reserved for future use
|
||
```
|
||
|
||
### Why the `var/` symlink
|
||
|
||
Etherpad's plugin installer
|
||
(`src/static/js/pluginfw/installer.ts`) writes
|
||
`installed_plugins.json` via `__dirname`-relative paths, which resolve
|
||
to absolute paths inside `$SNAP` — read-only squashfs. Snap layouts
|
||
can't intercept paths inside `$SNAP`, so we replace the shipped `var/`
|
||
directory with a **symlink** at build time pointing to
|
||
`/var/snap/etherpad/common/etherpad-app-var/` (created by the wrapper
|
||
on first run). The kernel transparently follows the symlink to writable
|
||
storage that survives `snap refresh`.
|
||
|
||
### Why the seeded `settings.json` is rewritten
|
||
|
||
The upstream `settings.json.template` defaults to `dbType: "dirty"` —
|
||
the template itself warns this is dev-only. The launch wrapper rewrites
|
||
the seeded copy on first run to:
|
||
|
||
- `dbType: "sqlite"` with file at `$SNAP_COMMON/var/etherpad.db`
|
||
- `ip: "${IP:0.0.0.0}"` — Etherpad's own env-substitution syntax
|
||
- `port: "${PORT:9001}"` — same
|
||
|
||
The wrapper then exports `IP` and `PORT` from the snap config (via
|
||
`snapctl get`), so `snap set etherpad port=N` actually moves the
|
||
listener.
|
||
|
||
### Why pnpm runs twice
|
||
|
||
`pnpm install --frozen-lockfile --prod=false` first (need devDeps to
|
||
build admin/ui/docs), then `rm -rf node_modules && pnpm install --prod
|
||
--frozen-lockfile --ignore-scripts` after the build. This is faster
|
||
than `pnpm prune --prod`, which is interactive on workspace projects
|
||
(prompts "Proceed? (Y/n)" to stdin) and deadlocks under the
|
||
non-interactive build environment. See
|
||
[nodejs/corepack#612](https://github.com/nodejs/corepack/issues/612)
|
||
for the corepack-keyring refresh in step 2.
|
||
|
||
### Why the daemon shares the snap name
|
||
|
||
`apps.etherpad` matches the snap name `etherpad`, so users invoke the
|
||
daemon via `snap install etherpad` → bare `etherpad` command. The CLI
|
||
passthrough is exposed as `etherpad.cli` (e.g.
|
||
`etherpad.cli importSqlFile something.sql`).
|
||
|
||
## Testing
|
||
|
||
Three layers, each independently runnable:
|
||
|
||
### 1. Wrapper unit tests (~5 s, no snapd/sudo)
|
||
|
||
```
|
||
bash snap/tests/run-all.sh
|
||
```
|
||
|
||
Runs `bash -n` syntax checks on every wrapper + hook, then sources
|
||
each `test-*.sh` and reports pass/fail counts. Coverage:
|
||
|
||
- `test-snapcraft-yaml.sh` — required keys, name validity, daemon-app
|
||
matches snap name, no `etherpad-lite` regression, environment vars
|
||
whitelist.
|
||
- `test-cli.sh` — path-traversal rejection (`../`, subdir, empty),
|
||
`.ts` / `.sh` dispatch, default-case rejection, no-args usage.
|
||
- `test-configure.sh` — port (1–65535 integer) and ip (v4/v6) validation
|
||
via mocked `snapctl`.
|
||
- `test-service-bootstrap.sh` — first-run seeding from
|
||
`settings.json.template`, sed rewrite of dbType/filename/ip/port,
|
||
writable-dir creation, snapctl override propagation to node env,
|
||
idempotency on second run, default fallbacks.
|
||
|
||
All tests use **port 9003** for any binding (per project convention,
|
||
since 9001 is reserved for ad-hoc local Etherpad work).
|
||
|
||
### 2. CI build verification
|
||
|
||
`.github/workflows/snap-build.yml` runs on every PR that touches
|
||
`snap/`, `settings.json.template`, or the workflow itself. Two jobs:
|
||
|
||
- `wrapper-tests` — runs `snap/tests/run-all.sh` (~5 s).
|
||
- `snap-pack` — runs `snapcraft pack --destructive-mode` and uploads
|
||
the resulting `.snap` as an artifact (downloadable from the run
|
||
summary so reviewers can sideload).
|
||
|
||
This is intentionally separate from `snap-publish.yml` (tag-triggered,
|
||
LXD-based, pushes to the store).
|
||
|
||
### 3. End-to-end smoke test (~3 min, requires sudo + snapd)
|
||
|
||
```
|
||
bash snap/tests/smoke.sh
|
||
```
|
||
|
||
Rebuilds via destructive-mode, installs the resulting `.snap`,
|
||
configures `port=9003`, restarts, waits for plugin migration to
|
||
finish, asserts a listener on 9003, hits `/health`, and tails the
|
||
last 20 log lines. Useful when changing the wrappers or the build
|
||
recipe before pushing.
|
||
|
||
## Development workflow
|
||
|
||
```
|
||
# 1. Make a change to snap/snapcraft.yaml or one of the wrappers.
|
||
|
||
# 2. Fast feedback loop — only the unit tests:
|
||
bash snap/tests/run-all.sh
|
||
|
||
# 3. Full local verification — actually build and install:
|
||
bash snap/tests/smoke.sh
|
||
|
||
# 4. Push. CI will run wrapper-tests + snap-pack on the PR.
|
||
git push
|
||
```
|
||
|
||
If `snapcraft pack` complains about the LXD provider,
|
||
`--destructive-mode` lets you build directly on the host (used by both
|
||
the smoke script and CI). It pollutes the host with build deps and
|
||
puts `parts/`, `stage/`, `prime/` in the worktree (gitignored). Wipe
|
||
with `sudo rm -rf parts stage prime`.
|
||
|
||
## Publishing
|
||
|
||
Maintainers only. See:
|
||
- [Register a snap](https://documentation.ubuntu.com/snapcraft/latest/how-to/publishing/register-a-snap/) — claims the name on the store
|
||
- [`snapcraft export-login`](https://documentation.ubuntu.com/snapcraft/reference/commands/export-login/) — generates the credential we put in `SNAPCRAFT_STORE_CREDENTIALS`
|
||
- [Snapcraft publishing how-to index](https://documentation.ubuntu.com/snapcraft/latest/how-to/publishing/)
|
||
|
||
One-time setup:
|
||
|
||
```
|
||
snapcraft register etherpad
|
||
snapcraft export-login --snaps etherpad \
|
||
--channels edge,stable \
|
||
--acls package_access,package_push,package_release -
|
||
```
|
||
|
||
Store the printed credential in the repo secret
|
||
`SNAPCRAFT_STORE_CREDENTIALS`. Create a GitHub Environment named
|
||
`snap-store-stable` with required reviewers so stable promotion is
|
||
gated.
|
||
|
||
`.github/workflows/snap-publish.yml` then handles the rest on every
|
||
`vX.Y.Z` (or `X.Y.Z`) tag: build → publish to `edge` → manual approval
|
||
gate → publish to `stable`.
|
||
|
||
## Troubleshooting
|
||
|
||
**Daemon flapping with `EROFS: read-only file system`** — Etherpad is
|
||
trying to write somewhere inside `$SNAP`. Check whether the path is
|
||
covered by the `var/` symlink (architecture section above). New write
|
||
targets need either an additional symlink at build time
|
||
(`snap/snapcraft.yaml` step 4) or a config knob to redirect into
|
||
`$SNAP_COMMON`.
|
||
|
||
**`Cannot find package 'tsx'`** — the wrapper must `cd "${APP_DIR}/src"`
|
||
before `node`, since `tsx` lives in the workspace's `node_modules` and
|
||
not at the install root under pnpm hoisting.
|
||
|
||
**`ERR_REQUIRE_CYCLE_MODULE`** — use bare `--import tsx`, not
|
||
`--import tsx/esm`. The ESM-only loader trips on Etherpad's mixed
|
||
CJS/ESM source.
|
||
|
||
**`snap install` fails with `unable to contact snap store`** — almost
|
||
always a Canonical-side outage. Check
|
||
[snapcraft.statuspage.io](https://snapcraft.statuspage.io). For
|
||
*local* development you can sidestep the store dependency entirely by
|
||
building with `snapcraft pack --destructive-mode` (no LXD container
|
||
provisioning, so no in-container `snap install`).
|
||
|
||
**`pnpm prune --prod` hangs forever** — never use it directly here. It
|
||
has an interactive "Proceed? (Y/n)" prompt for workspaces that
|
||
deadlocks under sudo/tee. The build recipe uses
|
||
`rm -rf node_modules && pnpm install --prod --frozen-lockfile
|
||
--ignore-scripts` instead.
|
||
|
||
**`snap refresh` blew away my data** — it didn't. Pad data is in
|
||
`/var/snap/etherpad/common/`, which is preserved across refreshes.
|
||
Check `/var/snap/etherpad/common/var/etherpad.db` exists.
|