Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions .github/workflows/deb-package.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
name: Debian package
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
- 'v[0-9]+.[0-9]+.[0-9]+-*'
workflow_dispatch:
inputs:
ref:
description: 'Git ref to package (defaults to current)'
required: false

permissions:
contents: write # attach release assets

env:
NFPM_VERSION: v2.43.0

jobs:
build:
name: Build .deb (${{ matrix.arch }})
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
runner: ubuntu-latest
- arch: arm64
runner: ubuntu-24.04-arm
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.ref }}

- name: Resolve version
id: v
run: |
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
VERSION="${GITHUB_REF_NAME#v}"
else
VERSION="$(node -p "require('./package.json').version")"
fi
echo "version=${VERSION}" >>"$GITHUB_OUTPUT"
echo "Packaging version: ${VERSION}"

- uses: pnpm/action-setup@v6
with:
version: 10

- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: '22'
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build UI + admin
run: pnpm run build:etherpad

- name: Install nfpm
run: |
set -e
NFPM_ARCH=amd64
[ "${{ matrix.arch }}" = "arm64" ] && NFPM_ARCH=arm64
curl -fsSL -o /tmp/nfpm.deb \
"https://github.com/goreleaser/nfpm/releases/download/${NFPM_VERSION}/nfpm_${NFPM_VERSION#v}_${NFPM_ARCH}.deb"
sudo dpkg -i /tmp/nfpm.deb

- name: Stage tree for packaging
run: |
set -eux
STAGE=staging/opt/etherpad
mkdir -p "${STAGE}"

# Production footprint = src/ + bin/ + node_modules/ + metadata.
cp -a src bin package.json pnpm-workspace.yaml README.md LICENSE \
node_modules "${STAGE}/"

# Make pnpm-workspace.yaml production-only (same trick Dockerfile uses).
printf 'packages:\n - src\n - bin\n' > "${STAGE}/pnpm-workspace.yaml"

Comment on lines +73 to +85
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Plugin migration startup failure 🐞 Bug ☼ Reliability

On fresh installs, Etherpad startup runs checkForMigration(), which falls back to running pnpm ls
and then writing /opt/etherpad/var/installed_plugins.json if the file is missing; the package does
not ship/create that file, and the systemd unit disallows writes under /opt/etherpad. This can
cause the service to crash during startup and never reach /health.
Agent Prompt
### Issue description
Fresh installs can fail because Etherpad calls `checkForMigration()` during startup, and if `/opt/etherpad/var/installed_plugins.json` is missing it tries to execute `pnpm ls` and write that file under `/opt/etherpad/var`. The .deb packaging currently doesn’t ship/create that file (or even `/opt/etherpad/var`), and the systemd unit doesn’t permit writes under `/opt/etherpad`.

### Issue Context
- Packaging stages `/opt/etherpad` from a curated file list.
- Etherpad’s plugin migration logic treats absence of `var/installed_plugins.json` as a trigger to run `pnpm` and then persist plugin state under `settings.root/var`.
- The systemd sandbox only allows writes to `/var/lib/etherpad`, `/var/log/etherpad`, and `/etc/etherpad`.

### Fix Focus Areas
- .github/workflows/deb-package.yml[73-85]
- packaging/nfpm.yaml[47-59]
- packaging/scripts/postinstall.sh[14-40]
- packaging/systemd/etherpad.service[22-44]

### Suggested fix approach
1. Ensure `/opt/etherpad/var/` exists in the packaged tree (or create it in `postinstall.sh`).
2. Seed `/opt/etherpad/var/installed_plugins.json` at install time with minimal valid content (for example, only `ep_etherpad-lite`), so `checkForMigration()` does not attempt to run `pnpm` on first boot.
3. If you want runtime plugin install/uninstall to work under the hardened unit, relocate plugin state to `/var/lib/etherpad` (and symlink `/opt/etherpad/var` to it), and/or extend `ReadWritePaths` to include the required writable plugin directories.

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

mkdir -p packaging/etc
cp settings.json.template packaging/etc/settings.json.dist

# Purge test fixtures and dev caches from node_modules to shrink size.
find "${STAGE}/node_modules" -type d \
\( -name test -o -name tests -o -name '__tests__' \
-o -name example -o -name examples -o -name docs \) \
-prune -exec rm -rf {} + 2>/dev/null || true
find "${STAGE}/node_modules" -type f \
\( -name '*.md' -o -name '*.ts.map' -o -name '*.map' \
-o -name 'CHANGELOG*' -o -name 'HISTORY*' \) \
-delete 2>/dev/null || true

- name: Build .deb
env:
VERSION: ${{ steps.v.outputs.version }}
ARCH: ${{ matrix.arch }}
run: |
mkdir -p dist
nfpm package --packager deb -f packaging/nfpm.yaml --target dist/

- name: Smoke-test the package (amd64 only)
if: matrix.arch == 'amd64'
run: |
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
test -f /etc/etherpad/settings.json
test -L /opt/etherpad/settings.json
# Confirm the postinstall actually rewrote the template to sqlite
# — shipping the dev-only "dirty" default would defeat the point.
grep -q '"dbType": "sqlite"' /etc/etherpad/settings.json
id etherpad
systemctl cat etherpad.service
sudo systemctl start etherpad
ok=
for i in $(seq 1 30); do
if curl -fsS http://127.0.0.1:9001/health; then
ok=1
break
fi
sleep 2
done
if [ -z "${ok}" ]; then
# Attach logs so the failing run is diagnosable.
sudo journalctl -u etherpad --no-pager -n 200 || true
exit 1
fi
sudo systemctl stop etherpad
sudo dpkg --purge etherpad
! id etherpad 2>/dev/null

- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: etherpad-${{ steps.v.outputs.version }}-${{ matrix.arch }}-deb
path: dist/*.deb
if-no-files-found: error

release:
name: Attach to GitHub Release
needs: build
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
path: dist
pattern: etherpad-*-deb
merge-multiple: true
- name: Attach .deb files to release
uses: softprops/action-gh-release@v2
with:
files: dist/*.deb
fail_on_unmatched_files: true
95 changes: 95 additions & 0 deletions packaging/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Etherpad Debian / RPM packaging

Produces native `.deb` (and, with the same manifest, `.rpm` / `.apk`)
packages for Etherpad using [nfpm](https://nfpm.goreleaser.com).

## 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 22, pnpm 10+, nfpm.

```sh
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/
```

## Installing

```sh
sudo apt install ./dist/etherpad_2.6.1_amd64.deb
sudo systemctl start etherpad
curl http://localhost:9001/health
```

`apt` will pull in `nodejs (>= 20)`; on Ubuntu 22.04 add NodeSource first:

```sh
curl -fsSL https://deb.nodesource.com/setup_22.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.
18 changes: 18 additions & 0 deletions packaging/bin/etherpad
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/bin/sh
# /usr/bin/etherpad - thin wrapper that runs Etherpad in production mode.
# Invoked by the etherpad.service systemd unit.
set -e

APP_DIR="${ETHERPAD_DIR:-/opt/etherpad}"
cd "${APP_DIR}"

: "${NODE_ENV:=production}"
export NODE_ENV
export ETHERPAD_PRODUCTION=true

# 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" \
"$@"
Loading
Loading