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

4.6 KiB

Etherpad Debian / RPM packaging

Produces native .deb (and, with the same manifest, .rpm / .apk) packages for Etherpad using nfpm.

Layout

packaging/
  nfpm.yaml                # nfpm package manifest
  bin/etherpad             # /usr/bin launcher
  scripts/                 # preinst / postinst / prerm / postrm
  systemd/etherpad.service
  systemd/etherpad.default
  etc/settings.json.dist   # populated in CI from settings.json.template

Built artefacts land in ./dist/.

Building locally

Prereqs: Node 24 (current LTS; engines.node floor is 20), pnpm 10+, nfpm.

pnpm install --frozen-lockfile
pnpm run build:etherpad

# Stage the tree the way CI does:
STAGE=staging/opt/etherpad
mkdir -p "$STAGE"
cp -a src bin package.json pnpm-workspace.yaml README.md LICENSE \
      node_modules "$STAGE/"
printf 'packages:\n  - src\n  - bin\n' > "$STAGE/pnpm-workspace.yaml"
cp settings.json.template packaging/etc/settings.json.dist

VERSION=$(node -p "require('./package.json').version") \
ARCH=amd64 \
    nfpm package --packager deb -f packaging/nfpm.yaml --target dist/

End-to-end test (Docker, no real systemd needed)

packaging/test-local.sh builds the .deb and runs the same smoke test the CI workflow does, inside a throwaway systemd-enabled container:

packaging/test-local.sh                # build + smoke + purge
packaging/test-local.sh --shell        # leave the container up so you can poke around
packaging/test-local.sh --build-only   # just produce dist/*.deb

This is the fastest way to validate that the systemd hardening, plugin path symlinks, and tsx wrapper actually work together before pushing.

The release workflow publishes a signed apt repository at https://etherpad.org/apt/ on every tagged release. Three lines 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. Repo metadata is signed with the GPG keypair documented in packaging/apt/key.asc (long key id AF0CD687D51A6E63).

Installing a single .deb directly

The release page publishes both versioned and stable filenames per arch:

# Stable URL — always points at the most recent release:
curl -fsSL -o etherpad-latest_amd64.deb \
  https://github.com/ether/etherpad/releases/latest/download/etherpad-latest_amd64.deb
sudo apt install ./etherpad-latest_amd64.deb

# Or pin to a specific version:
sudo apt install ./dist/etherpad_<version>_amd64.deb

sudo systemctl start etherpad
curl http://localhost:9001/health

apt will pull in nodejs (>= 22) (matches Etherpad's engines.node). Recommended runtime is the current Node.js LTS (24); on distros without a new enough Node, add NodeSource first:

curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -

Configuration

  • Edit /etc/etherpad/settings.json, then sudo systemctl restart etherpad.
  • Environment overrides: /etc/default/etherpad.
  • Logs: journalctl -u etherpad -f.
  • Data (sqlite default): /var/lib/etherpad/etherpad.db.

The shipped settings template defaults to dbType: "dirty", which the template itself warns is for testing only. postinstall rewrites the seeded /etc/etherpad/settings.json to sqlite and points it at /var/lib/etherpad/etherpad.db so fresh installs get an ACID-safe DB out of the box. Existing /etc/etherpad/settings.json is never touched on upgrade.

Upgrading

dpkg --install etherpad_<new>.deb (or apt install) replaces the app tree under /opt/etherpad while preserving /etc/etherpad/* and /var/lib/etherpad/*. The service is restarted automatically.

Removing

  • sudo apt remove etherpad — keeps config and data.
  • sudo apt purge etherpad — also removes config, data, and the etherpad system user.

Publishing to an APT repository (follow-up)

Out of scope here — requires credentials and ownership decisions. Recipes once a repo is picked:

  • Cloudsmith (easiest, free OSS tier): cloudsmith push deb ether/etherpad/any-distro/any-version dist/*.deb
  • Launchpad PPA: requires signed source packages (a debian/ tree), which nfpm does not produce — use debuild separately.
  • Self-hosted reprepro: reprepro -b /srv/apt includedeb stable dist/*.deb

Wire the chosen option into .github/workflows/deb-package.yml after the release job.