Skip to content

feat(packaging): publish Etherpad as a Snap#7558

Open
JohnMcLear wants to merge 2 commits intoether:developfrom
JohnMcLear:chore/packaging-snap
Open

feat(packaging): publish Etherpad as a Snap#7558
JohnMcLear wants to merge 2 commits intoether:developfrom
JohnMcLear:chore/packaging-snap

Conversation

@JohnMcLear
Copy link
Copy Markdown
Member

Summary

Adds first-class Snap packaging so Ubuntu / snapd users can install with sudo snap install etherpad-lite.

Part of #7529 — top-3 deployment targets (Snap, Apt, Home Assistant).

  • snap/snapcraft.yaml — core24, strict confinement, pnpm + pinned Node.js 22. Version auto-derived from src/package.json.
  • snap/local/bin/etherpad-service — launch wrapper; seeds settings.json into $SNAP_COMMON on first run, rewrites dirty-DB path, execs via node --import tsx/esm.
  • snap/local/bin/etherpad-healthcheck-wrapper — HTTP /health probe for external supervisors.
  • snap/local/bin/etherpad-cli — passthrough to bin/ scripts (importSqlFile, checkPad, …).
  • snap/hooks/configure — exposes snap set etherpad-lite port=<n> / ip=<addr> with validation.
  • snap/README.md — build / install / configure / publish instructions.
  • .github/workflows/snap-publish.yml — tag-triggered build → edge → gated stable via GitHub Environment approval.

Pad data (dirty DB, logs) lives in /var/snap/etherpad-lite/common/ and survives snap refresh. The read-only $SNAP squashfs is never written to at runtime.

Maintainer action required (one-time)

  1. snapcraft register etherpad-lite — claims the name.
  2. Generate a store credential and store it as repo secret SNAPCRAFT_STORE_CREDENTIALS:
    snapcraft export-login --snaps etherpad-lite \
      --channels edge,stable \
      --acls package_access,package_push,package_release -
    
  3. Create a GitHub Environment snap-store-stable with required reviewers so stable promotion is gated.

See https://snapcraft.io/docs/releasing-to-the-snap-store.

Test plan

  • snapcraft builds locally (LXD provider)
  • sudo snap install --dangerous etherpad-lite_*.snap installs cleanly on Ubuntu 24.04
  • curl http://127.0.0.1:9001/health returns 200 after sudo snap start etherpad-lite
  • sudo snap set etherpad-lite port=9100 && sudo snap restart etherpad-lite relocates the listener
  • Refresh preserves pad data in /var/snap/etherpad-lite/common

Refs #7529

🤖 Generated with Claude Code

Adds first-class Snap packaging so Ubuntu / snapd users can install via
`sudo snap install etherpad-lite`.

- snap/snapcraft.yaml — core24, strict confinement, builds with pnpm
  against a pinned Node.js 22 runtime. Version is auto-derived from
  src/package.json so `snap info` tracks upstream release numbering.
- snap/local/bin/etherpad-service — launch wrapper that seeds
  $SNAP_COMMON/etc/settings.json on first run (rewriting the default
  dirty-DB path to a writable $SNAP_COMMON location) and execs Etherpad
  via `node --import tsx/esm`.
- snap/local/bin/etherpad-healthcheck-wrapper — HTTP probe for external
  supervisors, falling back to Node if curl isn't staged.
- snap/local/bin/etherpad-cli — thin passthrough to Etherpad's bin/
  scripts (importSqlFile, checkPad, etc.).
- snap/hooks/configure — exposes `snap set etherpad-lite port=<n>` and
  `ip=<addr>` with validation, restarts the service when running.
- snap/README.md — build / install / configure / publish instructions.
- .github/workflows/snap-publish.yml — builds on every v* tag, uploads
  a short-lived artifact, publishes to `edge`, and then promotes to
  `stable` through a manually-approved GitHub Environment. Requires a
  one-time `snapcraft register etherpad-lite` plus provisioning of the
  `SNAPCRAFT_STORE_CREDENTIALS` repo secret (instructions inline).

Pad data (dirty DB, logs) lives in /var/snap/etherpad-lite/common/ and
survives snap refreshes. The read-only $SNAP squashfs is never written
to at runtime.

Refs ether#7529

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@qodo-free-for-open-source-projects
Copy link
Copy Markdown

Review Summary by Qodo

