mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-07-18 09:04:54 +00:00
* fix(deb): keep plugin_packages in-tree to fix admin-installed plugins
The .deb postinstall symlinked /opt/etherpad/src/plugin_packages to
/var/lib/etherpad/plugin_packages so the etherpad user could install
plugins under ProtectSystem=strict. Node.js resolves symlinks to their
realpath before walking node_modules, so a plugin installed via the
admin UI lived under /var/lib/etherpad/... and could no longer reach
the bundled ep_etherpad-lite in /opt/etherpad/node_modules. Every
require('ep_etherpad-lite/...') in installed plugins (and the matching
esbuild client-bundle build) failed with MODULE_NOT_FOUND, etherpad
exited, and the systemd unit restart-looped (ether/ep_comments_page#416).
Keep plugin_packages as a real in-tree directory, make it (and its
.versions/ subdir) group-writable by etherpad like node_modules already
is, and add it to ReadWritePaths= in the unit. Migrate the contents of
any pre-existing /var/lib/etherpad/plugin_packages symlink target back
in-tree on upgrade.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci(deb): update assertions for in-tree plugin_packages + cover migration
The previous assertions in packaging/test-local.sh and the deb-package
workflow checked that /opt/etherpad/src/plugin_packages was a symlink to
/var/lib/etherpad/plugin_packages -- the layout this PR is removing.
They would have failed under the new postinst.
- Assert plugin_packages is a real directory (not a symlink), owned by
group etherpad with mode 2775, matching node_modules.
- After the happy-path /health check, simulate a pre-fix install by
recreating the symlink with a marker plugin, re-run the postinst via
dpkg-reconfigure, and assert the marker payload was migrated back
in-tree and the symlink is gone. Locks in regression coverage for the
upgrade path (ether/ep_comments_page#416).
* ci(deb): add ep_layout_trip_wire fixture to gate plugin_packages layout
Layout assertions alone don't actually exercise the failure mode from
ether/ep_comments_page#416: they confirm /opt/etherpad/src/plugin_packages
is a real directory but never load a plugin whose index.js does
require('ep_etherpad-lite/...') from the on-disk realpath.
Ship a tiny test plugin under packaging/test-fixtures/ep_layout_trip_wire
that exercises the four require() patterns from the bug report (eejs,
Settings, log4js, pad_utils) and emits a marker line from
expressCreateServer. Both packaging/test-local.sh and the deb-package
workflow now stage it into plugin_packages/.versions/, wire up the
toplevel + node_modules symlinks live-plugin-manager would create,
list it in installed_plugins.json, restart etherpad, and assert:
* marker line appears in the journal (every require resolved), and
* no "Cannot find module 'ep_etherpad-lite" appears anywhere.
Verified locally that the fixture loads under the in-tree layout
(marker present) and fails under a symlinked-out-of-tree layout
(marker absent, MODULE_NOT_FOUND in the log) -- so the gate catches
the regression in both directions.
* fix(deb): clean up runtime plugin artifacts on purge
With plugin_packages now living under /opt/etherpad/src/plugin_packages,
admin-installed plugins land in dpkg-unmanaged paths (the .versions/
stage that live-plugin-manager populates plus matching ep_* symlinks
under src/node_modules/). dpkg --purge would leave them behind because
the manifest never recorded them.
In postremove's purge branch: explicitly rm -rf plugin_packages and any
runtime ep_* symlinks in node_modules, then rm -rf the whole APP_DIR
as belt-and-braces against other runtime drift. Verified in a sandbox
that a staged ep_runtime@1.0.0 + node_modules/ep_runtime symlink are
both gone after purge runs.
Add post-purge assertions to both packaging/test-local.sh and the
deb-package workflow: /opt/etherpad/src/plugin_packages and
/var/lib/etherpad must not exist after dpkg --purge.
Addresses Qodo PR #7750 review item 3 (\"Purge cleanup misses
plugins\", Reliability).
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
475 lines
21 KiB
YAML
475 lines
21 KiB
YAML
name: Debian package
|
|
on:
|
|
push:
|
|
tags:
|
|
# Actions tag filters are globs, not regex. Mirrors handleRelease.yml.
|
|
- 'v*.*.*'
|
|
- 'v*.*.*-*'
|
|
branches: [develop]
|
|
paths:
|
|
- 'packaging/**'
|
|
- '.github/workflows/deb-package.yml'
|
|
- 'src/package.json'
|
|
- 'pnpm-lock.yaml'
|
|
- 'src/node/server.ts'
|
|
- 'src/node/utils/run_cmd.ts'
|
|
- 'src/static/js/pluginfw/**'
|
|
- 'settings.json.template'
|
|
workflow_dispatch:
|
|
inputs:
|
|
ref:
|
|
description: 'Git ref to package (defaults to current)'
|
|
required: false
|
|
|
|
# Default to read-only for the workflow; the release job opts in to
|
|
# `contents: write` for itself only. Build jobs (which run on every PR)
|
|
# don't need write and shouldn't have it.
|
|
permissions:
|
|
contents: read
|
|
|
|
env:
|
|
NFPM_VERSION: v2.43.0
|
|
|
|
jobs:
|
|
build:
|
|
name: Build .deb (${{ matrix.arch }})
|
|
runs-on: ${{ matrix.runner }}
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- arch: amd64
|
|
runner: ubuntu-latest
|
|
- arch: arm64
|
|
runner: ubuntu-24.04-arm
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ inputs.ref || github.ref }}
|
|
|
|
- uses: pnpm/action-setup@v6
|
|
|
|
- name: Setup Node
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version: '25'
|
|
cache: pnpm
|
|
|
|
- name: Resolve version
|
|
id: v
|
|
# Runs after setup-node so `node` is guaranteed available on
|
|
# any runner image (some don't ship it preinstalled).
|
|
run: |
|
|
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
|
|
VERSION="${GITHUB_REF_NAME#v}"
|
|
else
|
|
VERSION="$(node -p "require('./package.json').version")"
|
|
fi
|
|
echo "version=${VERSION}" >>"$GITHUB_OUTPUT"
|
|
echo "Packaging version: ${VERSION}"
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Build UI + admin
|
|
run: pnpm run build:etherpad
|
|
|
|
- name: Install nfpm
|
|
run: |
|
|
set -euo pipefail
|
|
NFPM_ARCH=amd64
|
|
[ "${{ matrix.arch }}" = "arm64" ] && NFPM_ARCH=arm64
|
|
NFPM_DEB="nfpm_${NFPM_VERSION#v}_${NFPM_ARCH}.deb"
|
|
BASE="https://github.com/goreleaser/nfpm/releases/download/${NFPM_VERSION}"
|
|
curl -fsSL -o "/tmp/${NFPM_DEB}" "${BASE}/${NFPM_DEB}"
|
|
curl -fsSL -o /tmp/nfpm-checksums.txt "${BASE}/checksums.txt"
|
|
# Verify upstream artifact before sudo dpkg -i (defense in depth
|
|
# against a tampered release asset).
|
|
( cd /tmp && grep " ${NFPM_DEB}\$" nfpm-checksums.txt | sha256sum -c - )
|
|
sudo dpkg -i "/tmp/${NFPM_DEB}"
|
|
|
|
- name: Stage tree for packaging
|
|
run: |
|
|
set -eux
|
|
STAGE=staging/opt/etherpad
|
|
mkdir -p "${STAGE}"
|
|
|
|
# Production footprint = src/ + bin/ + node_modules/ + metadata.
|
|
cp -a src bin package.json pnpm-workspace.yaml README.md LICENSE \
|
|
node_modules "${STAGE}/"
|
|
|
|
# Make pnpm-workspace.yaml production-only (same trick Dockerfile uses).
|
|
printf 'packages:\n - src\n - bin\n' > "${STAGE}/pnpm-workspace.yaml"
|
|
|
|
mkdir -p packaging/etc
|
|
cp settings.json.template packaging/etc/settings.json.dist
|
|
|
|
# Purge test fixtures and dev caches from node_modules to shrink size.
|
|
find "${STAGE}/node_modules" -type d \
|
|
\( -name test -o -name tests -o -name '__tests__' \
|
|
-o -name example -o -name examples -o -name docs \) \
|
|
-prune -exec rm -rf {} + 2>/dev/null || true
|
|
find "${STAGE}/node_modules" -type f \
|
|
\( -name '*.md' -o -name '*.ts.map' -o -name '*.map' \
|
|
-o -name 'CHANGELOG*' -o -name 'HISTORY*' \) \
|
|
-delete 2>/dev/null || true
|
|
|
|
- name: Build .deb
|
|
env:
|
|
VERSION: ${{ steps.v.outputs.version }}
|
|
ARCH: ${{ matrix.arch }}
|
|
run: |
|
|
mkdir -p dist
|
|
nfpm package --packager deb -f packaging/nfpm.yaml --target dist/
|
|
|
|
- name: Smoke-test the package (amd64 only)
|
|
if: matrix.arch == 'amd64'
|
|
run: |
|
|
set -eux
|
|
# Ubuntu's default apt nodejs is 18 — too old for our
|
|
# `Depends: nodejs (>= 25)`. Add NodeSource's apt repo
|
|
# explicitly (key + sources.list) instead of `curl | sudo bash`
|
|
# so we don't execute network-fetched code as root.
|
|
NODE_MAJOR=25
|
|
# GitHub runner images often ship a NodeSource node_20.x list
|
|
# preinstalled (sometimes as a .sources deb822 file). Wipe any
|
|
# existing nodesource entries so the only Node candidate apt sees
|
|
# is our node_25.x repo. Otherwise `apt-get install -y nodejs`
|
|
# picks the higher-version 20.x build that's already cached and
|
|
# `dpkg -i` then fails on `Depends: nodejs (>= 25)`.
|
|
sudo rm -f /etc/apt/sources.list.d/nodesource.list \
|
|
/etc/apt/sources.list.d/nodesource.sources \
|
|
/etc/apt/preferences.d/nodesource \
|
|
/etc/apt/preferences.d/nodesource.pref
|
|
KEYRING=/usr/share/keyrings/nodesource.gpg
|
|
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
|
|
| sudo gpg --dearmor --yes -o "${KEYRING}"
|
|
echo "deb [signed-by=${KEYRING}] https://deb.nodesource.com/node_${NODE_MAJOR}.x nodistro main" \
|
|
| sudo tee /etc/apt/sources.list.d/nodesource.list
|
|
# Pin nodejs to the 25.x line so neither Ubuntu's noble-updates
|
|
# 20.x nor any leftover NodeSource cache can win the resolver.
|
|
printf 'Package: nodejs\nPin: version %s.*\nPin-Priority: 1001\n' "${NODE_MAJOR}" \
|
|
| sudo tee /etc/apt/preferences.d/nodesource >/dev/null
|
|
sudo apt-get update
|
|
# Pin the major explicitly on the install line as a belt-and-
|
|
# suspenders guard against any preference file being ignored.
|
|
sudo apt-get install -y "nodejs=${NODE_MAJOR}.*"
|
|
# Sanity check before invoking dpkg so the failure mode is obvious
|
|
# if pinning ever regresses again.
|
|
installed_major=$(dpkg-query -W -f='${Version}' nodejs | cut -d. -f1)
|
|
if [ "${installed_major}" != "${NODE_MAJOR}" ]; then
|
|
echo "::error::Expected nodejs major ${NODE_MAJOR}, got ${installed_major}"
|
|
apt-cache policy nodejs || true
|
|
exit 1
|
|
fi
|
|
sudo dpkg -i dist/*.deb || sudo apt-get install -f -y
|
|
# /etc/etherpad is mode 0750 root:etherpad on purpose (DB creds
|
|
# live here) so the runner user can't read into it. Each check
|
|
# that crosses /etc/etherpad or /var/lib/etherpad runs under sudo.
|
|
sudo test -x /usr/bin/etherpad
|
|
sudo test -f /etc/etherpad/settings.json
|
|
sudo test -L /opt/etherpad/settings.json
|
|
sudo test -L /opt/etherpad/var
|
|
[ "$(sudo readlink /opt/etherpad/var)" = "/var/lib/etherpad/var" ]
|
|
# plugin_packages must stay in-tree -- Node.js resolves symlinks
|
|
# to realpath before walking node_modules, so symlinking it
|
|
# outside /opt broke require("ep_etherpad-lite/...") in
|
|
# admin-installed plugins (ether/ep_comments_page#416).
|
|
sudo test -d /opt/etherpad/src/plugin_packages
|
|
sudo test ! -L /opt/etherpad/src/plugin_packages
|
|
[ "$(sudo stat -c '%G' /opt/etherpad/src/plugin_packages)" = "etherpad" ]
|
|
[ "$(sudo stat -c '%a' /opt/etherpad/src/plugin_packages)" = "2775" ]
|
|
[ "$(stat -c '%G' /opt/etherpad/src/node_modules)" = "etherpad" ]
|
|
sudo test -f /var/lib/etherpad/var/installed_plugins.json
|
|
sudo grep -q '"ep_etherpad-lite"' /var/lib/etherpad/var/installed_plugins.json
|
|
sudo grep -q '"dbType": "sqlite"' /etc/etherpad/settings.json
|
|
id etherpad
|
|
systemctl cat etherpad.service
|
|
sudo systemctl start etherpad
|
|
ok=
|
|
for i in $(seq 1 30); do
|
|
if curl -fsS http://127.0.0.1:9001/health; then
|
|
ok=1
|
|
break
|
|
fi
|
|
sleep 2
|
|
done
|
|
if [ -z "${ok}" ]; then
|
|
# Attach logs so the failing run is diagnosable.
|
|
sudo journalctl -u etherpad --no-pager -n 200 || true
|
|
exit 1
|
|
fi
|
|
sudo systemctl stop etherpad
|
|
# Regression: simulate a pre-fix install (plugin_packages as a
|
|
# symlink to /var/lib/etherpad/plugin_packages, with a marker
|
|
# plugin inside) and re-run the postinst. The new postinst must
|
|
# migrate the contents back in-tree and drop the symlink so
|
|
# admin-installed plugins keep resolving ep_etherpad-lite
|
|
# (ether/ep_comments_page#416).
|
|
sudo rm -rf /opt/etherpad/src/plugin_packages
|
|
sudo mkdir -p /var/lib/etherpad/plugin_packages/.versions/ep_migration_marker
|
|
echo '{"name":"ep_migration_marker"}' | \
|
|
sudo tee /var/lib/etherpad/plugin_packages/.versions/ep_migration_marker/package.json >/dev/null
|
|
sudo chown -R etherpad:etherpad /var/lib/etherpad/plugin_packages
|
|
sudo ln -sfn /var/lib/etherpad/plugin_packages /opt/etherpad/src/plugin_packages
|
|
sudo dpkg-reconfigure etherpad
|
|
sudo test -d /opt/etherpad/src/plugin_packages
|
|
sudo test ! -L /opt/etherpad/src/plugin_packages
|
|
sudo test -f /opt/etherpad/src/plugin_packages/.versions/ep_migration_marker/package.json
|
|
[ "$(sudo stat -c '%a' /opt/etherpad/src/plugin_packages)" = "2775" ]
|
|
|
|
# Regression: stage the ep_layout_trip_wire test fixture into
|
|
# plugin_packages and confirm etherpad loads it. The fixture's
|
|
# index.js does the require('ep_etherpad-lite/...') calls that
|
|
# broke under the old symlinked layout (#416). If the layout
|
|
# ever regresses, the marker line never reaches the journal
|
|
# and this step fails.
|
|
PLUGIN_DIR=/opt/etherpad/src/plugin_packages
|
|
FIXTURE_DIR=$GITHUB_WORKSPACE/packaging/test-fixtures/ep_layout_trip_wire
|
|
sudo install -d -o etherpad -g etherpad -m 2775 "${PLUGIN_DIR}/.versions"
|
|
sudo cp -a "${FIXTURE_DIR}" "${PLUGIN_DIR}/.versions/ep_layout_trip_wire@1.0.0"
|
|
sudo ln -sfn .versions/ep_layout_trip_wire@1.0.0 "${PLUGIN_DIR}/ep_layout_trip_wire"
|
|
sudo ln -sfn ../plugin_packages/ep_layout_trip_wire \
|
|
/opt/etherpad/src/node_modules/ep_layout_trip_wire
|
|
sudo chown -R etherpad:etherpad "${PLUGIN_DIR}/.versions/ep_layout_trip_wire@1.0.0" \
|
|
"${PLUGIN_DIR}/ep_layout_trip_wire" \
|
|
/opt/etherpad/src/node_modules/ep_layout_trip_wire
|
|
echo '{"plugins":[{"name":"ep_etherpad-lite","version":"0.0.0"},{"name":"ep_layout_trip_wire","version":"1.0.0"}]}' \
|
|
| sudo tee /var/lib/etherpad/var/installed_plugins.json >/dev/null
|
|
sudo chown etherpad:etherpad /var/lib/etherpad/var/installed_plugins.json
|
|
sudo systemctl start etherpad
|
|
ok=
|
|
for i in $(seq 1 30); do
|
|
if curl -fsS http://127.0.0.1:9001/health; then ok=1; break; fi
|
|
sleep 2
|
|
done
|
|
if [ -z "${ok}" ]; then
|
|
sudo journalctl -u etherpad --no-pager -n 300 || true
|
|
exit 1
|
|
fi
|
|
# Marker proves the require('ep_etherpad-lite/...') calls in
|
|
# the fixture's index.js all resolved.
|
|
sudo journalctl -u etherpad --no-pager -n 500 \
|
|
| grep -F 'ep_layout_trip_wire: plugin_packages layout OK'
|
|
# And no MODULE_NOT_FOUND involving ep_etherpad-lite, anywhere.
|
|
if sudo journalctl -u etherpad --no-pager -n 500 \
|
|
| grep -E "Cannot find module '?ep_etherpad-lite"; then
|
|
echo "::error::ep_etherpad-lite require failed inside an installed plugin"
|
|
exit 1
|
|
fi
|
|
sudo systemctl stop etherpad
|
|
|
|
sudo dpkg --purge etherpad
|
|
! id etherpad 2>/dev/null
|
|
# Purge must clean up runtime-created plugin artifacts that
|
|
# dpkg didn't ship (ether/ep_comments_page#416, Qodo #3).
|
|
sudo test ! -e /opt/etherpad/src/plugin_packages
|
|
sudo test ! -e /var/lib/etherpad
|
|
|
|
- name: Upload artifact
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: etherpad-${{ steps.v.outputs.version }}-${{ matrix.arch }}-deb
|
|
path: dist/*.deb
|
|
if-no-files-found: error
|
|
|
|
release:
|
|
name: Attach to GitHub Release
|
|
needs: build
|
|
if: startsWith(github.ref, 'refs/tags/v')
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write
|
|
steps:
|
|
- uses: actions/download-artifact@v8
|
|
with:
|
|
path: dist
|
|
pattern: etherpad-*-deb
|
|
merge-multiple: true
|
|
- name: Create stable "latest" aliases
|
|
run: |
|
|
set -euo pipefail
|
|
# Publish stable filenames alongside the versioned ones so users
|
|
# can curl https://github.com/.../releases/latest/download/etherpad-latest_amd64.deb
|
|
# without knowing the version.
|
|
for f in dist/etherpad_*_amd64.deb; do
|
|
[ -e "$f" ] && cp "$f" dist/etherpad-latest_amd64.deb
|
|
done
|
|
for f in dist/etherpad_*_arm64.deb; do
|
|
[ -e "$f" ] && cp "$f" dist/etherpad-latest_arm64.deb
|
|
done
|
|
ls -la dist/
|
|
- name: Attach .deb files to release
|
|
uses: softprops/action-gh-release@v3
|
|
with:
|
|
files: dist/*.deb
|
|
fail_on_unmatched_files: true
|
|
|
|
apt-publish:
|
|
# Generates a signed apt repository (Packages.gz + Release/InRelease)
|
|
# from the .deb artefacts and cross-pushes it into ether/ether.github.com
|
|
# under public/apt/. The Next.js site that powers etherpad.org serves
|
|
# public/ verbatim, so the repo lands at:
|
|
#
|
|
# https://etherpad.org/apt/ (apt repo root)
|
|
# https://etherpad.org/key.asc (public key for `apt-key`/keyring)
|
|
#
|
|
# Tag pushes go into the `stable` suite. Required secrets:
|
|
# APT_SIGNING_KEY ASCII-armoured private key for the Etherpad APT
|
|
# Repository keypair (fingerprint
|
|
# 6953FA0C6431F30347D65B03AF0CD687D51A6E63).
|
|
# SITE_DEPLOY_KEY SSH private key matching a deploy key with write
|
|
# access on ether/ether.github.com. The site repo
|
|
# holds the public half.
|
|
name: Publish apt repository to etherpad.org
|
|
needs: release
|
|
if: startsWith(github.ref, 'refs/tags/v')
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout etherpad source (for packaging/apt/key.asc)
|
|
uses: actions/checkout@v6
|
|
with:
|
|
fetch-depth: 1
|
|
|
|
- name: Configure deploy key for ether/ether.github.com
|
|
env:
|
|
SITE_DEPLOY_KEY: ${{ secrets.SITE_DEPLOY_KEY }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [ -z "${SITE_DEPLOY_KEY:-}" ]; then
|
|
echo "::error::SITE_DEPLOY_KEY secret is not set on ether/etherpad."
|
|
echo "::error::Add an SSH deploy key with write access on ether/ether.github.com and store the private key here."
|
|
exit 1
|
|
fi
|
|
mkdir -p ~/.ssh
|
|
chmod 700 ~/.ssh
|
|
printf '%s\n' "${SITE_DEPLOY_KEY}" > ~/.ssh/id_deploy
|
|
chmod 600 ~/.ssh/id_deploy
|
|
ssh-keyscan -t ed25519,rsa github.com >> ~/.ssh/known_hosts 2>/dev/null
|
|
cat > ~/.ssh/config <<'CFG'
|
|
Host github.com
|
|
HostName github.com
|
|
User git
|
|
IdentityFile ~/.ssh/id_deploy
|
|
IdentitiesOnly yes
|
|
CFG
|
|
chmod 600 ~/.ssh/config
|
|
|
|
- name: Clone ether/ether.github.com
|
|
run: git clone --depth 1 git@github.com:ether/ether.github.com.git site
|
|
|
|
- uses: actions/download-artifact@v8
|
|
with:
|
|
path: dist
|
|
pattern: etherpad-*-deb
|
|
merge-multiple: true
|
|
|
|
- name: Install apt-utils + gpg
|
|
run: |
|
|
sudo apt-get update -qq
|
|
sudo apt-get install -y -qq apt-utils gnupg
|
|
|
|
- name: Import signing key
|
|
env:
|
|
APT_SIGNING_KEY: ${{ secrets.APT_SIGNING_KEY }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [ -z "${APT_SIGNING_KEY:-}" ]; then
|
|
echo "::error::APT_SIGNING_KEY secret is not set; cannot sign Release file."
|
|
exit 1
|
|
fi
|
|
export GNUPGHOME="$(mktemp -d)"
|
|
chmod 700 "${GNUPGHOME}"
|
|
echo "GNUPGHOME=${GNUPGHOME}" >>"${GITHUB_ENV}"
|
|
printf '%s' "${APT_SIGNING_KEY}" | gpg --batch --import
|
|
# Sanity check: expected long key id.
|
|
gpg --list-secret-keys --keyid-format=long | grep -q AF0CD687D51A6E63
|
|
|
|
- name: Generate apt repo metadata
|
|
run: |
|
|
set -euo pipefail
|
|
REPO=site/public/apt
|
|
SUITE=stable
|
|
COMP=main
|
|
# Wipe any previous repo state so removed versions don't linger
|
|
# in pool/. Packages.gz is regenerated from whatever is in pool/
|
|
# right now, so this is the simplest correct option — alternative
|
|
# is per-version diffing which is fragile.
|
|
rm -rf "${REPO}"
|
|
# We ship one architecture-agnostic suite with per-arch pools.
|
|
# Layout: apt/dists/<suite>/main/binary-{amd64,arm64}/
|
|
for arch in amd64 arm64; do
|
|
mkdir -p "${REPO}/pool/main/e/etherpad" "${REPO}/dists/${SUITE}/${COMP}/binary-${arch}"
|
|
done
|
|
# Drop the .debs into pool/. The leading-digit pattern
|
|
# excludes the etherpad-latest_*.deb filename aliases the
|
|
# release job stages — apt resolves by package name + version,
|
|
# not filename, so including the alias would create duplicate
|
|
# Packages entries. (Also defends against any future alias that
|
|
# accidentally lands on dist/etherpad_<word>_<arch>.deb.)
|
|
shopt -s nullglob
|
|
DEBS=(dist/etherpad_[0-9]*_amd64.deb dist/etherpad_[0-9]*_arm64.deb)
|
|
shopt -u nullglob
|
|
# Refuse to publish nothing. Without this, a missing or renamed
|
|
# build artefact would wipe site/public/apt and push an empty,
|
|
# signed apt repo — breaking `apt update` for every existing
|
|
# subscriber until the next successful release.
|
|
if [ ${#DEBS[@]} -lt 2 ]; then
|
|
echo "::error::Expected per-arch .deb artifacts in dist/, found ${#DEBS[@]}: ${DEBS[*]:-<none>}"
|
|
echo "::error::Refusing to publish a partial / empty apt repository."
|
|
exit 1
|
|
fi
|
|
cp "${DEBS[@]}" "${REPO}/pool/main/e/etherpad/"
|
|
# Generate per-arch Packages files.
|
|
(
|
|
cd "${REPO}"
|
|
for arch in amd64 arm64; do
|
|
apt-ftparchive --arch "${arch}" packages pool/main \
|
|
> "dists/${SUITE}/${COMP}/binary-${arch}/Packages"
|
|
gzip -kf "dists/${SUITE}/${COMP}/binary-${arch}/Packages"
|
|
done
|
|
# Generate the suite's Release file. The heredoc lines
|
|
# MUST start at column 1 — apt parsers reject leading
|
|
# whitespace on header fields (RFC 822 / Debian control).
|
|
# printf is used over a heredoc to make that contract
|
|
# impossible to lose to a future re-indent.
|
|
printf '%s\n' \
|
|
"Origin: Etherpad" \
|
|
"Label: Etherpad" \
|
|
"Suite: ${SUITE}" \
|
|
"Codename: ${SUITE}" \
|
|
"Architectures: amd64 arm64" \
|
|
"Components: ${COMP}" \
|
|
"Description: Etherpad official apt repository (${SUITE} channel)" \
|
|
"Date: $(date -Ru)" \
|
|
> "dists/${SUITE}/Release"
|
|
# apt-ftparchive appends checksums.
|
|
apt-ftparchive release "dists/${SUITE}" >> "dists/${SUITE}/Release"
|
|
# Sign it (clear-signed InRelease + detached Release.gpg).
|
|
gpg --default-key AF0CD687D51A6E63 --batch --yes \
|
|
--clearsign -o "dists/${SUITE}/InRelease" "dists/${SUITE}/Release"
|
|
gpg --default-key AF0CD687D51A6E63 --batch --yes \
|
|
-abs -o "dists/${SUITE}/Release.gpg" "dists/${SUITE}/Release"
|
|
)
|
|
|
|
- name: Stage public key alongside the site
|
|
run: |
|
|
# Users curl this to add our key to their keyring before apt update.
|
|
cp packaging/apt/key.asc site/public/key.asc
|
|
|
|
- name: Commit + push to ether/ether.github.com
|
|
env:
|
|
TAG: ${{ github.ref_name }}
|
|
run: |
|
|
set -euo pipefail
|
|
cd site
|
|
git -c user.email=actions@github.com -c user.name='github-actions[bot]' \
|
|
add public/apt public/key.asc
|
|
if git diff --cached --quiet; then
|
|
echo "No apt-repo changes to publish."
|
|
exit 0
|
|
fi
|
|
git -c user.email=actions@github.com -c user.name='github-actions[bot]' \
|
|
commit -m "apt: publish Etherpad ${TAG}"
|
|
git push origin HEAD:master
|