diff --git a/.github/scripts/image-smoke.sh b/.github/scripts/image-smoke.sh index 946f9e3b..fbb86dfa 100755 --- a/.github/scripts/image-smoke.sh +++ b/.github/scripts/image-smoke.sh @@ -25,13 +25,13 @@ # # Usage: image-smoke.sh [] # Needs on PATH: docker, curl, jq, cmp, tar. Images (defaults match ci.yml): -# FORGE_SMOKE_POSTGRES_IMAGE (mirror.gcr.io/library/postgres:17) +# FORGE_SMOKE_POSTGRES_IMAGE (mirror.gcr.io/library/postgres:18) # FORGE_SMOKE_S3_IMAGE (mirror.gcr.io/versity/versitygw:v1.4.1) set -euo pipefail image="$1" commit="${2:-}" -postgres_image="${FORGE_SMOKE_POSTGRES_IMAGE:-mirror.gcr.io/library/postgres:17}" +postgres_image="${FORGE_SMOKE_POSTGRES_IMAGE:-mirror.gcr.io/library/postgres:18}" s3_image="${FORGE_SMOKE_S3_IMAGE:-mirror.gcr.io/versity/versitygw:v1.4.1}" package="$(realpath "$(dirname "$0")/../../fixtures/nuget/sample.lib.1.0.0.nupkg")" port="${FORGE_SMOKE_PORT:-18421}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b59f6427..5ca16238 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -174,7 +174,7 @@ jobs: # queries and schema. Pinned by digest; Dependabot does not see # workflow images, so bump it with deploy/kubernetes/components/postgres. postgres: - image: mirror.gcr.io/library/postgres:17@sha256:a63c1ea3f6a3a5a1621f97d2aba3b30faecfab4224c1b0000cc85ced57bb1939 + image: mirror.gcr.io/library/postgres:18@sha256:edb0c08dc33befd0262ab3b5cf1c4f3f8cfd8b09e88c05b45adee29d6761ddb6 env: POSTGRES_DB: forge POSTGRES_USER: forge @@ -341,7 +341,7 @@ jobs: cache-to: ${{ github.event_name == 'push' && format('type=gha,mode=max,scope=docker-{0}', matrix.arch) || '' }} - name: Smoke test the image (as the manifests run it) env: - FORGE_SMOKE_POSTGRES_IMAGE: mirror.gcr.io/library/postgres:17@sha256:a63c1ea3f6a3a5a1621f97d2aba3b30faecfab4224c1b0000cc85ced57bb1939 + FORGE_SMOKE_POSTGRES_IMAGE: mirror.gcr.io/library/postgres:18@sha256:edb0c08dc33befd0262ab3b5cf1c4f3f8cfd8b09e88c05b45adee29d6761ddb6 FORGE_SMOKE_S3_IMAGE: mirror.gcr.io/versity/versitygw:v1.4.1@sha256:0400cb59f59da0f1cf9f7fd49505191abc348dfadf54509bf1988caaff4eb96f run: .github/scripts/image-smoke.sh forge:ci "${{ github.sha }}" @@ -472,7 +472,7 @@ jobs: if: ${{ !cancelled() && needs.changes.outputs.rust != 'false' }} services: postgres: - image: mirror.gcr.io/library/postgres:17@sha256:a63c1ea3f6a3a5a1621f97d2aba3b30faecfab4224c1b0000cc85ced57bb1939 + image: mirror.gcr.io/library/postgres:18@sha256:edb0c08dc33befd0262ab3b5cf1c4f3f8cfd8b09e88c05b45adee29d6761ddb6 env: POSTGRES_DB: forge POSTGRES_USER: forge @@ -562,7 +562,7 @@ jobs: if: ${{ !cancelled() && needs.changes.outputs.rust != 'false' }} services: postgres: - image: mirror.gcr.io/library/postgres:17@sha256:a63c1ea3f6a3a5a1621f97d2aba3b30faecfab4224c1b0000cc85ced57bb1939 + image: mirror.gcr.io/library/postgres:18@sha256:edb0c08dc33befd0262ab3b5cf1c4f3f8cfd8b09e88c05b45adee29d6761ddb6 env: POSTGRES_DB: forge POSTGRES_USER: forge @@ -618,7 +618,7 @@ jobs: if: ${{ !cancelled() && needs.changes.outputs.rust != 'false' }} services: postgres: - image: mirror.gcr.io/library/postgres:17@sha256:a63c1ea3f6a3a5a1621f97d2aba3b30faecfab4224c1b0000cc85ced57bb1939 + image: mirror.gcr.io/library/postgres:18@sha256:edb0c08dc33befd0262ab3b5cf1c4f3f8cfd8b09e88c05b45adee29d6761ddb6 env: POSTGRES_DB: forge POSTGRES_USER: forge @@ -716,7 +716,7 @@ jobs: # (.github/scripts/image-pin-check.sh leaves `docker pull` alone). - name: Pre-pull container images run: | - docker pull mirror.gcr.io/library/postgres:17 + docker pull mirror.gcr.io/library/postgres:18 docker pull mirror.gcr.io/versity/versitygw:v1.4.1 docker pull mirror.gcr.io/versity/versitygw:v1.9.0 docker pull mirror.gcr.io/library/redis:5.0 @@ -780,7 +780,7 @@ jobs: # By tag, like the e2e job: web/e2e names them by tag. - name: Pre-pull container images run: | - docker pull mirror.gcr.io/library/postgres:17 + docker pull mirror.gcr.io/library/postgres:18 docker pull mirror.gcr.io/versity/versitygw:v1.4.1 - name: Install deps (frozen lockfile) run: pnpm install --frozen-lockfile diff --git a/AGENTS.md b/AGENTS.md index 4c735a7a..8ae56415 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,7 +11,7 @@ are rate-limited and fail with `429 Too Many Requests`. Pull it through Google's mirror, `mirror.gcr.io`, which serves the same images under the same digests: -- an official image `postgres:17` is `mirror.gcr.io/library/postgres:17`; +- an official image `postgres:18` is `mirror.gcr.io/library/postgres:18`; - any other `versity/versitygw:v1.4.1` is `mirror.gcr.io/versity/versitygw:v1.4.1`. This holds for every pull: `docker pull`, `docker run`, testcontainers in new diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 04db39f3..ac811307 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -53,7 +53,7 @@ By default (`--db local`) the server points at a host Postgres on **:5432** `FORGE_DATABASE__URL`; versitygw (S3) still comes up via docker. Use `--db docker` for a fully self-contained stack (Postgres on :5433) if you don't run a local PostgreSQL. `--db none` starts no infra at all. Run `cargo xtask help` for -the full command list. +the full command list. Forge needs PostgreSQL 18 or newer, the host's too. > First run compiles the workspace (a minute or two) and runs DB migrations > automatically. A bootstrap admin **`admin@example.com` / `changeme`** is created (the server @@ -752,6 +752,10 @@ server hashes its bootstrap admin password. - `cargo xtask down` stops the containers but **keeps** the Postgres volume. To wipe all state (start fresh): `docker compose down -v`. +- **An empty `--db docker` database after the move to PostgreSQL 18** — the + compose PostgreSQL 18 keeps its data in the volume `forge-dev_postgresql`. + The PostgreSQL 17 cluster in the earlier volume, `forge-dev_db_data`, is not + used (18 cannot open it): `docker volume rm forge-dev_db_data` removes it. - **`the database was created by Forge 1.x or by a build before 2.0.0`** — the schema started over at one migration in 2.0.0, and a database an earlier build migrated is refused (`cargo xtask migrate` and `prepare`, which run diff --git a/crates/core/tests/common/mod.rs b/crates/core/tests/common/mod.rs index 4c5c4ccb..759add35 100644 --- a/crates/core/tests/common/mod.rs +++ b/crates/core/tests/common/mod.rs @@ -45,7 +45,7 @@ async fn endpoint() -> &'static str { .with_user("forge") .with_password("forge") .with_name("mirror.gcr.io/library/postgres") - .with_tag("17") + .with_tag("18") .start() .await .expect("failed to start postgres testcontainer (is Docker running?)"); diff --git a/crates/db/tests/common/mod.rs b/crates/db/tests/common/mod.rs index 61ff67af..9ab130d2 100644 --- a/crates/db/tests/common/mod.rs +++ b/crates/db/tests/common/mod.rs @@ -45,7 +45,7 @@ async fn endpoint() -> &'static str { .with_user("forge") .with_password("forge") .with_name("mirror.gcr.io/library/postgres") - .with_tag("17") + .with_tag("18") .start() .await .expect("failed to start postgres testcontainer (is Docker running?)"); diff --git a/crates/db/tests/encryption.rs b/crates/db/tests/encryption.rs index d876c251..3f05b2f8 100644 --- a/crates/db/tests/encryption.rs +++ b/crates/db/tests/encryption.rs @@ -51,7 +51,7 @@ async fn database_url() -> &'static str { .with_user("forge") .with_password("forge") .with_name("mirror.gcr.io/library/postgres") - .with_tag("17") + .with_tag("18") .start() .await .expect("start postgres testcontainer (is Docker running? set FORGE_TEST_DATABASE_URL to use an external Postgres)"); diff --git a/crates/db/tests/migrations.rs b/crates/db/tests/migrations.rs index f10e1300..e7c60818 100644 --- a/crates/db/tests/migrations.rs +++ b/crates/db/tests/migrations.rs @@ -781,7 +781,7 @@ async fn a_database_of_an_earlier_build_is_refused() { } } -/// A Postgres server for one test: a throwaway Postgres 17 container matching +/// A Postgres server for one test: a throwaway Postgres 18 container matching /// docker-compose (db/user/pw = forge), or, with FORGE_TEST_DATABASE_URL (no /// Docker), that server. Returns the URL of a database on it, and the /// container to keep alive. @@ -794,7 +794,7 @@ async fn server() -> (String, Option>) { .with_user("forge") .with_password("forge") .with_name("mirror.gcr.io/library/postgres") - .with_tag("17") + .with_tag("18") .start() .await .expect("failed to start postgres testcontainer (is Docker running?)"); diff --git a/crates/e2e/src/stack.rs b/crates/e2e/src/stack.rs index ef20b7ff..574838dc 100644 --- a/crates/e2e/src/stack.rs +++ b/crates/e2e/src/stack.rs @@ -54,7 +54,7 @@ async fn pg_endpoint() -> &'static str { .with_user("forge") .with_password("forge") .with_name("mirror.gcr.io/library/postgres") - .with_tag("17") + .with_tag("18") .start() .await .expect("start postgres testcontainer (is Docker running?)"); diff --git a/crates/server/tests/common/mod.rs b/crates/server/tests/common/mod.rs index cc5ed05c..06c282b7 100644 --- a/crates/server/tests/common/mod.rs +++ b/crates/server/tests/common/mod.rs @@ -54,7 +54,7 @@ async fn pg_endpoint() -> &'static str { .with_user("forge") .with_password("forge") .with_name("mirror.gcr.io/library/postgres") - .with_tag("17") + .with_tag("18") .start() .await .expect("start postgres testcontainer (is Docker running?)"); diff --git a/crates/storage/tests/common/mod.rs b/crates/storage/tests/common/mod.rs index d16dee38..83aae1ea 100644 --- a/crates/storage/tests/common/mod.rs +++ b/crates/storage/tests/common/mod.rs @@ -48,7 +48,7 @@ async fn pg_endpoint() -> &'static str { .with_user("forge") .with_password("forge") .with_name("mirror.gcr.io/library/postgres") - .with_tag("17") + .with_tag("18") .start() .await .expect("failed to start postgres testcontainer (is Docker running?)"); diff --git a/deploy/helm/forge/README.md b/deploy/helm/forge/README.md index da0e0af6..2d0280a6 100644 --- a/deploy/helm/forge/README.md +++ b/deploy/helm/forge/README.md @@ -250,7 +250,7 @@ and wired into Forge when enabled: | Dependency | Enable with | Sets | | --- | --- | --- | -| PostgreSQL 17 (StatefulSet) | `postgresql.enabled` | `FORGE_DATABASE__URL` | +| PostgreSQL 18 (StatefulSet) | `postgresql.enabled` | `FORGE_DATABASE__URL` | | Valkey, cache only (Deployment) | `valkey.enabled` | `FORGE_REDIS__URL` | | versitygw S3 gateway (StatefulSet) | `versitygw.enabled` | `s3.endpoint`, `s3.bucket`, `FORGE_S3__ACCESS_KEY` / `_SECRET_KEY` (static root credentials only: not with short-lived `s3.credentials`) | diff --git a/deploy/helm/forge/values.yaml b/deploy/helm/forge/values.yaml index 72541d50..a9794224 100644 --- a/deploy/helm/forge/values.yaml +++ b/deploy/helm/forge/values.yaml @@ -488,7 +488,7 @@ extraObjects: null # auth.password or auth.existingSecret there (and, for Valkey with # auth.password, auth.checksumSalt). -# PostgreSQL 17 StatefulSet; sets FORGE_DATABASE__URL. +# PostgreSQL 18 StatefulSet; sets FORGE_DATABASE__URL. postgresql: enabled: false diff --git a/deploy/kubernetes/README.md b/deploy/kubernetes/README.md index e74a0278..2b743361 100644 --- a/deploy/kubernetes/README.md +++ b/deploy/kubernetes/README.md @@ -17,7 +17,7 @@ deploy/kubernetes/ │ ├── high-availability/ two replicas, spread over nodes, PodDisruptionBudget │ ├── local-cache/ local blob cache on an emptyDir (faster downloads, serves through an S3 outage) │ ├── local-cache-persistent/ the same on a PersistentVolume (one replica, survives restarts) -│ ├── postgres/ in-cluster PostgreSQL 17 (evaluation / small installs) +│ ├── postgres/ in-cluster PostgreSQL 18 (evaluation / small installs) │ ├── s3-versitygw/ in-cluster S3 gateway on a PersistentVolume (evaluation / small installs) │ ├── s3-workload-identity/ short-lived S3 credentials from a projected service-account token (no S3 keys) │ ├── opentelemetry/ metrics, traces and logs over OTLP to an OpenTelemetry Collector diff --git a/deploy/kubernetes/components/postgres/kustomization.yaml b/deploy/kubernetes/components/postgres/kustomization.yaml index d7845b9e..c59b83fa 100644 --- a/deploy/kubernetes/components/postgres/kustomization.yaml +++ b/deploy/kubernetes/components/postgres/kustomization.yaml @@ -1,4 +1,4 @@ -# A single-instance PostgreSQL 17 StatefulSet for evaluation and small +# A single-instance PostgreSQL 18 StatefulSet for evaluation and small # installations, wired into Forge. It has no backups, replication or # failover: for production use a managed PostgreSQL or an operator # (CloudNativePG, …) and set FORGE_DATABASE__URL in the forge-env Secret diff --git a/docker-compose.yml b/docker-compose.yml index ad98f9c6..097993a1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,9 @@ name: forge-dev services: - # PostgreSQL 17 — metadata store + # PostgreSQL 18 — metadata store db: - image: mirror.gcr.io/library/postgres:17 + image: mirror.gcr.io/library/postgres:18 environment: POSTGRES_DB: forge POSTGRES_USER: forge @@ -13,7 +13,10 @@ services: # already be listening on the host's default 5432. forge.toml points here. - "5433:5432" volumes: - - db_data:/var/lib/postgresql/data + # The 18 image keeps its cluster in /var/lib/postgresql/18/docker and + # refuses to start with a volume on /var/lib/postgresql/data. The volume + # of the earlier PostgreSQL 17 (db_data) is not used. + - postgresql:/var/lib/postgresql healthcheck: test: ["CMD-SHELL", "pg_isready -U forge -d forge"] interval: 5s @@ -54,5 +57,5 @@ services: retries: 10 volumes: - db_data: + postgresql: s3_data: diff --git a/docs/changes/2.2.0/637.upgrade.md b/docs/changes/2.2.0/637.upgrade.md new file mode 100644 index 00000000..978ba84e --- /dev/null +++ b/docs/changes/2.2.0/637.upgrade.md @@ -0,0 +1,5 @@ +Forge requires PostgreSQL 18 or newer; it asked for 15 or newer before. On +PostgreSQL 15 to 17, upgrade PostgreSQL first (`pg_upgrade`, or a dump and +restore), then Forge: Forge does not check the server's version, and on an +older server a query that uses what 18 added fails. The in-cluster PostgreSQL +of the Helm chart and of the kustomize component runs 18 already. diff --git a/docs/deployment.md b/docs/deployment.md index 0d9197e5..210c1260 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -9,7 +9,7 @@ CI publishing without stored API keys see | Dependency | Notes | | --- | --- | -| PostgreSQL 15+ (17 tested) | The authority for all metadata, sessions, RBAC and coordination. Migrations run automatically at startup. From PostgreSQL 15 on, `public` no longer grants CREATE to every role, so the role in `FORGE_DATABASE__URL` must own the database (`CREATE DATABASE forge OWNER forge`) or hold CREATE on its schema. | +| PostgreSQL 18+ | The authority for all metadata, sessions, RBAC and coordination. Migrations run automatically at startup. `public` grants CREATE to the database's owner only, so the role in `FORGE_DATABASE__URL` must own the database (`CREATE DATABASE forge OWNER forge`) or hold CREATE on its schema. | | S3-compatible object store | Package blobs (content-addressed). AWS S3, MinIO, Ceph RGW, versitygw, … A missing bucket is created in the configured region; see [S3 permissions](#s3-permissions). Static keys or [short-lived credentials](#short-lived-s3-credentials) (IRSA, EKS Pod Identity, STS web identity). | | TLS termination | A reverse proxy or load balancer in front of Forge (nginx, Traefik, an ingress, an ALB). Forge itself speaks plain HTTP. | | Valkey or Redis 6+ (optional) | Shared L2 cache and cross-instance invalidation. Recommended as soon as you run more than one instance. TLS via `rediss://`. The Helm chart (`valkey.enabled`) and the kustomize component `valkey` can run a Valkey in the cluster. | diff --git a/web/e2e/docker-compose.e2e.yml b/web/e2e/docker-compose.e2e.yml index a31336a0..deff0bd7 100644 --- a/web/e2e/docker-compose.e2e.yml +++ b/web/e2e/docker-compose.e2e.yml @@ -6,11 +6,11 @@ # harness.ts) so running e2e never disturbs a dev stack. # # Mirrors the Rust testcontainers harness (crates/e2e/src/stack.rs): the same -# Postgres 17 and versity/versitygw:v1.4.1 images (through mirror.gcr.io), the +# Postgres 18 and versity/versitygw:v1.4.1 images (through mirror.gcr.io), the # same credentials shape. services: db: - image: mirror.gcr.io/library/postgres:17 + image: mirror.gcr.io/library/postgres:18 environment: POSTGRES_DB: forge POSTGRES_USER: forge diff --git a/web/e2e/harness.ts b/web/e2e/harness.ts index 0cde94d9..9a732305 100644 --- a/web/e2e/harness.ts +++ b/web/e2e/harness.ts @@ -4,7 +4,7 @@ * bootstrap-admin credentials to the Playwright specs. * * This deliberately drives the **actual Rust server binary** against the same - * container images the Rust testcontainers harness uses (Postgres 17 + + * container images the Rust testcontainers harness uses (Postgres 18 + * versity/versitygw:v1.4.1), proving the single-origin serving model end to end: * the browser hits `/`, `/admin/*`, `/browse/*`, `/repository/*` on one origin with no * Node proxy.