Add Snap packaging with automated Store publishing

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Adds first-class Snap packaging for Ubuntu/snapd users
• Implements automated CI/CD pipeline for Snap Store publishing
• Provides configuration hooks for port and IP binding
• Includes health check and CLI wrapper utilities
Diagram
flowchart LR
  A["Source Code"] -->|snapcraft.yaml| B["Build Snap"]
  B -->|tag push| C["GitHub Actions"]
  C -->|build| D["Snap Artifact"]
  D -->|publish| E["Edge Channel"]
  E -->|manual approval| F["Stable Channel"]
  G["snap set config"] -->|configure hook| H["Port/IP Override"]
  H -->|etherpad-service| I["Running Service"]
  J["Health Check"] -->|HTTP probe| I
Loading

Grey Divider

File Changes

1. snap/snapcraft.yaml ⚙️ Configuration changes +139/-0

Core Snap recipe and build configuration

• Defines Snap recipe with core24 base, strict confinement, and Node.js 22 pinning
• Configures three apps: main daemon, healthcheck, and CLI wrapper
• Implements multi-stage build: downloads Node.js, installs pnpm, builds Etherpad, strips dev deps
• Stages wrappers and sets up environment variables for production runtime

snap/snapcraft.yaml


2. snap/local/bin/etherpad-service ✨ Enhancement +43/-0

Launch wrapper with settings initialization

• Bootstraps settings.json from template on first run to writable location
• Rewrites dirty DB path to writable $SNAP_COMMON directory
• Applies port and IP overrides from snap config via environment variables
• Executes Node.js with tsx loader for TypeScript server runtime

snap/local/bin/etherpad-service


3. snap/local/bin/etherpad-healthcheck-wrapper ✨ Enhancement +20/-0

Health check probe for external supervisors

• Implements HTTP health check probe against /health endpoint
• Falls back to Node.js native http module if curl unavailable
• Respects port configuration from snap settings
• Returns appropriate exit codes for supervisor integration

snap/local/bin/etherpad-healthcheck-wrapper


View more (4)
4. snap/local/bin/etherpad-cli ✨ Enhancement +24/-0

CLI wrapper for Etherpad utility scripts

• Provides passthrough to Etherpad bin/ scripts (importSqlFile, checkPad, etc.)
• Routes .sh scripts directly and .ts scripts through Node with tsx loader
• Lists available scripts on missing arguments
• Validates script existence before execution

snap/local/bin/etherpad-cli


5. snap/hooks/configure ✨ Enhancement +24/-0

Configuration validation and service restart

• Validates port configuration (integer 1-65535)
• Validates IP configuration (IPv4/IPv6 address format)
• Automatically restarts service when running after config changes
• Provides user-friendly error messages for invalid inputs

snap/hooks/configure


6. snap/README.md 📝 Documentation +61/-0

Build, install, and publish documentation

• Documents local build process using snapcraft and LXD
• Provides installation and testing instructions
• Explains configuration via snap set with port and IP options
• Details one-time maintainer setup for Snap Store publishing

snap/README.md


7. .github/workflows/snap-publish.yml ⚙️ Configuration changes +87/-0

Automated Snap Store CI/CD pipeline

• Triggers on v* version tags and manual workflow dispatch
• Builds snap artifact using snapcore/action-build
• Publishes to edge channel automatically on tag push
• Promotes to stable channel via gated GitHub Environment approval
• Includes comprehensive inline setup instructions for maintainers

.github/workflows/snap-publish.yml


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects bot commented Apr 19, 2026

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. 4-space indent in scripts📘 Rule violation ⚙ Maintainability
Description
New Bash scripts use 4-space indentation inside control blocks, violating the repository rule
requiring exactly 2-space indentation. This reduces consistency and can cause style-check failures
if enforced.
Code

snap/hooks/configure[R9-13]

+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
Evidence
PR Compliance ID 10 requires 2-space indentation and no tabs; the newly added scripts indent block
contents with 4 spaces (e.g., the if bodies).

snap/hooks/configure[9-13]
snap/local/bin/etherpad-cli[10-14]
snap/local/bin/etherpad-healthcheck-wrapper[8-11]
snap/local/bin/etherpad-service[23-30]
Best Practice: Repository guidelines

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The newly added Bash scripts use 4-space indentation inside `if`/`case` blocks, but the project compliance rule requires 2-space indentation (and no tabs).
## Issue Context
This PR adds several wrapper scripts under `snap/`. To stay compliant and consistent with repo formatting expectations, indentation should be normalized to 2 spaces.
## Fix Focus Areas
- snap/hooks/configure[9-13]
- snap/local/bin/etherpad-cli[10-14]
- snap/local/bin/etherpad-healthcheck-wrapper[8-11]
- snap/local/bin/etherpad-service[23-30]

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


2. Settings file ignored🐞 Bug ≡ Correctness
Description
The snap seeds $SNAP_COMMON/etc/settings.json and exports EP_SETTINGS, but Etherpad’s settings
loader does not read EP_SETTINGS, so it will keep looking for <install-root>/settings.json and fall
back to defaults. Defaults include a DB file under <install-root>/var/rusty.db, which is inside the
read-only snap mount, so the daemon will fail to persist data and may fail to start.
Code

