diff --git a/.github/workflows/deb-package.yml b/.github/workflows/deb-package.yml index 36796326e0b..107434d081c 100644 --- a/.github/workflows/deb-package.yml +++ b/.github/workflows/deb-package.yml @@ -226,3 +226,171 @@ jobs: 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//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__.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[*]:-}" + 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 diff --git a/packaging/README.md b/packaging/README.md index 1d7d7a28e0b..cb70e699f27 100644 --- a/packaging/README.md +++ b/packaging/README.md @@ -53,7 +53,25 @@ packaging/test-local.sh --build-only # just produce dist/*.deb This is the fastest way to validate that the systemd hardening, plugin path symlinks, and tsx wrapper actually work together before pushing. -## Installing +## Installing via the Etherpad apt repository (recommended) + +The release workflow publishes a signed apt repository at +`https://etherpad.org/apt/` on every tagged release. Three lines on +any Debian/Ubuntu/Mint: + +```sh +curl -fsSL https://etherpad.org/key.asc \ + | sudo gpg --dearmor -o /usr/share/keyrings/etherpad.gpg +echo "deb [signed-by=/usr/share/keyrings/etherpad.gpg] https://etherpad.org/apt stable main" \ + | sudo tee /etc/apt/sources.list.d/etherpad.list +sudo apt update && sudo apt install etherpad +``` + +`apt upgrade` works going forward. Repo metadata is signed with the +GPG keypair documented in `packaging/apt/key.asc` (long key id +`AF0CD687D51A6E63`). + +## Installing a single .deb directly The release page publishes both versioned and stable filenames per arch: diff --git a/packaging/apt/generate-signing-key.sh b/packaging/apt/generate-signing-key.sh new file mode 100755 index 00000000000..71222c76013 --- /dev/null +++ b/packaging/apt/generate-signing-key.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# One-time setup: generate a dedicated GPG keypair for signing the +# Etherpad apt repository's Release/InRelease files. Outputs go into +# ./etherpad-apt-{private,public}.asc in the directory you run this in. +# +# After running this script: +# 1. Paste the *private* key contents into a new GitHub repo/org secret +# called APT_SIGNING_KEY (Settings → Secrets and variables → Actions +# → New repository secret). Then delete the .asc file or move it to +# a password manager — GitHub is the canonical store. +# 2. Hand the *public* key contents to whoever is wiring up the apt +# workflow; it gets committed at packaging/apt/key.asc so end users +# can pull it from https://ether.github.io/etherpad/key.asc. +# 3. Note the printed long key ID — the workflow uses it as +# --default-key for `gpg --clearsign`. + +set -euo pipefail + +NAME_REAL="${NAME_REAL:-Etherpad APT Repository}" +NAME_EMAIL="${NAME_EMAIL:-contact@etherpad.org}" +EXPIRE_YEARS="${EXPIRE_YEARS:-5}" + +OUT_DIR="$(pwd)" +PRIV="${OUT_DIR}/etherpad-apt-private.asc" +PUB="${OUT_DIR}/etherpad-apt-public.asc" + +if [[ -e "${PRIV}" || -e "${PUB}" ]]; then + echo "!! Output files already exist in ${OUT_DIR}:" >&2 + ls -la "${PRIV}" "${PUB}" 2>/dev/null >&2 || true + echo " Move/delete them first, or set OUT_DIR to a clean directory." >&2 + exit 1 +fi + +if ! command -v gpg >/dev/null 2>&1; then + echo "!! gpg not found. Install with: sudo apt install gnupg" >&2 + exit 1 +fi + +echo "==> Generating Ed25519 signing key for: ${NAME_REAL} <${NAME_EMAIL}>" +echo " Expires in ${EXPIRE_YEARS} years. No passphrase (CI uses it unattended)." + +# Use a temp GNUPGHOME so we don't pollute the user's keyring with a +# CI-only key, and so subsequent re-runs don't need to delete keys. +TMP_GNUPG="$(mktemp -d)" +trap 'rm -rf "${TMP_GNUPG}"' EXIT +chmod 700 "${TMP_GNUPG}" +export GNUPGHOME="${TMP_GNUPG}" + +gpg --batch --gen-key < Key generated. Details:" +gpg --list-secret-keys --keyid-format=long "${NAME_EMAIL}" + +KEY_ID="$(gpg --list-secret-keys --with-colons "${NAME_EMAIL}" \ + | awk -F: '/^sec/ {print $5; exit}')" + +echo +echo "==> Exporting to ${OUT_DIR}/" +gpg --armor --export-secret-keys "${NAME_EMAIL}" > "${PRIV}" +gpg --armor --export "${NAME_EMAIL}" > "${PUB}" +chmod 600 "${PRIV}" +chmod 644 "${PUB}" + +echo +echo "Done." +echo +echo " Private key (UPLOAD AS GITHUB SECRET 'APT_SIGNING_KEY'):" +echo " ${PRIV}" +echo " Public key (commit as packaging/apt/key.asc, hand to me):" +echo " ${PUB}" +echo " Long key ID (note this somewhere; used as --default-key in the workflow):" +echo " ${KEY_ID}" +echo +echo "Next steps:" +echo " 1. Open https://github.com/ether/etherpad/settings/secrets/actions/new" +echo " Name: APT_SIGNING_KEY" +echo " Value: " +echo " 2. Securely store ${PRIV} (password manager) or delete it after upload." +echo " 3. Send me ${PUB} (or its contents) for the public-key commit." diff --git a/packaging/apt/key.asc b/packaging/apt/key.asc new file mode 100644 index 00000000000..6658b8b4808 --- /dev/null +++ b/packaging/apt/key.asc @@ -0,0 +1,14 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEae+WgxYJKwYBBAHaRw8BAQdAlcdLkrHestdHPWsBdAHX/S48DAmIiU9wu9JH +dPZbpmO0LkV0aGVycGFkIEFQVCBSZXBvc2l0b3J5IDxjb250YWN0QGV0aGVycGFk +Lm9yZz6ImQQTFgoAQRYhBGlT+gxkMfMDR9ZbA68M1ofVGm5jBQJp75aDAhsjBQkJ +ZgGABQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJEK8M1ofVGm5jerkBAKd2 +PtrZikAXFeUrlM2BLinXFCL6UOTra9tvhjsuM2ZrAP4/5yqSMIVCwiHluyg08Nzd +aUW0YK9hJOKQkgL3RXTHCLg4BGnvloMSCisGAQQBl1UBBQEBB0BEuHcDkjBQCfPH ++zjFwbcPj06ODzuqhHbWDVLdqVhTcQMBCAeIfgQYFgoAJhYhBGlT+gxkMfMDR9Zb +A68M1ofVGm5jBQJp75aDAhsMBQkJZgGAAAoJEK8M1ofVGm5jlYwBAMvcavJ5/PKH +IcAsZt0SLv2NkeRcTd58oadCivcrAi1WAQDugqCn8Og39e64ND7LpUKPuqO/02gD +shfWz77UlCy3Cw== +=Bcop +-----END PGP PUBLIC KEY BLOCK-----