diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 041f7c1a9..f32093ac4 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -30,7 +30,8 @@ If applicable, add screenshots to help explain your problem. - OS: [e.g., Ubuntu 20.04] - Node.js version (`node --version`): - npm version (`npm --version`): - - Is the server free of plugins: + - Is the server free of plugins: + - Are you using any abstraction IE docker? **Desktop (please complete the following information):** - OS: [e.g. iOS] diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1e7ac79ed..e7fcee26f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -17,4 +17,11 @@ updates: open-pull-requests-limit: 30 groups: dev-dependencies: - dependency-type: "development" \ No newline at end of file + dependency-type: "development" + cooldown: + default-days: 1 # fallback for anything not covered below + semver-major-days: 7 + semver-minor-days: 3 + semver-patch-days: 1 + include: + - "*" diff --git a/.github/workflows/backend-tests.yml b/.github/workflows/backend-tests.yml index 55ea64b90..64ba0e90c 100644 --- a/.github/workflows/backend-tests.yml +++ b/.github/workflows/backend-tests.yml @@ -27,49 +27,49 @@ jobs: strategy: fail-fast: false matrix: - # PRs: test on latest Node only. Push to develop: full matrix. - node: ${{ github.event_name == 'pull_request' && fromJSON('[">=24.0.0 <25.0.0"]') || fromJSON('[">=20.0.0 <21.0.0", ">=22.0.0 <23.0.0", ">=24.0.0 <25.0.0"]') }} + # Etherpad requires Node >= 24 (see package.json engines.node). + node: ${{ fromJSON('[24]') }} steps: - name: Checkout repository - uses: actions/checkout@v6 - - uses: actions/cache@v5 - name: Setup gnpm cache - if: always() + uses: actions/checkout@v7 + - uses: actions/cache@v6 + name: Cache pnpm store with: - path: | - ${{ env.PNPM_HOME }} - ~/.local/share/gnpm - /usr/local/bin/gnpm - /usr/local/bin/gnpm-0.0.12 - key: ${{ runner.os }}-gnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + path: ${{ env.PNPM_HOME }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | - ${{ runner.os }}-gnpm-store- - - name: Setup gnpm - uses: SamTV12345/gnpm-setup@main + ${{ runner.os }}-pnpm-store- + - uses: pnpm/action-setup@v6 + name: Install pnpm with: - version: 0.0.12 + run_install: false + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node }} + cache: pnpm - name: Install libreoffice - uses: awalsh128/cache-apt-pkgs-action@v1.6.0 + uses: awalsh128/cache-apt-pkgs-action@v1.6.3 with: packages: libreoffice libreoffice-pdfimport version: 1.0 - name: Install all dependencies and symlink for ep_etherpad-lite - run: gnpm i --frozen-lockfile --runtimeVersion="${{ matrix.node }}" + run: pnpm i --frozen-lockfile - name: Install admin ui working-directory: admin - run: gnpm install --runtimeVersion="${{ matrix.node }}" + run: pnpm install - name: Build admin ui working-directory: admin - run: gnpm build --runtimeVersion="${{ matrix.node }}" + run: pnpm build - name: Run the backend tests - run: gnpm test --runtimeVersion="${{ matrix.node }}" + run: pnpm test - name: Run the new vitest tests working-directory: src - run: gnpm run test:vitest --runtimeVersion="${{ matrix.node }}" + run: pnpm run test:vitest withpluginsLinux: env: @@ -84,60 +84,62 @@ jobs: strategy: fail-fast: false matrix: - node: ${{ github.event_name == 'pull_request' && fromJSON('[">=24.0.0 <25.0.0"]') || fromJSON('[">=20.0.0 <21.0.0", ">=22.0.0 <23.0.0", ">=24.0.0 <25.0.0"]') }} + node: ${{ fromJSON('[24]') }} steps: - name: Checkout repository - uses: actions/checkout@v6 - - uses: actions/cache@v5 - name: Setup pnpm cache - if: always() + uses: actions/checkout@v7 + - uses: actions/cache@v6 + name: Cache pnpm store with: - path: | - ${{ env.PNPM_HOME }} - ~/.local/share/gnpm - /usr/local/bin/gnpm - /usr/local/bin/gnpm-0.0.12 - key: ${{ runner.os }}-gnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + path: ${{ env.PNPM_HOME }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | - ${{ runner.os }}-gnpm-store- - - name: Setup gnpm - uses: SamTV12345/gnpm-setup@main + ${{ runner.os }}-pnpm-store- + - uses: pnpm/action-setup@v6 + name: Install pnpm with: - version: 0.0.12 + run_install: false + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node }} + cache: pnpm - name: Install libreoffice - uses: awalsh128/cache-apt-pkgs-action@v1.6.0 + uses: awalsh128/cache-apt-pkgs-action@v1.6.3 with: packages: libreoffice libreoffice-pdfimport version: 1.0 - name: Install all dependencies and symlink for ep_etherpad-lite - run: gnpm install --frozen-lockfile --runtimeVersion="${{ matrix.node }}" + run: pnpm install --frozen-lockfile - name: Build admin ui working-directory: admin - run: gnpm build --runtimeVersion="${{ matrix.node }}" + run: pnpm build - name: Install Etherpad plugins run: > - gnpm install --workspace-root + pnpm add -w ep_align ep_author_hover ep_cursortrace + ep_font_color ep_font_size + ep_hash_auth ep_headings2 ep_markdown - ep_readonly_guest + ep_guest ep_set_title_on_pad ep_spellcheck ep_subscript_and_superscript ep_table_of_contents - name: Run the backend tests - run: gnpm test --runtimeVersion="${{ matrix.node }}" + run: pnpm test - name: Run the new vitest tests working-directory: src - run: gnpm run test:vitest --runtimeVersion="${{ matrix.node }}" + run: pnpm run test:vitest # Windows tests only run on push to develop/master, not on PRs withoutpluginsWindows: @@ -148,34 +150,47 @@ jobs: strategy: fail-fast: false matrix: - node: [">=20.0.0 <21.0.0", ">=22.0.0 <23.0.0", ">=24.0.0 <25.0.0"] + # Etherpad requires Node >= 24 (see package.json engines.node). + # Windows pins Node 24.16.0, not the runner-default 24.15.0. Node 24.15.0's + # bundled libuv (1.51.0) has a stack buffer overrun in the Windows TCP-connect + # path (uv__tcp_connect), proven by a /GS __fastfail full-memory dump under the + # backend suite's heavy localhost connection churn -- the long-standing Windows + # backend silent-ELIFECYCLE flake (memory corruption, so no JS/Node + # observability). Bisect: 24.15.0 crashes (4/4 on 127.0.0.1), 24.16.0 + # (libuv 1.52.1) is clean (0/8). Stays on the 24 LTS line. Pin explicitly + # because setup-node's default check-latest:false reuses the runner's + # pre-cached 24.15.0 for a bare "24". + # Tracking: https://github.com/nodejs/node/issues/63620 — drop this pin + # back to plain "24" once the fix is across the supported 24.x baseline. + node: ${{ fromJSON('["24.16.0"]') }} name: Windows without plugins runs-on: windows-latest steps: - name: Checkout repository - uses: actions/checkout@v6 - - uses: actions/cache@v5 - name: Setup pnpm cache - if: always() + uses: actions/checkout@v7 + - uses: actions/cache@v6 + name: Cache pnpm store with: - path: | - ${{ env.PNPM_HOME }} - C:\gnpm\ - C:\Users\runneradmin\AppData\Roaming\gnpm\ - key: ${{ runner.os }}-gnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + path: ${{ env.PNPM_HOME }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | - ${{ runner.os }}-gnpm-store- - - name: Setup gnpm - uses: SamTV12345/gnpm-setup@main + ${{ runner.os }}-pnpm-store- + - uses: pnpm/action-setup@v6 + name: Install pnpm with: - version: 0.0.12 + run_install: false + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node }} + cache: pnpm - name: Install all dependencies and symlink for ep_etherpad-lite - run: gnpm install --frozen-lockfile --runtimeVersion="${{ matrix.node }}" + run: pnpm install --frozen-lockfile - name: Build admin ui working-directory: admin - run: gnpm build --runtimeVersion="${{ matrix.node }}" + run: pnpm build - name: Fix up the settings.json run: | @@ -183,11 +198,14 @@ jobs: powershell -Command "(gc settings.json.holder) -replace '\"points\": 10', '\"points\": 1000' | Out-File -encoding ASCII settings.json" - name: Run the backend tests + shell: bash working-directory: src - run: gnpm test --runtimeVersion="${{ matrix.node }}" + # --exit makes mocha call process.exit() after the run so a leaked handle + # cannot hang the job on Windows. + run: pnpm test -- --exit - name: Run the new vitest tests working-directory: src - run: gnpm run test:vitest --runtimeVersion="${{ matrix.node }}" + run: pnpm run test:vitest withpluginsWindows: env: @@ -197,45 +215,60 @@ jobs: strategy: fail-fast: false matrix: - node: [">=20.0.0 <21.0.0", ">=22.0.0 <23.0.0", ">=24.0.0 <25.0.0"] + # Etherpad requires Node >= 24 (see package.json engines.node). + # Windows pins Node 24.16.0, not the runner-default 24.15.0. Node 24.15.0's + # bundled libuv (1.51.0) has a stack buffer overrun in the Windows TCP-connect + # path (uv__tcp_connect), proven by a /GS __fastfail full-memory dump under the + # backend suite's heavy localhost connection churn -- the long-standing Windows + # backend silent-ELIFECYCLE flake (memory corruption, so no JS/Node + # observability). Bisect: 24.15.0 crashes (4/4 on 127.0.0.1), 24.16.0 + # (libuv 1.52.1) is clean (0/8). Stays on the 24 LTS line. Pin explicitly + # because setup-node's default check-latest:false reuses the runner's + # pre-cached 24.15.0 for a bare "24". + # Tracking: https://github.com/nodejs/node/issues/63620 — drop this pin + # back to plain "24" once the fix is across the supported 24.x baseline. + node: ${{ fromJSON('["24.16.0"]') }} name: Windows with Plugins runs-on: windows-latest steps: - name: Checkout repository - uses: actions/checkout@v6 - - uses: actions/cache@v5 - name: Setup pnpm cache - if: always() + uses: actions/checkout@v7 + - uses: actions/cache@v6 + name: Cache pnpm store with: - path: | - ${{ env.PNPM_HOME }} - C:\gnpm\ - C:\Users\runneradmin\AppData\Roaming\gnpm\ - key: ${{ runner.os }}-gnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + path: ${{ env.PNPM_HOME }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | - ${{ runner.os }}-gnpm-store- - - name: Setup gnpm - uses: SamTV12345/gnpm-setup@main + ${{ runner.os }}-pnpm-store- + - uses: pnpm/action-setup@v6 + name: Install pnpm with: - version: 0.0.12 + run_install: false + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node }} + cache: pnpm - name: Install dependencies - run: gnpm install --runtimeVersion="${{ matrix.node }}" + run: pnpm install - name: Build admin ui working-directory: admin - run: gnpm build --runtimeVersion="${{ matrix.node }}" + run: pnpm build - name: Install Etherpad plugins run: > - gnpm install --workspace-root + pnpm add -w ep_align ep_author_hover ep_cursortrace + ep_font_color ep_font_size + ep_hash_auth ep_headings2 ep_markdown - ep_readonly_guest + ep_guest ep_set_title_on_pad ep_spellcheck ep_subscript_and_superscript @@ -251,7 +284,7 @@ jobs: # rules. - name: Install all dependencies and symlink for ep_etherpad-lite - run: gnpm install --frozen-lockfile --runtimeVersion="${{ matrix.node }}" + run: pnpm install --frozen-lockfile - name: Fix up the settings.json run: | @@ -259,8 +292,11 @@ jobs: powershell -Command "(gc settings.json.holder) -replace '\"points\": 10', '\"points\": 1000' | Out-File -encoding ASCII settings.json" - name: Run the backend tests + shell: bash working-directory: src - run: gnpm test --runtimeVersion="${{ matrix.node }}" + # --exit makes mocha call process.exit() after the run so a leaked handle + # cannot hang the job on Windows. + run: pnpm test -- --exit - name: Run the new vitest tests working-directory: src - run: gnpm run test:vitest --runtimeVersion="${{ matrix.node }}" + run: pnpm run test:vitest diff --git a/.github/workflows/build-and-deploy-docs.yml b/.github/workflows/build-and-deploy-docs.yml index 6482328bd..f49cf2974 100644 --- a/.github/workflows/build-and-deploy-docs.yml +++ b/.github/workflows/build-and-deploy-docs.yml @@ -1,68 +1,96 @@ -# Workflow for deploying static content to GitHub Pages +# Workflow for building and deploying the VitePress site to GitHub Pages. +# Build runs on every push to develop and on every PR that touches docs (so +# a build regression is caught at review time instead of breaking develop +# after merge — see #7640). Deploy runs only on push: the github-pages +# environment has protection rules that reject PR refs, and a PR build +# never produced an artifact to deploy anyway. name: Deploy Docs to GitHub Pages on: - # Runs on pushes targeting the default branch push: branches: ["develop"] paths: - - doc/** # Only run workflow when changes are made to the doc directory - # Allows you to run this workflow manually from the Actions tab + - doc/** + - .github/workflows/build-and-deploy-docs.yml + pull_request: + paths: + - doc/** + - .github/workflows/build-and-deploy-docs.yml workflow_dispatch: -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: contents: read pages: write id-token: write packages: read -# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. -# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +# Allow only one concurrent deployment, skipping runs queued between the run +# in-progress and latest queued. Do NOT cancel in-progress runs — production +# deployments are allowed to complete. concurrency: group: "pages" cancel-in-progress: false jobs: - # Single deploy job since we're just deploying + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + - uses: actions/cache@v6 + name: Cache pnpm store + with: + path: ~/.pnpm-store + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + - uses: actions/cache@v6 + name: Cache vitepress build + with: + path: doc/.vitepress/cache + key: ${{ runner.os }}-vitepress-${{ hashFiles('doc/**/*.md', 'doc/.vitepress/config.*') }} + restore-keys: | + ${{ runner.os }}-vitepress- + - uses: pnpm/action-setup@v6 + name: Install pnpm + with: + run_install: false + # Pin Node so the build does not silently fall back to whatever the + # runner image ships with. The repo declares engines.node >=24.0.0. + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: pnpm + - name: Setup Pages + if: github.event_name == 'push' + uses: actions/configure-pages@v6 + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Build app + working-directory: doc + run: pnpm run docs:build + env: + GITHUB_PAGES: ${{ github.event_name == 'push' && 'true' || '' }} + COMMIT_REF: ${{ github.sha }} + - name: Upload artifact + if: github.event_name == 'push' + uses: actions/upload-pages-artifact@v5 + with: + path: './doc/.vitepress/dist' + + # Deploy to GitHub Pages on push to develop only. Kept as a separate job + # because the github-pages environment's protection rules reject any + # non-develop ref (including PR merge refs), which used to fail the entire + # workflow at job-creation time before any build step could run. deploy: + needs: build + if: github.event_name == 'push' environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v6 - - uses: actions/cache@v5 - name: Setup gnpm cache - if: always() - with: - path: | - ${{ env.STORE_PATH }} - ~/.local/share/gnpm - /usr/local/bin/gnpm - /usr/local/bin/gnpm-0.0.12 - key: ${{ runner.os }}-gnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-gnpm-store- - - name: Setup gnpm - uses: SamTV12345/gnpm-setup@main - with: - version: 0.0.12 - - name: Setup Pages - uses: actions/configure-pages@v6 - - name: Install dependencies - run: gnpm install - - name: Build app - working-directory: doc - run: gnpm run docs:build - env: - COMMIT_REF: ${{ github.sha }} - - name: Upload artifact - uses: actions/upload-pages-artifact@v5 - with: - # Upload entire repository - path: './doc/.vitepress/dist' - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v5 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index c6c10937b..37d2d28b7 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -25,7 +25,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. diff --git a/.github/workflows/deb-package.yml b/.github/workflows/deb-package.yml new file mode 100644 index 000000000..51bad3b93 --- /dev/null +++ b/.github/workflows/deb-package.yml @@ -0,0 +1,493 @@ +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' + # Also build + smoke-test the package on PRs that touch the production + # footprint. The smoke step boots the packaged server and waits for /health, + # which is the only check that catches "server starts then exits before + # binding the port" startup regressions (e.g. a dependency bump whose change + # lets the event loop drain mid-boot). Previously this workflow ran only on + # push to develop, so such a regression was caught *after* merge — by which + # point develop was already red. Running it pre-merge blocks the PR instead. + # The release/apt-publish jobs are tag-guarded, so PRs run the build job only. + pull_request: + 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@v7 + 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 (>= 24)`. 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 (>= 24)`. + 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" ] + # 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@v7 + 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/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 774c7434a..da2ac6321 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -15,6 +15,6 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Checkout Repository' - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: 'Dependency Review' - uses: actions/dependency-review-action@v4 + uses: actions/dependency-review-action@v5 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 4c97bcf52..cca0feb45 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Check out - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: path: etherpad - @@ -42,27 +42,37 @@ jobs: tags: ${{ env.TEST_TAG }} cache-from: type=gha cache-to: type=gha,mode=max - - uses: actions/cache@v5 - name: Setup gnpm cache - if: always() + - uses: actions/cache@v6 + name: Cache pnpm store with: - path: | - ${{ env.PNPM_HOME }} - ~/.local/share/gnpm - /usr/local/bin/gnpm - /usr/local/bin/gnpm-0.0.12 - key: ${{ runner.os }}-gnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + path: ${{ env.PNPM_HOME }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | - ${{ runner.os }}-gnpm-store- - - name: Setup gnpm - uses: SamTV12345/gnpm-setup@main + ${{ runner.os }}-pnpm-store- + - uses: pnpm/action-setup@v6 + name: Install pnpm with: - version: 0.0.12 + run_install: false + # Repo is checked out into ./etherpad, so the action can't find + # packageManager in a root package.json. + package_json_file: etherpad/package.json + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: pnpm + cache-dependency-path: etherpad/pnpm-lock.yaml - name: Test working-directory: etherpad run: | - docker run --rm -d -p 9001:9001 --name test ${{ env.TEST_TAG }} + # ADMIN_PASSWORD provisions settings.json.docker's admin user so + # the adminSettings_7819 container spec can authenticate against + # /admin and the /settings socket. pad.js doesn't touch /admin + # so the existing API specs are unaffected. + docker run --rm -d -p 9001:9001 \ + -e ADMIN_PASSWORD=changeme1 \ + --name test ${{ env.TEST_TAG }} ./bin/installDeps.sh docker logs -f test & while true; do @@ -74,10 +84,149 @@ jobs: *) printf %s\\n "unexpected status: ${status}" >&2; exit 1;; esac done - (cd src && gnpm run test-container) + (cd src && pnpm run test-container) + # Regression for #7819. adminSettings_7819.ts saves a marker via + # the admin /settings socket and intentionally leaves it on + # disk. We assert here that the save actually hit the file + # (mocha only sees the next `load` reply — this catches a + # scenario where the load is served from cache and the file + # never actually changed). + docker exec test grep -q persist-marker-7819 /opt/etherpad-lite/settings.json || { + echo "[#7819] socket-driven save did NOT reach /opt/etherpad-lite/settings.json on disk" + docker exec test cat /opt/etherpad-lite/settings.json | head -50 + exit 1 + } + # Now prove the on-disk file survives an in-place container + # restart. This is the scenario a docker-compose user with + # `restart: always` hits on every host reboot. + docker restart test >/dev/null + for i in $(seq 1 60); do + status=$(docker container inspect -f '{{.State.Health.Status}}' test 2>/dev/null) || { docker logs test; exit 1; } + case ${status} in + healthy) break;; + starting) sleep 2;; + *) docker logs test; exit 1;; + esac + done + docker exec test grep -q persist-marker-7819 /opt/etherpad-lite/settings.json || { + echo "[#7819 REGRESSION] settings.json was reset on docker restart — ep_oauth block vanished" + docker logs test + exit 1 + } + docker rm -f test git clean -dxf . + - + # Regression test for #7718. Reproduces the production + # docker-compose layout reported in that issue: a named volume + # mounted on src/plugin_packages with no TTY allocated. Under + # the previous `CMD ["pnpm", "run", "prod"]`, pnpm 11's + # runDepsStatusCheck spuriously decided node_modules was out of + # sync at boot and tried to wipe + reinstall, aborting with + # ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY before the HTTP + # server ever came up. If the CMD is ever reverted to go + # through `pnpm run`, this step will time out waiting for the + # health endpoint and fail. + name: Regression — boot with named volume on plugin_packages (#7718) + working-directory: etherpad + run: | + docker volume create ep7718-plugins + docker run --rm -d -p 9001:9001 \ + -v ep7718-plugins:/opt/etherpad-lite/src/plugin_packages \ + --name test-7718 ${{ env.TEST_TAG }} + docker logs -f test-7718 & + ok=0 + for i in $(seq 1 60); do + status=$(docker container inspect -f '{{.State.Health.Status}}' test-7718 2>/dev/null) || { + echo "container exited prematurely — likely #7718 regression" + docker logs test-7718 || true + break + } + case ${status} in + healthy) ok=1; echo "container healthy after $((i*2))s"; break;; + starting) sleep 2;; + *) echo "unexpected status: ${status}"; docker logs test-7718; break;; + esac + done + docker rm -f test-7718 >/dev/null 2>&1 || true + docker volume rm ep7718-plugins >/dev/null 2>&1 || true + [ "$ok" = "1" ] || exit 1 + + - + # Regression test: Etherpad container must boot without external + # network access (e.g., air-gapped host or restrictive runtime policy). + name: Regression — boot with disabled network (--network none) + working-directory: etherpad + run: | + docker run --rm -d \ + --network none \ + --name test-offline ${{ env.TEST_TAG }} + docker logs -f test-offline & + ok=0 + for i in $(seq 1 60); do + status=$(docker container inspect -f '{{.State.Health.Status}}' test-offline 2>/dev/null) || { + echo "container exited prematurely while offline" + docker logs test-offline || true + break + } + case ${status} in + healthy) ok=1; echo "offline boot healthy after $((i*2))s"; break;; + starting) sleep 2;; + *) echo "unexpected status: ${status}"; docker logs test-offline || true; break;; + esac + done + docker rm -f test-offline >/dev/null 2>&1 || true + [ "$ok" = "1" ] || exit 1 + + build-test-local-plugin: + # Regression coverage for #7687: the Docker image's + # `bin/installLocalPlugins.sh` step runs as the `etherpad` user and + # invokes pnpm via the corepack shim. A previous corepack/cache bug + # made that path fail when ETHERPAD_LOCAL_PLUGINS was set. This job + # builds the development target with a stub local plugin so the + # regression cannot silently come back. + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - + name: Check out + uses: actions/checkout@v7 + with: + path: etherpad + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + - + name: Stub a local plugin + run: | + mkdir -p etherpad/local_plugins/ep_test_corepack + cat > etherpad/local_plugins/ep_test_corepack/package.json <<'EOF' + { + "name": "ep_test_corepack", + "version": "0.0.1", + "description": "regression-test stub for ether/etherpad#7687", + "main": "index.js" + } + EOF + cat > etherpad/local_plugins/ep_test_corepack/index.js <<'EOF' + exports.placeholder = true; + EOF + - + name: Build with ETHERPAD_LOCAL_PLUGINS (must succeed) + uses: docker/build-push-action@v7 + with: + context: ./etherpad + target: development + load: false + build-args: | + ETHERPAD_LOCAL_PLUGINS=ep_test_corepack + cache-from: type=gha build-test-db-drivers: + # Spinning up MySQL + Postgres + cross-driver smoke is expensive; only + # run it on pushes to develop (and tagged release pushes), not on every + # feature-branch PR. + if: github.event_name == 'push' runs-on: ubuntu-latest permissions: contents: read @@ -112,7 +261,7 @@ jobs: steps: - name: Check out - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: path: etherpad - @@ -209,7 +358,7 @@ jobs: steps: - name: Check out - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: path: etherpad - @@ -267,7 +416,7 @@ jobs: enable-url-completion: true - name: Check out ether-charts if: github.ref == 'refs/heads/develop' - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: path: ether-charts repository: ether/ether-charts @@ -276,13 +425,13 @@ jobs: if: success() && github.ref == 'refs/heads/develop' working-directory: ether-charts run: | - sed -i 's/tag: ".*"/tag: "${{ steps.build-docker.outputs.digest }}"/' values-dev.yaml + sed -i 's/tag: ".*"/tag: "${{ steps.build-docker.outputs.digest }}"/' charts/etherpad/values-dev.yaml - name: Commit and push changes working-directory: ether-charts if: success() && github.ref == 'refs/heads/develop' run: | git config --global user.name 'github-actions[bot]' git config --global user.email 'github-actions[bot]@users.noreply.github.com' - git add values-dev.yaml + git add charts/etherpad/values-dev.yaml git commit -m 'Update develop image tag' git push diff --git a/.github/workflows/downstream-smoke.yml b/.github/workflows/downstream-smoke.yml new file mode 100644 index 000000000..fc42cfe3e --- /dev/null +++ b/.github/workflows/downstream-smoke.yml @@ -0,0 +1,108 @@ +name: Downstream smoke + +# Boots a real Etherpad from the PR and verifies the separate downstream clients +# (Rust terminal editor, Node CLI, desktop/mobile) still round-trip against it. +# Phase 1 lands the boot/healthcheck/self-check/teardown harness + manifest; the +# per-client matrix activates as each client flips `enabled:true` in clients.json. + +on: + pull_request: + paths-ignore: + - "doc/**" + - "docs/**" + schedule: + - cron: '0 4 * * *' # nightly against the default branch + +permissions: + contents: read + +jobs: + smoke: + name: Boot + downstream clients + runs-on: ubuntu-latest + timeout-minutes: 25 + env: + PNPM_HOME: ~/.pnpm-store + APIKEY: downstream-smoke-key + steps: + - name: Checkout core (PR) + uses: actions/checkout@v7 + + - uses: actions/cache@v6 + name: Cache pnpm store + with: + path: ${{ env.PNPM_HOME }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - uses: pnpm/action-setup@v6 + name: Install pnpm + with: + run_install: false + + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: pnpm + + - name: Install dependencies + run: pnpm i --frozen-lockfile + + - name: Boot Etherpad on :9003 (apikey auth) + run: | + # The template ships a literal "port": 9001 and sso auth; rewrite both. + # (PORT env is ignored once the settings file specifies a port.) + sed -e 's#"port": 9001,#"port": 9003,#' \ + -e 's#${AUTHENTICATION_METHOD:sso}#apikey#' \ + settings.json.template > settings.json + # Fail fast if the template format drifted and sed silently no-op'd. + grep -q '"port": 9003,' settings.json \ + || { echo "::error::port rewrite failed — settings.json.template format changed"; exit 1; } + grep -q '"authenticationMethod": "apikey"' settings.json \ + || { echo "::error::auth rewrite failed — settings.json.template format changed"; exit 1; } + printf '%s' "$APIKEY" > APIKEY.txt + pnpm run prod > /tmp/ep.log 2>&1 & + echo $! > /tmp/ep.pid + echo "booted pid $(cat /tmp/ep.pid)" + + - name: Wait for healthcheck + run: | + for i in $(seq 1 60); do + if curl -fsS "http://localhost:9003/api/" >/dev/null 2>&1; then + echo "server up after ${i} tries"; exit 0 + fi + sleep 2 + done + echo "::error::server did not come up"; tail -50 /tmp/ep.log; exit 1 + + - name: Self-check — authenticated create + read roundtrip + run: | + K="$APIKEY" + curl -fsS "http://localhost:9003/api/1/createPad?apikey=${K}&padID=smoke&text=hi%0A" | tee /tmp/create.json + grep -q '"code":0' /tmp/create.json + curl -fsS "http://localhost:9003/api/1/getText?apikey=${K}&padID=smoke" | tee /tmp/get.json + grep -q '"text":"hi' /tmp/get.json + + - name: Generate canonical wire-vectors + run: cd src && pnpm run vectors:gen + + # Rust toolchain for the `rust`-kind client (etherpad-pad). Other kinds + # use the node+pnpm already set up above. + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Run enabled downstream clients + env: + SMOKE_URL: http://localhost:9003 + SMOKE_APIKEY: ${{ env.APIKEY }} + run: bash src/tests/downstream/run-clients.sh + + - name: Teardown (by PID, never pkill) + if: always() + run: | + if [ -f /tmp/ep.pid ]; then + kill "$(cat /tmp/ep.pid)" 2>/dev/null || true + echo "killed $(cat /tmp/ep.pid)" + fi diff --git a/.github/workflows/frontend-admin-tests.yml b/.github/workflows/frontend-admin-tests.yml index f31f442f8..5ff9dea29 100644 --- a/.github/workflows/frontend-admin-tests.yml +++ b/.github/workflows/frontend-admin-tests.yml @@ -21,34 +21,34 @@ jobs: strategy: fail-fast: false matrix: - # PRs: single Node version. Push: full matrix. - node: ${{ github.event_name == 'pull_request' && fromJSON('[24]') || fromJSON('[20, 22, 24]') }} + # Etherpad requires Node >= 24 (see package.json engines.node). + node: ${{ fromJSON('[24]') }} steps: - name: Checkout repository - uses: actions/checkout@v6 - - uses: actions/cache@v5 - name: Setup gnpm cache - if: always() + uses: actions/checkout@v7 + - uses: actions/cache@v6 + name: Cache pnpm store with: - path: | - ${{ env.PNPM_HOME }} - ~/.local/share/gnpm - /usr/local/bin/gnpm - /usr/local/bin/gnpm-0.0.12 - key: ${{ runner.os }}-gnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + path: ${{ env.PNPM_HOME }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | - ${{ runner.os }}-gnpm-store- - - name: Setup gnpm - uses: SamTV12345/gnpm-setup@main + ${{ runner.os }}-pnpm-store- + - uses: pnpm/action-setup@v6 + name: Install pnpm with: - version: 0.0.12 + run_install: false + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node }} + cache: pnpm - name: Install all dependencies and symlink for ep_etherpad-lite - run: gnpm i --runtimeVersion="${{ matrix.node }}" + run: pnpm i - name: Cache Playwright browsers - uses: actions/cache@v5 + uses: actions/cache@v6 id: playwright-cache with: path: ~/.cache/ms-playwright @@ -71,11 +71,11 @@ jobs: - name: Build admin frontend working-directory: admin run: | - gnpm run build --runtimeVersion="${{ matrix.node }}" + pnpm run build - name: Run the frontend admin tests shell: bash run: | - gnpm run prod --runtimeVersion="${{ matrix.node }}" > /tmp/etherpad-server.log 2>&1 & + pnpm run prod > /tmp/etherpad-server.log 2>&1 & connected=false can_connect() { curl -sSfo /dev/null http://localhost:9001/ || return 1 @@ -87,7 +87,7 @@ jobs: sleep 1 done cd src - gnpm run test-admin --runtimeVersion="${{ matrix.node }}" + pnpm run test-admin - name: Upload server log on failure uses: actions/upload-artifact@v7 if: failure() diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml index da5ce0a3b..0def07b51 100644 --- a/.github/workflows/frontend-tests.yml +++ b/.github/workflows/frontend-tests.yml @@ -20,47 +20,58 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 - - uses: actions/cache@v5 - name: Setup gnpm cache - if: always() + uses: actions/checkout@v7 + - uses: actions/cache@v6 + name: Cache pnpm store with: - path: | - ${{ env.PNPM_HOME }} - ~/.cache/ms-playwright - ~/.local/share/gnpm - /usr/local/bin/gnpm - /usr/local/bin/gnpm-0.0.12 - key: ${{ runner.os }}-gnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + path: ${{ env.PNPM_HOME }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | - ${{ runner.os }}-gnpm-store- - - name: Setup gnpm - uses: SamTV12345/gnpm-setup@main + ${{ runner.os }}-pnpm-store- + - uses: actions/cache@v6 + name: Cache Playwright browsers with: - version: 0.0.12 + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ hashFiles('src/package.json', 'pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-playwright- + - uses: pnpm/action-setup@v6 + name: Install pnpm + with: + run_install: false + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: pnpm - name: Install all dependencies and symlink for ep_etherpad-lite - run: gnpm install --frozen-lockfile + run: pnpm install --frozen-lockfile - name: Create settings.json run: cp ./src/tests/settings.json settings.json - name: Run the frontend tests shell: bash run: | - gnpm run prod > /tmp/etherpad-server.log 2>&1 & - connected=false - can_connect() { - curl -sSfo /dev/null http://localhost:9001/ || return 1 - connected=true - } - now() { date +%s; } - start=$(now) - while [ $(($(now) - $start)) -le 15 ] && ! can_connect; do - sleep 1 - done + set -euo pipefail + pnpm run prod > /tmp/etherpad-server.log 2>&1 & + # Generous 90s budget so a slow runner (or, in the with-plugins + # variant, plugin boot) doesn't lose the race against the test + # phase. Fail loudly on timeout rather than silently falling + # through to tests against a half-started server. + # --max-time bounds each probe so a stuck server can't make a + # single curl call eat the whole 90s budget. + can_connect() { curl --max-time 3 -sSfo /dev/null http://localhost:9001/; } + for i in $(seq 1 90); do can_connect && break; sleep 1; done + if ! can_connect; then + echo "::error::Etherpad did not respond on :9001 within 90s" + echo "----- server log -----" + tail -n 200 /tmp/etherpad-server.log || true + exit 1 + fi cd src - gnpm exec playwright install chromium --with-deps - gnpm run test-ui --project=chromium + pnpm exec playwright install chromium --with-deps + pnpm run test-ui --project=chromium - name: Upload server log on failure uses: actions/upload-artifact@v7 if: failure() @@ -81,45 +92,56 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v6 - - uses: actions/cache@v5 - name: Setup gnpm cache - if: always() + uses: actions/checkout@v7 + - uses: actions/cache@v6 + name: Cache pnpm store with: - path: | - ${{ env.PNPM_HOME }} - ~/.local/share/gnpm - ~/.cache/ms-playwright - /usr/local/bin/gnpm - /usr/local/bin/gnpm-0.0.12 - key: ${{ runner.os }}-gnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + path: ${{ env.PNPM_HOME }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | - ${{ runner.os }}-gnpm-store- - - name: Setup gnpm - uses: SamTV12345/gnpm-setup@main + ${{ runner.os }}-pnpm-store- + - uses: actions/cache@v6 + name: Cache Playwright browsers with: - version: 0.0.12 + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ hashFiles('src/package.json', 'pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-playwright- + - uses: pnpm/action-setup@v6 + name: Install pnpm + with: + run_install: false + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: pnpm - name: Install all dependencies and symlink for ep_etherpad-lite - run: gnpm install --frozen-lockfile + run: pnpm install --frozen-lockfile - name: Create settings.json run: cp ./src/tests/settings.json settings.json - name: Run the frontend tests shell: bash run: | - gnpm run prod > /tmp/etherpad-server.log 2>&1 & - connected=false - can_connect() { - curl -sSfo /dev/null http://localhost:9001/ || return 1 - connected=true - } - now() { date +%s; } - start=$(now) - while [ $(($(now) - $start)) -le 15 ] && ! can_connect; do - sleep 1 - done + set -euo pipefail + pnpm run prod > /tmp/etherpad-server.log 2>&1 & + # Generous 90s budget so a slow runner (or, in the with-plugins + # variant, plugin boot) doesn't lose the race against the test + # phase. Fail loudly on timeout rather than silently falling + # through to tests against a half-started server. + # --max-time bounds each probe so a stuck server can't make a + # single curl call eat the whole 90s budget. + can_connect() { curl --max-time 3 -sSfo /dev/null http://localhost:9001/; } + for i in $(seq 1 90); do can_connect && break; sleep 1; done + if ! can_connect; then + echo "::error::Etherpad did not respond on :9001 within 90s" + echo "----- server log -----" + tail -n 200 /tmp/etherpad-server.log || true + exit 1 + fi cd src - gnpm exec playwright install firefox --with-deps - gnpm run test-ui --project=firefox + pnpm exec playwright install firefox --with-deps + pnpm run test-ui --project=firefox - name: Upload server log on failure uses: actions/upload-artifact@v7 if: failure() @@ -133,3 +155,198 @@ jobs: name: playwright-report-firefox path: src/playwright-report/ retention-days: 30 + + # Frontend tests with the same /ether plugin set that backend-tests.yml + # exercises, so a core change that breaks plugin UX is caught in PR CI + # rather than after release. Re-introduces coverage that was lost when + # the workflows were nuked & rebuilt in 2023 (commit cc80db2d3) and the + # backend equivalent was restored without the frontend half. + playwright-chrome-with-plugins: + env: + PNPM_HOME: ~/.pnpm-store + name: Playwright Chrome with plugins + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v7 + - uses: actions/cache@v6 + name: Cache pnpm store + with: + path: ${{ env.PNPM_HOME }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + - uses: actions/cache@v6 + name: Cache Playwright browsers + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ hashFiles('src/package.json', 'pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-playwright- + - uses: pnpm/action-setup@v6 + name: Install pnpm + with: + run_install: false + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: pnpm + - name: Install all dependencies and symlink for ep_etherpad-lite + run: pnpm install --frozen-lockfile + - name: Install Etherpad plugins + # Same plugin set as backend-tests.yml's withpluginsLinux job, + # MINUS ep_cursortrace. + # + # ep_cursortrace's `aceEditEvent` hook fires on every keyboard + # event (handleClick, handleKeyEvent, idleWorkTimer) and sends a + # cursor-position socket message per call. Under the test + # harness's `writeToPad` bursts (insertText + Enter loops) that + # stream of socket messages saturates the editor's input + # pipeline in Firefox specifically, causing intermittent + # keystroke drops and a long tail of test flakiness. + # + # Bisected via a 4-iteration probe on this branch — see commit + # history of .github/workflows/frontend-tests.yml around the + # PR-7630 timeframe. Tracked for a follow-up fix + # (debounce / throttle in ep_cursortrace's main.js). + run: > + pnpm add -w + ep_align + ep_author_hover + ep_font_color + ep_font_size + ep_hash_auth + ep_headings2 + ep_markdown + ep_guest + ep_set_title_on_pad + ep_spellcheck + ep_subscript_and_superscript + ep_table_of_contents + - name: Create settings.json + run: cp ./src/tests/settings.json settings.json + - name: Run the frontend tests + shell: bash + run: | + set -euo pipefail + pnpm run prod > /tmp/etherpad-server.log 2>&1 & + # Generous 90s budget so a slow runner (or, in the with-plugins + # variant, plugin boot) doesn't lose the race against the test + # phase. Fail loudly on timeout rather than silently falling + # through to tests against a half-started server. + # --max-time bounds each probe so a stuck server can't make a + # single curl call eat the whole 90s budget. + can_connect() { curl --max-time 3 -sSfo /dev/null http://localhost:9001/; } + for i in $(seq 1 90); do can_connect && break; sleep 1; done + if ! can_connect; then + echo "::error::Etherpad did not respond on :9001 within 90s" + echo "----- server log -----" + tail -n 200 /tmp/etherpad-server.log || true + exit 1 + fi + cd src + pnpm exec playwright install chromium --with-deps + WITH_PLUGINS=1 pnpm run test-ui --project=chromium + - name: Upload server log on failure + uses: actions/upload-artifact@v7 + if: failure() + with: + name: server-log-chrome-with-plugins + path: /tmp/etherpad-server.log + retention-days: 7 + - uses: actions/upload-artifact@v7 + if: always() + with: + name: playwright-report-chrome-with-plugins + path: src/playwright-report/ + retention-days: 30 + + playwright-firefox-with-plugins: + env: + PNPM_HOME: ~/.pnpm-store + name: Playwright Firefox with plugins + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v7 + - uses: actions/cache@v6 + name: Cache pnpm store + with: + path: ${{ env.PNPM_HOME }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + - uses: actions/cache@v6 + name: Cache Playwright browsers + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ hashFiles('src/package.json', 'pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-playwright- + - uses: pnpm/action-setup@v6 + name: Install pnpm + with: + run_install: false + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: pnpm + - name: Install all dependencies and symlink for ep_etherpad-lite + run: pnpm install --frozen-lockfile + - name: Install Etherpad plugins + # See sibling Playwright Chrome with plugins job for the full + # rationale on why ep_cursortrace is excluded from the test + # plugin set. + run: > + pnpm add -w + ep_align + ep_author_hover + ep_font_color + ep_font_size + ep_hash_auth + ep_headings2 + ep_markdown + ep_guest + ep_set_title_on_pad + ep_spellcheck + ep_subscript_and_superscript + ep_table_of_contents + - name: Create settings.json + run: cp ./src/tests/settings.json settings.json + - name: Run the frontend tests + shell: bash + run: | + set -euo pipefail + pnpm run prod > /tmp/etherpad-server.log 2>&1 & + # Generous 90s budget so a slow runner (or, in the with-plugins + # variant, plugin boot) doesn't lose the race against the test + # phase. Fail loudly on timeout rather than silently falling + # through to tests against a half-started server. + # --max-time bounds each probe so a stuck server can't make a + # single curl call eat the whole 90s budget. + can_connect() { curl --max-time 3 -sSfo /dev/null http://localhost:9001/; } + for i in $(seq 1 90); do can_connect && break; sleep 1; done + if ! can_connect; then + echo "::error::Etherpad did not respond on :9001 within 90s" + echo "----- server log -----" + tail -n 200 /tmp/etherpad-server.log || true + exit 1 + fi + cd src + pnpm exec playwright install firefox --with-deps + WITH_PLUGINS=1 pnpm run test-ui --project=firefox + - name: Upload server log on failure + uses: actions/upload-artifact@v7 + if: failure() + with: + name: server-log-firefox-with-plugins + path: /tmp/etherpad-server.log + retention-days: 7 + - uses: actions/upload-artifact@v7 + if: always() + with: + name: playwright-report-firefox-with-plugins + path: src/playwright-report/ + retention-days: 30 diff --git a/.github/workflows/handleRelease.yml b/.github/workflows/handleRelease.yml index a69d20b6f..03ca88667 100644 --- a/.github/workflows/handleRelease.yml +++ b/.github/workflows/handleRelease.yml @@ -27,32 +27,34 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 - - uses: actions/cache@v5 - name: Setup gnpm cache - if: always() + uses: actions/checkout@v7 + - uses: actions/cache@v6 + name: Cache pnpm store with: - path: | - ${{ env.STORE_PATH }} - ~/.local/share/gnpm - ~/.cache/ms-playwright - /usr/local/bin/gnpm - /usr/local/bin/gnpm-0.0.12 - key: ${{ runner.os }}-gnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + path: ${{ env.PNPM_HOME }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | - ${{ runner.os }}-gnpm-store- - - name: Setup gnpm - uses: SamTV12345/gnpm-setup@main + ${{ runner.os }}-pnpm-store- + - uses: pnpm/action-setup@v6 + name: Install pnpm with: - version: 0.0.12 + run_install: false + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: pnpm - name: Install all dependencies and symlink for ep_etherpad-lite - run: gnpm install --frozen-lockfile + run: pnpm install --frozen-lockfile - name: Build etherpad - run: gnpm run build:etherpad - # On release, create release + run: pnpm run build:etherpad + # On release, create release. `--silent` suppresses pnpm's lifecycle + # banner ("> generateChangelog\n> node ...") that would otherwise be + # captured into CHANGELOG.txt and end up at the top of the GitHub + # release notes. - name: Generate Changelog working-directory: bin - run: gnpm run generateChangelog ${{ github.ref }} > ${{ github.workspace }}-CHANGELOG.txt + run: pnpm --silent run generateChangelog ${{ github.ref }} > ${{ github.workspace }}-CHANGELOG.txt - name: Release uses: softprops/action-gh-release@v3 if: ${{startsWith(github.ref, 'refs/tags/v') }} diff --git a/.github/workflows/installer-test.yml b/.github/workflows/installer-test.yml index d77c277a1..a40cd9db2 100644 --- a/.github/workflows/installer-test.yml +++ b/.github/workflows/installer-test.yml @@ -23,7 +23,7 @@ jobs: name: shellcheck runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Run shellcheck on installer.sh run: | sudo apt-get update @@ -38,11 +38,11 @@ jobs: matrix: os: [ubuntu-latest, macos-latest] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/setup-node@v6 with: - node-version: 22 + node-version: 24 - name: Pre-install pnpm (avoid sudo prompt in the installer) run: npm install -g pnpm @@ -50,8 +50,8 @@ jobs: - name: Run bin/installer.sh against this commit env: ETHERPAD_DIR: ${{ runner.temp }}/etherpad-installer-test - ETHERPAD_REPO: ${{ github.server_url }}/${{ github.repository }}.git - ETHERPAD_BRANCH: ${{ github.head_ref || github.ref_name }} + ETHERPAD_REPO: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.clone_url || format('{0}/{1}.git', github.server_url, github.repository) }} + ETHERPAD_BRANCH: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref_name }} NO_COLOR: "1" run: sh ./bin/installer.sh @@ -70,13 +70,30 @@ jobs: - name: Smoke test - start Etherpad and curl /api shell: bash + # Hard backstop: if teardown ever fails to reap the server the step + # fails in minutes instead of burning to GitHub's 6h job ceiling. + timeout-minutes: 8 env: ETHERPAD_DIR: ${{ runner.temp }}/etherpad-installer-test run: | set -eu + # Enable job control so the backgrounded launcher gets its own + # process group, letting us reap the whole pnpm -> node tree below. + set -m cd "$ETHERPAD_DIR" pnpm run prod >/tmp/etherpad.log 2>&1 & PID=$! + # `pnpm run prod` is a nested launcher (pnpm -> pnpm --filter -> node), + # so killing $PID alone orphans the node server, which keeps the step's + # output pipe open and hangs CI. Kill the entire process group, with a + # SIGKILL fallback in case SIGTERM is swallowed (e.g. a live flush timer + # keeping the event loop alive), so the step always exits cleanly. + reap() { + kill -TERM "-$PID" 2>/dev/null || true + sleep 5 + kill -KILL "-$PID" 2>/dev/null || true + } + trap reap EXIT # Wait up to 60s for the API to come up. ok=0 for i in $(seq 1 60); do @@ -90,21 +107,18 @@ jobs: if [ "$ok" != "1" ]; then echo "Etherpad did not start within 60s. Last 200 lines of log:" >&2 tail -200 /tmp/etherpad.log >&2 || true - kill "$PID" 2>/dev/null || true exit 1 fi - kill "$PID" 2>/dev/null || true - wait "$PID" 2>/dev/null || true installer-windows: name: end-to-end install (windows-latest) runs-on: windows-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/setup-node@v6 with: - node-version: 22 + node-version: 24 - name: Pre-install pnpm run: npm install -g pnpm @@ -113,8 +127,8 @@ jobs: shell: pwsh env: ETHERPAD_DIR: ${{ runner.temp }}\etherpad-installer-test - ETHERPAD_REPO: ${{ github.server_url }}/${{ github.repository }}.git - ETHERPAD_BRANCH: ${{ github.head_ref || github.ref_name }} + ETHERPAD_REPO: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.clone_url || format('{0}/{1}.git', github.server_url, github.repository) }} + ETHERPAD_BRANCH: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref_name }} NO_COLOR: "1" run: ./bin/installer.ps1 @@ -131,6 +145,7 @@ jobs: - name: Smoke test - start Etherpad and curl /api shell: pwsh + timeout-minutes: 8 env: ETHERPAD_DIR: ${{ runner.temp }}\etherpad-installer-test run: | diff --git a/.github/workflows/load-test.yml b/.github/workflows/load-test.yml index 46a960aab..9b498416c 100644 --- a/.github/workflows/load-test.yml +++ b/.github/workflows/load-test.yml @@ -24,37 +24,32 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 - - uses: actions/cache@v5 - name: Setup gnpm cache - if: always() + uses: actions/checkout@v7 + - uses: actions/cache@v6 + name: Cache pnpm store with: - path: | - ${{ env.STORE_PATH }} - ~/.local/share/gnpm - ~/.cache/ms-playwright - /usr/local/bin/gnpm - /usr/local/bin/gnpm-0.0.12 - key: ${{ runner.os }}-gnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + path: ${{ env.PNPM_HOME }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | - ${{ runner.os }}-gnpm-store- - - name: Setup gnpm - uses: SamTV12345/gnpm-setup@main + ${{ runner.os }}-pnpm-store- + - uses: pnpm/action-setup@v6 + name: Install pnpm with: - version: 0.0.12 + run_install: false + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: pnpm - name: Install all dependencies and symlink for ep_etherpad-lite - run: gnpm install --frozen-lockfile + run: pnpm install --frozen-lockfile - name: Install etherpad-load-test - run: sudo npm install -g etherpad-load-test-socket-io + run: sudo npm install -g etherpad-load-test - name: Run load test - run: | - gnpm --gnpmEnv - eval "$(gnpm --gnpmEnv)" - echo $PATH - src/tests/frontend/travis/runnerLoadTest.sh 25 50 + run: src/tests/frontend/travis/runnerLoadTest.sh 25 50 withplugins: # run on pushes to any branch @@ -67,33 +62,30 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 - - uses: actions/cache@v5 - name: Setup gnpm cache - if: always() + uses: actions/checkout@v7 + - uses: actions/cache@v6 + name: Cache pnpm store with: - path: | - ${{ env.STORE_PATH }} - ~/.local/share/gnpm - ~/.cache/ms-playwright - /usr/local/bin/gnpm - /usr/local/bin/gnpm-0.0.12 - key: ${{ runner.os }}-gnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + path: ${{ env.PNPM_HOME }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | - ${{ runner.os }}-gnpm-store- - - name: Setup gnpm - uses: SamTV12345/gnpm-setup@main + ${{ runner.os }}-pnpm-store- + - uses: pnpm/action-setup@v6 + name: Install pnpm with: - version: 0.0.12 + run_install: false + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: pnpm - name: Install etherpad-load-test - run: sudo npm install -g etherpad-load-test-socket-io + run: sudo npm install -g etherpad-load-test - name: Install etherpad plugins - # The --legacy-peer-deps flag is required to work around a bug in npm v7: - # https://github.com/npm/cli/issues/2199 run: > - gnpm install --workspace-root + pnpm add -w ep_align ep_author_hover ep_cursortrace @@ -101,7 +93,7 @@ jobs: ep_hash_auth ep_headings2 ep_markdown - ep_readonly_guest + ep_guest ep_set_title_on_pad ep_spellcheck ep_subscript_and_superscript @@ -117,12 +109,10 @@ jobs: # rules. - name: Install all dependencies and symlink for ep_etherpad-lite - run: gnpm install --frozen-lockfile + run: pnpm install --frozen-lockfile - name: Run load test - run: | - eval "$(gnpm --gnpmEnv)" - src/tests/frontend/travis/runnerLoadTest.sh 25 50 + run: src/tests/frontend/travis/runnerLoadTest.sh 25 50 long: # run on pushes to any branch @@ -135,34 +125,29 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 - - uses: actions/cache@v5 - name: Setup gnpm cache - if: always() + uses: actions/checkout@v7 + - uses: actions/cache@v6 + name: Cache pnpm store with: - path: | - ${{ env.STORE_PATH }} - ~/.local/share/gnpm - ~/.cache/ms-playwright - /usr/local/bin/gnpm - /usr/local/bin/gnpm-0.0.12 - key: ${{ runner.os }}-gnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + path: ${{ env.PNPM_HOME }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | - ${{ runner.os }}-gnpm-store- - - name: Setup gnpm - uses: SamTV12345/gnpm-setup@main + ${{ runner.os }}-pnpm-store- + - uses: pnpm/action-setup@v6 + name: Install pnpm with: - version: 0.0.12 + run_install: false + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: pnpm - name: Install all dependencies and symlink for ep_etherpad-lite - run: gnpm install --frozen-lockfile + run: pnpm install --frozen-lockfile - name: Install etherpad-load-test - run: sudo npm install -g etherpad-load-test-socket-io + run: sudo npm install -g etherpad-load-test - name: Run load test - run: | - gnpm --gnpmEnv - eval "$(gnpm --gnpmEnv)" - echo $PATH - src/tests/frontend/travis/runnerLoadTest.sh 5000 5 + run: src/tests/frontend/travis/runnerLoadTest.sh 5000 5 diff --git a/.github/workflows/perform-type-check.yml b/.github/workflows/perform-type-check.yml index a2105615e..fbed6df2c 100644 --- a/.github/workflows/perform-type-check.yml +++ b/.github/workflows/perform-type-check.yml @@ -24,27 +24,25 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v6 - - uses: actions/cache@v5 - name: Setup gnpm cache - if: always() + uses: actions/checkout@v7 + - uses: actions/cache@v6 + name: Cache pnpm store with: - path: | - ${{ env.STORE_PATH }} - ~/.local/share/gnpm - ~/.cache/ms-playwright - /usr/local/bin/gnpm - /usr/local/bin/gnpm-0.0.12 - key: ${{ runner.os }}-gnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + path: ${{ env.PNPM_HOME }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | - ${{ runner.os }}-gnpm-store- - - name: Setup gnpm - uses: SamTV12345/gnpm-setup@main + ${{ runner.os }}-pnpm-store- + - uses: pnpm/action-setup@v6 + name: Install pnpm with: - version: 0.0.12 - - - name: Install all dependencies and symlink for ep_etherpad-lite - run: gnpm install --frozen-lockfile + run_install: false + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: pnpm + - name: Install all dependencies and symlink for ep_etherpad-lite + run: pnpm install --frozen-lockfile - name: Perform type check working-directory: ./src - run: gnpm run ts-check + run: pnpm run ts-check diff --git a/.github/workflows/rate-limit.yml b/.github/workflows/rate-limit.yml index 0a0699c6f..3cd791164 100644 --- a/.github/workflows/rate-limit.yml +++ b/.github/workflows/rate-limit.yml @@ -27,24 +27,23 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 - - uses: actions/cache@v5 - name: Setup gnpm cache - if: always() + uses: actions/checkout@v7 + - uses: actions/cache@v6 + name: Cache pnpm store with: - path: | - ${{ env.STORE_PATH }} - ~/.local/share/gnpm - ~/.cache/ms-playwright - /usr/local/bin/gnpm - /usr/local/bin/gnpm-0.0.12 - key: ${{ runner.os }}-gnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + path: ${{ env.PNPM_HOME }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | - ${{ runner.os }}-gnpm-store- - - name: Setup gnpm - uses: SamTV12345/gnpm-setup@main + ${{ runner.os }}-pnpm-store- + - uses: pnpm/action-setup@v6 + name: Install pnpm with: - version: 0.0.12 + run_install: false + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: pnpm - name: docker network @@ -59,11 +58,36 @@ jobs: name: run docker images run: | docker run --name etherpad-docker -p 9000:9001 --rm --network ep_net --ip 172.23.42.2 -e 'TRUST_PROXY=true' epl-debian-slim & - docker run -p 8081:80 --rm --network ep_net --ip 172.23.42.1 -d nginx-latest + docker run --name nginx-docker -p 8081:80 --rm --network ep_net --ip 172.23.42.1 -d nginx-latest docker run --rm --network ep_net --ip 172.23.42.3 --name anotherip -dt anotherip - name: install dependencies and create symlink for ep_etherpad-lite - run: gnpm install --frozen-lockfile + run: pnpm install --frozen-lockfile + - + # The etherpad container is started in the background above; without + # this wait the test step can hit nginx before etherpad is listening + # and nginx returns 502, failing the run on a cold cache. Poll the + # nginx-proxied endpoint (which is also what the test hits) until it + # stops returning 5xx. + name: Wait for etherpad behind nginx to be ready + run: | + # ~60s budget: 30 iterations × (1s curl timeout + 1s sleep). + # Cold-start of the etherpad container is well under that. + for i in $(seq 1 30); do + if curl -fsS -o /dev/null --max-time 1 http://127.0.0.1:8081/; then + echo "etherpad is ready behind nginx" + exit 0 + fi + sleep 1 + done + echo "ERROR: etherpad behind nginx did not become ready in time" + echo "--- docker ps ---" + docker ps -a || true + echo "--- nginx-docker logs ---" + docker logs nginx-docker || true + echo "--- etherpad-docker logs ---" + docker logs etherpad-docker || true + exit 1 - name: run rate limit test run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d77f94641..b0b697845 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: repository: ether/etherpad-lite path: etherpad @@ -42,30 +42,37 @@ jobs: git checkout develop git reset --hard origin/develop - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: repository: ether/ether.github.com path: ether.github.com token: '${{ secrets.ETHER_RELEASE_TOKEN }}' - - uses: actions/cache@v5 - name: Setup gnpm cache - if: always() + - uses: actions/cache@v6 + name: Cache pnpm store with: - path: | - ${{ env.STORE_PATH }} - ~/.local/share/gnpm - ~/.cache/ms-playwright - /usr/local/bin/gnpm - /usr/local/bin/gnpm-0.0.12 - key: ${{ runner.os }}-gnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + path: ${{ env.PNPM_HOME }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | - ${{ runner.os }}-gnpm-store- - - name: Setup gnpm - uses: SamTV12345/gnpm-setup@main + ${{ runner.os }}-pnpm-store- + - uses: pnpm/action-setup@v6 + name: Install pnpm with: - version: 0.0.12 + run_install: false + # Repo is checked out into ./etherpad, so the action can't find + # packageManager in a root package.json. + package_json_file: etherpad/package.json + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: pnpm + cache-dependency-path: etherpad/pnpm-lock.yaml - name: Install dependencies ether.github.com - run: gnpm install --frozen-lockfile + # ether.github.com depends on sharp (Next.js image pipeline), whose + # install script must run to fetch the platform binary. pnpm 11 + # turned ignored-builds into an error; allow all builds for this + # external repo since we don't control its pnpm-workspace.yaml. + run: pnpm install --frozen-lockfile --config.dangerously-allow-all-builds=true working-directory: ether.github.com - name: Set git user run: | @@ -75,15 +82,15 @@ jobs: with: ruby-version: 2.7 - - uses: reitzig/actions-asciidoctor@v2.0.4 + - uses: reitzig/actions-asciidoctor@v2.0.5 with: version: 2.0.18 - name: Prepare release working-directory: etherpad run: | cd bin - gnpm install - gnpm run release ${{ inputs.release_type }} + pnpm install + pnpm run release ${{ inputs.release_type }} - name: Push after release working-directory: etherpad run: | diff --git a/.github/workflows/releaseEtherpad.yml b/.github/workflows/releaseEtherpad.yml index e9575a494..dd53cf9ae 100644 --- a/.github/workflows/releaseEtherpad.yml +++ b/.github/workflows/releaseEtherpad.yml @@ -1,9 +1,33 @@ +# PARKED — npm publish of the core package is not part of the standard release. +# +# This workflow renames `ep_etherpad-lite` -> `ep_etherpad` and publishes +# `./src` to npm. As of 2026-06, that publish serves no load-bearing purpose: +# - `ep_etherpad` has 0 dependents on npm and nothing in this repo depends on it; +# - plugins import `ep_etherpad-lite` resolved from the LOCAL core install, +# and plugin CI clones `ether/etherpad` rather than `npm install`-ing core; +# - Etherpad is run via git clone / Docker / zip / snap, never `npm install`. +# The publish has been failing (E404 PUT — the `ep_etherpad` package has no OIDC +# trusted publisher configured on npmjs.com), which is why npm is stuck at 2.5.0 +# while 3.x shipped fine without it. +# +# It is therefore gated behind an explicit `confirm: true` dispatch input so a +# stray run fails fast with a clear message instead of a confusing 404. To +# actually publish, the npm owner of `ep_etherpad` (samtv12345) must first +# configure a trusted publisher: npmjs.com -> ep_etherpad -> Settings -> +# Trusted Publisher -> repo `ether/etherpad`, workflow `releaseEtherpad.yml`. +# Decision pending: finish that config, or remove this workflow. See AGENTS.MD. name: releaseEtherpad.yaml permissions: contents: read id-token: write # for npm OIDC trusted publishing on: workflow_dispatch: + inputs: + confirm: + description: 'PARKED — publish ep_etherpad to npm? Requires a trusted publisher configured on npmjs.com first (see workflow header). Set true only if that is done.' + required: true + default: false + type: boolean env: PNPM_HOME: ~/.pnpm-store @@ -12,44 +36,42 @@ jobs: release: runs-on: ubuntu-latest steps: + - name: Guard — refuse unless explicitly confirmed + if: ${{ inputs.confirm != true }} + run: | + echo "::error::releaseEtherpad is PARKED. The ep_etherpad npm publish is non-functional" + echo "::error::(no trusted publisher configured on npmjs.com; 0 dependents on npm)." + echo "::error::Re-run with confirm=true only after the owner configures a trusted" + echo "::error::publisher. See the workflow header / AGENTS.MD 'Releasing' section." + exit 1 - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 - uses: actions/setup-node@v6 with: # OIDC trusted publishing needs npm >= 11.5.1, which requires - # Node >= 20.17.0. setup-node's `20` resolves to the latest - # 20.x, which satisfies that. - node-version: 20 + # Node >= 22.9.0. Node 24 satisfies that and matches the rest of CI. + node-version: 24 registry-url: https://registry.npmjs.org/ - name: Upgrade npm to >=11.5.1 (required for trusted publishing) run: npm install -g npm@latest - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - uses: actions/cache@v5 - name: Setup pnpm cache - if: always() + - uses: actions/cache@v6 + name: Cache pnpm store with: - path: | - ${{ env.STORE_PATH }} - ~/.local/share/gnpm - /usr/local/bin/gnpm - /usr/local/bin/gnpm-0.0.12 - key: ${{ runner.os }}-gnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + path: ${{ env.PNPM_HOME }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | - ${{ runner.os }}-gnpm-store- - - name: Setup gnpm - uses: SamTV12345/gnpm-setup@main + ${{ runner.os }}-pnpm-store- + - uses: pnpm/action-setup@v6 + name: Install pnpm with: - version: 0.0.12 + run_install: false - name: Install dependencies - run: gnpm install --frozen-lockfile + run: pnpm install --frozen-lockfile - name: Rename etherpad working-directory: ./src run: sed -i 's/ep_etherpad-lite/ep_etherpad/g' package.json - # Use `npm publish` directly (not `gnpm`/`pnpm` wrappers) because OIDC - # trusted publishing requires npm CLI >= 11.5.1 and the wrappers shell + # Use `npm publish` directly (not the `pnpm` wrapper) because OIDC + # trusted publishing requires npm CLI >= 11.5.1 and the wrapper shells # out to npm; calling npm directly avoids any shim ambiguity. The # ep_etherpad package must have a trusted publisher configured on # npmjs.com pointing at this workflow file. See: diff --git a/.github/workflows/snap-build.yml b/.github/workflows/snap-build.yml new file mode 100644 index 000000000..9255db173 --- /dev/null +++ b/.github/workflows/snap-build.yml @@ -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@v7 + - 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@v7 + + - 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@v7 + 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 diff --git a/.github/workflows/snap-publish.yml b/.github/workflows/snap-publish.yml new file mode 100644 index 000000000..6f692d27f --- /dev/null +++ b/.github/workflows/snap-publish.yml @@ -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@v7 + + - name: Build snap + id: build + uses: snapcore/action-build@v1 + + - name: Upload snap artifact + uses: actions/upload-artifact@v7 + 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@v8 + 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@v8 + 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 diff --git a/.github/workflows/update-plugins.yml b/.github/workflows/update-plugins.yml index 4b6a282a8..2f3ca4783 100644 --- a/.github/workflows/update-plugins.yml +++ b/.github/workflows/update-plugins.yml @@ -16,18 +16,17 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out etherpad-lite - uses: actions/checkout@v6 + uses: actions/checkout@v7 - uses: pnpm/action-setup@v6 name: Install pnpm with: - version: 10 run_install: false - name: Use Node.js uses: actions/setup-node@v6 with: - node-version: 22 + node-version: 24 - name: Install bin dependencies working-directory: ./bin diff --git a/.github/workflows/upgrade-from-latest-release.yml b/.github/workflows/upgrade-from-latest-release.yml index c60af163a..02c527e70 100644 --- a/.github/workflows/upgrade-from-latest-release.yml +++ b/.github/workflows/upgrade-from-latest-release.yml @@ -27,46 +27,48 @@ jobs: strategy: fail-fast: false matrix: - # PRs: single Node version. Push: full matrix. - node: ${{ github.event_name == 'pull_request' && fromJSON('[24]') || fromJSON('[20, 22, 24]') }} + # Etherpad requires Node >= 24 (see package.json engines.node). + node: ${{ fromJSON('[24]') }} steps: - name: Check out latest release - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: - ref: develop #FIXME change to master when doing release - - uses: actions/cache@v5 - name: Setup gnpm cache - if: always() + fetch-depth: 0 + - + name: Check out latest release tag + run: git checkout "$(git tag --list 'v*' --sort=-version:refname | head -n1)" + - uses: actions/cache@v6 + name: Cache pnpm store with: - path: | - ${{ env.STORE_PATH }} - ~/.local/share/gnpm - ~/.cache/ms-playwright - /usr/local/bin/gnpm - /usr/local/bin/gnpm-0.0.12 - key: ${{ runner.os }}-gnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + path: ${{ env.PNPM_HOME }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | - ${{ runner.os }}-gnpm-store- - - name: Setup gnpm - uses: SamTV12345/gnpm-setup@main + ${{ runner.os }}-pnpm-store- + - uses: pnpm/action-setup@v6 + name: Install pnpm with: - version: 0.0.12 + run_install: false + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node }} + cache: pnpm - name: Install libreoffice - uses: awalsh128/cache-apt-pkgs-action@v1.6.0 + uses: awalsh128/cache-apt-pkgs-action@v1.6.3 with: packages: libreoffice libreoffice-pdfimport version: 1.0 - name: Install all dependencies and symlink for ep_etherpad-lite - run: gnpm install --frozen-lockfile --runtimeVersion="${{ matrix.node }}" + run: pnpm install --frozen-lockfile - name: Build admin ui working-directory: admin - run: gnpm build --runtimeVersion="${{ matrix.node }}" + run: pnpm build - name: Install Etherpad plugins run: > - gnpm run install-plugins + pnpm run install-plugins ep_align ep_author_hover ep_cursortrace @@ -74,24 +76,21 @@ jobs: ep_hash_auth ep_headings2 ep_markdown - ep_readonly_guest + ep_guest ep_set_title_on_pad ep_spellcheck ep_subscript_and_superscript - ep_table_of_contents --runtimeVersion="${{ matrix.node }}" + ep_table_of_contents - name: Run the backend tests - run: gnpm run test --runtimeVersion="${{ matrix.node }}" + run: pnpm run test - name: Install all dependencies and symlink for ep_etherpad-lite - run: gnpm install --frozen-lockfile --runtimeVersion="${{ matrix.node }}" - # Because actions/checkout@v6 is called with "ref: master" and without - # "fetch-depth: 0", the local clone does not have the ${GITHUB_SHA} - # commit. Fetch ${GITHUB_REF} to get the ${GITHUB_SHA} commit. Note that a - # plain "git fetch" only fetches "normal" references (refs/heads/* and - # refs/tags/*), and for pull requests none of the normal references - # include ${GITHUB_SHA}, so we have to explicitly tell Git to fetch - # ${GITHUB_REF}. + run: pnpm install --frozen-lockfile + # The job starts from the latest release tag, so fetch the pull-request + # ref explicitly before checking out ${GITHUB_SHA}. A plain "git fetch" + # only brings "normal" references (refs/heads/* and refs/tags/*), and for + # pull requests none of those include the synthetic merge commit. - name: Fetch the new Git commits run: git fetch --depth=1 origin "${GITHUB_REF}" @@ -101,4 +100,4 @@ jobs: # commit that merges the PR's source branch to its destination branch. run: git checkout "${GITHUB_SHA}" - name: Run the backend tests - run: gnpm run test --runtimeVersion="${{ matrix.node }}" + run: pnpm run test diff --git a/.gitignore b/.gitignore index 28fabc00b..99e10ca74 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,22 @@ 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 + +# Generated by `pnpm --filter admin gen:api` from src/node/hooks/express/openapi.ts. +# Regenerated by build/test/dev scripts; not committed. +/admin/src/api/schema.d.ts +/admin/src/api/version.ts + +# Local git worktrees used by /release-review and similar workflows. +/.worktrees/ diff --git a/.pr_agent.toml b/.pr_agent.toml deleted file mode 100644 index 93e106ea2..000000000 --- a/.pr_agent.toml +++ /dev/null @@ -1,5 +0,0 @@ -[pr_reviewer] -run_on_pr_sync = true - -[pr_description] -run_on_pr_sync = true diff --git a/AGENTS.MD b/AGENTS.MD index bf8632594..80abdce91 100644 --- a/AGENTS.MD +++ b/AGENTS.MD @@ -6,8 +6,8 @@ Welcome to the Etherpad project. This guide provides essential context and instr Etherpad is a real-time collaborative editor designed to be lightweight, scalable, and highly extensible via plugins. ## Technical Stack -- **Runtime:** Node.js >= 20.0.0 -- **Package Manager:** pnpm (>= 8.3.0) +- **Runtime:** Node.js >= 22.12.0 +- **Package Manager:** pnpm (>= 11.0.0) - **Languages:** TypeScript (primary for new code), JavaScript (legacy), CSS, HTML - **Backend:** Express.js 5, Socket.io 4 - **Frontend:** Legacy core (`src/static`), Modern React UI (`ui/`), Admin UI (`admin/`) @@ -46,6 +46,25 @@ pnpm --filter ep_etherpad-lite run prod # Start production server - **Comments:** Provide clear comments for complex logic only. - **Backward Compatibility:** Always ensure compatibility with older versions of the database and configuration files. +### Internationalisation (i18n) — Mandatory +- **Every user-facing string MUST go through i18n.** That means JSX text, `placeholder`, `title`, `aria-label`, `alt`, toast/alert titles, `