Skip to content

feat(packaging): add Debian (.deb) build via nfpm with systemd unit#7559

Merged
JohnMcLear merged 2 commits intoether:developfrom
JohnMcLear:chore/packaging-apt
Apr 22, 2026
Merged

feat(packaging): add Debian (.deb) build via nfpm with systemd unit#7559
JohnMcLear merged 2 commits intoether:developfrom
JohnMcLear:chore/packaging-apt

Conversation

@JohnMcLear
Copy link
Copy Markdown
Member

Summary

First-class Debian packaging for Etherpad. Produces etherpad-lite_<version>_<arch>.deb for amd64 and arm64 from a single nfpm manifest. Installing the package gives users:

  • /opt/etherpad-lite with a prebuilt, self-contained node_modules/ — no pnpm required at runtime, only nodejs (>= 20).
  • etherpad system user/group, created via adduser in preinst.
  • /etc/etherpad-lite/settings.json (seeded from the template on first install; preserved across upgrades; removed on purge).
  • /var/lib/etherpad-lite owned by etherpad:etherpad, with the default dirty-DB path retargeted there so ProtectSystem=strict works.
  • /lib/systemd/system/etherpad-lite.service — hardened unit (NoNewPrivileges, ProtectSystem=strict, ProtectHome, PrivateTmp, RestrictAddressFamilies) with Restart=on-failure.
  • /usr/bin/etherpad-lite CLI wrapper running node --import tsx/esm.

Part of #7529 — top-3 deployment targets (Snap #7558, Apt, Home Assistant).

CI

.github/workflows/deb-package.yml triggers on v* tags, builds both arches via native runners (ubuntu-latest + ubuntu-24.04-arm), and smoke-tests the amd64 package end-to-end:

  1. dpkg -i installs
  2. systemctl start etherpad-lite
  3. curl /health returns 200
  4. dpkg --purge removes config + user

Artefacts are attached to the GitHub Release.

Not included (follow-up)

Publishing to an APT repo (Cloudsmith / Launchpad PPA / self-hosted reprepro) is out of scope — needs a governance decision on who holds the signing key. Recipes are in packaging/README.md to be wired in once that's decided.

Legacy

bin/buildDebian.sh and bin/deb-src/ are stale (Etherpad v1.3, init-based, unmaintained). Flagged for removal in a follow-up PR so this one stays mechanical.

Test plan

  • pnpm install --frozen-lockfile && pnpm run build:etherpad succeeds
  • Local nfpm package --packager deb produces a well-formed .deb (dpkg-deb -I / -c)
  • Fresh install on Ubuntu 24.04: service starts, /health returns OK
  • Upgrade install: /etc/etherpad-lite/settings.json untouched; service restarted
  • apt remove keeps /etc and /var/lib; apt purge removes them plus the etherpad user
  • CI workflow succeeds on amd64 and arm64 (the workflow itself runs the smoke test)

Refs #7529

🤖 Generated with Claude Code

First-class Debian packaging for Etherpad, producing signed-ready
etherpad-lite_<version>_<arch>.deb artefacts for amd64 and arm64 from a
single nfpm manifest. Installing the package gives users:

- /opt/etherpad-lite with a prebuilt, self-contained node_modules/ — no
  pnpm required at runtime, just `nodejs (>= 20)`.
- etherpad system user/group, created via `adduser` in preinst.
- /etc/etherpad-lite/settings.json seeded from the template on first
  install, preserved across upgrades, removed on `purge`.
- /var/lib/etherpad-lite owned by etherpad:etherpad, with the default
  dirty-DB retargeted there so ProtectSystem=strict works.
- /lib/systemd/system/etherpad-lite.service — hardened unit
  (NoNewPrivileges, ProtectSystem=strict, ProtectHome, PrivateTmp,
  RestrictAddressFamilies) with Restart=on-failure.
- /usr/bin/etherpad-lite CLI wrapper running `node --import tsx/esm`.

CI (.github/workflows/deb-package.yml) triggers on v* tags, builds both
arches via native runners (ubuntu-latest + ubuntu-24.04-arm), smoke-tests
the amd64 package end-to-end (install → systemctl start → curl /health
→ purge → confirm user removed), and attaches the artefacts to the
GitHub Release.

Publishing to an APT repo (Cloudsmith, Launchpad PPA, self-hosted
reprepro) is intentionally out of scope — needs a governance decision on
who holds the signing key. Recipes are documented in packaging/README.md.

Refs ether#7529

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@qodo-free-for-open-source-projects
Copy link
Copy Markdown

Review Summary by Qodo

Add Debian (.deb) packaging via nfpm with systemd service

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add first-class Debian (.deb) packaging via nfpm for amd64 and arm64
• Create systemd service with hardened security settings and auto-restart
• Implement Debian lifecycle scripts (preinst/postinst/prerm/postrm)
• Add CI workflow for automated package building and smoke testing
Diagram
flowchart LR
  A["Source Code<br/>+ node_modules"] -->|Stage| B["staging/<br/>opt/etherpad-lite"]
  B -->|nfpm package| C[".deb Artifact<br/>amd64/arm64"]
  D["nfpm.yaml<br/>manifest"] -->|Configure| C
  E["Lifecycle Scripts<br/>preinst/postinst/prerm/postrm"] -->|Package| C
  F["systemd Unit<br/>+ Hardening"] -->|Include| C
  C -->|CI Workflow| G["GitHub Release<br/>Assets"]
  C -->|Smoke Test| H["Install → Start<br/>→ Health Check"]
Loading

Grey Divider

File Changes

1. packaging/nfpm.yaml ⚙️ Configuration changes +112/-0

nfpm manifest for Debian package configuration

packaging/nfpm.yaml


2. packaging/bin/etherpad-lite ✨ Enhancement +18/-0

CLI wrapper for production Node.js execution

packaging/bin/etherpad-lite


3. packaging/systemd/etherpad-lite.service ⚙️ Configuration changes +48/-0

Hardened systemd unit with security sandboxing

packaging/systemd/etherpad-lite.service


View more (7)
4. packaging/systemd/etherpad-lite.default ⚙️ Configuration changes +7/-0

Environment variable defaults for systemd service

packaging/systemd/etherpad-lite.default


5. packaging/scripts/preinstall.sh ✨ Enhancement +28/-0

Create etherpad system user and group

packaging/scripts/preinstall.sh


6. packaging/scripts/postinstall.sh ✨ Enhancement +68/-0

Configure directories, settings, and enable service

packaging/scripts/postinstall.sh


7. packaging/scripts/preremove.sh ✨ Enhancement +20/-0

Stop service before package removal

packaging/scripts/preremove.sh


8. packaging/scripts/postremove.sh ✨ Enhancement +43/-0

Clean up config, data, and user on purge

packaging/scripts/postremove.sh


9. .github/workflows/deb-package.yml ⚙️ Configuration changes +151/-0

CI workflow for building and testing .deb packages

.github/workflows/deb-package.yml


10. packaging/README.md 📝 Documentation +89/-0

Documentation for packaging, building, and deployment

packaging/README.md


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented Apr 19, 2026

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Shell scripts use 4-space indent📘 Rule violation ⚙ Maintainability
Description
The newly added packaging shell scripts use 4-space indentation (e.g., within case branches),
violating the 2-space indentation standard. This can reduce consistency and trigger formatting/lint
issues where the repo enforces 2-space indentation.
Code

packaging/scripts/preinstall.sh[R6-18]

+case "$1" in
+    install|upgrade)
+        if ! getent group etherpad >/dev/null 2>&1; then
+            addgroup --system etherpad
+        fi
+        if ! getent passwd etherpad >/dev/null 2>&1; then
+            adduser --system --ingroup etherpad \
+                    --home /var/lib/etherpad-lite \
+                    --no-create-home \
+                    --shell /usr/sbin/nologin \
+                    --gecos "Etherpad service user" \
+                    etherpad
+        fi
Evidence
PR Compliance ID 8 requires 2-space indentation with no tabs. The added shell scripts contain blocks
indented with 4 spaces (for example, inside the install|upgrade) branch).

packaging/scripts/preinstall.sh[6-18]
packaging/scripts/postinstall.sh[13-50]
Best Practice: Repository guidelines

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New packaging shell scripts use 4-space indentation inside blocks, violating the repo requirement for 2-space indentation.
## Issue Context
This affects newly added Debian packaging maintainer scripts and should be corrected to match the enforced formatting standard.
## Fix Focus Areas
- packaging/scripts/preinstall.sh[6-18]
- packaging/scripts/postinstall.sh[13-50]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Smoke test false-positive🐞 Bug ≡ Correctness
Description
The workflow’s /health polling loop never fails the job if the endpoint never becomes healthy, so CI
can report success for a broken .deb/service. This can lead to attaching and releasing non-working
packages.
Code

.github/workflows/deb-package.yml[R110-122]

+          set -eux
+          sudo apt-get update
+          sudo apt-get install -y nodejs
+          sudo dpkg -i dist/*.deb || sudo apt-get install -f -y
+          test -x /usr/bin/etherpad-lite
+          test -f /etc/etherpad-lite/settings.json
+          test -L /opt/etherpad-lite/settings.json
+          id etherpad
+          systemctl cat etherpad-lite.service
+          sudo systemctl start etherpad-lite
+          for i in $(seq 1 30); do
+            curl -fsS http://127.0.0.1:9001/health && break || sleep 2
+          done
Evidence
The smoke test uses a curl loop that breaks on success but has no validation after the loop
completes; if curl never succeeds, the last executed command is sleep 2 (exit 0), so the step
continues successfully.

.github/workflows/deb-package.yml[107-126]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The smoke test loop does not fail if `/health` never returns 200, so the workflow can pass even when Etherpad never becomes ready.
### Issue Context
Because `curl ... && break || sleep 2` is inside a `for` loop, a permanently failing curl results in the loop ending with the exit code of the final `sleep` (0). With `set -e`, this still won’t fail.
### Fix Focus Areas
- .github/workflows/deb-package.yml[107-126]
### Suggested change
Track success and explicitly `exit 1` if the endpoint never becomes healthy, e.g.:

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Secrets readable in /etc/default🐞 Bug ⛨ Security
Description
The package installs /etc/default/etherpad-lite as world-readable (0644) even though it is the
documented place to put environment overrides for settings.json, which can include passwords and
secrets. Any local user on the system can read those secrets.
Code

packaging/nfpm.yaml[R62-68]

+  # Default environment file (conffile: preserved on upgrade)
+  - src: ./packaging/systemd/etherpad-lite.default
+    dst: /etc/default/etherpad-lite
+    type: config|noreplace
+    file_info:
+      mode: 0644
+
Evidence
The systemd unit loads /etc/default/etherpad-lite, and the packaged file is explicitly intended for
settings.json environment overrides; Etherpad supports env-var substitution including passwords, so
secrets placed there would be exposed if the file is 0644.

packaging/nfpm.yaml[62-68]
packaging/systemd/etherpad-lite.service[7-14]
packaging/systemd/etherpad-lite.default[1-7]
settings.json.template[10-16]
settings.json.template[78-83]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`/etc/default/etherpad-lite` is installed with mode `0644`, making any secrets placed there readable by all local users.
### Issue Context
The systemd unit uses `EnvironmentFile=-/etc/default/etherpad-lite`, and Etherpad supports `${ENV_VAR}` substitution for config values, including passwords.
### Fix Focus Areas
- packaging/nfpm.yaml[62-68]
- packaging/systemd/etherpad-lite.service[7-14]
### Suggested change
Tighten permissions and ownership so only root and the `etherpad` service user can read it:

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. RPM/APK scripts not portable 🐞 Bug ≡ Correctness
Description
The packaging manifest and documentation imply RPM/APK support, but the maintainer scripts use
Debian-specific user/group commands (adduser/addgroup/deluser/delgroup) that will fail or behave
incorrectly on RPM/APK systems. This makes RPM/APK builds non-functional if someone attempts them
with the current nfpm.yaml.
Code

packaging/scripts/preinstall.sh[R6-18]

+case "$1" in
+    install|upgrade)
+        if ! getent group etherpad >/dev/null 2>&1; then
+            addgroup --system etherpad
+        fi
+        if ! getent passwd etherpad >/dev/null 2>&1; then
+            adduser --system --ingroup etherpad \
+                    --home /var/lib/etherpad-lite \
+                    --no-create-home \
+                    --shell /usr/sbin/nologin \
+                    --gecos "Etherpad service user" \
+                    etherpad
+        fi
Evidence
The README explicitly claims the same manifest can produce RPM/APK, and nfpm.yaml includes an RPM
override, but the install/remove scripts call Debian-specific tools rather than portable equivalents
or per-packager scripts.

packaging/README.md[1-5]
packaging/nfpm.yaml[1-3]
packaging/nfpm.yaml[96-112]
packaging/scripts/preinstall.sh[6-18]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Maintainer scripts are Debian-specific, but the packaging docs/manifest suggest RPM/APK support.
### Issue Context
`preinstall.sh` uses `addgroup/adduser` and removal uses `deluser/delgroup`, which are not generally present (or have different semantics) on RPM/APK distros.
### Fix Focus Areas
- packaging/nfpm.yaml[96-112]
- packaging/scripts/preinstall.sh[6-18]
- packaging/README.md[1-5]
### Suggested change
Either:
1) Narrow the claim (README/manifest) to Debian-only for now (remove RPM/APK messaging and RPM override), **or**
2) Provide per-packager scripts via nfpm `overrides` (e.g., rpm scripts that use `useradd/groupadd/userdel/groupdel`, apk equivalents), keeping Debian scripts for `.deb`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Brittle tsx loader path 🐞 Bug ⚙ Maintainability
Description
The /usr/bin/etherpad-lite wrapper hardcodes tsx’s internal dist/esm/index.mjs file path, which
creates a maintenance trap if tsx’s internal layout changes. Etherpad already uses the stable tsx
entrypoints (tsx/cjs, --import=tsx) in its own scripts.
Code

packaging/bin/etherpad-lite[R13-17]

+# Run the server through tsx's ESM loader (shipped in node_modules).
+# No pnpm needed at runtime.
+exec node \
+    --import "file://${APP_DIR}/src/node_modules/tsx/dist/esm/index.mjs" \
+    "${APP_DIR}/src/node/server.ts" \
Evidence
The wrapper relies on a concrete internal file path under node_modules, while the main package uses
tsx via exported entrypoints in package scripts, indicating a more stable invocation pattern is
already used elsewhere in the repo.

packaging/bin/etherpad-lite[13-18]
src/package.json[75-84]
src/package.json[134-141]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The wrapper imports `tsx/dist/esm/index.mjs` by absolute file path, which is an internal layout detail.
### Issue Context
Repo scripts already use stable tsx entrypoints such as `--require tsx/cjs` and `--import=tsx`.
### Fix Focus Areas
- packaging/bin/etherpad-lite[13-18]
- src/package.json[134-141]
### Suggested change
Align the wrapper with the repo’s existing runtime pattern, e.g.:

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread packaging/scripts/preinstall.sh Outdated
Comment thread .github/workflows/deb-package.yml
Comment thread packaging/nfpm.yaml Outdated
…rms, 2-space indent

Addresses Qodo review feedback on ether#7559:

1. Smoke test false-positive: the `for` loop polling /health never failed
   the job if the endpoint stayed down — `curl && break || sleep 2`
   keeps returning 0 from the trailing `sleep`, so `set -e` never
   trips. CI could attach a broken .deb to a release. Fix: track
   success explicitly and exit 1 (plus dump journald logs for
   diagnostics) when the service never becomes healthy.

2. /etc/default/etherpad-lite was world-readable (0644). systemd loads
   it via `EnvironmentFile=…`, and Etherpad supports
   ${ENV_VAR}-substitution for secrets (DB_PASSWORD etc.), so any
   local user could read anything admins drop there. Fix: install the
   conffile as root:etherpad 0640 — only root and the service user can
   read it.

3. Indentation: reflow maintainer 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>
@JohnMcLear
Copy link
Copy Markdown
Member Author

Qodo's Smoke test false-positive concern is already addressed on current HEAD (.github/workflows/deb-package.yml lines 120-132): the loop now sets ok= on success, and the job exit 1s (with a journald dump) if /health never becomes healthy.

@JohnMcLear
Copy link
Copy Markdown
Member Author

Thoughts on dirtydb vs something like sqlite?

@JohnMcLear JohnMcLear merged commit 6bb879e into ether:develop Apr 22, 2026
14 checks passed
JohnMcLear added a commit that referenced this pull request Apr 27, 2026
…v2) (#7583)

* feat(packaging): add Debian (.deb) build via nfpm with systemd unit

First-class Debian packaging for Etherpad, producing
etherpad_<version>_<arch>.deb artefacts for amd64 and arm64 from a
single nfpm manifest. Installing the package gives users:

- /opt/etherpad with a prebuilt, self-contained node_modules/ — no
  pnpm required at runtime, just `nodejs (>= 20)`.
- etherpad system user/group, created via `adduser` in preinst.
- /etc/etherpad/settings.json seeded from the template on first
  install, preserved across upgrades, removed on `purge`. Seed rewrites
  dbType from the template's dev-only `dirty` default to `sqlite`,
  pointed at /var/lib/etherpad/etherpad.db so fresh installs get an
  ACID-safe DB without manual config. sqlite is shipped by ueberdb2
  (rusty-store-kv), so no additional apt deps are needed.
- /var/lib/etherpad owned by etherpad:etherpad, writable under the
  hardened unit's ProtectSystem=strict.
- /lib/systemd/system/etherpad.service — hardened unit
  (NoNewPrivileges, ProtectSystem=strict, ProtectHome, PrivateTmp,
  RestrictAddressFamilies) with Restart=on-failure.
- /usr/bin/etherpad CLI wrapper running `node --import tsx/esm`.

CI (.github/workflows/deb-package.yml) triggers on v* tags, builds both
arches via native runners (ubuntu-latest + ubuntu-24.04-arm),
smoke-tests the amd64 package end-to-end (install → verify sqlite
default → systemctl start → curl /health → purge → confirm user
removed), and attaches the artefacts to the GitHub Release.

Re-introduces the work from #7559 (reverted in #7582) with two
corrections:

1. Package name and all installed paths use `etherpad`, not
   `etherpad-lite` — matches the repo rename. Kept replaces/conflicts
   on `etherpad-lite` so any dev builds of the reverted PR upgrade
   cleanly.
2. Default dbType is `sqlite`, not `dirty`. The template's own comment
   says dirty is for testing only; shipping it by default to everyone
   who runs `apt install etherpad` is the wrong tradeoff for a
   production package.

Publishing to an APT repo (Cloudsmith, Launchpad PPA, self-hosted
reprepro) is intentionally out of scope — needs a governance decision
on who holds the signing key. Recipes are documented in
packaging/README.md.

Refs #7529, #7559, #7582

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(packaging): address PR review — startup crashes, supply chain, Node LTS

Addresses Qodo and SamTV12345 review feedback on #7583:

- postinstall: symlink /opt/etherpad/var → /var/lib/etherpad/var so
  ProtectSystem=strict doesn't block runtime writes (var/js,
  installed_plugins.json, etc.). Existing ReadWritePaths covers it.
- postinstall: seed installed_plugins.json with ep_etherpad-lite so
  checkForMigration() does not spawn `pnpm ls` on first boot — pnpm is
  not a runtime dep, and the bundled node_modules already contains
  every shipped plugin. Prevents network plugin installs at first run.
- postremove: clean up the new var symlink on remove.
- workflow: verify nfpm .deb sha256 against upstream checksums.txt
  before sudo dpkg -i (defense in depth).
- workflow: bump Node 22 → 24 (current LTS, per SamTV12345). The deb
  Depends stays at nodejs (>= 20) to match Etherpad's engines.node.
- workflow: smoke-test now asserts the var symlink and seeded
  installed_plugins.json exist post-install.
- workflow: publish stable etherpad-latest_{amd64,arm64}.deb aliases
  alongside the versioned files in the GitHub Release.
- README: bump Node guidance to 24, document /releases/latest URL,
  link to engines.node floor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(packaging): tsx CJS hook, plugin paths writable, glob tag triggers

Addresses second-round Qodo review on #7583:

- bin/etherpad: switch from `--import tsx/.../esm` to `--require
  tsx/cjs`. server.ts uses `exports.start = ...` which throws under
  the ESM loader; the prod script in src/package.json uses tsx/cjs
  for the same reason.
- postinstall: symlink /opt/etherpad/src/plugin_packages →
  /var/lib/etherpad/plugin_packages and chgrp /opt/etherpad/src/node_modules
  to etherpad with mode 2775. Otherwise admin-UI plugin install
  EACCESes — those are the dirs LinkInstaller writes to.
- systemd unit: add /opt/etherpad/src/node_modules to ReadWritePaths
  so symlink creation by the etherpad user is allowed under
  ProtectSystem=strict. plugin_packages is already covered via the
  symlink into /var/lib/etherpad.
- postremove: clean up the new plugin_packages symlink on remove.
- workflow: tag filters were `v[0-9]+.[0-9]+.[0-9]+`, but Actions tag
  filters are globs, not regex. `[0-9]+` matches one character, so
  multi-digit tags like v2.10.0 would never trigger. Switch to
  `v*.*.*` / `v*.*.*-*`, matching handleRelease.yml.
- workflow smoke test now asserts plugin_packages symlink target,
  ownership of plugin_packages and node_modules.
- test-local.sh: new script that builds the .deb and runs the same
  smoke test in a throwaway systemd-enabled Docker container, so
  failures are caught before pushing.
- README: document test-local.sh.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(packaging): test-local.sh — fix cgroups v2, add --no-systemd mode

- systemd-in-docker on cgroups v2 needs --cgroupns=host and a writable
  /sys/fs/cgroup mount; the previous :ro version booted to nothing.
- New --no-systemd mode: drops the systemd container in favour of plain
  ubuntu:24.04 + manual launch under the etherpad user. Validates the
  postinstall, wrapper, plugin paths, and /health without depending on
  the host's systemd-in-docker setup. Use it when --privileged systemd
  containers don't boot on your kernel/docker combo.
- On systemd container exit the script now dumps the last 50 log lines
  and points at --no-systemd as the fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(packaging): test-local.sh — reuse cached image in --no-systemd

If ubuntu:24.04 isn't on disk and the registry is unreachable, fall
back to whichever ubuntu/debian image is already cached (e.g. the
jrei/systemd-ubuntu image we pulled for the systemd path). Avoids a
registry round-trip on flaky networks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: handle spawn errors in run_cmd; deb-package install order + offline-safe test

src/node/utils/run_cmd.ts:
  Without `proc.on('error', ...)` a spawn failure (e.g. ENOENT for a
  missing binary) is emitted as an unlistened 'error' event, which
  Node treats as an uncaught exception that bypasses the awaiting
  try/catch and kills the process. The .deb hits this on first boot
  because plugins.ts spawns `pnpm --version` for a startup log line
  and pnpm isn't a runtime dep — Etherpad logs "Starting" then
  immediately stops. Reject the promise on 'error' so the existing
  try/catch in the caller actually catches it.

packaging/scripts/postinstall.sh:
  chown /var/lib/etherpad/plugin_packages AFTER `cp -a` from the
  staged tree — `cp -a` preserves source (root) ownership and was
  re-rooting the directory we'd just chowned to etherpad. Same
  ordering the var symlink block already used.

packaging/test-local.sh:
  Run `CI=1 pnpm install --frozen-lockfile` before staging so the
  package is built from a fresh, lockfile-consistent tree (matches
  CI). Fixes spurious "Cannot find module 'X'" failures from stale
  local symlinks pointing at out-of-date pnpm store paths.

End-to-end test now passes: postinstall asserts pass, /health
returns 200, dpkg --purge cleans up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: gitignore packaging build artefacts; drop accidental commit

Drop packaging/etc/settings.json.dist that snuck into the previous
commit (generated at build time by test-local.sh / CI from
settings.json.template). Add /staging/, /dist/, /packaging/etc/ to
.gitignore so they don't recur.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(plugins): downgrade missing-pnpm log from ERROR to debug

The startup IIFE that logs the pnpm version is informational only.
pnpm is a dev-only dependency: admin-UI plugin install goes through
live-plugin-manager directly, and plugin migration is short-circuited
when var/installed_plugins.json is present (e.g. on packaged
installs). A missing pnpm on PATH is therefore expected on hardened
deployments and shouldn't surface as a red ERROR in journalctl.

Detect ENOENT specifically and log at debug; treat other errors
(permission denied, etc.) as warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(packaging): smoke deb on PRs + backend test for run_cmd spawn errors

CI gap: deb-package.yml only fired on v* tag pushes, so a PR that
broke the .deb wasn't caught until release time. Wire it to PRs and
develop pushes via a paths filter covering packaging files and the
runtime files Etherpad needs at first boot. The release job already
gates on `if: startsWith(github.ref, 'refs/tags/v')` so PR runs
won't try to publish.

Test gap: the run_cmd.ts spawn-error fix (commit 5eee789) had no
test, which is how the bug shipped originally — plugins.ts spawned
`pnpm --version` at startup, the rejection was never caught, and
the .deb crashed mid-boot. Add a backend spec that exercises:
  - ENOENT for a missing binary -> rejects (regression test)
  - successful command -> resolves stdout
  - non-zero exit -> rejects with code

backend-tests.yml's recursive mocha glob picks up the new spec
automatically; no workflow change needed there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(packaging-ci): use NodeSource LTS for the smoke test (was Ubuntu's node 18)

ubuntu-latest's default apt nodejs is 18.19.1, but our package requires
nodejs (>= 20). The smoke test was doing `apt-get install nodejs`
followed by `dpkg -i ... || apt-get install -f`, which on a node-18
host fails the dep check, then `-f` "fixes" by REMOVING the etherpad
package — and the next assertion (test -x /usr/bin/etherpad) crashes.

Match what packaging/test-local.sh and the README recommend: install
node from NodeSource (current LTS) before installing the .deb.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(packaging-ci): sudo-prefix smoke assertions that read /etc/etherpad

postinstall sets /etc/etherpad to 0750 root:etherpad (DB creds live
here) and /var/lib/etherpad similarly. The GH Actions runner user
isn't in the etherpad group, so 'test -f /etc/etherpad/settings.json'
hits EACCES. Add sudo to each check that crosses one of those dirs.

(Wrapping the whole block in `sudo bash <<EOF` would have been
cleaner but YAML literal-block + heredoc terminator don't play well
together at this indent.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(packaging): close chown -R symlink-deref escalation; Pre-Depends adduser

postinstall:
  Use `chown -hR` instead of `chown -R` on /var/lib/etherpad/var and
  /var/lib/etherpad/plugin_packages. Both directories are writable by
  the unprivileged etherpad service user, so a symlink planted there
  could redirect root's chown onto arbitrary system files (e.g.
  /etc/shadow) on the next `apt upgrade`. -hR makes chown act on the
  symlink itself rather than its target — standard mitigation for this
  TOCTOU-style local privilege escalation.

nfpm:
  Move adduser from Depends to Pre-Depends. preinst creates the
  etherpad user before unpacking; with plain `dpkg -i` (no apt) the
  Depends list isn't installed beforehand, so a minimal system without
  adduser would fail preinst before unpack and apt-get -f couldn't
  recover. Pre-Depends guarantees adduser is configured first.

Both flagged in Qodo's persistent review of 3daf300.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(packaging): predepends lives at top-level deb:, not under overrides

nfpm's Overridables schema doesn't include predepends; it's a deb-only
top-level field. Previous commit nested it under overrides.deb, which
caused nfpm to reject the entire manifest with "field predepends not
found in type nfpm.Overridables" and broke both arch builds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(packaging): four Qodo follow-ups (CI ordering, secure node install, disable on remove, writable settings)

deb-package.yml:
  - Move 'Resolve version' (which calls `node -p`) to AFTER setup-node
    so it doesn't depend on the runner image preinstalling node.
  - Replace `curl ... | sudo bash` NodeSource installer with the
    explicit gpg-key + sources.list approach. Same outcome (NodeSource
    LTS apt repo), but no execution of network-fetched code as root.
    Reduces blast radius if NodeSource's setup endpoint is ever
    compromised — we only trust the signed apt repo metadata.

postinstall.sh:
  - /etc/etherpad/settings.json now etherpad:etherpad mode 0660 (was
    root:etherpad 0640). The admin /admin/settings UI persists changes
    by writing back to settings.settingsFilename; with the previous
    perms the etherpad user could read but not write, so saving via
    the admin UI failed silently. Group-only access preserved (DB
    creds still unreadable by other users).

postremove.sh:
  - On `dpkg --remove`, run `systemctl disable etherpad.service` before
    `daemon-reload` so the wants/ symlink doesn't dangle after dpkg
    deletes the unit file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(packaging): narrow workflow token scope; pin local nfpm to NFPM_VERSION

deb-package.yml:
  Workflow-level permissions was `contents: write` so the build job got
  write access on every PR run, even though only the release job needs
  it (to attach release assets). Narrow the workflow default to
  `contents: read` and let the release job opt back in to write — it
  already declares its own job-level `contents: write` block, so this
  is just removing an over-broad default.

test-local.sh:
  The script defined NFPM_VERSION but then unconditionally ran
  `goreleaser/nfpm:latest`, so local builds could diverge from CI's
  pinned v2.43.0. Use the variable in the docker tag (stripping the
  leading "v" to match the image's tag scheme).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant