From f10206522826eb4a1131cf552830e30acc34d225 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Andrei?= Date: Thu, 8 Oct 2026 20:55:49 -0300 Subject: [PATCH] Add -demo variants of the Plone 4.3 - 5.2 images Each -demo image is the official plone image for the series (4.3.19, 5.0.8, 5.1.6, 5.2.14), pinned by digest, plus a database with a Plone site already created at /Plone. The bases are not rebuilt: their Debian releases have left deb.debian.org. demo/seed.sh starts the official image on the host, creates the site through legacy/shared/create-plone-site.sh, stops Zope with the signal that closes the storage cleanly (INT for 5.2, TERM for the others) and copies the database out. demo/Dockerfile bakes it at /app/seed, and a wrapper entrypoint copies it into /data on first start before handing over to the base image's entrypoint. The demo build workflow tests every series and publishes -demo and -demo to plone/plone and GHCR from main. Shared legacy scripts: - create-plone-site.sh checks the Barceloneta theme on Plone 5, where the Plone 4 skin check reads nothing. - smoke-test.sh can read product-load errors from docker logs, for 5.2, which writes no log file. - The JSON gate passed on 5.2 without the probe running: the probe was Python 2 only, and the gate matched the marker inside the traceback that quoted the probe's source. The probe now runs on Python 2.3 - 3.8, and the gate only accepts the marker at the start of a line. Refs #211 --- .github/workflows/demo-build.yml | 115 +++++++++++++++++++++++++++++ CHANGES.md | 4 + README.md | 13 ++-- demo/.gitignore | 2 + demo/Dockerfile | 31 ++++++++ demo/Makefile | 112 ++++++++++++++++++++++++++++ demo/README.md | 76 +++++++++++++++++++ demo/demo-entrypoint.sh | 29 ++++++++ demo/seed.sh | 102 +++++++++++++++++++++++++ legacy/shared/create-plone-site.sh | 15 ++++ legacy/shared/json-probe.py | 14 +++- legacy/shared/smoke-test.sh | 50 ++++++++----- 12 files changed, 538 insertions(+), 25 deletions(-) create mode 100644 .github/workflows/demo-build.yml create mode 100644 demo/.gitignore create mode 100644 demo/Dockerfile create mode 100644 demo/Makefile create mode 100644 demo/README.md create mode 100755 demo/demo-entrypoint.sh create mode 100755 demo/seed.sh diff --git a/.github/workflows/demo-build.yml b/.github/workflows/demo-build.yml new file mode 100644 index 0000000..8f00c5d --- /dev/null +++ b/.github/workflows/demo-build.yml @@ -0,0 +1,115 @@ +name: demo build + +# Builds the -demo variants of the 4.3 - 5.2 images under demo/: the official +# image for each series, plus a database with a Plone site already created. +# +# The base images are not rebuilt. They are the official `plone` images, +# pinned by digest in demo/Makefile; see mirror-official.yml for why these +# series cannot be rebuilt faithfully. + +on: + push: + branches: [main] + paths: + - 'demo/**' + - 'legacy/shared/create-plone-site.sh' + - 'legacy/shared/smoke-test.sh' + - 'legacy/shared/json-probe.py' + - '.github/workflows/demo-build.yml' + pull_request: + paths: + - 'demo/**' + - 'legacy/shared/create-plone-site.sh' + - 'legacy/shared/smoke-test.sh' + - 'legacy/shared/json-probe.py' + - '.github/workflows/demo-build.yml' + workflow_dispatch: + +permissions: + contents: read + packages: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: make lint + working-directory: demo + run: make lint + + build: + needs: + - lint + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + # Keep in sync with SERIES in demo/Makefile. + version: ["4.3", "5.0", "5.1", "5.2"] + env: + # Pinned on purpose, as in legacy-build.yml (see #209). + GHCR_IMAGE: ghcr.io/plone/plone.docker + DOCKERHUB_IMAGE: plone/plone + # Secrets cannot be referenced from an `if:` condition; see + # legacy-build.yml for the same pattern. + HAS_DOCKERHUB: ${{ secrets.DOCKERHUB_USERNAME != '' && secrets.DOCKERHUB_TOKEN != '' }} + steps: + - uses: actions/checkout@v7 + + - name: Resolve full version from the Makefile + id: ver + run: | + FULL=$(sed -n "s/^FULL_VERSION_${{ matrix.version }} := //p" demo/Makefile) + test -n "${FULL}" + echo "full=${FULL}" >> "${GITHUB_OUTPUT}" + + # Starts the official image, creates the site through + # legacy/shared/create-plone-site.sh, stops Zope cleanly and builds + # demo/Dockerfile on top. No buildx builder is set up on purpose: the + # plain `docker build` uses the daemon's own, which is all this needs. + - name: Build -demo image + working-directory: demo + run: make demo-${{ matrix.version }} + + # SMOKE_EXPECT_SITE=1 asserts the site is ALREADY there on first start, + # with no creation step - the only check that distinguishes a correctly + # seeded demo image from its own base image. + - name: Smoke test -demo + working-directory: demo + run: make test-demo-${{ matrix.version }} + + - name: Login to GHCR + if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to Docker Hub + if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' && env.HAS_DOCKERHUB == 'true' + uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + # Two tags per series: the full version and the series, each -demo. + - name: Push + if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + run: | + for tag in "${{ steps.ver.outputs.full }}-demo" "${{ matrix.version }}-demo"; do + docker tag "plone-demo:${tag}" "${GHCR_IMAGE}:${tag}" + docker push "${GHCR_IMAGE}:${tag}" + if [ "${HAS_DOCKERHUB}" = "true" ]; then + docker tag "plone-demo:${tag}" "${DOCKERHUB_IMAGE}:${tag}" + docker push "${DOCKERHUB_IMAGE}:${tag}" + fi + done + if [ "${HAS_DOCKERHUB}" != "true" ]; then + echo "::notice::DOCKERHUB_USERNAME/DOCKERHUB_TOKEN not set - pushed to GHCR only." + fi diff --git a/CHANGES.md b/CHANGES.md index e5d92ca..6ad0f0e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,8 @@ # Changelog +- Add `-demo` variants of Plone 4.3, 5.0, 5.1 and 5.2 (`4.3-demo`, `5.0-demo`, `5.1-demo`, `5.2-demo` and their full-version tags), each the official image with a Plone site already created at `/Plone`. Refs #211 + [@ericof] +- Fix the smoke test's JSON gate passing when the probe script failed to compile, and make the probe run on Python 3. Refs #211 + [@ericof] - Publish Plone 5.2.14 as `plone/plone:5.2.14` (and GHCR), copied by digest from the official `plone:5.2.14`. `plone/plone:latest`, `5`, `5.2` and `python38` now point at 5.2.14 instead of 5.2.4. Refs #210 [@ericof] - Add Plone 1.0 image to the legacy matrix, completing it. Refs #197 diff --git a/README.md b/README.md index 13ff2af..17431c4 100644 --- a/README.md +++ b/README.md @@ -34,15 +34,15 @@ listed; earlier point releases remain in the repository and on Docker Hub. | Plone | Python | Variant | Built from | Published tags | |---|---|---|---|---| -| 5.2.14 | 3.8 | Debian | [`5.2/5.2.14/debian/Dockerfile`](5.2/5.2.14/debian/Dockerfile) | `latest`, `5`, `5.2`, `5.2.14`, `python38`, `5-python38`, `5.2-python38`, `5.2.14-python38` | +| 5.2.14 | 3.8 | Debian | [`5.2/5.2.14/debian/Dockerfile`](5.2/5.2.14/debian/Dockerfile) | `latest`, `5`, `5.2`, `5.2.14`, `python38`, `5-python38`, `5.2-python38`, `5.2.14-python38`, `5.2-demo`, `5.2.14-demo` | | 5.2.14 | 3.8 | Alpine | [`5.2/5.2.14/alpine/Dockerfile`](5.2/5.2.14/alpine/Dockerfile) | *not published* | | 5.2.14 | 3.7 | Debian | [`5.2/5.2.14/python37/Dockerfile`](5.2/5.2.14/python37/Dockerfile) | *not published* | | 5.2.14 | 2.7 | Debian | [`5.2/5.2.14/python2/Dockerfile`](5.2/5.2.14/python2/Dockerfile) | *not published* | -| 5.1.6 | 2.7 | Debian | [`5.1/5.1.6/debian/Dockerfile`](5.1/5.1.6/debian/Dockerfile) | `5.1`, `5.1.6` | +| 5.1.6 | 2.7 | Debian | [`5.1/5.1.6/debian/Dockerfile`](5.1/5.1.6/debian/Dockerfile) | `5.1`, `5.1.6`, `5.1-demo`, `5.1.6-demo` | | 5.1.6 | 2.7 | Alpine | [`5.1/5.1.6/alpine/Dockerfile`](5.1/5.1.6/alpine/Dockerfile) | `5.1-alpine`, `5.1.6-alpine` | -| 5.0.8 | 2.7 | Debian | [`5.0/5.0.8/debian/Dockerfile`](5.0/5.0.8/debian/Dockerfile) | `5.0`, `5.0.8` | +| 5.0.8 | 2.7 | Debian | [`5.0/5.0.8/debian/Dockerfile`](5.0/5.0.8/debian/Dockerfile) | `5.0`, `5.0.8`, `5.0-demo`, `5.0.8-demo` | | 5.0.8 | 2.7 | Alpine | [`5.0/5.0.8/alpine/Dockerfile`](5.0/5.0.8/alpine/Dockerfile) | *not published* | -| 4.3.19 | 2.7 | Debian | [`4.3/4.3.19/debian/Dockerfile`](4.3/4.3.19/debian/Dockerfile) | `4`, `4.3`, `4.3.19` | +| 4.3.19 | 2.7 | Debian | [`4.3/4.3.19/debian/Dockerfile`](4.3/4.3.19/debian/Dockerfile) | `4`, `4.3`, `4.3.19`, `4.3-demo`, `4.3.19-demo` | | 4.3.19 | 2.7 | Alpine | [`4.3/4.3.19/alpine/Dockerfile`](4.3/4.3.19/alpine/Dockerfile) | `4-alpine`, `4.3-alpine`, `4.3.19-alpine` | | 4.2.6 | 2.7.18 | Debian *(legacy)* | [`legacy/4.2/Dockerfile`](legacy/4.2/Dockerfile) | `4.2`, `4.2.6`, `4.2-demo`, `4.2.6-demo` | | 4.1.6 | 2.6.9 | Debian *(legacy)* | [`legacy/4.1/Dockerfile`](legacy/4.1/Dockerfile) | `4.1`, `4.1.6`, `4.1-demo`, `4.1.6-demo` | @@ -65,6 +65,7 @@ alone do not say which: |---|---|---| | 4.3 - 5.2 | the official `plone` image | `docker pull plone:5.2.14` | | 5.2.14 (Debian) | also `plone/plone` and GHCR | `docker pull plone/plone:5.2.14`
`docker pull ghcr.io/plone/plone.docker:5.2.14` | +| 4.3 - 5.2 `-demo` | `plone/plone` and GHCR only | `docker pull plone/plone:5.2-demo`
`docker pull ghcr.io/plone/plone.docker:5.2-demo` | | `legacy/` | `plone/plone` and GHCR | `docker pull plone/plone:4.2`
`docker pull ghcr.io/plone/plone.docker:4.2` | The 5.2.14 tags on `plone/plone` and GHCR are copies of the official image, made @@ -79,7 +80,9 @@ them. prefer `plone:`. Each legacy series publishes four tags — the full version and the series, each -with a `-demo` counterpart that ships a Plone site already created. +with a `-demo` counterpart that ships a Plone site already created. The 4.3 - +5.2 series have the same `-demo` counterparts, built on the official images; +see [`demo/`](demo/). The 4.3 - 5.2 images take Python from an upstream `python:*` base image. The `legacy/` images build the interpreter from source, because no usable base diff --git a/demo/.gitignore b/demo/.gitignore new file mode 100644 index 0000000..23e6614 --- /dev/null +++ b/demo/.gitignore @@ -0,0 +1,2 @@ +# Seeded databases, written by seed.sh and baked into the images. +build/ diff --git a/demo/Dockerfile b/demo/Dockerfile new file mode 100644 index 0000000..e07afb5 --- /dev/null +++ b/demo/Dockerfile @@ -0,0 +1,31 @@ +# syntax=docker/dockerfile:1 +# +# The -demo variant of the 4.3 - 5.2 images. +# +# One Dockerfile serves all four series: it takes an official base image and a +# database seeded from it (see seed.sh), and adds nothing else — same Zope, +# same Python, same add-ons, same entrypoint behind a small wrapper. +# +# ./seed.sh plone:5.2.14 build/5.2 +# docker build --build-arg BASE_IMAGE=plone:5.2.14 --build-arg SEED=build/5.2 \ +# -t plone-demo:5.2.14-demo . +# +# or just `make demo-5.2`. +# +# NOT FOR PRODUCTION: the site's admin account is admin / admin, inherited +# from the base image's buildout. +ARG BASE_IMAGE +FROM ${BASE_IMAGE} + +ARG BASE_IMAGE +ARG SEED=build/seed + +LABEL org.opencontainers.image.description="Demo variant of ${BASE_IMAGE}: identical, but ships a database with a Plone site already created at /Plone. Migration, demonstration and archaeology only — NOT for production." + +# The only differences from the base image: the seeded database, and the +# wrapper that copies it into /data on first start. +COPY --chown=plone:plone ${SEED}/ /app/seed/ +COPY --chmod=755 demo-entrypoint.sh /demo-entrypoint.sh + +ENTRYPOINT ["/demo-entrypoint.sh"] +CMD ["start"] diff --git a/demo/Makefile b/demo/Makefile new file mode 100644 index 0000000..3a9bf5c --- /dev/null +++ b/demo/Makefile @@ -0,0 +1,112 @@ +# -demo variants of the 4.3 - 5.2 images. +# All tooling runs via docker: no local installs required beyond curl. + +IMAGE_PREFIX ?= plone-demo +DOCKER ?= docker + +# The official images are linux/amd64 only. On an arm64 host (Apple Silicon) +# they run under emulation. +PLATFORM ?= linux/amd64 + +# Every series with a -demo variant, oldest first. Keep in sync with the +# matrix in ../.github/workflows/demo-build.yml. +SERIES := 4.3 5.0 5.1 5.2 + +# series -> full version, and the official image it is built on, pinned by +# digest so that a demo image is always seeded from exactly the image that was +# reviewed. Resolve a digest with: +# docker buildx imagetools inspect --format '{{.Manifest.Digest}}' plone: +FULL_VERSION_4.3 := 4.3.19 +FULL_VERSION_5.0 := 5.0.8 +FULL_VERSION_5.1 := 5.1.6 +FULL_VERSION_5.2 := 5.2.14 +BASE_4.3 := docker.io/library/plone:4.3.19@sha256:a717118d21b880e96f83a96fce868ce15bd96b102225b1691f61fa1157d26840 +BASE_5.0 := docker.io/library/plone:5.0.8@sha256:010d8e92106ef5aa3ad43664780f29c8844b79a4b89bb203b051f9a690c38274 +BASE_5.1 := docker.io/library/plone:5.1.6@sha256:067ec2433da954c1fb1f9abad7233b7a7409688a913ff048871a0644d24827a7 +BASE_5.2 := docker.io/library/plone:5.2.14@sha256:e73ae6e9864e43d4d49169f9fda09012d48c7720e8e204a8b0304b764600923b + +# [V 2026-10-08] The signal that stops each base image cleanly. 4.3 - 5.1 trap +# TERM and run `bin/instance stop`. 5.2 execs `bin/instance console`, which +# ignored TERM until docker's 120s timeout killed it, and stops in about a +# second on INT. +STOP_SIGNAL_4.3 := TERM +STOP_SIGNAL_5.0 := TERM +STOP_SIGNAL_5.1 := TERM +STOP_SIGNAL_5.2 := INT + +# [V 2026-10-08] Where each base image logs, for the smoke test's product-load +# check: 4.3 - 5.1 write /data/log/instance.log; 5.2 logs to stdout/stderr +# and writes no log file at all. +LOG_SOURCE_4.3 := file +LOG_SOURCE_5.0 := file +LOG_SOURCE_5.1 := file +LOG_SOURCE_5.2 := docker + +define require_version + @test -n "$(FULL_VERSION_$(1))" || { \ + echo "No FULL_VERSION_$(1) in the Makefile — is '$(1)' a real series?"; \ + exit 1; \ + } +endef + +.PHONY: help +help: ## Show this help + @grep -hE '^[a-zA-Z0-9_.%-]+:.*## ' $(MAKEFILE_LIST) | \ + awk 'BEGIN {FS=":.*## "}; {printf "\033[36m%-18s\033[0m %s\n", $$1, $$2}' + +.PHONY: seed-% +seed-%: ## Seed a site from the official image, e.g. make seed-5.2 + $(call require_version,$*) + PLATFORM=$(PLATFORM) STOP_SIGNAL=$(STOP_SIGNAL_$*) \ + ./seed.sh $(BASE_$*) build/$* + +# Defined BEFORE demo-%: macOS ships GNU Make 3.81, which takes the first +# matching pattern rather than the shortest stem (see ../legacy/Makefile). +.PHONY: demo-image-% +demo-image-%: ## Build only the -demo image from an existing seed, e.g. make demo-image-5.2 + $(call require_version,$*) + @test -f build/$*/filestorage/Data.fs || { echo "No seed in build/$*/ — run make seed-$*"; exit 1; } + $(DOCKER) build \ + --platform $(PLATFORM) \ + --build-arg BASE_IMAGE=$(BASE_$*) \ + --build-arg SEED=build/$* \ + -t $(IMAGE_PREFIX):$(FULL_VERSION_$*)-demo \ + -t $(IMAGE_PREFIX):$*-demo \ + . + +.PHONY: demo-% +demo-%: ## Seed and build the -demo image for a series, e.g. make demo-5.2 + $(call require_version,$*) + $(MAKE) seed-$* + $(MAKE) demo-image-$* + +.PHONY: test-demo-% +test-demo-%: ## Smoke-test a built -demo image, e.g. make test-demo-5.2 + $(call require_version,$*) + SMOKE_EXPECT_SITE=1 \ + SMOKE_LOG_SOURCE=$(LOG_SOURCE_$*) \ + IMAGE=$(IMAGE_PREFIX):$(FULL_VERSION_$*)-demo \ + ../legacy/shared/smoke-test.sh + +.PHONY: demos +demos: ## Build every -demo image (slow: each one boots Zope and creates a site) + @for v in $(SERIES); do $(MAKE) demo-$$v || exit 1; done + +.PHONY: test-demos +test-demos: ## Smoke-test every built -demo image + @for v in $(SERIES); do $(MAKE) test-demo-$$v || exit 1; done + +.PHONY: lint +lint: ## Lint the Dockerfile (hadolint) and shell scripts (shellcheck) + @echo "hadolint Dockerfile" + @$(DOCKER) run --rm -v "$(CURDIR):/mnt:ro" -w /mnt \ + hadolint/hadolint hadolint Dockerfile + @echo "shellcheck *.sh" + @$(DOCKER) run --rm -v "$(CURDIR):/mnt:ro" -w /mnt koalaman/shellcheck:stable \ + -s sh seed.sh demo-entrypoint.sh + @echo "lint OK" + +.PHONY: clean +clean: ## Remove seeds and locally built -demo images + rm -rf build + -@$(DOCKER) rmi -f $(foreach v,$(SERIES),$(IMAGE_PREFIX):$(v)-demo $(IMAGE_PREFIX):$(FULL_VERSION_$(v))-demo) 2>/dev/null diff --git a/demo/README.md b/demo/README.md new file mode 100644 index 0000000..9ed43bc --- /dev/null +++ b/demo/README.md @@ -0,0 +1,76 @@ +# The `-demo` variants (Plone 4.3 - 5.2) + +Each of the 4.3 - 5.2 series has a `-demo` image that **already has a Plone +site**, created at the id `Plone`. Start one and browse straight to +`http://localhost:8080/Plone`. + +```sh +docker run -d -p 8080:8080 plone/plone:5.2-demo +open http://localhost:8080/Plone +``` + +| Series | Built on | Published tags | +|---|---|---| +| 5.2 | `plone:5.2.14` | `5.2-demo`, `5.2.14-demo` | +| 5.1 | `plone:5.1.6` | `5.1-demo`, `5.1.6-demo` | +| 5.0 | `plone:5.0.8` | `5.0-demo`, `5.0.8-demo` | +| 4.3 | `plone:4.3.19` | `4.3-demo`, `4.3.19-demo` | + +The tags are on `plone/plone` and `ghcr.io/plone/plone.docker`, next to the +legacy `-demo` images for 1.0 - 4.2 (see [`../legacy/demo/`](../legacy/demo/)). + +| | | +|---|---| +| Site id | `Plone` | +| Admin | `admin` / `admin` | +| Everything else | identical to the base image — same Zope, Python, add-ons, environment variables | + +The admin password comes from the base images, which bake `admin:admin` into +their buildout and do not read `ADMIN_PASSWORD`. These are demo images for a +throwaway stack. **Do not put them anywhere reachable.** + +## Building locally + +```sh +cd demo +make demo-5.2 # seeds build/5.2/, then builds plone-demo:5.2.14-demo and :5.2-demo +make test-demo-5.2 # smoke test: the site must already be there on first start +``` + +`make demos` and `make test-demos` cover every series. Building needs `curl` +on the host, and runs the amd64-only images under emulation on Apple Silicon. + +## How it works + +The base images are the **official `plone` images**, pinned by digest in the +[`Makefile`](Makefile), and are not rebuilt: their Debian releases have left +`deb.debian.org`, so a rebuild would fail or produce a different image. For the +same reason nothing can be installed into them — they ship neither `curl` nor +`wget` — so the site is created from outside: + +1. [`seed.sh`](seed.sh) starts the official image as it is, waits for HTTP, + and creates the site through + [`legacy/shared/create-plone-site.sh`](../legacy/shared/create-plone-site.sh), + the same script the legacy images use. It stops Zope with the signal that + closes the storage cleanly, checks that it did, and copies `Data.fs` and the + blob storage out with `docker cp`. +2. [`Dockerfile`](Dockerfile) adds that seed at `/app/seed`, plus + [`demo-entrypoint.sh`](demo-entrypoint.sh): on first start it copies the + seed into `/data`, then runs the base image's own `/docker-entrypoint.sh` + with the same arguments. When `/data` already holds a database nothing is + copied, so mounting a real site over a demo image still works. + +## Validated facts + +- **[V 2026-10-08]** **5.2 needs SIGINT to stop cleanly.** Its entrypoint + execs `bin/instance console`, which ignored SIGTERM until `docker stop`'s + timeout killed it — leaving no `Data.fs.index`, the evidence that the + storage was closed. On SIGINT it stops in about a second. 4.3 - 5.1 trap + SIGTERM and run `bin/instance stop`. See `STOP_SIGNAL_*` in the Makefile. +- **[V 2026-10-08]** **5.2 writes no log file.** It logs to stdout/stderr, so + the smoke test reads its product-load errors from `docker logs` + (`SMOKE_LOG_SOURCE=docker`); 4.3 - 5.1 log to `/data/log/instance.log`. +- **[V 2026-10-08]** **Plone 5's theme is not a skin.** The add-site form + forces `plonetheme.barceloneta:default`, a Diazo theme, so the skin check + `create-plone-site.sh` uses for Plone 4 finds nothing on Plone 5. The script + checks for `++theme++barceloneta` in the rendered page there instead. diff --git a/demo/demo-entrypoint.sh b/demo/demo-entrypoint.sh new file mode 100755 index 0000000..426e917 --- /dev/null +++ b/demo/demo-entrypoint.sh @@ -0,0 +1,29 @@ +#!/bin/sh +# Entrypoint of the 4.3 - 5.2 -demo images. +# +# The official base images know nothing about seeding, and are left exactly as +# released: this wrapper puts the baked database in place, then hands over to +# the base image's own /docker-entrypoint.sh with the same arguments. +# +# The seed lives at /app/seed and NOT under /data: /data is a VOLUME in the +# base image, so it is copied in on first start instead. Nothing is copied when +# /data already holds a database, so mounting a real site over a demo image +# still works exactly as it does on the base image. +set -e + +SEED_DIR="${SEED_DIR:-/app/seed}" + +if [ ! -e /data/filestorage/Data.fs ]; then + echo "demo: seeding /data from ${SEED_DIR}" + mkdir -p /data/filestorage /data/blobstorage + cp -p "${SEED_DIR}/filestorage/Data.fs" /data/filestorage/Data.fs + cp -Rp "${SEED_DIR}/blobstorage/." /data/blobstorage/ + # 4.3, 5.1 and 5.2 start as root and drop to `plone` through gosu; 5.0 + # starts as `plone` already and owns /data, so it neither needs nor may + # chown. + if [ "$(id -u)" = "0" ]; then + chown -R plone:plone /data/filestorage /data/blobstorage + fi +fi + +exec /docker-entrypoint.sh "$@" diff --git a/demo/seed.sh b/demo/seed.sh new file mode 100755 index 0000000..372f988 --- /dev/null +++ b/demo/seed.sh @@ -0,0 +1,102 @@ +#!/bin/sh +# Seed a Plone site from an official base image, for the -demo variants. +# +# Usage: seed.sh +# e.g. seed.sh plone:5.2.14 build/5.2 +# +# Runs on the HOST, not inside a build: the official images ship neither curl +# nor wget, and the Debian releases they are built on have left +# deb.debian.org, so nothing can be installed into them. Instead the base +# image is started as-is, the site is created through +# legacy/shared/create-plone-site.sh — the same script the legacy demo images +# and the CI gates use — Zope is stopped cleanly, and the database is copied +# out with `docker cp`. demo/Dockerfile then bakes into the image. +set -eu + +BASE_IMAGE="${1:?usage: seed.sh }" +OUT_DIR="${2:?usage: seed.sh }" + +SITE_ID="${SITE_ID:-Plone}" +SITE_TITLE="${SITE_TITLE:-Plone demo}" +PLATFORM="${PLATFORM:-linux/amd64}" +PORT="${PORT:-18090}" +START_TIMEOUT="${START_TIMEOUT:-300}" +STOP_TIMEOUT="${STOP_TIMEOUT:-120}" +# The signal that makes the base image shut Zope down cleanly. 4.3 - 5.1 trap +# TERM and run `bin/instance stop`; 5.2 execs `bin/instance console`, which +# does not stop on TERM and needs INT. +STOP_SIGNAL="${STOP_SIGNAL:-TERM}" +# The base images bake admin:admin into their buildout, and do not read +# ADMIN_PASSWORD. +ADMIN_USER="admin" +ADMIN_PASSWORD="admin" + +CREATE_SITE="$(cd "$(dirname "$0")/../legacy/shared" && pwd)/create-plone-site.sh" +NAME="seed-$$" + +cleanup() { + docker rm -f "${NAME}" >/dev/null 2>&1 || true +} +trap cleanup EXIT INT TERM + +echo "=== seeding ${SITE_ID} from ${BASE_IMAGE} ===" +docker run -d --platform "${PLATFORM}" --name "${NAME}" \ + -p "${PORT}:8080" "${BASE_IMAGE}" >/dev/null + +echo "waiting for HTTP on :${PORT} (max ${START_TIMEOUT}s)..." +elapsed=0 +until curl -sf -o /dev/null "http://localhost:${PORT}/"; do + if [ "$(docker inspect -f '{{.State.Running}}' "${NAME}")" != "true" ]; then + echo "FAIL: Zope exited before answering HTTP" >&2 + docker logs "${NAME}" 2>&1 | tail -n 30 >&2 + exit 1 + fi + if [ "${elapsed}" -ge "${START_TIMEOUT}" ]; then + echo "FAIL: no HTTP answer within ${START_TIMEOUT}s" >&2 + exit 1 + fi + sleep 3 + elapsed=$((elapsed + 3)) +done +echo "OK: instance is up" + +SITE_TITLE="${SITE_TITLE}" "${CREATE_SITE}" \ + "http://localhost:${PORT}" "${SITE_ID}" "${ADMIN_USER}" "${ADMIN_PASSWORD}" + +# Clean shutdown, then verify it: copying a FileStorage that was killed rather +# than closed is how a half-flushed transaction ends up baked into an image. +echo "stopping Zope with SIG${STOP_SIGNAL} (max ${STOP_TIMEOUT}s)..." +docker kill -s "${STOP_SIGNAL}" "${NAME}" >/dev/null +elapsed=0 +while [ "$(docker inspect -f '{{.State.Running}}' "${NAME}")" = "true" ]; do + if [ "${elapsed}" -ge "${STOP_TIMEOUT}" ]; then + echo "FAIL: Zope did not stop within ${STOP_TIMEOUT}s of SIG${STOP_SIGNAL}" >&2 + exit 1 + fi + sleep 2 + elapsed=$((elapsed + 2)) +done +echo "OK: Zope stopped after ${elapsed}s" + +rm -rf "${OUT_DIR}" +mkdir -p "${OUT_DIR}" +docker cp -q "${NAME}:/data/filestorage" "${OUT_DIR}/" +docker cp -q "${NAME}:/data/blobstorage" "${OUT_DIR}/" + +# FileStorage writes Data.fs.index when it is closed, and not when the process +# dies. It is evidence only: the index is a cache Zope rebuilds on first open, +# and baking a stale one is worse than baking none, so it is dropped with the +# lock and any temporary files. +if [ ! -f "${OUT_DIR}/filestorage/Data.fs.index" ]; then + echo "FAIL: no Data.fs.index after shutdown — the storage was not closed" >&2 + exit 1 +fi +find "${OUT_DIR}/filestorage" -type f ! -name Data.fs -exec rm -f {} + + +SEED_BYTES=$(wc -c < "${OUT_DIR}/filestorage/Data.fs" | tr -d ' ') +if [ "${SEED_BYTES}" -lt 100000 ]; then + echo "FAIL: seeded Data.fs is only ${SEED_BYTES} bytes — no site in it" >&2 + exit 1 +fi +BLOBS=$(find "${OUT_DIR}/blobstorage" -name '*.blob' | wc -l | tr -d ' ') +echo "OK: seeded ${OUT_DIR} (Data.fs ${SEED_BYTES} bytes, ${BLOBS} blob(s))" diff --git a/legacy/shared/create-plone-site.sh b/legacy/shared/create-plone-site.sh index 3c27324..9820d6a 100755 --- a/legacy/shared/create-plone-site.sh +++ b/legacy/shared/create-plone-site.sh @@ -169,7 +169,22 @@ echo "OK: site root renders Plone markup via ${SITE_MARKER} ($(printf '%s' "${SI # the regression being guarded against IS a discovery that finds nothing, and # a guard keyed on `${FORCED_PROFILES}` would go quiet in exactly that case. # Verified by mutation — neutering the discovery must turn this red. +# +# [V 2026-10-08] Plone 5 forces plonetheme.barceloneta:default, which is a +# Diazo theme rather than a skin: portal_skins/manage_propertiesForm has no +# selected option at all there, so the skin check reads nothing and fails a +# correctly themed site. The theme's resources are served under +# ++theme++barceloneta, so look for that in the rendered page instead. case "${ADD_FORM:-}" in + *plonetheme.barceloneta*) + if ! printf '%s' "${SITE_HTML}" | grep -q '++theme++barceloneta'; then + echo "FAIL: the add-form forced plonetheme.barceloneta, but the site" >&2 + echo " does not render through ++theme++barceloneta — the theme" >&2 + echo " profile was not applied" >&2 + exit 1 + fi + echo "OK: theme profiles applied — site renders through ++theme++barceloneta" + ;; *plonetheme.*) DEFAULT_SKIN=$(curl -sf -u "${AUTH}" \ "${BASE_URL}/${SITE_ID}/portal_skins/manage_propertiesForm" | diff --git a/legacy/shared/json-probe.py b/legacy/shared/json-probe.py index 884722e..07a4655 100644 --- a/legacy/shared/json-probe.py +++ b/legacy/shared/json-probe.py @@ -27,10 +27,18 @@ # Non-ASCII has to survive as unicode: Plone content is full of it, and a # codec-level break would otherwise only surface on real data. +# +# [V 2026-10-08] The probe also runs on Python 3 (Plone 5.2), where the text +# type is `str` and print is a function. The single-argument print(...) below +# is valid in every Python from 2.3 on. +try: + text_type = unicode +except NameError: + text_type = str text = mod.loads('{"k": "\\u00e7\\u00e3o"}')["k"] -assert isinstance(text, unicode), repr(text) +assert isinstance(text, text_type), repr(text) assert len(text) == 3, repr(text) assert mod.loads(mod.dumps({"k": text}))["k"] == text, repr(text) -print "JSON-GATE-OK %s %s" % ( - mod.__name__, getattr(mod, "__version__", "stdlib")) +print("JSON-GATE-OK %s %s" % ( + mod.__name__, getattr(mod, "__version__", "stdlib"))) diff --git a/legacy/shared/smoke-test.sh b/legacy/shared/smoke-test.sh index 868243e..447b18e 100755 --- a/legacy/shared/smoke-test.sh +++ b/legacy/shared/smoke-test.sh @@ -66,25 +66,36 @@ echo "OK: /manage_main authenticates as admin:${ADMIN_PW}" # The first 3.1 run "passed" this check against a path that did not exist. So # glob the directory, and treat *finding no log at all* as a failure — that way # a future rename breaks the build instead of quietly disabling the gate. -LOG_FILES=$(docker exec "${NAME}" sh -c 'ls /data/log/*.log 2>/dev/null' 2>/dev/null || true) -if [ -z "${LOG_FILES}" ]; then - echo "FAIL: no /data/log/*.log found — cannot verify product load" >&2 - docker exec "${NAME}" ls -la /data/log/ >&2 || true - exit 1 +# +# [V 2026-10-08] SMOKE_LOG_SOURCE=docker reads the container's own output +# instead, for images that log to stdout/stderr and write no log file at all: +# the official Plone 5.2 image does, since 5.2.5. The empty-log check still +# applies there, so a container that printed nothing still fails. +PRODUCT_PATTERN='ERROR Zope (Could not import|Couldn.t install)' +if [ "${SMOKE_LOG_SOURCE:-file}" = "docker" ]; then + LOG_TEXT=$(docker logs "${NAME}" 2>&1 || true) + if [ -z "${LOG_TEXT}" ]; then + echo "FAIL: the container has logged nothing — cannot verify product load" >&2 + exit 1 + fi + echo "checking product load in: docker logs" +else + LOG_FILES=$(docker exec "${NAME}" sh -c 'ls /data/log/*.log 2>/dev/null' 2>/dev/null || true) + if [ -z "${LOG_FILES}" ]; then + echo "FAIL: no /data/log/*.log found — cannot verify product load" >&2 + docker exec "${NAME}" ls -la /data/log/ >&2 || true + exit 1 + fi + echo "checking product load in: $(echo "${LOG_FILES}" | tr '\n' ' ')" + LOG_TEXT=$(docker exec "${NAME}" sh -c 'cat /data/log/*.log' 2>/dev/null || true) fi -echo "checking product load in: $(echo "${LOG_FILES}" | tr '\n' ' ')" -# -h suppresses filename prefixes and `wc -l` counts matching lines across every -# log at once, which avoids needing `bc` to sum per-file counts from `grep -c`. -PRODUCT_ERRORS=$(docker exec "${NAME}" sh -c \ - 'grep -hE "ERROR Zope (Could not import|Couldn.t install)" /data/log/*.log 2>/dev/null | wc -l' \ - 2>/dev/null | tr -d ' ') +PRODUCT_ERRORS=$(printf '%s\n' "${LOG_TEXT}" | grep -cE "${PRODUCT_PATTERN}" || true) [ -n "${PRODUCT_ERRORS}" ] || PRODUCT_ERRORS=0 if [ "${PRODUCT_ERRORS}" -gt 0 ]; then - echo "WARNING: ${PRODUCT_ERRORS} product import/install failure(s) in event.log:" >&2 - docker exec "${NAME}" sh -c \ - 'grep -hE "ERROR Zope (Could not import|Couldn.t install)" /data/log/*.log' >&2 || true + echo "WARNING: ${PRODUCT_ERRORS} product import/install failure(s) in the log:" >&2 + printf '%s\n' "${LOG_TEXT}" | grep -E "${PRODUCT_PATTERN}" >&2 || true if [ "${SMOKE_STRICT_PRODUCTS:-1}" = "1" ]; then echo "FAIL: products failed to load (set SMOKE_STRICT_PRODUCTS=0 to allow)" >&2 exit 1 @@ -124,13 +135,18 @@ if [ "${SMOKE_JSON:-1}" = "1" ]; then test -f "${JSON_PROBE}" JSON_OUT=$(docker run --rm -v "${JSON_PROBE}:/json-probe.py:ro" \ "${IMAGE}" run /json-probe.py 2>&1) || true - if ! printf '%s' "${JSON_OUT}" | grep -q 'JSON-GATE-OK'; then + # + # [V 2026-10-08] The marker must START a line. A probe that fails to + # compile prints a traceback quoting its own source — including the line + # that would have printed the marker — so an unanchored match passed the + # gate on Plone 5.2, where the probe had not run at all. + if ! printf '%s\n' "${JSON_OUT}" | grep -q '^JSON-GATE-OK '; then echo "FAIL: no working JSON module reachable from the script runner" >&2 printf '%s\n' "${JSON_OUT}" | tail -n 20 >&2 exit 1 fi - echo "OK: JSON round-trip via $(printf '%s' "${JSON_OUT}" | - sed -n 's/.*JSON-GATE-OK //p')" + echo "OK: JSON round-trip via $(printf '%s\n' "${JSON_OUT}" | + sed -n 's/^JSON-GATE-OK //p')" else echo "NOTE: JSON gate skipped by SMOKE_JSON=0" fi