From aa4f1e04399a6cf6a3255e55f55214b6a01bb7ff Mon Sep 17 00:00:00 2001 From: Michael <67710348+kilo666mj@users.noreply.github.com> Date: Sun, 18 Oct 2026 11:46:00 +0200 Subject: [PATCH 1/3] Upgrade bundled PostgreSQL to 18 with a safe migration path The postgres:18 image moved PGDATA to /var/lib/postgresql/18/docker and its volume to /var/lib/postgresql, and its entrypoint refuses to start when anything is mounted at the old /var/lib/postgresql/data path. The plain tag bump in #33 would therefore stop every Compose install, including fresh ones. - compose.yaml: postgres 18-alpine, volume mounted at /var/lib/postgresql. The volume name is unchanged on purpose: an existing PostgreSQL 17 volume makes the 18 container refuse to start instead of silently coming up empty. - docs/operations.md: dump/restore procedure for 17 -> 18, keeping the old data in a rollback volume, plus the rollback steps. - CI: run go test with -count=1. The setup-go cache restored every package as "(cached)", so the PostgreSQL integration tests did not run against a new server version. - CI: new "compose postgres" job starts PostgreSQL from compose.yaml on a fresh volume and checks the server major matches the image tag; image publishing now depends on it. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 33 +++++++++++++-- compose.yaml | 4 +- docs/operations.md | 77 +++++++++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9223f3b..cd42707 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest services: postgres: - image: postgres:17-alpine@sha256:b0f9560a2de083e2cc7382e75f808c7381a32852a7ec49117deedb300e552b24 + image: postgres:18-alpine@sha256:77f585114c32fbca283dc835b0596f4e52b51b4c6662d7810b2f4084f60a1873 env: POSTGRES_PASSWORD: test-password ports: ["5432:5432"] @@ -51,10 +51,37 @@ jobs: exit 1 fi - name: Test - run: go test -race ./... + run: go test -race -count=1 ./... + + compose: + name: compose postgres + runs-on: ubuntu-latest + env: + POSTGRES_PASSWORD: compose-test-password + RENDERCASE_PUBLIC_URL: https://rendercase.example.com + RENDERCASE_CONTENT_URL: https://content.rendercase.example.com + RENDERCASE_COOKIE_SECRET: compose-test-cookie-secret-not-for-production + RENDERCASE_OIDC_ISSUER: https://id.example.com + RENDERCASE_OAUTH_AUDIENCE: https://rendercase.example.com/mcp + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Start PostgreSQL from compose.yaml on a fresh volume + run: docker compose up -d --wait --wait-timeout 120 postgres + - name: Check server version and data directory + run: | + docker compose exec -T postgres psql -U rendercase -d rendercase -Atc 'show server_version' | tee version + docker compose exec -T postgres psql -U rendercase -d rendercase -Atc 'show data_directory' + image_major=$(sed -n 's/.*image: postgres:\([0-9]*\)-.*/\1/p' compose.yaml) + grep -q "^${image_major}\." version + - name: Show PostgreSQL logs + if: failure() + run: docker compose logs postgres + - name: Clean up + if: always() + run: docker compose down --volumes docker: - needs: test + needs: [test, compose] runs-on: ubuntu-latest if: github.event_name != 'pull_request' permissions: diff --git a/compose.yaml b/compose.yaml index f441808..f6201fd 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,13 +1,13 @@ services: postgres: - image: postgres:17-alpine@sha256:b0f9560a2de083e2cc7382e75f808c7381a32852a7ec49117deedb300e552b24 + image: postgres:18-alpine@sha256:77f585114c32fbca283dc835b0596f4e52b51b4c6662d7810b2f4084f60a1873 restart: unless-stopped environment: POSTGRES_DB: rendercase POSTGRES_USER: rendercase POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env} volumes: - - postgres-data:/var/lib/postgresql/data + - postgres-data:/var/lib/postgresql healthcheck: test: ["CMD-SHELL", "pg_isready -U rendercase"] interval: 5s diff --git a/docs/operations.md b/docs/operations.md index f068b8e..7e97f30 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -50,6 +50,83 @@ Do not edit `schema_version` or mark a schema change complete by hand. Roll back to the recorded application revision only with a database and object-store state that it supports. +## Upgrade PostgreSQL to a new major version + +A PostgreSQL major version cannot open data files written by an earlier major +version, so a major upgrade in Compose is a dump and restore. The bundled +`compose.yaml` uses PostgreSQL 18. Its image keeps data in +`/var/lib/postgresql/18/docker` and mounts the `postgres-data` volume at +`/var/lib/postgresql`; PostgreSQL 17 and earlier used `/var/lib/postgresql/data`. + +If you start the PostgreSQL 18 revision on an existing PostgreSQL 17 volume, the +`postgres` container exits with `Error: in 18+, these Docker images are +configured to store database data in a format which is compatible with +"pg_ctlcluster"` and Rendercase does not start. Nothing is written to the old +data. Upgrade it as follows. + +The commands below assume the Compose project is named `rendercase` (the +default for a checkout in a `rendercase` directory), so the volume is +`rendercase_postgres-data`. Check yours with +`docker volume ls --filter name=postgres-data`. + +1. While still on the old revision, stop Rendercase, then dump the database. + Also back up artifact storage and `.env` as described in [Back up](#back-up). + + ```sh + docker compose stop rendercase + docker compose exec -T postgres \ + pg_dump -U rendercase -d rendercase -Fc > rendercase-pg17.dump + ``` + +2. Stop the stack. This keeps the volumes. + + ```sh + docker compose down + ``` + +3. Copy the old data into a separate rollback volume, then remove the original + so PostgreSQL 18 can initialize it: + + ```sh + docker volume create rendercase-pg17-data + docker run --rm \ + -v rendercase_postgres-data:/from:ro -v rendercase-pg17-data:/to \ + alpine cp -a /from/. /to/ + docker volume rm rendercase_postgres-data + ``` + +4. Update the checkout to the new revision and start only PostgreSQL. Do not + start Rendercase yet: it would create an empty schema that the restore then + collides with. + + ```sh + docker compose up -d --wait postgres + ``` + +5. Restore the dump: + + ```sh + docker compose exec -T postgres \ + pg_restore --exit-on-error --single-transaction \ + -U rendercase -d rendercase < rendercase-pg17.dump + ``` + +6. Start Rendercase and run the checks from step 5 of [Upgrade](#upgrade): + + ```sh + docker compose up -d --build rendercase + ``` + +Keep `rendercase-pg17.dump` and the `rendercase-pg17-data` volume until the +upgraded installation has been verified, then remove the volume with +`docker volume rm rendercase-pg17-data`. + +To roll back before then, run `docker compose down`, remove +`rendercase_postgres-data`, copy `rendercase-pg17-data` back into a new +`rendercase_postgres-data` volume with the same `docker run ... cp -a` command +(source and destination swapped), check out the previous revision and run +`docker compose up -d`. Any changes made after the upgrade are lost. + ## Back up PostgreSQL contains identities, authorization, artifact/version metadata, From bce2da4d90bb3da5747322315c62e498be564992 Mon Sep 17 00:00:00 2001 From: Michael <67710348+kilo666mj@users.noreply.github.com> Date: Sun, 18 Oct 2026 11:46:00 +0200 Subject: [PATCH 2/3] TEMPORARY: rehearse PostgreSQL 17 -> 18 upgrade procedure Runs the docs/operations.md steps on a real PostgreSQL 17 volume. To be removed before merge. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/pg18-upgrade-rehearsal.yml | 92 ++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 .github/workflows/pg18-upgrade-rehearsal.yml diff --git a/.github/workflows/pg18-upgrade-rehearsal.yml b/.github/workflows/pg18-upgrade-rehearsal.yml new file mode 100644 index 0000000..d19db29 --- /dev/null +++ b/.github/workflows/pg18-upgrade-rehearsal.yml @@ -0,0 +1,92 @@ +# TEMPORARY: rehearses docs/operations.md "Upgrade PostgreSQL to a new major +# version" on a real PostgreSQL 17 volume. Removed before merge. +name: pg18 upgrade rehearsal + +on: + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + rehearse: + runs-on: ubuntu-latest + env: + COMPOSE_PROJECT_NAME: rendercase + POSTGRES_PASSWORD: compose-test-password + RENDERCASE_PUBLIC_URL: https://rendercase.example.com + RENDERCASE_CONTENT_URL: https://content.rendercase.example.com + RENDERCASE_COOKIE_SECRET: compose-test-cookie-secret-not-for-production + RENDERCASE_OIDC_ISSUER: https://id.example.com + RENDERCASE_OAUTH_AUDIENCE: https://rendercase.example.com/mcp + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + - name: Run old (PostgreSQL 17) revision and seed real schema and data + run: | + set -eux + mkdir -p ../old && git show origin/main:compose.yaml > ../old/compose.yaml + grep 'postgres:17' ../old/compose.yaml + cd ../old + docker compose up -d --wait postgres + docker compose exec -T postgres psql -v ON_ERROR_STOP=1 -U rendercase -d rendercase < "$GITHUB_WORKSPACE/internal/store/schema.sql" + docker compose exec -T postgres psql -v ON_ERROR_STOP=1 -U rendercase -d rendercase \ + -c "INSERT INTO users (id, oidc_subject, username) VALUES ('u_rehearsal', 'sub-rehearsal', 'rehearsal')" + - name: "Doc step 1-2: dump on old revision, then compose down" + run: | + set -eux + cd ../old + docker compose exec -T postgres pg_dump -U rendercase -d rendercase -Fc > "$RUNNER_TEMP/rendercase-pg17.dump" + docker compose down + docker volume ls + - name: "Fail-safe: new revision on the old volume must refuse to start" + run: | + set -ux + if docker compose up -d --wait --wait-timeout 60 postgres; then + echo "PostgreSQL 18 unexpectedly started on PostgreSQL 17 data"; exit 1 + fi + docker compose logs postgres | tee "$RUNNER_TEMP/failsafe.log" + grep -q 'in 18+, these Docker images are configured' "$RUNNER_TEMP/failsafe.log" + docker compose down + - name: "Doc step 3: copy old data to rollback volume, remove original" + run: | + set -eux + docker volume create rendercase-pg17-data + docker run --rm \ + -v rendercase_postgres-data:/from:ro -v rendercase-pg17-data:/to \ + alpine cp -a /from/. /to/ + docker volume rm rendercase_postgres-data + docker run --rm -v rendercase-pg17-data:/d:ro alpine cat /d/PG_VERSION + - name: "Doc step 4-5: start PostgreSQL 18 and restore" + run: | + set -eux + docker compose up -d --wait postgres + docker compose exec -T postgres \ + pg_restore --exit-on-error --single-transaction \ + -U rendercase -d rendercase < "$RUNNER_TEMP/rendercase-pg17.dump" + - name: Verify restored data and that startup migration applies on 18 + run: | + set -eux + docker compose exec -T postgres psql -U rendercase -d rendercase -Atc 'show server_version' | grep '^18\.' + docker compose exec -T postgres psql -U rendercase -d rendercase -Atc "SELECT username FROM users WHERE id='u_rehearsal'" | grep -x rehearsal + docker compose exec -T postgres psql -v ON_ERROR_STOP=1 -U rendercase -d rendercase < internal/store/schema.sql + - name: "Rollback path: old revision starts from the rollback volume" + run: | + set -eux + docker compose down + docker volume rm rendercase_postgres-data + docker volume create rendercase_postgres-data + docker run --rm \ + -v rendercase-pg17-data:/from:ro -v rendercase_postgres-data:/to \ + alpine cp -a /from/. /to/ + cd ../old + docker compose up -d --wait postgres + docker compose exec -T postgres psql -U rendercase -d rendercase -Atc 'show server_version' | grep '^17\.' + docker compose exec -T postgres psql -U rendercase -d rendercase -Atc "SELECT username FROM users WHERE id='u_rehearsal'" | grep -x rehearsal + - name: Logs on failure + if: failure() + run: | + docker compose logs postgres || true + (cd ../old && docker compose logs postgres) || true From 4a8808c50053d6fb14da4b92b049dfed8be703ea Mon Sep 17 00:00:00 2001 From: Michael <67710348+kilo666mj@users.noreply.github.com> Date: Sun, 18 Oct 2026 11:48:11 +0200 Subject: [PATCH 3/3] Remove temporary PostgreSQL upgrade rehearsal The rehearsal passed on the PR (run 39116646413): PostgreSQL 18 refused the old volume, the dump restored on 18, the startup schema re-applied, and the rollback volume brought 17 back. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/pg18-upgrade-rehearsal.yml | 92 -------------------- 1 file changed, 92 deletions(-) delete mode 100644 .github/workflows/pg18-upgrade-rehearsal.yml diff --git a/.github/workflows/pg18-upgrade-rehearsal.yml b/.github/workflows/pg18-upgrade-rehearsal.yml deleted file mode 100644 index d19db29..0000000 --- a/.github/workflows/pg18-upgrade-rehearsal.yml +++ /dev/null @@ -1,92 +0,0 @@ -# TEMPORARY: rehearses docs/operations.md "Upgrade PostgreSQL to a new major -# version" on a real PostgreSQL 17 volume. Removed before merge. -name: pg18 upgrade rehearsal - -on: - pull_request: - branches: [main] - -permissions: - contents: read - -jobs: - rehearse: - runs-on: ubuntu-latest - env: - COMPOSE_PROJECT_NAME: rendercase - POSTGRES_PASSWORD: compose-test-password - RENDERCASE_PUBLIC_URL: https://rendercase.example.com - RENDERCASE_CONTENT_URL: https://content.rendercase.example.com - RENDERCASE_COOKIE_SECRET: compose-test-cookie-secret-not-for-production - RENDERCASE_OIDC_ISSUER: https://id.example.com - RENDERCASE_OAUTH_AUDIENCE: https://rendercase.example.com/mcp - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - - name: Run old (PostgreSQL 17) revision and seed real schema and data - run: | - set -eux - mkdir -p ../old && git show origin/main:compose.yaml > ../old/compose.yaml - grep 'postgres:17' ../old/compose.yaml - cd ../old - docker compose up -d --wait postgres - docker compose exec -T postgres psql -v ON_ERROR_STOP=1 -U rendercase -d rendercase < "$GITHUB_WORKSPACE/internal/store/schema.sql" - docker compose exec -T postgres psql -v ON_ERROR_STOP=1 -U rendercase -d rendercase \ - -c "INSERT INTO users (id, oidc_subject, username) VALUES ('u_rehearsal', 'sub-rehearsal', 'rehearsal')" - - name: "Doc step 1-2: dump on old revision, then compose down" - run: | - set -eux - cd ../old - docker compose exec -T postgres pg_dump -U rendercase -d rendercase -Fc > "$RUNNER_TEMP/rendercase-pg17.dump" - docker compose down - docker volume ls - - name: "Fail-safe: new revision on the old volume must refuse to start" - run: | - set -ux - if docker compose up -d --wait --wait-timeout 60 postgres; then - echo "PostgreSQL 18 unexpectedly started on PostgreSQL 17 data"; exit 1 - fi - docker compose logs postgres | tee "$RUNNER_TEMP/failsafe.log" - grep -q 'in 18+, these Docker images are configured' "$RUNNER_TEMP/failsafe.log" - docker compose down - - name: "Doc step 3: copy old data to rollback volume, remove original" - run: | - set -eux - docker volume create rendercase-pg17-data - docker run --rm \ - -v rendercase_postgres-data:/from:ro -v rendercase-pg17-data:/to \ - alpine cp -a /from/. /to/ - docker volume rm rendercase_postgres-data - docker run --rm -v rendercase-pg17-data:/d:ro alpine cat /d/PG_VERSION - - name: "Doc step 4-5: start PostgreSQL 18 and restore" - run: | - set -eux - docker compose up -d --wait postgres - docker compose exec -T postgres \ - pg_restore --exit-on-error --single-transaction \ - -U rendercase -d rendercase < "$RUNNER_TEMP/rendercase-pg17.dump" - - name: Verify restored data and that startup migration applies on 18 - run: | - set -eux - docker compose exec -T postgres psql -U rendercase -d rendercase -Atc 'show server_version' | grep '^18\.' - docker compose exec -T postgres psql -U rendercase -d rendercase -Atc "SELECT username FROM users WHERE id='u_rehearsal'" | grep -x rehearsal - docker compose exec -T postgres psql -v ON_ERROR_STOP=1 -U rendercase -d rendercase < internal/store/schema.sql - - name: "Rollback path: old revision starts from the rollback volume" - run: | - set -eux - docker compose down - docker volume rm rendercase_postgres-data - docker volume create rendercase_postgres-data - docker run --rm \ - -v rendercase-pg17-data:/from:ro -v rendercase_postgres-data:/to \ - alpine cp -a /from/. /to/ - cd ../old - docker compose up -d --wait postgres - docker compose exec -T postgres psql -U rendercase -d rendercase -Atc 'show server_version' | grep '^17\.' - docker compose exec -T postgres psql -U rendercase -d rendercase -Atc "SELECT username FROM users WHERE id='u_rehearsal'" | grep -x rehearsal - - name: Logs on failure - if: failure() - run: | - docker compose logs postgres || true - (cd ../old && docker compose logs postgres) || true