Skip to content

feat(socialMeta): settings.socialMeta.description override (#7599 fol… #82

feat(socialMeta): settings.socialMeta.description override (#7599 fol…

feat(socialMeta): settings.socialMeta.description override (#7599 fol… #82

Workflow file for this run

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: '24'
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 (>= 22)`. 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=24
# 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_24.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 (>= 22)`.
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 24.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" ]
sudo test -L /opt/etherpad/src/plugin_packages
[ "$(sudo readlink /opt/etherpad/src/plugin_packages)" = "/var/lib/etherpad/plugin_packages" ]
sudo test -d /var/lib/etherpad/plugin_packages
[ "$(sudo stat -c '%U' /var/lib/etherpad/plugin_packages)" = "etherpad" ]
[ "$(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
sudo dpkg --purge etherpad
! id etherpad 2>/dev/null
- 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