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
87 changes: 87 additions & 0 deletions .github/workflows/snap-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Builds and publishes the Etherpad snap on tagged releases.
# Mirrors the trigger pattern from .github/workflows/docker.yml / release.yml
# (tags matching v?X.Y.Z).
#
# One-time maintainer setup:
# 1. `snapcraft register etherpad-lite` claims the name.
# 2. Generate a store credential:
# snapcraft export-login --snaps etherpad-lite \
# --channels edge,stable \
# --acls package_access,package_push,package_release -
# Store the output as repo secret SNAPCRAFT_STORE_CREDENTIALS.
# 3. Create a GitHub Environment called `snap-store-stable` with required
# reviewers so stable promotion is gated.
#
# Ref: https://snapcraft.io/docs/releasing-to-the-snap-store
name: Snap
on:
push:
tags:
- 'v?[0-9]+.[0-9]+.[0-9]+'
workflow_dispatch:

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest
outputs:
snap-file: ${{ steps.build.outputs.snap }}
steps:
- name: Check out
uses: actions/checkout@v6

- name: Build snap
id: build
uses: snapcore/action-build@v1

- name: Upload snap artifact
uses: actions/upload-artifact@v4
with:
name: etherpad-lite-snap
path: ${{ steps.build.outputs.snap }}
if-no-files-found: error
retention-days: 7

publish-edge:
needs: build
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Download snap artifact
uses: actions/download-artifact@v4
with:
name: etherpad-lite-snap

- name: Publish to edge
uses: snapcore/action-publish@v1
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
with:
snap: ${{ needs.build.outputs.snap-file }}
release: edge

publish-stable:
needs: [build, publish-edge]
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: read
# Manual gate: promote edge -> stable via GitHub Environments approval.
environment: snap-store-stable
steps:
- name: Download snap artifact
uses: actions/download-artifact@v4
with:
name: etherpad-lite-snap

- name: Publish to stable
uses: snapcore/action-publish@v1
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
with:
snap: ${{ needs.build.outputs.snap-file }}
release: stable
61 changes: 61 additions & 0 deletions snap/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Etherpad snap

Packages Etherpad as a [Snap](https://snapcraft.io/) for publishing to the
Snap Store.

## Build locally

```
sudo snap install --classic snapcraft
sudo snap install lxd && sudo lxd init --auto
snapcraft # from repo root; uses LXD by default
```

Output: `etherpad-lite_<version>_<arch>.snap`.

## Install the local build

```
sudo snap install --dangerous ./etherpad-lite_*.snap
sudo snap start etherpad-lite
curl http://127.0.0.1:9001/health
```

Logs: `sudo snap logs etherpad-lite -f`.

## Configure

The snap seeds `$SNAP_COMMON/etc/settings.json` from the upstream
template on first run. Edit that file to customise Etherpad, then:

```
sudo snap restart etherpad-lite
```

A few values are exposed as snap config for convenience:

| Key | Default | Notes |
| ----------------------------------- | --------- | --------------- |
| `snap set etherpad-lite port=9001` | `9001` | Listen port |
| `snap set etherpad-lite ip=0.0.0.0` | `0.0.0.0` | Bind address |

Pad data (dirty DB, logs) lives in `/var/snap/etherpad-lite/common/` and
survives `snap refresh`.

## Publish to the Snap Store

Maintainers only. See
[Releasing to the Snap Store](https://snapcraft.io/docs/releasing-to-the-snap-store).

One-time setup:

```
snapcraft register etherpad-lite
snapcraft export-login --snaps etherpad-lite \
--channels edge,stable \
--acls package_access,package_push,package_release -
```

Store the printed credential in the repo secret
`SNAPCRAFT_STORE_CREDENTIALS`. CI (`.github/workflows/snap-publish.yml`)
handles the rest on every `v*` tag.
24 changes: 24 additions & 0 deletions snap/hooks/configure
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/bin/bash
# Validates values set via `snap set etherpad-lite key=value`.
# Supported keys:
# port : integer 1-65535 (default 9001). Ports <1024 require AppArmor override.
# ip : bind address (default 0.0.0.0)
set -euo pipefail

PORT="$(snapctl get port || true)"
if [ -n "${PORT}" ]; then
if ! [[ "${PORT}" =~ ^[0-9]+$ ]] || [ "${PORT}" -lt 1 ] || [ "${PORT}" -gt 65535 ]; then
echo "port must be an integer 1-65535" >&2
exit 1
fi
fi

IP="$(snapctl get ip || true)"
if [ -n "${IP}" ] && ! [[ "${IP}" =~ ^[0-9a-fA-F.:]+$ ]]; then
echo "ip must be a valid IPv4/IPv6 address" >&2
exit 1
fi

if snapctl services etherpad-lite.etherpad-lite 2>/dev/null | grep -q active; then
snapctl restart etherpad-lite.etherpad-lite
fi
24 changes: 24 additions & 0 deletions snap/local/bin/etherpad-cli
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/bin/bash
# Thin passthrough to Etherpad's bin/ scripts.
# Usage: etherpad-lite.etherpad <bin-script> [args...]
set -euo pipefail

APP_DIR="${SNAP}/opt/etherpad-lite"
NODE_BIN="${SNAP}/opt/node/bin/node"
export PATH="${SNAP}/opt/node/bin:${PATH}"

if [ "$#" -eq 0 ]; then
echo "Usage: etherpad-lite.etherpad <bin-script> [args...]"
echo "Available scripts:"
ls "${APP_DIR}/bin" | grep -E '\.(ts|sh)$' | sed 's/^/ /'
exit 2
fi

SCRIPT_NAME="$1"; shift
SCRIPT_PATH="${APP_DIR}/bin/${SCRIPT_NAME}"
[ -f "${SCRIPT_PATH}" ] || { echo "no such script: ${SCRIPT_NAME}"; exit 2; }

case "${SCRIPT_PATH}" in
*.sh) exec "${SCRIPT_PATH}" "$@" ;;
*.ts) exec "${NODE_BIN}" --import tsx/esm "${SCRIPT_PATH}" "$@" ;;
esac
20 changes: 20 additions & 0 deletions snap/local/bin/etherpad-healthcheck-wrapper
Original file line number Diff line number Diff line change
@@ -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); });
'
58 changes: 58 additions & 0 deletions snap/local/bin/etherpad-service
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/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 dirty-DB / 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-lite"
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"
mkdir -p "${ETC_DIR}" "${VAR_DIR}" "${LOG_DIR}"

SETTINGS="${ETC_DIR}/settings.json"
if [ ! -f "${SETTINGS}" ]; then
echo "[etherpad-snap] bootstrapping ${SETTINGS} from template"
cp "${APP_DIR}/settings.json.template" "${SETTINGS}"
# Point the default dirty-DB at $SNAP_COMMON (absolute path, writable).
sed -i \
-e 's|"filename": "var/dirty.db"|"filename": "'"${VAR_DIR}"'/dirty.db"|' \
"${SETTINGS}"
# Rewrite ip/port literals to Etherpad's env-substitution syntax so
# `snap set etherpad-lite 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}"
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.

cd "${APP_DIR}"
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.
exec "${NODE_BIN}" --import tsx/esm src/node/server.ts \
--settings "${SETTINGS}" \
"$@"
Loading
Loading