Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 168 additions & 0 deletions .github/workflows/deb-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/<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
20 changes: 19 additions & 1 deletion packaging/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
90 changes: 90 additions & 0 deletions packaging/apt/generate-signing-key.sh
Original file line number Diff line number Diff line change
@@ -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 <<EOF
%no-protection
Key-Type: EDDSA
Key-Curve: ed25519
Subkey-Type: ECDH
Subkey-Curve: cv25519
Name-Real: ${NAME_REAL}
Name-Email: ${NAME_EMAIL}
Expire-Date: ${EXPIRE_YEARS}y
%commit
EOF

echo
echo "==> 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: <paste the contents of ${PRIV}>"
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."
14 changes: 14 additions & 0 deletions packaging/apt/key.asc
Original file line number Diff line number Diff line change
@@ -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-----
Loading