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:
Comment on lines +17 to +21
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. Tag filter never matches 🐞 Bug ☼ Reliability

In .github/workflows/snap-publish.yml the tag filter uses a regex-like pattern
(v?[0-9]+.[0-9]+.[0-9]+) but GitHub Actions uses glob matching, so + is treated literally and
typical release tags like v2.6.1 will not match. As a result the Snap publish workflow will not
trigger on release tags and nothing will be built/published automatically.
Agent Prompt
### Issue description
The workflow tag trigger pattern is written like a regex, but GitHub Actions uses glob matching for `on.push.tags`. The current pattern will not match normal semver tags like `v2.6.1`, so the workflow will not run on releases.

### Issue Context
The workflow intends to run on tags matching `v?X.Y.Z` (optional leading `v`).

### Fix Focus Areas
- .github/workflows/snap-publish.yml[17-21]

### Suggested change
Replace the single regex-like entry with glob patterns, for example:
- `v[0-9]*.[0-9]*.[0-9]*`
- `[0-9]*.[0-9]*.[0-9]*`
(or whichever variant matches your actual tagging scheme).

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


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
65 changes: 65 additions & 0 deletions snap/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# 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 (sqlite DB at `var/etherpad.db`, logs) lives in
`/var/snap/etherpad-lite/common/` and survives `snap refresh`. The
shipped `settings.json.template` defaults to `dbType: "dirty"`, which
the template itself warns is dev-only; the launch wrapper rewrites the
seeded copy to `sqlite` on first run so users get an ACID-safe DB out
of the box.

## 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
Comment on lines +17 to +24
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

2. Cli path traversal exec 🐞 Bug ⛨ Security

snap/local/bin/etherpad-cli builds SCRIPT_PATH from unvalidated user input, so a caller can pass
a value containing / or .. to escape the intended ${APP_DIR}/bin directory and execute
arbitrary .ts/.sh files shipped in the snap. Additionally, there is no default case branch, so
if a file exists but is not .ts or .sh the command silently does nothing and exits successfully.
Agent Prompt
### Issue description
The snap CLI wrapper allows path traversal via the `<bin-script>` argument and can execute unintended files. It also silently succeeds for unsupported extensions.

### Issue Context
This command is meant to be a thin, safe passthrough to scripts under `$SNAP/opt/etherpad-lite/bin`.

### Fix Focus Areas
- snap/local/bin/etherpad-cli[17-24]

### Suggested change
- Reject any `SCRIPT_NAME` that contains `/` or `..` (or normalize to `basename` and compare).
- Optionally enforce an allowlist derived from `$APP_DIR/bin`.
- Add a default `*)` case that prints an error like `unsupported script type` and exits non-zero.

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

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); });
'
62 changes: 62 additions & 0 deletions snap/local/bin/etherpad-service
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/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 the DB (switched from dev-only dirty to sqlite) / 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}"
# Switch the template's dev-only dirty default to sqlite and point it
# at $SNAP_COMMON (absolute path, writable under strict confinement).
# sqlite is shipped by ueberdb2 via the prebuilt rusty-store-kv native
# module — no additional build deps required.
sed -i \
-e 's|"dbType": "dirty"|"dbType": "sqlite"|' \
-e 's|"filename": "var/dirty.db"|"filename": "'"${VAR_DIR}"'/etherpad.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