mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-05-05 04:06:37 +02:00
feat(packaging): publish Etherpad as a Snap (#7558)
* feat(packaging): publish Etherpad as a Snap 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 #7529 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(snap): pass --settings flag, env-subst ip/port, 2-space indent Addresses Qodo review feedback on #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> * fix(snap): default seeded settings to sqlite, not dirty settings.json.template's own comment says dirty is for testing only. A Snap install is the "not testing" case — shipping it by default means every `sudo snap install etherpad-lite` starts on a DB the project explicitly recommends against. Rewrite the postinstall sed to switch dbType: "dirty" → "sqlite" and point filename at $SNAP_COMMON/var/etherpad.db. sqlite is already shipped in-tree via ueberdb2 → rusty-store-kv (prebuilt napi-rs binary, no build deps), so this works under strict confinement with zero snap.yaml changes. Only affects first-run seeding; existing $SNAP_COMMON/etc/settings.json is never touched on refresh. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(snap): rename to "etherpad", glob tag filter, harden cli - Snap is registered as `etherpad` (the project's only name) — drops the legacy `etherpad-lite` from the name, app, paths, install dir, configure hook, README and workflow artifact. The daemon app shares the snap name, so `snap install etherpad` exposes a bare `etherpad` command; the bin/ passthrough is now `etherpad.cli`. - snap-publish.yml: GitHub Actions tag filters use globs, not regex. The prior `v?[0-9]+.[0-9]+.[0-9]+` pattern would never match a real release tag (Qodo review). Replace with two glob entries covering `vX.Y.Z` and `X.Y.Z`. - etherpad-cli: reject path-traversal in the `<bin-script>` arg (anything containing `/`, `..`, or empty) and add a default `*)` case so files with unsupported extensions fail loud instead of silently exiting 0 (Qodo review). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(snap): unbreak build — refresh corepack, drop pnpm prune Two issues hit on the first real `snapcraft pack` of this recipe: - `corepack prepare pnpm@10.33.0 --activate` failed with `Cannot find matching keyid` because Node 22.12's bundled corepack ships a stale signing-key list and rejects newer pnpm releases (nodejs/corepack#612). Refresh corepack itself via npm before preparing pnpm. - `pnpm prune --prod` is interactive on workspace projects: it asks "The modules directories will be removed and reinstalled from scratch. Proceed? (Y/n)" and deadlocks on stdin under sudo + tee. Replace it with the explicit "wipe node_modules + prod reinstall" pattern, which is non-interactive, faster (pnpm resolves the prod graph from its CAS cache), and byte-identical in result. Verified locally: `snapcraft pack --destructive-mode` produces `etherpad_2.6.1_amd64.snap` end-to-end in ~3 min. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(snap): unbreak runtime — tsx resolution, var/ writability, env Three runtime crashes surfaced when actually installing the built snap under strict confinement. Fixed each, plus a smoke-test script. - `tsx` is in the `src` workspace's node_modules under pnpm hoisting, not at the snap install root. The wrapper now `cd "${APP_DIR}/src"` and uses bare `--import tsx` (matching `bin/cleanRun.sh`); the prior `--import tsx/esm` triggered ERR_REQUIRE_CYCLE on Etherpad's mixed CJS/ESM source tree. - Etherpad's plugin installer writes `var/installed_plugins.json` via __dirname-relative paths, which resolve to absolute paths inside the read-only snap squashfs (EROFS). snap layouts can't intercept paths inside `$SNAP`, so replace the shipped `var/` dir with a symlink to `/var/snap/etherpad/common/etherpad-app-var/` (auto-created by the wrapper on first run). Persistent state survives `snap refresh`. - Drop the unused `EP_SETTINGS` and `EP_DATA_DIR` env vars from the app's `environment:` block. Etherpad's settings loader doesn't read them — it reads `argv.settings`, which the wrapper already passes via `--settings`. They were producing `[WARN] settings - Unknown Setting` noise on every start. Add `snap/tests/smoke.sh`: rebuild + install + configure test port 9003 + assert listener + curl /health + tail logs. Local verified output: HTTP 200, body {"status":"pass","releaseId":"2.6.1"}, server logs `Etherpad is running` on `http://0.0.0.0:9003/`. .gitignore now excludes destructive-mode build outputs (parts/, stage/, prime/, .craft/, *.snap). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(snap): wrapper unit tests, PR CI build, expanded docs Coverage in snap/tests/ (47 assertions, ~5s, no snapd/sudo/network): - test-snapcraft-yaml.sh: required keys, name validity, daemon-app matches snap name, no etherpad-lite regression, env-var whitelist. - test-cli.sh: path-traversal rejection, .ts/.sh dispatch, default-case rejection, no-args usage. - test-configure.sh: port (1-65535) and ip (v4/v6) validation via mocked snapctl. - test-service-bootstrap.sh: first-run seeding from settings.json.template, sed rewrite of dbType/filename/ip/port, writable-dir creation, snapctl override propagation to node env, idempotency on second run, default fallbacks. - run-all.sh: bash -n syntax check on every wrapper + hook, then sources each test file and reports totals. All assertions use port 9003 (project test convention). CI in .github/workflows/snap-build.yml: - Triggers on PR / push-to-develop touching snap/, settings.json.template, or the workflow itself. - Job 1 wrapper-tests: runs run-all.sh. - Job 2 snap-pack: snapcraft pack --destructive-mode, uploads .snap as PR artifact for sideload. - Stays separate from snap-publish.yml (tag-triggered, store-bound). snap/README.md fully rewritten: - User-facing usage, install, configure - Architecture: file layout, var/-symlink rationale, settings.json rewrite rationale, double-pnpm-install rationale, daemon-name-shares- snap-name rationale - Three test layers with exactly when/why to run each - Dev workflow loop - Publishing maintainer setup - Troubleshooting for every failure mode hit during this PR (EROFS, tsx not found, ERR_REQUIRE_CYCLE, snap-store-down, pnpm prune hang) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(snap): replace dead snapcraft.io/docs/releasing-to-the-snap-store link That URL now 404s. Point at the canonical documentation.ubuntu.com locations instead, broken out into the specific pages a maintainer actually needs: - Register a snap (to claim the name) - snapcraft export-login (to generate the SNAPCRAFT_STORE_CREDENTIALS secret) - Publishing how-to index (root index for everything else) Same fix in the snap-publish.yml header comment. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
983aec5b2f
commit
02e37e0112
71
.github/workflows/snap-build.yml
vendored
Normal file
71
.github/workflows/snap-build.yml
vendored
Normal file
@ -0,0 +1,71 @@
|
||||
# Snap build verification — runs on every PR (and on develop) that
|
||||
# touches snap/ or this workflow. Catches breakage in the wrappers and
|
||||
# the build recipe before it reaches a release tag.
|
||||
#
|
||||
# Two jobs:
|
||||
# wrapper-tests — runs the bash unit tests under snap/tests/. Fast
|
||||
# (~5s); no snapd / sudo / network needed. Always runs first.
|
||||
# snap-pack — builds the actual snap with `snapcraft pack
|
||||
# --destructive-mode` (no LXD). Faster than the publish workflow
|
||||
# because it skips container provisioning and store auth. Uploads
|
||||
# the .snap as an artifact so reviewers can sideload it.
|
||||
#
|
||||
# Production publishing lives in `snap-publish.yml` (tag-triggered).
|
||||
name: Snap build (PR)
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'snap/**'
|
||||
- 'settings.json.template'
|
||||
- '.github/workflows/snap-build.yml'
|
||||
push:
|
||||
branches: [develop]
|
||||
paths:
|
||||
- 'snap/**'
|
||||
- 'settings.json.template'
|
||||
- '.github/workflows/snap-build.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
wrapper-tests:
|
||||
name: Wrapper unit tests
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Run snap/tests/run-all.sh
|
||||
run: bash snap/tests/run-all.sh
|
||||
|
||||
snap-pack:
|
||||
name: Build snap (destructive-mode)
|
||||
needs: wrapper-tests
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Install snapcraft
|
||||
run: sudo snap install --classic snapcraft
|
||||
|
||||
- name: Pack snap
|
||||
run: sudo snapcraft pack --destructive-mode --verbose
|
||||
|
||||
- name: Locate built artifact
|
||||
id: artifact
|
||||
run: |
|
||||
set -e
|
||||
SNAP=$(ls etherpad_*.snap | head -1)
|
||||
if [ -z "${SNAP}" ]; then
|
||||
echo "::error::no .snap file produced"
|
||||
exit 1
|
||||
fi
|
||||
echo "snap=${SNAP}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Upload .snap artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: etherpad-snap-pr-${{ github.event.pull_request.number || github.run_id }}
|
||||
path: ${{ steps.artifact.outputs.snap }}
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
92
.github/workflows/snap-publish.yml
vendored
Normal file
92
.github/workflows/snap-publish.yml
vendored
Normal file
@ -0,0 +1,92 @@
|
||||
# Builds and publishes the Etherpad snap on tagged releases.
|
||||
# Mirrors the trigger pattern from .github/workflows/docker.yml / release.yml
|
||||
# (semver tags, with or without a leading `v`).
|
||||
#
|
||||
# Note: `on.push.tags` uses GitHub's filter-pattern globs, NOT regex — so the
|
||||
# pattern must be expressed as two glob entries, not a single `v?...` regex.
|
||||
# https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet
|
||||
#
|
||||
# One-time maintainer setup:
|
||||
# 1. `snapcraft register etherpad` claims the name.
|
||||
# 2. Generate a store credential:
|
||||
# snapcraft export-login --snaps etherpad \
|
||||
# --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://documentation.ubuntu.com/snapcraft/latest/how-to/publishing/
|
||||
name: Snap
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
- '[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-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-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-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
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@ -27,8 +27,14 @@ plugin_packages
|
||||
playwright-report
|
||||
state.json
|
||||
/src/static/oidc
|
||||
|
||||
# Build artefacts produced by packaging/test-local.sh and the deb-package CI workflow.
|
||||
/staging/
|
||||
/dist/
|
||||
/packaging/etc/
|
||||
|
||||
# Snapcraft destructive-mode build outputs.
|
||||
parts/
|
||||
stage/
|
||||
prime/
|
||||
.craft/
|
||||
*.snap
|
||||
|
||||
266
snap/README.md
Normal file
266
snap/README.md
Normal file
@ -0,0 +1,266 @@
|
||||
# Etherpad snap
|
||||
|
||||
Packages Etherpad as a [Snap](https://snapcraft.io/) for publishing to the
|
||||
Snap Store.
|
||||
|
||||
- [User-facing usage](#user-facing-usage)
|
||||
- [Architecture](#architecture)
|
||||
- [Testing](#testing)
|
||||
- [Development workflow](#development-workflow)
|
||||
- [Publishing](#publishing)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
## User-facing usage
|
||||
|
||||
### Install from the store
|
||||
|
||||
```
|
||||
sudo snap install etherpad
|
||||
```
|
||||
|
||||
The default listen port is **9001**. Pad data lives in
|
||||
`/var/snap/etherpad/common/` and survives `snap refresh`.
|
||||
|
||||
### Configure
|
||||
|
||||
The snap seeds `$SNAP_COMMON/etc/settings.json` from the upstream
|
||||
template on first run. Edit that file directly to customise Etherpad,
|
||||
then:
|
||||
|
||||
```
|
||||
sudo snap restart etherpad
|
||||
```
|
||||
|
||||
A few values are exposed as snap config so users don't have to edit the
|
||||
file by hand:
|
||||
|
||||
| Key | Default | Notes |
|
||||
| ------------------------------ | --------- | --------------- |
|
||||
| `snap set etherpad port=9001` | `9001` | Listen port |
|
||||
| `snap set etherpad ip=0.0.0.0` | `0.0.0.0` | Bind address |
|
||||
|
||||
The configure hook validates these (`port` must be 1–65535 integer,
|
||||
`ip` must be a valid v4/v6 address) and restarts the daemon on change.
|
||||
|
||||
### 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_<version>_<arch>.snap`.
|
||||
|
||||
### Install a local build
|
||||
|
||||
```
|
||||
sudo snap install --dangerous ./etherpad_*.snap
|
||||
sudo snap start etherpad
|
||||
curl http://127.0.0.1:9001/health # → {"status":"pass","releaseId":"X.Y.Z"}
|
||||
```
|
||||
|
||||
Logs: `sudo snap logs etherpad -f`.
|
||||
|
||||
## Architecture
|
||||
|
||||
### File layout inside the snap
|
||||
|
||||
```
|
||||
$SNAP/ # = /snap/etherpad/current (read-only squashfs)
|
||||
├── opt/
|
||||
│ ├── node/bin/node # pinned Node.js 22.12.0
|
||||
│ └── etherpad/
|
||||
│ ├── src/ # ep_etherpad-lite workspace package (with node_modules incl. tsx)
|
||||
│ ├── admin/, ui/, doc/ # other workspace packages (built artefacts)
|
||||
│ ├── settings.json.template # template, copied to $SNAP_COMMON on first run
|
||||
│ └── var → /var/snap/etherpad/common/etherpad-app-var/ # symlink (see below)
|
||||
├── bin/
|
||||
│ ├── etherpad-service # daemon launch wrapper
|
||||
│ ├── etherpad-cli # passthrough to bin/ scripts
|
||||
│ └── etherpad-healthcheck-wrapper # HTTP /health probe
|
||||
└── meta/snap.yaml
|
||||
|
||||
$SNAP_COMMON/ # = /var/snap/etherpad/common (read-write, persists across refreshes)
|
||||
├── etc/settings.json # seeded from template on first run, never overwritten
|
||||
├── var/etherpad.db # sqlite database
|
||||
├── etherpad-app-var/installed_plugins.json # plugin registry, written by Etherpad core
|
||||
└── logs/ # reserved for future use
|
||||
```
|
||||
|
||||
### Why the `var/` symlink
|
||||
|
||||
Etherpad's plugin installer
|
||||
(`src/static/js/pluginfw/installer.ts`) writes
|
||||
`installed_plugins.json` via `__dirname`-relative paths, which resolve
|
||||
to absolute paths inside `$SNAP` — read-only squashfs. Snap layouts
|
||||
can't intercept paths inside `$SNAP`, so we replace the shipped `var/`
|
||||
directory with a **symlink** at build time pointing to
|
||||
`/var/snap/etherpad/common/etherpad-app-var/` (created by the wrapper
|
||||
on first run). The kernel transparently follows the symlink to writable
|
||||
storage that survives `snap refresh`.
|
||||
|
||||
### Why the seeded `settings.json` is rewritten
|
||||
|
||||
The upstream `settings.json.template` defaults to `dbType: "dirty"` —
|
||||
the template itself warns this is dev-only. The launch wrapper rewrites
|
||||
the seeded copy on first run to:
|
||||
|
||||
- `dbType: "sqlite"` with file at `$SNAP_COMMON/var/etherpad.db`
|
||||
- `ip: "${IP:0.0.0.0}"` — Etherpad's own env-substitution syntax
|
||||
- `port: "${PORT:9001}"` — same
|
||||
|
||||
The wrapper then exports `IP` and `PORT` from the snap config (via
|
||||
`snapctl get`), so `snap set etherpad port=N` actually moves the
|
||||
listener.
|
||||
|
||||
### Why pnpm runs twice
|
||||
|
||||
`pnpm install --frozen-lockfile --prod=false` first (need devDeps to
|
||||
build admin/ui/docs), then `rm -rf node_modules && pnpm install --prod
|
||||
--frozen-lockfile --ignore-scripts` after the build. This is faster
|
||||
than `pnpm prune --prod`, which is interactive on workspace projects
|
||||
(prompts "Proceed? (Y/n)" to stdin) and deadlocks under the
|
||||
non-interactive build environment. See
|
||||
[nodejs/corepack#612](https://github.com/nodejs/corepack/issues/612)
|
||||
for the corepack-keyring refresh in step 2.
|
||||
|
||||
### Why the daemon shares the snap name
|
||||
|
||||
`apps.etherpad` matches the snap name `etherpad`, so users invoke the
|
||||
daemon via `snap install etherpad` → bare `etherpad` command. The CLI
|
||||
passthrough is exposed as `etherpad.cli` (e.g.
|
||||
`etherpad.cli importSqlFile something.sql`).
|
||||
|
||||
## Testing
|
||||
|
||||
Three layers, each independently runnable:
|
||||
|
||||
### 1. Wrapper unit tests (~5 s, no snapd/sudo)
|
||||
|
||||
```
|
||||
bash snap/tests/run-all.sh
|
||||
```
|
||||
|
||||
Runs `bash -n` syntax checks on every wrapper + hook, then sources
|
||||
each `test-*.sh` and reports pass/fail counts. Coverage:
|
||||
|
||||
- `test-snapcraft-yaml.sh` — required keys, name validity, daemon-app
|
||||
matches snap name, no `etherpad-lite` regression, environment vars
|
||||
whitelist.
|
||||
- `test-cli.sh` — path-traversal rejection (`../`, subdir, empty),
|
||||
`.ts` / `.sh` dispatch, default-case rejection, no-args usage.
|
||||
- `test-configure.sh` — port (1–65535 integer) and ip (v4/v6) validation
|
||||
via mocked `snapctl`.
|
||||
- `test-service-bootstrap.sh` — first-run seeding from
|
||||
`settings.json.template`, sed rewrite of dbType/filename/ip/port,
|
||||
writable-dir creation, snapctl override propagation to node env,
|
||||
idempotency on second run, default fallbacks.
|
||||
|
||||
All tests use **port 9003** for any binding (per project convention,
|
||||
since 9001 is reserved for ad-hoc local Etherpad work).
|
||||
|
||||
### 2. CI build verification
|
||||
|
||||
`.github/workflows/snap-build.yml` runs on every PR that touches
|
||||
`snap/`, `settings.json.template`, or the workflow itself. Two jobs:
|
||||
|
||||
- `wrapper-tests` — runs `snap/tests/run-all.sh` (~5 s).
|
||||
- `snap-pack` — runs `snapcraft pack --destructive-mode` and uploads
|
||||
the resulting `.snap` as an artifact (downloadable from the run
|
||||
summary so reviewers can sideload).
|
||||
|
||||
This is intentionally separate from `snap-publish.yml` (tag-triggered,
|
||||
LXD-based, pushes to the store).
|
||||
|
||||
### 3. End-to-end smoke test (~3 min, requires sudo + snapd)
|
||||
|
||||
```
|
||||
bash snap/tests/smoke.sh
|
||||
```
|
||||
|
||||
Rebuilds via destructive-mode, installs the resulting `.snap`,
|
||||
configures `port=9003`, restarts, waits for plugin migration to
|
||||
finish, asserts a listener on 9003, hits `/health`, and tails the
|
||||
last 20 log lines. Useful when changing the wrappers or the build
|
||||
recipe before pushing.
|
||||
|
||||
## Development workflow
|
||||
|
||||
```
|
||||
# 1. Make a change to snap/snapcraft.yaml or one of the wrappers.
|
||||
|
||||
# 2. Fast feedback loop — only the unit tests:
|
||||
bash snap/tests/run-all.sh
|
||||
|
||||
# 3. Full local verification — actually build and install:
|
||||
bash snap/tests/smoke.sh
|
||||
|
||||
# 4. Push. CI will run wrapper-tests + snap-pack on the PR.
|
||||
git push
|
||||
```
|
||||
|
||||
If `snapcraft pack` complains about the LXD provider,
|
||||
`--destructive-mode` lets you build directly on the host (used by both
|
||||
the smoke script and CI). It pollutes the host with build deps and
|
||||
puts `parts/`, `stage/`, `prime/` in the worktree (gitignored). Wipe
|
||||
with `sudo rm -rf parts stage prime`.
|
||||
|
||||
## Publishing
|
||||
|
||||
Maintainers only. See:
|
||||
- [Register a snap](https://documentation.ubuntu.com/snapcraft/latest/how-to/publishing/register-a-snap/) — claims the name on the store
|
||||
- [`snapcraft export-login`](https://documentation.ubuntu.com/snapcraft/reference/commands/export-login/) — generates the credential we put in `SNAPCRAFT_STORE_CREDENTIALS`
|
||||
- [Snapcraft publishing how-to index](https://documentation.ubuntu.com/snapcraft/latest/how-to/publishing/)
|
||||
|
||||
One-time setup:
|
||||
|
||||
```
|
||||
snapcraft register etherpad
|
||||
snapcraft export-login --snaps etherpad \
|
||||
--channels edge,stable \
|
||||
--acls package_access,package_push,package_release -
|
||||
```
|
||||
|
||||
Store the printed credential in the repo secret
|
||||
`SNAPCRAFT_STORE_CREDENTIALS`. Create a GitHub Environment named
|
||||
`snap-store-stable` with required reviewers so stable promotion is
|
||||
gated.
|
||||
|
||||
`.github/workflows/snap-publish.yml` then handles the rest on every
|
||||
`vX.Y.Z` (or `X.Y.Z`) tag: build → publish to `edge` → manual approval
|
||||
gate → publish to `stable`.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Daemon flapping with `EROFS: read-only file system`** — Etherpad is
|
||||
trying to write somewhere inside `$SNAP`. Check whether the path is
|
||||
covered by the `var/` symlink (architecture section above). New write
|
||||
targets need either an additional symlink at build time
|
||||
(`snap/snapcraft.yaml` step 4) or a config knob to redirect into
|
||||
`$SNAP_COMMON`.
|
||||
|
||||
**`Cannot find package 'tsx'`** — the wrapper must `cd "${APP_DIR}/src"`
|
||||
before `node`, since `tsx` lives in the workspace's `node_modules` and
|
||||
not at the install root under pnpm hoisting.
|
||||
|
||||
**`ERR_REQUIRE_CYCLE_MODULE`** — use bare `--import tsx`, not
|
||||
`--import tsx/esm`. The ESM-only loader trips on Etherpad's mixed
|
||||
CJS/ESM source.
|
||||
|
||||
**`snap install` fails with `unable to contact snap store`** — almost
|
||||
always a Canonical-side outage. Check
|
||||
[snapcraft.statuspage.io](https://snapcraft.statuspage.io). For
|
||||
*local* development you can sidestep the store dependency entirely by
|
||||
building with `snapcraft pack --destructive-mode` (no LXD container
|
||||
provisioning, so no in-container `snap install`).
|
||||
|
||||
**`pnpm prune --prod` hangs forever** — never use it directly here. It
|
||||
has an interactive "Proceed? (Y/n)" prompt for workspaces that
|
||||
deadlocks under sudo/tee. The build recipe uses
|
||||
`rm -rf node_modules && pnpm install --prod --frozen-lockfile
|
||||
--ignore-scripts` instead.
|
||||
|
||||
**`snap refresh` blew away my data** — it didn't. Pad data is in
|
||||
`/var/snap/etherpad/common/`, which is preserved across refreshes.
|
||||
Check `/var/snap/etherpad/common/var/etherpad.db` exists.
|
||||
24
snap/hooks/configure
vendored
Executable file
24
snap/hooks/configure
vendored
Executable file
@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
# Validates values set via `snap set etherpad 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.etherpad 2>/dev/null | grep -q active; then
|
||||
snapctl restart etherpad.etherpad
|
||||
fi
|
||||
35
snap/local/bin/etherpad-cli
Executable file
35
snap/local/bin/etherpad-cli
Executable file
@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
# Thin passthrough to Etherpad's bin/ scripts.
|
||||
# Usage: etherpad.cli <bin-script> [args...]
|
||||
set -euo pipefail
|
||||
|
||||
APP_DIR="${SNAP}/opt/etherpad"
|
||||
NODE_BIN="${SNAP}/opt/node/bin/node"
|
||||
export PATH="${SNAP}/opt/node/bin:${PATH}"
|
||||
|
||||
if [ "$#" -eq 0 ]; then
|
||||
echo "Usage: etherpad.cli <bin-script> [args...]"
|
||||
echo "Available scripts:"
|
||||
ls "${APP_DIR}/bin" | grep -E '\.(ts|sh)$' | sed 's/^/ /'
|
||||
exit 2
|
||||
fi
|
||||
|
||||
SCRIPT_NAME="$1"; shift
|
||||
|
||||
# Reject path-traversal attempts: only a bare filename is allowed, since
|
||||
# the script lookup is anchored at $APP_DIR/bin and must not escape it.
|
||||
case "${SCRIPT_NAME}" in
|
||||
*/*|*..*|"")
|
||||
echo "invalid script name: ${SCRIPT_NAME} (must be a bare filename)" >&2
|
||||
exit 2 ;;
|
||||
esac
|
||||
|
||||
SCRIPT_PATH="${APP_DIR}/bin/${SCRIPT_NAME}"
|
||||
[ -f "${SCRIPT_PATH}" ] || { echo "no such script: ${SCRIPT_NAME}" >&2; exit 2; }
|
||||
|
||||
case "${SCRIPT_PATH}" in
|
||||
*.sh) exec "${SCRIPT_PATH}" "$@" ;;
|
||||
*.ts) exec "${NODE_BIN}" --import tsx/esm "${SCRIPT_PATH}" "$@" ;;
|
||||
*) echo "unsupported script type: ${SCRIPT_NAME} (expected .sh or .ts)" >&2
|
||||
exit 2 ;;
|
||||
esac
|
||||
20
snap/local/bin/etherpad-healthcheck-wrapper
Executable file
20
snap/local/bin/etherpad-healthcheck-wrapper
Executable file
@ -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); });
|
||||
'
|
||||
71
snap/local/bin/etherpad-service
Executable file
71
snap/local/bin/etherpad-service
Executable file
@ -0,0 +1,71 @@
|
||||
#!/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"
|
||||
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"
|
||||
# etherpad-app-var/ is the symlink target for ${APP_DIR}/var inside the
|
||||
# read-only snap mount — see snapcraft.yaml step 4. Etherpad writes its
|
||||
# plugin registry (installed_plugins.json) here.
|
||||
APP_VAR_DIR="${SNAP_COMMON}/etherpad-app-var"
|
||||
mkdir -p "${ETC_DIR}" "${VAR_DIR}" "${LOG_DIR}" "${APP_VAR_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 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}"
|
||||
|
||||
# `tsx` lives in the `src` workspace's node_modules under pnpm's hoisting
|
||||
# layout, not at ${APP_DIR}/node_modules. Run from src/ so node's import
|
||||
# resolution finds it (mirrors the `dev` script in src/package.json).
|
||||
cd "${APP_DIR}/src"
|
||||
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.
|
||||
# Use bare `tsx` (not `tsx/esm`) to match bin/cleanRun.sh — Etherpad's
|
||||
# source mixes CJS+ESM and the ESM-only loader trips ERR_REQUIRE_CYCLE.
|
||||
exec "${NODE_BIN}" --import tsx node/server.ts \
|
||||
--settings "${SETTINGS}" \
|
||||
"$@"
|
||||
161
snap/snapcraft.yaml
Normal file
161
snap/snapcraft.yaml
Normal file
@ -0,0 +1,161 @@
|
||||
# snap/snapcraft.yaml — Snap recipe for Etherpad
|
||||
#
|
||||
# Design notes:
|
||||
# - base: core24 chosen because Etherpad requires Node.js >= 20 and
|
||||
# core24 (Ubuntu 24.04 LTS) ships glibc/OpenSSL versions matching modern
|
||||
# Node 20/22 binaries. core22 also works but ships older TLS/CA bundles.
|
||||
# - confinement: strict. Etherpad is a pure Node.js HTTP service. The only
|
||||
# native Node module (`rusty-store-kv`) ships as a prebuilt napi-rs
|
||||
# binary, so no node-gyp compile is performed at install time and
|
||||
# strict confinement works cleanly.
|
||||
# - We use `dump` + a manual override-build (rather than the npm plugin)
|
||||
# because this repo is a pnpm workspace and we pin Node.js 22 manually.
|
||||
name: etherpad
|
||||
title: Etherpad
|
||||
summary: Real-time collaborative document editor
|
||||
description: |
|
||||
Etherpad is a highly customizable open-source online editor providing
|
||||
collaborative editing in real-time. This snap bundles Etherpad with a
|
||||
pinned Node.js 22 runtime. On first launch a default `settings.json`
|
||||
is copied into `$SNAP_COMMON/etc` where it can be edited. Pad data is
|
||||
stored in `$SNAP_COMMON/var` and survives snap refreshes.
|
||||
|
||||
Default listen port: 9001.
|
||||
|
||||
Upstream: https://etherpad.org
|
||||
Source: https://github.com/ether/etherpad
|
||||
license: Apache-2.0
|
||||
website: https://etherpad.org
|
||||
source-code: https://github.com/ether/etherpad
|
||||
issues: https://github.com/ether/etherpad/issues
|
||||
contact: https://etherpad.org/#community
|
||||
|
||||
adopt-info: etherpad
|
||||
grade: stable
|
||||
confinement: strict
|
||||
base: core24
|
||||
compression: lzo
|
||||
|
||||
platforms:
|
||||
amd64:
|
||||
arm64:
|
||||
|
||||
apps:
|
||||
# Bare app name matches the snap name → users invoke it as plain `etherpad`.
|
||||
etherpad:
|
||||
command: bin/etherpad-service
|
||||
daemon: simple
|
||||
install-mode: enable
|
||||
restart-condition: on-failure
|
||||
plugs:
|
||||
- network
|
||||
- network-bind
|
||||
environment:
|
||||
HOME: $SNAP_DATA
|
||||
NODE_ENV: production
|
||||
# PORT/IP are env-substituted into settings.json on first run and
|
||||
# overridden by the wrapper from `snap set etherpad port=…` / `ip=…`.
|
||||
PORT: "9001"
|
||||
IP: "0.0.0.0"
|
||||
|
||||
healthcheck:
|
||||
command: bin/etherpad-healthcheck-wrapper
|
||||
plugs:
|
||||
- network
|
||||
|
||||
cli:
|
||||
command: bin/etherpad-cli
|
||||
plugs:
|
||||
- network
|
||||
- network-bind
|
||||
|
||||
parts:
|
||||
etherpad:
|
||||
plugin: dump
|
||||
source: .
|
||||
source-type: local
|
||||
build-packages:
|
||||
- curl
|
||||
- ca-certificates
|
||||
- git
|
||||
- python3
|
||||
- build-essential
|
||||
stage-packages:
|
||||
- ca-certificates
|
||||
- libstdc++6
|
||||
- openssl
|
||||
override-pull: |
|
||||
craftctl default
|
||||
VERSION="$(grep -m1 '"version"' src/package.json | sed -E 's/.*"([^"]+)".*/\1/')"
|
||||
craftctl set version="${VERSION}"
|
||||
override-build: |
|
||||
set -eu
|
||||
|
||||
# -- 1. Install Node.js 22 from the official tarball.
|
||||
NODE_VERSION=22.12.0
|
||||
ARCH="$(dpkg --print-architecture)"
|
||||
case "${ARCH}" in
|
||||
amd64) NODE_ARCH=x64 ;;
|
||||
arm64) NODE_ARCH=arm64 ;;
|
||||
*) echo "Unsupported arch ${ARCH}"; exit 1 ;;
|
||||
esac
|
||||
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
|
||||
|
||||
export PATH="${CRAFT_PART_INSTALL}/opt/node/bin:${PATH}"
|
||||
|
||||
# -- 2. Install pnpm via corepack. The corepack version bundled with
|
||||
# Node 22.12 ships a stale signing-key list and rejects newer pnpm
|
||||
# releases (nodejs/corepack#612), so refresh corepack itself first.
|
||||
"${CRAFT_PART_INSTALL}/opt/node/bin/npm" install \
|
||||
--prefix "${CRAFT_PART_INSTALL}/opt/node" -g corepack@latest
|
||||
corepack enable --install-directory "${CRAFT_PART_INSTALL}/opt/node/bin"
|
||||
corepack prepare pnpm@10.33.0 --activate
|
||||
|
||||
# -- 3. Copy source into install dir and build.
|
||||
APP_DIR="${CRAFT_PART_INSTALL}/opt/etherpad"
|
||||
mkdir -p "${APP_DIR}"
|
||||
cp -a "${CRAFT_PART_SRC}/." "${APP_DIR}/"
|
||||
cd "${APP_DIR}"
|
||||
|
||||
pnpm install --frozen-lockfile --prod=false
|
||||
pnpm run build:etherpad
|
||||
|
||||
# Strip dev deps to shrink the payload. `pnpm prune --prod` walks
|
||||
# every workspace symlink and can deadlock on cross-referenced
|
||||
# packages — wipe node_modules and reinstall prod-only instead;
|
||||
# pnpm resolves the smaller graph from cache in a fraction of the
|
||||
# time and the result is byte-identical to a clean prod install.
|
||||
find . -type d -name node_modules -prune -exec rm -rf {} +
|
||||
pnpm install --prod --frozen-lockfile --ignore-scripts
|
||||
|
||||
rm -rf .git tests/frontend-new/.cache \
|
||||
src/tests/frontend-new/test-results || true
|
||||
|
||||
# -- 4. Etherpad's plugin installer writes installed_plugins.json
|
||||
# under its own install dir's var/ at runtime
|
||||
# (src/static/js/pluginfw/installer.ts uses __dirname, which
|
||||
# resolves to an absolute path inside the read-only snap squashfs).
|
||||
# snap layouts can't redirect __dirname-relative paths, so replace
|
||||
# the shipped var/ dir with a symlink into $SNAP_COMMON instead;
|
||||
# the kernel transparently resolves the symlink to writable storage
|
||||
# that survives `snap refresh`. The wrapper mkdirs the target.
|
||||
rm -rf "${APP_DIR}/var"
|
||||
ln -s /var/snap/etherpad/common/etherpad-app-var "${APP_DIR}/var"
|
||||
|
||||
# -- 5. Install wrappers.
|
||||
install -Dm755 "${CRAFT_PROJECT_DIR}/snap/local/bin/etherpad-service" \
|
||||
"${CRAFT_PART_INSTALL}/bin/etherpad-service"
|
||||
install -Dm755 "${CRAFT_PROJECT_DIR}/snap/local/bin/etherpad-healthcheck-wrapper" \
|
||||
"${CRAFT_PART_INSTALL}/bin/etherpad-healthcheck-wrapper"
|
||||
install -Dm755 "${CRAFT_PROJECT_DIR}/snap/local/bin/etherpad-cli" \
|
||||
"${CRAFT_PART_INSTALL}/bin/etherpad-cli"
|
||||
|
||||
hooks:
|
||||
configure:
|
||||
plugs:
|
||||
- network
|
||||
73
snap/tests/lib.sh
Executable file
73
snap/tests/lib.sh
Executable file
@ -0,0 +1,73 @@
|
||||
#!/bin/bash
|
||||
# Tiny helpers for the snap wrapper test scripts. Source from each test.
|
||||
set -uo pipefail
|
||||
|
||||
# Counters maintained by the runner.
|
||||
: "${PASS_COUNT:=0}"
|
||||
: "${FAIL_COUNT:=0}"
|
||||
: "${TEST_NAME:?TEST_NAME must be set by the calling script}"
|
||||
|
||||
# Path to the wrapper / hook directory, computed once.
|
||||
SNAP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
|
||||
WRAPPERS_DIR="${SNAP_DIR}/local/bin"
|
||||
HOOKS_DIR="${SNAP_DIR}/hooks"
|
||||
|
||||
red() { printf '\033[31m%s\033[0m' "$*"; }
|
||||
green() { printf '\033[32m%s\033[0m' "$*"; }
|
||||
gray() { printf '\033[90m%s\033[0m' "$*"; }
|
||||
|
||||
pass() {
|
||||
PASS_COUNT=$((PASS_COUNT + 1))
|
||||
printf ' %s %s\n' "$(green ✓)" "$1"
|
||||
}
|
||||
|
||||
fail() {
|
||||
FAIL_COUNT=$((FAIL_COUNT + 1))
|
||||
printf ' %s %s\n' "$(red ✗)" "$1"
|
||||
if [ -n "${2:-}" ]; then
|
||||
printf ' %s\n' "$(gray "$2")"
|
||||
fi
|
||||
}
|
||||
|
||||
# assert_eq actual expected name
|
||||
assert_eq() {
|
||||
local actual="$1" expected="$2" name="$3"
|
||||
if [ "$actual" = "$expected" ]; then
|
||||
pass "$name"
|
||||
else
|
||||
fail "$name" "expected: $(printf '%q' "$expected") got: $(printf '%q' "$actual")"
|
||||
fi
|
||||
}
|
||||
|
||||
# assert_exit cmd expected_exit name [stdin]
|
||||
assert_exit() {
|
||||
local expected="$1" name="$2"; shift 2
|
||||
local out actual
|
||||
out=$("$@" 2>&1) || true
|
||||
actual=$?
|
||||
# bash quirk: $? from the assignment is the assignment's, not the command's.
|
||||
# Re-run inline to capture exit:
|
||||
"$@" >/dev/null 2>&1
|
||||
actual=$?
|
||||
if [ "$actual" = "$expected" ]; then
|
||||
pass "$name"
|
||||
else
|
||||
fail "$name" "expected exit $expected, got $actual; output: $out"
|
||||
fi
|
||||
}
|
||||
|
||||
# assert_grep cmd needle name — fail if cmd's combined output doesn't match
|
||||
assert_grep() {
|
||||
local needle="$1" name="$2"; shift 2
|
||||
local out
|
||||
out=$("$@" 2>&1 || true)
|
||||
if printf '%s' "$out" | grep -q -F -- "$needle"; then
|
||||
pass "$name"
|
||||
else
|
||||
fail "$name" "expected output to contain: $needle; got: $(printf '%s' "$out" | head -3)"
|
||||
fi
|
||||
}
|
||||
|
||||
section() {
|
||||
printf '\n%s %s\n' "$(gray '##')" "$1"
|
||||
}
|
||||
47
snap/tests/run-all.sh
Executable file
47
snap/tests/run-all.sh
Executable file
@ -0,0 +1,47 @@
|
||||
#!/bin/bash
|
||||
# Runs every test-*.sh under snap/tests/ and reports a final summary.
|
||||
# Intended to be runnable both locally (`bash snap/tests/run-all.sh`)
|
||||
# and in CI (`.github/workflows/snap-build.yml`). No snapd, snapcraft,
|
||||
# or sudo required — every test mocks the snap surface.
|
||||
set -uo pipefail
|
||||
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
|
||||
TESTS=(test-snapcraft-yaml.sh test-cli.sh test-configure.sh test-service-bootstrap.sh)
|
||||
|
||||
# Bash-syntax sanity check on every wrapper / hook before running anything.
|
||||
echo "## bash -n syntax check"
|
||||
for f in ../local/bin/*.sh ../local/bin/etherpad-cli ../local/bin/etherpad-service \
|
||||
../local/bin/etherpad-healthcheck-wrapper ../hooks/configure; do
|
||||
[ -f "$f" ] || continue
|
||||
if bash -n "$f" 2>/dev/null; then
|
||||
printf ' \033[32m✓\033[0m %s\n' "$f"
|
||||
else
|
||||
printf ' \033[31m✗\033[0m %s\n' "$f"
|
||||
bash -n "$f"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
TOTAL_PASS=0
|
||||
TOTAL_FAIL=0
|
||||
|
||||
for t in "${TESTS[@]}"; do
|
||||
echo
|
||||
echo "## ${t}"
|
||||
PASS_COUNT=0 FAIL_COUNT=0
|
||||
# Source so child counters bubble up.
|
||||
set +u
|
||||
source "./${t}"
|
||||
set -u
|
||||
TOTAL_PASS=$((TOTAL_PASS + PASS_COUNT))
|
||||
TOTAL_FAIL=$((TOTAL_FAIL + FAIL_COUNT))
|
||||
done
|
||||
|
||||
echo
|
||||
echo "==========================="
|
||||
printf ' Passed: %d\n' "${TOTAL_PASS}"
|
||||
printf ' Failed: %d\n' "${TOTAL_FAIL}"
|
||||
echo "==========================="
|
||||
|
||||
[ "${TOTAL_FAIL}" = 0 ]
|
||||
53
snap/tests/smoke.sh
Executable file
53
snap/tests/smoke.sh
Executable file
@ -0,0 +1,53 @@
|
||||
#!/bin/bash
|
||||
# Local smoke test for the etherpad snap.
|
||||
# Rebuild → install → set port=9003 → wait → check listener → curl /health → tail logs.
|
||||
# Run from the worktree root: bash snap/tests/smoke.sh
|
||||
set -uo pipefail
|
||||
|
||||
WORKTREE="/home/jose/etherpad/etherpad-lite/.claude/worktrees/pkg-snap"
|
||||
SNAP_FILE="${WORKTREE}/etherpad_2.6.1_amd64.snap"
|
||||
TEST_PORT=9003
|
||||
BUILD_LOG=/tmp/snapcraft-build.log
|
||||
|
||||
cd "${WORKTREE}" || exit 1
|
||||
|
||||
echo "==> Rebuilding snap (destructive mode)"
|
||||
sudo rm -rf parts stage prime
|
||||
sudo snapcraft pack --destructive-mode --verbose 2>&1 \
|
||||
| tee "${BUILD_LOG}" \
|
||||
| grep -E "Building|Staging|Priming|Packing|Created snap|Packed|error|Error|FAIL"
|
||||
|
||||
if [ ! -f "${SNAP_FILE}" ]; then
|
||||
echo "FAIL: ${SNAP_FILE} was not produced — see ${BUILD_LOG}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "==> Installing snap"
|
||||
sudo snap install --dangerous "${SNAP_FILE}"
|
||||
|
||||
echo
|
||||
echo "==> Configuring test port ${TEST_PORT} (production default stays 9001)"
|
||||
sudo snap set etherpad port="${TEST_PORT}"
|
||||
sudo snap restart etherpad
|
||||
|
||||
echo
|
||||
echo "==> Waiting 12s for plugin migration + bind"
|
||||
sleep 12
|
||||
|
||||
echo
|
||||
echo "==> Service status"
|
||||
sudo snap services etherpad
|
||||
|
||||
echo
|
||||
echo "==> Listening sockets in 9000-9009"
|
||||
sudo ss -tlnp 2>&1 | grep -E ':900[0-9]' || echo "(nothing listening in 9000-9009)"
|
||||
|
||||
echo
|
||||
echo "==> /health response"
|
||||
curl -sS -o /tmp/health.body -w 'HTTP %{http_code}\n' "http://127.0.0.1:${TEST_PORT}/health"
|
||||
echo "body: $(cat /tmp/health.body 2>/dev/null)"
|
||||
|
||||
echo
|
||||
echo "==> Last 20 log lines"
|
||||
sudo snap logs etherpad -n 25 | tail -20
|
||||
77
snap/tests/test-cli.sh
Executable file
77
snap/tests/test-cli.sh
Executable file
@ -0,0 +1,77 @@
|
||||
#!/bin/bash
|
||||
# Unit tests for snap/local/bin/etherpad-cli.
|
||||
# Exercises path-traversal rejection, extension dispatch, default-case
|
||||
# rejection, no-args usage, and missing-script rejection — all with a
|
||||
# mocked $SNAP root so no real install is needed.
|
||||
set -uo pipefail
|
||||
TEST_NAME="etherpad-cli"
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
|
||||
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf "${TMP}"' EXIT
|
||||
|
||||
# Layout: $SNAP/opt/etherpad/bin/{checkPad.sh,importSqlFile.ts,orphan.txt}
|
||||
# $SNAP/opt/node/bin/node (mock that just echoes its argv)
|
||||
mkdir -p "${TMP}/opt/etherpad/bin" "${TMP}/opt/node/bin"
|
||||
cat > "${TMP}/opt/node/bin/node" <<'EOF'
|
||||
#!/bin/bash
|
||||
echo "node-mock argv: $*"
|
||||
EOF
|
||||
chmod +x "${TMP}/opt/node/bin/node"
|
||||
cat > "${TMP}/opt/etherpad/bin/checkPad.sh" <<'EOF'
|
||||
#!/bin/bash
|
||||
echo "checkPad.sh args: $*"
|
||||
EOF
|
||||
chmod +x "${TMP}/opt/etherpad/bin/checkPad.sh"
|
||||
touch "${TMP}/opt/etherpad/bin/importSqlFile.ts"
|
||||
touch "${TMP}/opt/etherpad/bin/orphan.txt"
|
||||
|
||||
CLI="${WRAPPERS_DIR}/etherpad-cli"
|
||||
export SNAP="${TMP}"
|
||||
|
||||
section "path-traversal rejection"
|
||||
|
||||
# We assert exit code == 2 and that the stderr message matches "invalid"
|
||||
run_cli() { "${CLI}" "$@"; }
|
||||
|
||||
assert_exit 2 "rejects ../../etc/passwd" run_cli "../../etc/passwd"
|
||||
assert_exit 2 "rejects subdir/script.ts" run_cli "subdir/script.ts"
|
||||
assert_exit 2 "rejects ..hidden" run_cli "..hidden"
|
||||
assert_exit 2 "rejects empty argument" run_cli ""
|
||||
|
||||
assert_grep "invalid script name" "traversal error message mentions 'invalid'" \
|
||||
run_cli "../etc/passwd"
|
||||
|
||||
section "missing / unsupported scripts"
|
||||
|
||||
assert_exit 2 "rejects nonexistent.ts" run_cli "nonexistent.ts"
|
||||
assert_grep "no such script" "missing-script message" \
|
||||
run_cli "nonexistent.ts"
|
||||
|
||||
assert_exit 2 "rejects orphan.txt (no .ts/.sh)" run_cli "orphan.txt"
|
||||
assert_grep "unsupported script type" "unsupported-extension message" \
|
||||
run_cli "orphan.txt"
|
||||
|
||||
section "valid dispatch"
|
||||
|
||||
# .sh runs the script directly
|
||||
out=$("${CLI}" "checkPad.sh" hello world 2>&1) || true
|
||||
assert_eq "$out" "checkPad.sh args: hello world" "checkPad.sh forwards args"
|
||||
|
||||
# .ts runs node --import tsx with the script path appended
|
||||
out=$("${CLI}" "importSqlFile.ts" --some-arg 2>&1) || true
|
||||
case "$out" in
|
||||
*"--import tsx/esm"*"importSqlFile.ts"*"--some-arg"*) pass "importSqlFile.ts dispatched via node tsx" ;;
|
||||
*) fail "importSqlFile.ts dispatched via node tsx" "got: $out" ;;
|
||||
esac
|
||||
|
||||
section "no-args usage"
|
||||
|
||||
out=$("${CLI}" 2>&1) || true
|
||||
case "$out" in
|
||||
*"Usage: etherpad.cli"*"checkPad.sh"*"importSqlFile.ts"*) pass "no-args prints usage and lists scripts" ;;
|
||||
*) fail "no-args prints usage and lists scripts" "got: $out" ;;
|
||||
esac
|
||||
|
||||
# Summary handed back to the runner via env counters.
|
||||
return 0 2>/dev/null || exit 0
|
||||
62
snap/tests/test-configure.sh
Executable file
62
snap/tests/test-configure.sh
Executable file
@ -0,0 +1,62 @@
|
||||
#!/bin/bash
|
||||
# Unit tests for snap/hooks/configure.
|
||||
# Validates port/ip values via a mocked snapctl. Restart-on-change paths
|
||||
# are not tested here (require running snapd).
|
||||
set -uo pipefail
|
||||
TEST_NAME="configure hook"
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
|
||||
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf "${TMP}"' EXIT
|
||||
|
||||
# Mock snapctl: returns env-provided values for `get`, no-ops on other verbs.
|
||||
cat > "${TMP}/snapctl" <<'EOF'
|
||||
#!/bin/bash
|
||||
case "$1 $2" in
|
||||
"get port") printf '%s' "${SNAPCTL_PORT-}" ;;
|
||||
"get ip") printf '%s' "${SNAPCTL_IP-}" ;;
|
||||
"services etherpad.etherpad") echo "etherpad.etherpad enabled inactive -" ;;
|
||||
"restart etherpad.etherpad") exit 0 ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
EOF
|
||||
chmod +x "${TMP}/snapctl"
|
||||
|
||||
HOOK="${HOOKS_DIR}/configure"
|
||||
|
||||
run_hook() {
|
||||
env -i PATH="${TMP}:/usr/bin:/bin" \
|
||||
SNAPCTL_PORT="${1-}" SNAPCTL_IP="${2-}" \
|
||||
bash "${HOOK}"
|
||||
}
|
||||
|
||||
section "port validation"
|
||||
|
||||
# Tests using the project-reserved test port 9003 (per memory).
|
||||
assert_exit 0 "valid port 9003" run_hook 9003 ""
|
||||
assert_exit 0 "valid port 1" run_hook 1 ""
|
||||
assert_exit 0 "valid port 65535" run_hook 65535 ""
|
||||
assert_exit 0 "empty port (no override)" run_hook "" ""
|
||||
|
||||
assert_exit 1 "rejects port 0" run_hook 0 ""
|
||||
assert_exit 1 "rejects port 70000" run_hook 70000 ""
|
||||
assert_exit 1 "rejects port 'abc'" run_hook abc ""
|
||||
assert_exit 1 "rejects port '-1'" run_hook -1 ""
|
||||
|
||||
assert_grep "1-65535" "out-of-range error message references valid range" \
|
||||
run_hook 99999 ""
|
||||
|
||||
section "ip validation"
|
||||
|
||||
assert_exit 0 "valid ip 0.0.0.0" run_hook "" "0.0.0.0"
|
||||
assert_exit 0 "valid ip 127.0.0.1" run_hook "" "127.0.0.1"
|
||||
assert_exit 0 "valid ip ::1" run_hook "" "::1"
|
||||
assert_exit 0 "empty ip (no override)" run_hook "" ""
|
||||
|
||||
assert_exit 1 "rejects ip 'not-an-ip'" run_hook "" "not-an-ip"
|
||||
assert_exit 1 "rejects ip 'localhost'" run_hook "" "localhost"
|
||||
|
||||
assert_grep "valid IPv4/IPv6" "ip error message mentions IPv4/IPv6" \
|
||||
run_hook "" "bogus"
|
||||
|
||||
return 0 2>/dev/null || exit 0
|
||||
138
snap/tests/test-service-bootstrap.sh
Executable file
138
snap/tests/test-service-bootstrap.sh
Executable file
@ -0,0 +1,138 @@
|
||||
#!/bin/bash
|
||||
# Unit tests for snap/local/bin/etherpad-service first-run bootstrap.
|
||||
# Verifies that the wrapper:
|
||||
# - seeds settings.json from the upstream template into $SNAP_COMMON/etc
|
||||
# - rewrites the dirty default to sqlite at $SNAP_COMMON/var/etherpad.db
|
||||
# - rewrites ip/port literals to ${IP:…} / ${PORT:…} env-substitution
|
||||
# - leaves an existing settings.json untouched on subsequent runs
|
||||
# - exports PORT/IP from snapctl overrides
|
||||
#
|
||||
# We mock node, snapctl, and the SNAP/SNAP_COMMON dirs. node-mock writes
|
||||
# its argv + selected env vars to a log file, then exits 0 instead of
|
||||
# actually starting Etherpad.
|
||||
set -uo pipefail
|
||||
TEST_NAME="etherpad-service bootstrap"
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
|
||||
|
||||
REPO_ROOT="$(cd "${SNAP_DIR}/.." && pwd -P)"
|
||||
TEMPLATE="${REPO_ROOT}/settings.json.template"
|
||||
[ -f "${TEMPLATE}" ] || { fail "fixture: settings.json.template not found at ${TEMPLATE}"; exit 1; }
|
||||
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf "${TMP}"' EXIT
|
||||
|
||||
export SNAP="${TMP}/snap"
|
||||
export SNAP_COMMON="${TMP}/common"
|
||||
mkdir -p "${SNAP}/opt/etherpad/src" "${SNAP}/opt/node/bin" \
|
||||
"${TMP}/bin"
|
||||
|
||||
cp "${TEMPLATE}" "${SNAP}/opt/etherpad/settings.json.template"
|
||||
|
||||
# node mock: log argv + env vars we care about, exit 0 (do not exec server)
|
||||
NODE_LOG="${TMP}/node-invocation.log"
|
||||
cat > "${SNAP}/opt/node/bin/node" <<EOF
|
||||
#!/bin/bash
|
||||
{
|
||||
echo "ARGV: \$*"
|
||||
echo "PORT=\${PORT-unset}"
|
||||
echo "IP=\${IP-unset}"
|
||||
echo "NODE_ENV=\${NODE_ENV-unset}"
|
||||
} > "${NODE_LOG}"
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x "${SNAP}/opt/node/bin/node"
|
||||
|
||||
# snapctl mock — controlled via SNAPCTL_PORT / SNAPCTL_IP env vars set per call.
|
||||
cat > "${TMP}/bin/snapctl" <<'EOF'
|
||||
#!/bin/bash
|
||||
case "$1 $2" in
|
||||
"get port") printf '%s' "${SNAPCTL_PORT-}" ;;
|
||||
"get ip") printf '%s' "${SNAPCTL_IP-}" ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
EOF
|
||||
chmod +x "${TMP}/bin/snapctl"
|
||||
export PATH="${TMP}/bin:${PATH}"
|
||||
|
||||
WRAPPER="${WRAPPERS_DIR}/etherpad-service"
|
||||
|
||||
run_wrapper() {
|
||||
# Use env -i would strip too much; instead just clear the override vars.
|
||||
unset PORT IP NODE_ENV
|
||||
SNAPCTL_PORT="${1-}" SNAPCTL_IP="${2-}" bash "${WRAPPER}"
|
||||
}
|
||||
|
||||
section "first-run seed and rewrite"
|
||||
|
||||
run_wrapper "" ""
|
||||
|
||||
SEEDED="${SNAP_COMMON}/etc/settings.json"
|
||||
[ -f "${SEEDED}" ] && pass "settings.json seeded into \$SNAP_COMMON/etc" \
|
||||
|| fail "settings.json seeded into \$SNAP_COMMON/etc" "missing: ${SEEDED}"
|
||||
|
||||
# dbType: dirty -> sqlite
|
||||
assert_grep '"dbType": "sqlite"' "dbType rewritten to sqlite" \
|
||||
cat "${SEEDED}"
|
||||
|
||||
# filename: var/dirty.db -> $SNAP_COMMON/var/etherpad.db
|
||||
assert_grep "${SNAP_COMMON}/var/etherpad.db" "sqlite filename points at \$SNAP_COMMON/var" \
|
||||
cat "${SEEDED}"
|
||||
|
||||
# ip / port: literal values -> env-substitution syntax
|
||||
assert_grep '"ip": "${IP:0.0.0.0}"' "ip rewritten to \${IP:0.0.0.0}" \
|
||||
cat "${SEEDED}"
|
||||
assert_grep '"port": "${PORT:9001}"' "port rewritten to \${PORT:9001}" \
|
||||
cat "${SEEDED}"
|
||||
|
||||
# dbSettings.port lower in the file MUST NOT have been touched (the sed
|
||||
# uses 0,/.../ to bound to the first match).
|
||||
dbsettings_port=$(grep -c '^[[:space:]]*"port":[[:space:]]*3306' "${SEEDED}" || true)
|
||||
assert_eq "${dbsettings_port}" "1" "dbSettings.port (3306) untouched by ip/port rewrite"
|
||||
|
||||
section "writable directories created"
|
||||
|
||||
for d in etc var logs etherpad-app-var; do
|
||||
if [ -d "${SNAP_COMMON}/${d}" ]; then
|
||||
pass "\$SNAP_COMMON/${d} created"
|
||||
else
|
||||
fail "\$SNAP_COMMON/${d} created" "missing: ${SNAP_COMMON}/${d}"
|
||||
fi
|
||||
done
|
||||
|
||||
section "snapctl overrides propagate to node env"
|
||||
|
||||
# Re-run with port=9003 (project test port), ip=127.0.0.1.
|
||||
run_wrapper 9003 127.0.0.1
|
||||
|
||||
assert_grep "PORT=9003" "PORT exported from snapctl override" \
|
||||
cat "${NODE_LOG}"
|
||||
assert_grep "IP=127.0.0.1" "IP exported from snapctl override" \
|
||||
cat "${NODE_LOG}"
|
||||
assert_grep "NODE_ENV=production" "NODE_ENV=production exported" \
|
||||
cat "${NODE_LOG}"
|
||||
assert_grep -- "--settings ${SEEDED}" "wrapper passes --settings explicitly" \
|
||||
cat "${NODE_LOG}"
|
||||
|
||||
section "second run does not re-seed"
|
||||
|
||||
# Mark the seeded settings.json so we can detect rewrites.
|
||||
echo '/* TEST MARKER */' >> "${SEEDED}"
|
||||
run_wrapper "" ""
|
||||
if grep -q "TEST MARKER" "${SEEDED}"; then
|
||||
pass "existing settings.json preserved on subsequent run"
|
||||
else
|
||||
fail "existing settings.json preserved on subsequent run" \
|
||||
"marker was removed — wrapper re-seeded the file"
|
||||
fi
|
||||
|
||||
section "snapctl defaults when unset"
|
||||
|
||||
# Remove the seeded file and run with no overrides.
|
||||
rm -f "${SEEDED}"
|
||||
run_wrapper "" ""
|
||||
assert_grep "PORT=9001" "PORT defaults to 9001 when snapctl is empty" \
|
||||
cat "${NODE_LOG}"
|
||||
assert_grep "IP=0.0.0.0" "IP defaults to 0.0.0.0 when snapctl is empty" \
|
||||
cat "${NODE_LOG}"
|
||||
|
||||
return 0 2>/dev/null || exit 0
|
||||
80
snap/tests/test-snapcraft-yaml.sh
Executable file
80
snap/tests/test-snapcraft-yaml.sh
Executable file
@ -0,0 +1,80 @@
|
||||
#!/bin/bash
|
||||
# Static checks on snap/snapcraft.yaml — purely structural, no snapcraft
|
||||
# binary required. Catches schema drift before a slow `snapcraft pack`.
|
||||
set -uo pipefail
|
||||
TEST_NAME="snapcraft.yaml"
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
|
||||
|
||||
YAML="${SNAP_DIR}/snapcraft.yaml"
|
||||
[ -f "${YAML}" ] || { fail "snapcraft.yaml exists at ${YAML}"; exit 1; }
|
||||
|
||||
# Use python3 for parsing — no extra deps.
|
||||
python3 - "${YAML}" <<'PY' || exit 1
|
||||
import sys, re, yaml
|
||||
path = sys.argv[1]
|
||||
with open(path) as f:
|
||||
data = yaml.safe_load(f)
|
||||
|
||||
failures = []
|
||||
|
||||
def check(cond, msg):
|
||||
print(f" {'✓' if cond else '✗'} {msg}")
|
||||
if not cond: failures.append(msg)
|
||||
|
||||
# Required top-level keys.
|
||||
for k in ("name","title","summary","description","base","confinement","apps","parts"):
|
||||
check(k in data, f"required key present: {k}")
|
||||
|
||||
# Name must be lowercase, no underscores, valid for snap store.
|
||||
name = data.get("name","")
|
||||
check(bool(re.match(r"^[a-z][a-z0-9-]{0,39}$", name)) and not name.endswith("-"),
|
||||
f"snap name '{name}' is store-valid")
|
||||
check(name == "etherpad", "snap name is exactly 'etherpad' (no etherpad-lite)")
|
||||
|
||||
# core24 + strict.
|
||||
check(data.get("base") == "core24", "base is core24")
|
||||
check(data.get("confinement") == "strict", "confinement is strict")
|
||||
|
||||
# adopt-info points at a real part.
|
||||
ai = data.get("adopt-info")
|
||||
parts = data.get("parts", {})
|
||||
check(ai in parts, f"adopt-info '{ai}' references an existing part")
|
||||
|
||||
# Daemon app shares the snap name → bare invocation works.
|
||||
apps = data.get("apps", {})
|
||||
daemon_apps = [n for n,v in apps.items() if "daemon" in v]
|
||||
check(name in daemon_apps,
|
||||
f"daemon app shares snap name (so `{name}` is the bare command)")
|
||||
|
||||
# Apps don't reintroduce the legacy 'etherpad-lite' name anywhere.
|
||||
flat = yaml.dump(data)
|
||||
check("etherpad-lite" not in flat,
|
||||
"no 'etherpad-lite' references in snapcraft.yaml")
|
||||
|
||||
# settings.json env vars should NOT include EP_SETTINGS / EP_DATA_DIR
|
||||
# (Etherpad doesn't read them; they trigger noisy warnings).
|
||||
env = apps.get(name, {}).get("environment", {}) or {}
|
||||
for forbidden in ("EP_SETTINGS","EP_DATA_DIR"):
|
||||
check(forbidden not in env,
|
||||
f"environment does not set unused var {forbidden}")
|
||||
|
||||
# PORT and IP env vars must be present so settings.json env-subst has defaults.
|
||||
for required in ("PORT","IP","NODE_ENV"):
|
||||
check(required in env,
|
||||
f"environment sets {required}")
|
||||
|
||||
print()
|
||||
print(f"{len(failures)} failure(s)" if failures else "OK")
|
||||
sys.exit(1 if failures else 0)
|
||||
PY
|
||||
yaml_rc=$?
|
||||
|
||||
if [ "$yaml_rc" = 0 ]; then
|
||||
PASS_COUNT=$((PASS_COUNT + 1))
|
||||
pass "snapcraft.yaml structural checks all pass"
|
||||
else
|
||||
FAIL_COUNT=$((FAIL_COUNT + 1))
|
||||
fail "snapcraft.yaml structural checks failed (see python output above)"
|
||||
fi
|
||||
|
||||
return 0 2>/dev/null || exit 0
|
||||
Loading…
x
Reference in New Issue
Block a user