5 Commits

Author SHA1 Message Date
John McLear
0a2facb3fc
ci(packaging): publish signed apt repository to etherpad.org/apt (closes #7610) (#7624)
* ci(packaging): publish signed apt repository to etherpad.org/apt (closes #7610)

Adds an `apt-publish` workflow job that turns the existing `.deb`
build artefacts into a signed apt repository hosted at:

  https://etherpad.org/apt/

End-user install on any Debian/Ubuntu/Mint:

  curl -fsSL https://etherpad.org/key.asc \
    | sudo gpg --dearmor -o /usr/share/keyrings/etherpad.gpg
  echo "deb [signed-by=/usr/share/keyrings/etherpad.gpg] \
       https://etherpad.org/apt stable main" \
    | sudo tee /etc/apt/sources.list.d/etherpad.list
  sudo apt update && sudo apt install etherpad

`apt upgrade` works going forward — every tagged release republishes
the repo metadata.

Change type: patch (CI/distribution; no production behaviour change).

## Why etherpad.org/apt and not ether.github.io/etherpad/apt

ether/etherpad's GitHub Pages is already configured as
build-from-workflow on `develop` with CNAME `docs.etherpad.org`, and
a repo can only have one Pages source. Pushing the apt repo to a
gh-pages branch would either be ignored (Pages is reading from the
docs workflow) or, if Pages were switched to it, would kill the docs
site. ether/ether.github.com is a separate Next.js site that already
deploys etherpad.org and serves `public/` verbatim, so cross-pushing
the apt repo into `public/apt/` lands it at the canonical Etherpad
URL with no infrastructure conflicts.

## What this PR ships

1. `apt-publish` job in `.github/workflows/deb-package.yml`. Runs after
   `release` on `v*` tag pushes:
     - Clones ether/ether.github.com over SSH using a deploy key.
     - Wipes site/public/apt/ and rebuilds it from the per-arch .deb
       artefacts using apt-ftparchive.
     - Signs Release + emits InRelease/Release.gpg using the keypair
       in APT_SIGNING_KEY.
     - Drops key.asc into site/public/key.asc.
     - Asserts both per-arch .debs are present before the wipe takes
       effect — refuses to publish a partial / empty repo if an
       artefact is missing or renamed.
     - Commits and pushes to master; the site repo's existing build
       pipeline picks it up.
2. `packaging/apt/key.asc` — Etherpad APT Repository public key,
   fingerprint 6953FA0C6431F30347D65B03AF0CD687D51A6E63. Served at
   https://etherpad.org/key.asc after the next release.
3. `packaging/apt/generate-signing-key.sh` — one-shot helper that
   generated the keypair, kept for documented future rotation.
4. `packaging/README.md` — apt-repo install recipe is now the
   recommended path.

## Required secrets before the next tagged release

Two secrets on ether/etherpad before the next `v*` tag push:

- APT_SIGNING_KEY — ASCII-armoured private key for the Etherpad APT
  Repository keypair (long key id AF0CD687D51A6E63), generated with
  packaging/apt/generate-signing-key.sh.
- SITE_DEPLOY_KEY — SSH private key. The public half registered as a
  deploy key with WRITE access on ether/ether.github.com.

If either is missing the job fails fast with a clear error.

## What this PR does not change

- The release job still attaches both versioned (etherpad_<v>_<arch>.deb)
  and stable-aliased (etherpad-latest_<arch>.deb) artefacts to the
  GitHub Release. Anyone pulling from
  releases/latest/download/etherpad-latest_amd64.deb keeps working.
- The build-job smoke test (start under systemd, /health, purge) is
  unchanged.
- docs.etherpad.org is untouched; this PR never pushes to gh-pages.

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

* ci(packaging): emit unindented Release headers + tighten artefact glob

Two corrections from a fresh Qodo review of the rebased apt-publish
job:

1. The dists/${SUITE}/Release heredoc was indented with the workflow's
   YAML scope, which means the resulting file had 10-space-prefixed
   field lines (`          Origin: Etherpad`). apt parsers reject any
   leading whitespace on header fields per RFC 822 / Debian control
   format, so the entire suite would have failed to parse on `apt
   update` even before checksums were appended.

   Replace the heredoc with `printf '%s\n' ...` so the indentation is
   entirely under workflow control and impossible to break with a
   future YAML re-indent.

2. Tighten the artefact glob from `etherpad_*_amd64.deb` to
   `etherpad_[0-9]*_amd64.deb`. The hyphen-separator distinction
   (etherpad_<v>_… vs etherpad-latest_…) already kept the alias out
   of the array — Qodo's analysis of a duplicate-Packages bug was
   incorrect. But pinning to a leading-digit version segment makes
   the contract explicit and defends against any future alias that
   accidentally lands on `dist/etherpad_<word>_<arch>.deb`.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 00:20:00 +01:00
SamTV12345
c55007361c
chore: updated node to supported 22,24,25 (#7628)
* chore: updated node to supported 22,24,25

* chore: updated node to supported 22,24,25

* chore: updated node to supported 22,24,25

* chore: updated node to supported 22,24,25

* chore: upgrade deb

* chore: upgrade dockerfile

* chore: use explicit node

* chore: use node 22

* chore: use node 22
2026-04-28 22:45:28 +02:00
John McLear
0b40bfc784
feat(packaging): add Debian (.deb) build via nfpm with systemd unit (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 5eee7895a) 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 3daf300f0.

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>
2026-04-27 10:33:30 +01:00
John McLear
de5feb2eb5
Revert "feat(packaging): add Debian (.deb) build via nfpm with systemd unit (…" (#7582)
This reverts commit 6bb879ed03933d60e365ab3ed00fd6d966d84ccc.
2026-04-22 18:47:13 +01:00
John McLear
6bb879ed03
feat(packaging): add Debian (.deb) build via nfpm with systemd unit (#7559)
* feat(packaging): add Debian (.deb) build via nfpm with systemd unit

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 #7529

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

* fix(deb): fail smoke test on /health timeout, tighten default-file perms, 2-space indent

Addresses Qodo review feedback on #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>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 18:45:50 +01:00