etherpad-lite/.github/workflows/docker.yml
John McLear fe9727b31e
fix(docker): share corepack cache so etherpad user can resolve pnpm (#7689)
* fix(docker): share corepack cache so etherpad user can resolve pnpm (#7687)

PR #7674 switched the Dockerfile from `npm install -g pnpm` to corepack
and `corepack prepare pnpm@${PnpmVersion} --activate`. The activate step
runs as root and writes its lastKnownGood pin into `$COREPACK_HOME`,
which defaults to `~/.cache/node/corepack` — i.e. a per-user path. The
Dockerfile then drops to `USER etherpad` and later runs
`bin/installLocalPlugins.sh`, which invokes `pnpm` as etherpad. With an
empty per-user corepack cache and no shared activation file, corepack
re-resolves pnpm and (for forks/configs without a `packageManager` pin
matching the activated version) can fall back to "latest" from the
registry — pulling `pnpm@10.33.4` instead of the requested 11.x and
failing the workspace's `engines.pnpm` check.

Pin `COREPACK_HOME=/opt/corepack` and chown it to etherpad after the
prepare step. Both root and etherpad now share the same lastKnownGood
file and tarball cache, so etherpad inherits the activated pnpm without
hitting the registry again.

Verified end-to-end:

- `docker build --target development --build-arg ETHERPAD_LOCAL_PLUGINS=ep_test`
  with a stub local plugin runs `installLocalPlugins.sh` cleanly:
  `Done in 16.6s using pnpm v11.0.6`.
- `docker run ... pnpm --version` as etherpad reports 11.0.6 from the
  shared cache — no "Unsupported environment" error.

Note: corepack still emits a one-time "about to download" line at
runtime because `corepack prepare pnpm@11.0.6` resolves to the highest
matching patch (11.0.8) at build time while the project's
`packageManager` field pins exactly 11.0.6. That's a follow-up — the
download succeeds non-interactively and the engine check passes.

Fixes #7687.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(docker): action Qodo PR review (#7687 follow-up)

- Replace hard-coded /opt/corepack with ${COREPACK_HOME} in mkdir/chown
  so the env var stays the single source of truth (Qodo: "COREPACK_HOME
  path duplication").

- Add a build-test-local-plugin job to .github/workflows/docker.yml that
  builds the development target with a stub ETHERPAD_LOCAL_PLUGINS so
  the original failure mode (corepack/pnpm cache invisible across the
  USER switch) cannot silently regress (Qodo: "COREPACK_HOME fix lacks
  test"). The job is small — `docker build` only, no run — and uses the
  shared GHA buildx cache.

Verified: same docker build + `docker run pnpm --version` flow on the
variable form gives identical output (pnpm 11.0.6 from the etherpad-owned
cache).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 10:09:27 +01:00

341 lines
11 KiB
YAML

name: "Docker"
on:
pull_request:
paths-ignore:
- 'doc/**'
push:
branches:
- 'develop'
paths-ignore:
- 'doc/**'
tags:
- 'v?[0-9]+.[0-9]+.[0-9]+'
env:
TEST_TAG: etherpad/etherpad:test
permissions:
contents: read
jobs:
build-test:
runs-on: ubuntu-latest
permissions:
contents: read
env:
PNPM_HOME: ~/.pnpm-store
steps:
-
name: Check out
uses: actions/checkout@v6
with:
path: etherpad
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
-
name: Build and export to Docker
uses: docker/build-push-action@v7
with:
context: ./etherpad
target: production
load: true
tags: ${{ env.TEST_TAG }}
cache-from: type=gha
cache-to: type=gha,mode=max
- uses: actions/cache@v5
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
# 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: 22
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 }}
./bin/installDeps.sh
docker logs -f test &
while true; do
echo "Waiting for Docker container to start..."
status=$(docker container inspect -f '{{.State.Health.Status}}' test) || exit 1
case ${status} in
healthy) break;;
starting) sleep 2;;
*) printf %s\\n "unexpected status: ${status}" >&2; exit 1;;
esac
done
(cd src && pnpm run test-container)
git clean -dxf .
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@v6
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
services:
mysql:
image: mysql:8
env:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: etherpad
MYSQL_USER: etherpad
MYSQL_PASSWORD: password
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping -h 127.0.0.1 -uroot -ppassword"
--health-interval=5s
--health-timeout=5s
--health-retries=20
postgres:
image: postgres:16
env:
POSTGRES_DB: etherpad
POSTGRES_USER: etherpad
POSTGRES_PASSWORD: password
ports:
- 5432:5432
options: >-
--health-cmd="pg_isready -U etherpad -d etherpad"
--health-interval=5s
--health-timeout=5s
--health-retries=20
steps:
-
name: Check out
uses: actions/checkout@v6
with:
path: etherpad
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
-
name: Build production image
uses: docker/build-push-action@v7
with:
context: ./etherpad
target: production
load: true
tags: ${{ env.TEST_TAG }}
cache-from: type=gha
cache-to: type=gha,mode=max
-
name: Driver presence test (all 10 ueberdb2 drivers must resolve)
run: |
docker run --rm -w /opt/etherpad-lite/src "$TEST_TAG" node -e "
const mods = [
'@elastic/elasticsearch','cassandra-driver','mongodb','mssql',
'mysql2','nano','pg','redis','rethinkdb','surrealdb'
];
let fail = false;
for (const m of mods) {
try { require(m); console.log('ok', m); }
catch (e) { console.error('MISSING', m, e.message); fail = true; }
}
if (fail) process.exit(1);
"
-
name: MySQL smoke — start Etherpad against mysql service
run: |
docker run --rm -d \
--network host \
-e NODE_ENV=production \
-e ADMIN_PASSWORD=admin \
-e DB_TYPE=mysql \
-e DB_HOST=127.0.0.1 \
-e DB_PORT=3306 \
-e DB_NAME=etherpad \
-e DB_USER=etherpad \
-e DB_PASS=password \
-e DB_CHARSET=utf8mb4 \
-e DEFAULT_PAD_TEXT="Test " \
--name et-mysql "$TEST_TAG"
for i in $(seq 1 60); do
if curl -sf http://127.0.0.1:9001/ >/dev/null; then
echo "mysql smoke: Etherpad is serving /"
docker rm -f et-mysql
exit 0
fi
sleep 2
done
echo "mysql smoke: timed out waiting for Etherpad"
docker logs et-mysql || true
docker rm -f et-mysql || true
exit 1
-
name: Postgres smoke — start Etherpad against postgres service
run: |
docker run --rm -d \
--network host \
-e NODE_ENV=production \
-e ADMIN_PASSWORD=admin \
-e DB_TYPE=postgres \
-e DB_HOST=127.0.0.1 \
-e DB_PORT=5432 \
-e DB_NAME=etherpad \
-e DB_USER=etherpad \
-e DB_PASS=password \
-e DEFAULT_PAD_TEXT="Test " \
--name et-postgres "$TEST_TAG"
for i in $(seq 1 60); do
if curl -sf http://127.0.0.1:9001/ >/dev/null; then
echo "postgres smoke: Etherpad is serving /"
docker rm -f et-postgres
exit 0
fi
sleep 2
done
echo "postgres smoke: timed out waiting for Etherpad"
docker logs et-postgres || true
docker rm -f et-postgres || true
exit 1
publish:
needs: [build-test, build-test-db-drivers]
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
-
name: Check out
uses: actions/checkout@v6
with:
path: etherpad
-
name: Set up QEMU
uses: docker/setup-qemu-action@v4
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
-
name: Docker meta
id: meta
uses: docker/metadata-action@v6
with:
images: |
etherpad/etherpad
ghcr.io/ether/etherpad
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
-
name: Log in to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
-
name: Build and push
id: build-docker
uses: docker/build-push-action@v7
with:
context: ./etherpad
target: production
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
- name: Update repo description
uses: peter-evans/dockerhub-description@v5
if: github.ref == 'refs/heads/master'
with:
readme-filepath: ./etherpad/README.md
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: etherpad/etherpad
enable-url-completion: true
- name: Check out ether-charts
if: github.ref == 'refs/heads/develop'
uses: actions/checkout@v6
with:
path: ether-charts
repository: ether/ether-charts
token: ${{ secrets.ETHER_CHART_TOKEN }}
- name: Update tag in values-dev.yaml
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
- 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 commit -m 'Update develop image tag'
git push