feat(packaging): publish Etherpad as a Snap#7558
Conversation
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 ether#7529 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Review Summary by Qodo(Agentic_describe updated until commit b86e80a)Add Snap packaging with wrappers, tests, and publishing workflows
WalkthroughsDescription• Adds comprehensive Snap packaging for Ubuntu/snapd users • Implements launch wrapper with first-run bootstrap and settings rewriting • Includes CLI passthrough, healthcheck, and configuration hook • Provides extensive test suite and CI/CD workflows for building and publishing Diagramflowchart LR
A["snapcraft.yaml<br/>core24 strict"] --> B["etherpad-service<br/>launch wrapper"]
A --> C["etherpad-cli<br/>bin passthrough"]
A --> D["etherpad-healthcheck<br/>HTTP probe"]
B --> E["settings.json<br/>seeded + rewritten"]
E --> F["sqlite DB<br/>$SNAP_COMMON/var"]
G["configure hook<br/>port/ip validation"] --> H["snap set<br/>etherpad"]
I["snap-build.yml<br/>PR verification"] --> J["wrapper tests<br/>+ snap-pack"]
K["snap-publish.yml<br/>tag-triggered"] --> L["edge channel<br/>+ stable gate"]
File Changes1. snap/snapcraft.yaml
|
Code Review by Qodo
1. CLI tsx invocation broken
|
Addresses Qodo review feedback on ether#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>
|
Qodo's |
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>
|
/review |
|
Persistent review updated to latest commit 0db0063 |
- 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>
|
still working on testing this one |
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>
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>
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>
ⓘ You've reached your Qodo monthly free-tier limit. Reviews pause until next month — upgrade your plan to continue now, or link your paid account if you already have one. |
|
Persistent review updated to latest commit b86e80a |
Merge develop to pick up the apt/deb packaging work, GDPR PRs, OG metadata, auto-update tier 1, and other recent feature merges. Single conflict was in .gitignore — both sides added entries; resolved by keeping both packaging/ (deb) and snap (parts/stage/prime) ignores. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Pushed: rebased on latest develop (single .gitignore conflict resolved by keeping both deb-packaging and snap entries), description updated with full local test results, 47/47 wrapper unit tests still passing. @CodiumAI-Agent /review (reposting for an updated Qodo pass on commits a4e16b1..4e34d8a now that the rename, build fixes, runtime fixes, tests, CI, and docs have all landed) |
| APP_DIR="${SNAP}/opt/etherpad" | ||
| NODE_BIN="${SNAP}/opt/node/bin/node" | ||
| export PATH="${SNAP}/opt/node/bin:${PATH}" | ||
|
|
||
| if [ "$#" -eq 0 ]; then | ||
| echo "Usage: etherpad.cli <bin-script> [args...]" | ||
| echo "Available scripts:" | ||
| ls "${APP_DIR}/bin" | grep -E '\.(ts|sh)$' | sed 's/^/ /' | ||
| exit 2 | ||
| fi | ||
|
|
||
| SCRIPT_NAME="$1"; shift | ||
|
|
||
| # Reject path-traversal attempts: only a bare filename is allowed, since | ||
| # the script lookup is anchored at $APP_DIR/bin and must not escape it. | ||
| case "${SCRIPT_NAME}" in | ||
| */*|*..*|"") | ||
| echo "invalid script name: ${SCRIPT_NAME} (must be a bare filename)" >&2 | ||
| exit 2 ;; | ||
| esac | ||
|
|
||
| SCRIPT_PATH="${APP_DIR}/bin/${SCRIPT_NAME}" | ||
| [ -f "${SCRIPT_PATH}" ] || { echo "no such script: ${SCRIPT_NAME}" >&2; exit 2; } | ||
|
|
||
| case "${SCRIPT_PATH}" in | ||
| *.sh) exec "${SCRIPT_PATH}" "$@" ;; | ||
| *.ts) exec "${NODE_BIN}" --import tsx/esm "${SCRIPT_PATH}" "$@" ;; | ||
| *) echo "unsupported script type: ${SCRIPT_NAME} (expected .sh or .ts)" >&2 |
There was a problem hiding this comment.
1. Cli tsx invocation broken 🐞 Bug ≡ Correctness
snap/local/bin/etherpad-cli runs .ts scripts with --import tsx/esm and does not cd into
${APP_DIR}/src, contradicting the snap’s own documentation and the daemon wrapper; this can cause
ERR_REQUIRE_CYCLE_MODULE and/or Cannot find package 'tsx' when users run etherpad.cli ....
Agent Prompt
### Issue description
`snap/local/bin/etherpad-cli` is supposed to run Etherpad’s `bin/*.ts` scripts, but it currently:
1) uses `--import tsx/esm` (known to break Etherpad with `ERR_REQUIRE_CYCLE_MODULE`), and
2) does not `cd` to `${APP_DIR}/src`, which the snap docs say is required for `tsx` module resolution under pnpm hoisting.
This makes `etherpad.cli <ts-script>` unreliable or non-functional.
### Issue Context
The daemon wrapper (`etherpad-service`) already uses the correct approach: `cd "${APP_DIR}/src"` and `--import tsx`.
### Fix Focus Areas
- snap/local/bin/etherpad-cli[6-35]
- snap/tests/test-cli.sh[55-67]
### Concrete change
- Before executing node, `cd "${APP_DIR}/src"`.
- For `.ts` scripts, use: `exec "${NODE_BIN}" --import tsx "${SCRIPT_PATH}" "$@"`
- Update `snap/tests/test-cli.sh` expectations accordingly (it currently asserts `tsx/esm`).
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
…e 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>
Summary
Adds first-class Snap packaging so Ubuntu / snapd users can install with
sudo snap install etherpad.Part of #7529 — top-3 deployment targets (Snap, Apt, Home Assistant).
snap/snapcraft.yaml— core24, strict confinement, pnpm + pinned Node.js 22. Version auto-derived fromsrc/package.json. The daemon app shares the snap name, so users get a bareetherpadcommand; the bin/ passthrough is exposed asetherpad.cli.snap/local/bin/etherpad-service— launch wrapper; seedssettings.jsoninto$SNAP_COMMONon first run, rewrites the shipped dirty default to sqlite at$SNAP_COMMON/var/etherpad.db, env-substitutesip/portsosnap setactually takes effect, and execs Node from${APP_DIR}/srcwith--import tsx --settings <seeded>(Etherpad's loader readsargv.settings, notEP_SETTINGS;tsxlives in thesrcworkspace'snode_modulesunder pnpm hoisting).snap/local/bin/etherpad-healthcheck-wrapper— HTTP/healthprobe for external supervisors.snap/local/bin/etherpad-cli— passthrough tobin/scripts (importSqlFile,checkPad, …). Rejects path-traversal and unsupported extensions.snap/hooks/configure— exposessnap set etherpad port=<n>/ip=<addr>with validation.snap/README.md— full architecture, testing, dev workflow, publishing, and troubleshooting docs.snap/tests/— wrapper unit tests + smoke harness (see "Test plan" below)..github/workflows/snap-build.yml— runs unit tests +snapcraft pack --destructive-modeon every PR touchingsnap/. Uploads the built.snapas an artifact..github/workflows/snap-publish.yml— tag-triggered build →edge→ gatedstablevia GitHub Environment approval. Tag filter uses GitHub Actions glob syntax (vX.Y.ZandX.Y.Z).Default DB is sqlite, not dirty
settings.json.templateships withdbType: "dirty", and the template itself warns "You shouldn't use 'dirty' for anything else than testing". A Snap install is exactly the "not testing" case, so the launch wrapper's first-run sed switches the seeded config to sqlite ($SNAP_COMMON/var/etherpad.db). sqlite is already in-tree viaueberdb2→rusty-store-kv(prebuilt napi-rs binary), so strict confinement works with zerosnap.yamlchanges. Existing seededsettings.jsonfiles are never touched on refresh.Pad data (sqlite DB, logs) lives in
/var/snap/etherpad/common/and survivessnap refresh. The read-only$SNAPsquashfs is never written to at runtime.var/installed_plugins.jsonwritabilityEtherpad's plugin installer (
src/static/js/pluginfw/installer.ts) writesvar/installed_plugins.jsonat runtime via__dirname-relative paths, which resolve to absolute paths inside the read-only snap squashfs and raiseEROFS. Snap layouts can't intercept paths inside$SNAP, so the build replaces the shippedvar/directory with a symlink pointing to/var/snap/etherpad/common/etherpad-app-var/. The wrappermkdir -ps the target on first run; the kernel transparently follows the symlink to writable storage that survivessnap refresh.Test plan — verified locally on Ubuntu 24.04
Wrapper unit tests (
bash snap/tests/run-all.sh, ~5 s, no snapd/sudo): 47/47 passingtest-snapcraft-yaml.sh— required keys, name validity, daemon-app matches snap name, noetherpad-literegression, env-var whitelisttest-cli.sh— path-traversal rejection (../, subdir, empty),.ts/.shdispatch, default-case rejection, no-args usagetest-configure.sh— port (1–65535 integer) and ip (v4/v6) validation via mockedsnapctltest-service-bootstrap.sh— first-run seeding fromsettings.json.template, sed rewrite of dbType/filename/ip/port, writable-dir creation, snapctl override propagation to node env, idempotency on second run, default fallbacksBuild (
snapcraft pack --destructive-mode): succeeds in ~3 min, producesetherpad_2.6.1_amd64.snap. CI workflowsnap-build.ymlreproduces this on every PR.End-to-end smoke (
bash snap/tests/smoke.sh):sudo snap install --dangerous etherpad_2.6.1_amd64.snapinstalls cleanlysudo snap set etherpad port=9003 && sudo snap restart etherpadrelocates the listener (verified viass -tlnp | grep :9003; production default 9001 is preserved when no override is set)curl http://127.0.0.1:9003/health→HTTP 200{"status":"pass","releaseId":"2.6.1"}sudo snap services etherpad→enabled / activegrep dbType /var/snap/etherpad/common/etc/settings.json→"sqlite"(wrapper sed rewrite confirmed)grep ip /var/snap/etherpad/common/etc/settings.json→"${IP:0.0.0.0}"(env-substitution confirmed)/var/snap/etherpad/common/var/etherpad.dbcreated by the daemon/var/snap/etherpad/common/etherpad-app-var/installed_plugins.jsoncreated by Etherpad's plugin migration (proves thevar/symlink works under strict confinement)[INFO] http - HTTP server listening for connectionsandEtherpad is runningsnap refresh(simulated by reinstalling):/var/snap/etherpad/common/preservedThe local rusty-store-kv musl native module loaded cleanly under strict confinement (no manual library-binding needed); the
librarylinter warning about it is the standard prebuilt-binary noise, not a runtime problem.Maintainer action required (one-time)
snapcraft register etherpad— claims the name.SNAPCRAFT_STORE_CREDENTIALS:snap-store-stablewith required reviewers so stable promotion is gated.See Register a snap and snapcraft export-login.
Refs #7529
🤖 Generated with Claude Code