snap/local/bin/etherpad-service[R39-43]

+cd "${APP_DIR}"
+export EP_SETTINGS="${SETTINGS}"
+export NODE_ENV=production
+
+exec "${NODE_BIN}" --import tsx/esm src/node/server.ts "$@"
Evidence
The wrapper exports EP_SETTINGS and execs server.ts, but Etherpad only supports overriding the
settings file via the --settings/-s CLI arg (argv.settings), not an EP_SETTINGS environment
variable. When no settings file is found, Etherpad continues with defaults, including a default DB
filename under settings.root/var/rusty.db, which resolves inside the snap’s read-only install
root.

snap/local/bin/etherpad-service[22-43]
src/node/utils/Settings.ts[301-306]
src/node/utils/Cli.ts[25-44]
src/node/utils/Settings.ts[685-689]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The snap wrapper exports `EP_SETTINGS` and seeds `$SNAP_COMMON/etc/settings.json`, but Etherpad ignores `EP_SETTINGS` and uses `argv.settings` (from `--settings/-s`) or defaults to `<install-root>/settings.json`. This prevents the snap from using the seeded writable settings and can force DB paths into the read-only snap mount.
### Issue Context
The snap already sets `EP_SETTINGS` (wrapper + snapcraft.yaml). The minimal, snap-friendly fix is to make Etherpad honor `process.env.EP_SETTINGS` (and optionally `process.env.EP_CREDENTIALS`) as a fallback when CLI flags are not provided.
### Fix Focus Areas
- src/node/utils/Settings.ts[301-306]
- src/node/utils/Cli.ts[25-44]
- snap/local/bin/etherpad-service[39-43]
### Suggested approach
- Update Settings filename resolution to prefer `argv.settings`, else `process.env.EP_SETTINGS`, else `'settings.json'`.
- Do the same for credentials (`argv.credentials` -> `process.env.EP_CREDENTIALS` -> `'credentials.json'`).
- Keep CLI flag precedence so existing behavior is unchanged.
- (Optional) Add a small unit/integration check or comment documenting the env var support.

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


3. snap set overrides no-op🐞 Bug ≡ Correctness
Description
The service wrapper exports PORT/IP from snapctl, but the seeded settings.json uses literal ip/port
values, so the configured settings file will override the env-based defaults and ignore `snap set
port= / snap set ip=`. Users following the snap README will not see the listener move after
restart.
Code

snap/local/bin/etherpad-service[R32-37]

+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}"
Evidence
The wrapper sets PORT and IP environment variables, but the bootstrapped settings.json is
copied from settings.json.template, where ip and port are hard-coded (not ${IP...} /
${PORT...} placeholders). Etherpad’s default port reads from process.env.PORT, but once a
settings file is loaded those explicit values take precedence, so snap-set overrides won’t apply.

snap/local/bin/etherpad-service[22-37]
settings.json.template[151-162]
src/node/utils/Settings.ts[341-349]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`snap set etherpad-lite port=...` / `ip=...` is implemented by exporting `PORT`/`IP`, but the seeded settings file hard-codes `"ip": "0.0.0.0"` and `"port": 9001`, which overrides env defaults. As a result, snap config changes do nothing.
### Issue Context
Etherpad supports env-var substitution **inside** settings.json via strings like `"${PORT:9001}"`, but the current template copy does not use that syntax for `ip`/`port`.
### Fix Focus Areas
- snap/local/bin/etherpad-service[22-37]
- settings.json.template[151-162]
### Suggested approach
- During first-run bootstrap (right after copying the template), rewrite the `ip` and `port` entries to use Etherpad’s substitution syntax:
- `"ip": "${IP:0.0.0.0}"`
- `"port": "${PORT:9001}"`  (must be quoted per template rules)
- Only apply the rewrite if the file still contains the template’s default literal values, to avoid overwriting user customizations.
- Keep the existing dirty-db path rewrite.

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



Remediation recommended

4. Wrong service active check 🐞 Bug ☼ Reliability
Description
The configure hook uses grep -q active on snapctl services output, which also matches the
substring in inactive, so it can attempt a restart even when the service is inactive/disabled.
This can cause snap set to fail if the restart command errors under those states.
Code

snap/hooks/configure[R22-24]

+if snapctl services etherpad-lite.etherpad-lite 2>/dev/null | grep -q active; then
+    snapctl restart etherpad-lite.etherpad-lite
+fi
Evidence
The hook’s status check is substring-based (active), which is contained within inactive, so the
conditional can evaluate true for inactive services and run an unintended restart.

snap/hooks/configure[22-24]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The configure hook checks service status with `grep -q active`, which matches both `active` and `inactive`, potentially triggering restarts at the wrong time.
### Issue Context
This runs during `snap set ...` and can make configuration fail if an unnecessary restart errors.
### Fix Focus Areas
- snap/hooks/configure[22-24]
### Suggested approach
- Replace `grep -q active` with a whole-word match (e.g., `grep -qw active`), or parse the status column explicitly.
- Ensure the condition only triggers when the service is actually active.

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


5. Node download not verified 🐞 Bug ⛨ Security
Description
snapcraft.yaml downloads and extracts a Node.js tarball without verifying its checksum or signature.
A compromised download would be baked into the built snap and distributed to users.
Code

snap/snapcraft.yaml[R100-105]

+      NODE_TGZ="node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz"
+      curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/${NODE_TGZ}" \
+        -o "/tmp/${NODE_TGZ}"
+      mkdir -p "${CRAFT_PART_INSTALL}/opt/node"
+      tar -xJf "/tmp/${NODE_TGZ}" -C "${CRAFT_PART_INSTALL}/opt/node" \
+        --strip-components=1
Evidence
The build recipe fetches a Node.js tarball via curl and extracts it directly, with no integrity
check step (checksum or signature verification) before use.

snap/snapcraft.yaml[92-105]
Best Practice: SLSA / supply-chain integrity best practices

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The snap build downloads Node.js and extracts it without verifying integrity, creating a supply-chain risk.
### Issue Context
Even with HTTPS, checksum/signature verification is recommended for externally fetched build artifacts that become part of a shipped package.
### Fix Focus Areas
- snap/snapcraft.yaml[92-105]
### Suggested approach
- Download Node’s `SHASUMS256.txt` for the pinned version and verify the tarball checksum before extracting.
- (Optional, stronger) Verify the signed SHASUMS file using Node release keys.
- Fail the build if verification fails.

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



Advisory comments

6. Artifact path not robust 🐞 Bug ☼ Reliability
Description
The publish jobs pass needs.build.outputs.snap-file into action-publish, but they don’t derive the
snap filename from the downloaded artifact, so publishing can break if the build output contains a
path or if the artifact is extracted under a different directory. Making the publish step locate
*.snap after download avoids coupling to action-build’s output format.
Code

.github/workflows/snap-publish.yml[R27-66]

+  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
+
Evidence
The workflow uploads an artifact, then in later jobs downloads it, but still uses the build job’s
recorded output to decide what file to publish rather than the downloaded file’s actual path/name in
the publish job workspace.

.github/workflows/snap-publish.yml[27-66]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Publishing relies on `${{ needs.build.outputs.snap-file }}` instead of determining the snap file path from the downloaded artifact. This can fail if the output includes a non-portable path or if the artifact extracts into a subdirectory.
### Issue Context
`actions/download-artifact` materializes files into the publish job filesystem; that filesystem should be the source of truth for the snap path.
### Fix Focus Areas
- .github/workflows/snap-publish.yml[27-66]
### Suggested approach
- Set an explicit download path (e.g., `path: snap-out/`).
- Pass a stable glob/path to action-publish, e.g. `snap: snap-out/*.snap`, or add a step that resolves the single `.snap` filename and exports it to an env var used by both publish jobs.

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


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment thread snap/hooks/configure Outdated
Comment thread snap/local/bin/etherpad-service Outdated
Comment thread snap/local/bin/etherpad-service
Addresses Qodo review feedback on ether#7558:

1. Settings file ignored: Etherpad's Settings loader reads `argv.settings`,
   not the `EP_SETTINGS` env var. Without `--settings`, the launcher's
   seeded $SNAP_COMMON/etc/settings.json is never loaded; Etherpad falls
   back to <install-root>/settings.json, which lives on the read-only
   squashfs — so the default dirty-DB path ends up unwritable and the
   daemon fails to persist pads. Fix: pass `--settings "${SETTINGS}"` to
   node; drop the EP_SETTINGS export.

2. `snap set` overrides were no-ops: the seeded settings.json carries the
   template's literal `"ip": "0.0.0.0"` / `"port": 9001` values, which
   override the env-based defaults Etherpad exposes via ${…}
   substitution. Users following the README saw the listener stay put
   after `snap set etherpad-lite port=…`. Fix: after copying the
   template on first run, rewrite the top-level `ip` and `port` lines
   to `"${IP:0.0.0.0}"` / `"${PORT:9001}"`. Use `0,/…/` anchors so the
   `dbSettings.port` entry further down stays literal.

3. Indentation: reflow the new shell 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant