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 (#7558)
* 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>
This commit is contained in:
parent
983aec5b2f
commit
02e37e0112
16 changed files with 1277 additions and 1 deletions
35
snap/local/bin/etherpad-cli
Executable file
35
snap/local/bin/etherpad-cli
Executable file
|
|
@ -0,0 +1,35 @@
|
|||
#!/bin/bash
|
||||
# Thin passthrough to Etherpad's bin/ scripts.
|
||||
# Usage: etherpad.cli <bin-script> [args...]
|
||||
set -euo pipefail
|
||||
|
||||
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
|
||||
exit 2 ;;
|
||||
esac
|
||||
20
snap/local/bin/etherpad-healthcheck-wrapper
Executable file
20
snap/local/bin/etherpad-healthcheck-wrapper
Executable file
|
|
@ -0,0 +1,20 @@
|
|||
#!/bin/bash
|
||||
# HTTP healthcheck. Returns 0 if /health returns 200.
|
||||
set -euo pipefail
|
||||
|
||||
PORT="$(snapctl get port 2>/dev/null || true)"
|
||||
: "${PORT:=9001}"
|
||||
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
exec curl --fail --silent --show-error --max-time 5 \
|
||||
"http://127.0.0.1:${PORT}/health"
|
||||
fi
|
||||
|
||||
NODE_BIN="${SNAP}/opt/node/bin/node"
|
||||
exec "${NODE_BIN}" -e '
|
||||
const http = require("http");
|
||||
http.get("http://127.0.0.1:'"${PORT}"'/health", r => {
|
||||
if (r.statusCode === 200) process.exit(0);
|
||||
console.error("HTTP " + r.statusCode); process.exit(1);
|
||||
}).on("error", e => { console.error(e.message); process.exit(1); });
|
||||
'
|
||||
71
snap/local/bin/etherpad-service
Executable file
71
snap/local/bin/etherpad-service
Executable file
|
|
@ -0,0 +1,71 @@
|
|||
#!/bin/bash
|
||||
# Launch wrapper for the Etherpad snap daemon.
|
||||
#
|
||||
# 1. On first run, copy settings.json.template -> $SNAP_COMMON/etc/settings.json
|
||||
# so the admin can edit it outside the read-only squashfs. Patch the
|
||||
# seeded file so the DB (switched from dev-only dirty to sqlite) / ip /
|
||||
# port point at writable paths and pick up env-var overrides.
|
||||
# 2. Create writable data dirs under $SNAP_COMMON.
|
||||
# 3. Apply `snap set` overrides (port, ip) via env vars — Etherpad's
|
||||
# settings.json supports ${PORT:9001}-style substitution natively.
|
||||
# 4. Exec Node with tsx loader to run server.ts, passing the seeded
|
||||
# settings file via --settings (Etherpad reads `argv.settings`, not
|
||||
# an env var, so EP_SETTINGS alone would be ignored).
|
||||
set -euo pipefail
|
||||
|
||||
APP_DIR="${SNAP}/opt/etherpad"
|
||||
NODE_BIN="${SNAP}/opt/node/bin/node"
|
||||
|
||||
export PATH="${SNAP}/opt/node/bin:${SNAP}/usr/bin:${SNAP}/bin:${PATH}"
|
||||
|
||||
ETC_DIR="${SNAP_COMMON}/etc"
|
||||
VAR_DIR="${SNAP_COMMON}/var"
|
||||
LOG_DIR="${SNAP_COMMON}/logs"
|
||||
# etherpad-app-var/ is the symlink target for ${APP_DIR}/var inside the
|
||||
# read-only snap mount — see snapcraft.yaml step 4. Etherpad writes its
|
||||
# plugin registry (installed_plugins.json) here.
|
||||
APP_VAR_DIR="${SNAP_COMMON}/etherpad-app-var"
|
||||
mkdir -p "${ETC_DIR}" "${VAR_DIR}" "${LOG_DIR}" "${APP_VAR_DIR}"
|
||||
|
||||
SETTINGS="${ETC_DIR}/settings.json"
|
||||
if [ ! -f "${SETTINGS}" ]; then
|
||||
echo "[etherpad-snap] bootstrapping ${SETTINGS} from template"
|
||||
cp "${APP_DIR}/settings.json.template" "${SETTINGS}"
|
||||
# Switch the template's dev-only dirty default to sqlite and point it
|
||||
# at $SNAP_COMMON (absolute path, writable under strict confinement).
|
||||
# sqlite is shipped by ueberdb2 via the prebuilt rusty-store-kv native
|
||||
# module — no additional build deps required.
|
||||
sed -i \
|
||||
-e 's|"dbType": "dirty"|"dbType": "sqlite"|' \
|
||||
-e 's|"filename": "var/dirty.db"|"filename": "'"${VAR_DIR}"'/etherpad.db"|' \
|
||||
"${SETTINGS}"
|
||||
# Rewrite ip/port literals to Etherpad's env-substitution syntax so
|
||||
# `snap set etherpad port=<n>` / `ip=<addr>` actually take effect.
|
||||
# Only substitute the first (top-level) occurrence — `dbSettings.port`
|
||||
# has the same key name lower down and must not be touched.
|
||||
sed -i \
|
||||
-e '0,/"ip": "0.0.0.0"/{s|"ip": "0.0.0.0"|"ip": "${IP:0.0.0.0}"|}' \
|
||||
-e '0,/"port": 9001/{s|"port": 9001|"port": "${PORT:9001}"|}' \
|
||||
"${SETTINGS}"
|
||||
fi
|
||||
|
||||
PORT_OVERRIDE="$(snapctl get port || true)"
|
||||
IP_OVERRIDE="$(snapctl get ip || true)"
|
||||
: "${PORT_OVERRIDE:=9001}"
|
||||
: "${IP_OVERRIDE:=0.0.0.0}"
|
||||
export PORT="${PORT_OVERRIDE}"
|
||||
export IP="${IP_OVERRIDE}"
|
||||
|
||||
# `tsx` lives in the `src` workspace's node_modules under pnpm's hoisting
|
||||
# layout, not at ${APP_DIR}/node_modules. Run from src/ so node's import
|
||||
# resolution finds it (mirrors the `dev` script in src/package.json).
|
||||
cd "${APP_DIR}/src"
|
||||
export NODE_ENV=production
|
||||
|
||||
# Pass --settings explicitly; Etherpad's Settings loader reads argv only,
|
||||
# so exporting EP_SETTINGS is not enough to redirect the config file.
|
||||
# Use bare `tsx` (not `tsx/esm`) to match bin/cleanRun.sh — Etherpad's
|
||||
# source mixes CJS+ESM and the ESM-only loader trips ERR_REQUIRE_CYCLE.
|
||||
exec "${NODE_BIN}" --import tsx node/server.ts \
|
||||
--settings "${SETTINGS}" \
|
||||
"$@"
|
||||
Loading…
Add table
Add a link
Reference in a new issue