Skip to content

feat: upgrade API Platform to 5.0, Mercure to 1.0 and dependencies to latest stable - #691

Open
vincentchalamon wants to merge 13 commits into
mainfrom
fix/go-toolchain
Open

vincentchalamon wants to merge 13 commits into
mainfrom
fix/go-toolchain

Conversation

@vincentchalamon

@vincentchalamon vincentchalamon commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Docker builds have been failing on every branch since Mercure 1.0.0 was released. Rather than pin around it, this upgrades API Platform to 5.0, Mercure to 1.0, and the remaining dependencies to their latest stable, majors included.

Unbreaking the build

dunglas/frankenphp:1.12.7-builder ships Go 1.26.8 and pins GOTOOLCHAIN=local, which forbids downloading a newer toolchain. Mercure Caddy 1.0.0 declares go 1.27, so xcaddy build aborted:

go: github.com/dunglas/mercure/caddy@v1.0.0 requires go >= 1.27
    (running go 1.26.8; GOTOOLCHAIN=local)

1.12.7 is the latest FrankenPHP release, so restoring the Go default is the fix. It also prevents the next module that raises its minimum from breaking the build again.

Mercure 1.0

1.0 replaces the flat publisher_jwt / subscriber_jwt directives with issuer blocks, and expects RFC 9068 access tokens: iss, aud, exp and a typ of at+jwt, verified against a declared issuer. The old directives still work, but only in a compatibility mode that, in upstream words, "drops the required exp, the audience check, the at+jwt typ check and the verified-issuer check, and re-accepts the access token in the URL query string", and additionally needs the deprecated_topic and deprecated_claim build tags. That is three switches to import a major and disable most of it, so this migrates instead.

  • Caddyfile: an issuer block with publisher and subscriber verifiers, plus a pinned resource_identifier. The API publishes through the internal MERCURE_URL while tokens are minted for the public URL, so left unpinned the hub derives a different audience per request and rejects them. The value is substituted at parse time, because the hub validates it as a URL before runtime placeholders resolve.
  • mercure.yaml: protocol_version: 1.0, which makes symfony/mercure-bundle mint RFC 9068 tokens, plus the iss, sub and client_id claims it then requires.
  • The demo directive is now playground, and its UI moved from /.well-known/mercure/ui/ to /.well-known/mercure/debug/.

Subscribers are unaffected: the PWA subscribes anonymously, with no token.

API Platform 5.0

ApiPlatform\Symfony\Bundle\Test\{ApiTestCase,Client} are deprecated in favour of ApiPlatform\Test\{ApiTestCase,Client}, now shipped by a separate api-platform/test package. The deprecated shims extend classes from that package, so without it the suite fatals rather than warns, which is also why Rector briefly proposed deleting seven setUp() methods: it could no longer resolve the parent class. Those proposals were a symptom, not a change, and are gone now that the package is installed.

Nothing else in the demo needed touching. PHPStan reports no error and no use of a deprecated API, Rector has nothing to propose, and the 27 unit tests pass with failOnDeprecation active.

Dependencies held back

Three, all blocked by the ecosystem rather than by the demo:

Dependency Held at Reason
phpunit/phpunit 12.x api-platform/test 5.0.0 supports ^11.5 || ^12.2; there is no release supporting 13
typescript 6.x typescript-eslint refuses to load under TS 7, tracking >= 7.1 in typescript-eslint/typescript-eslint#10940
eslint 9.x eslint-plugin-react 7.37.5, pulled transitively by eslint-config-next, uses the context API ESLint 10 removed and crashes on the first React file

PostgreSQL is left alone in the Helm chart as requested. external-dns and Redis are already on their latest. GitHub Actions are handled by Renovate on its monthly schedule.

Verification

Run locally before pushing:

  • FrankenPHP builder stage built, and the resulting binary inspected: all seven Caddy modules present, none silently dropped.
  • frankenphp validate against the real Caddyfile, which provisions the modules rather than only parsing: valid. The same command reproduces the exact CI failure when the issuer block is reverted.
  • Symfony container recompiles against API Platform 5.0, which also validates the new mercure.yaml.
  • phpstan analyse: no errors. rector process --dry-run: nothing to change. phpunit --testsuite Unit: 27 tests, 114 assertions, green.
  • PWA in a clean container: pnpm lint reports no error, pnpm build passes.
  • helm lint passes.

Functional and E2E suites need a database, so CI is the arbiter there.

Closes #692

The FrankenPHP builder image ships Go 1.26.8 and pins GOTOOLCHAIN=local,
which forbids downloading a newer toolchain. github.com/dunglas/mercure/caddy
v1.0.0 declares `go 1.27` in its go.mod, so xcaddy fails:

    go: github.com/dunglas/mercure/caddy@v1.0.0 requires go >= 1.27
        (running go 1.26.8; GOTOOLCHAIN=local)

This breaks every Docker build on every branch since Mercure 1.0.0 was
released. Bumping the builder image is not an option, 1.12.7 is the latest
FrankenPHP release and it is the one shipping Go 1.26.8.

Restore the Go default so the toolchain a module asks for is fetched on
demand.
@vincentchalamon vincentchalamon added the dependencies Pull requests that update a dependency file label Sep 18, 2026
@vincentchalamon vincentchalamon changed the title fix(api): let Go fetch the toolchain required by Caddy modules fix: unbreak the Docker build after Mercure 1.0.0 Sep 18, 2026
@vincentchalamon
vincentchalamon force-pushed the fix/go-toolchain branch 2 times, most recently from 2e48946 to c0ad26c Compare September 18, 2026 14:46
@vincentchalamon vincentchalamon changed the title fix: unbreak the Docker build after Mercure 1.0.0 feat: upgrade API Platform to 5.0, Mercure to 1.0 and dependencies to latest stable Sep 18, 2026
Mercure 1.0 replaces the flat publisher_jwt/subscriber_jwt directives with
issuer blocks and expects RFC 9068 access tokens: iss, aud, exp and a typ of
at+jwt, verified against a declared issuer. The old directives now only work
in a compatibility mode that drops those checks and re-accepts the token in
the query string, so migrate rather than opt into it.

- Caddyfile: declare an issuer with publisher and subscriber verifiers, and
  pin resource_identifier. The API publishes through the internal MERCURE_URL
  while tokens are minted for the public URL, so without pinning, the hub
  derives a different audience per request and rejects them. The value is
  substituted at parse time, since the hub validates it as a URL before
  runtime placeholders resolve.
- mercure.yaml: protocol_version 1.0, which makes symfony/mercure-bundle mint
  RFC 9068 tokens, plus the iss, sub and client_id claims it then requires.
- The demo directive is now playground, and its UI moved from
  /.well-known/mercure/ui/ to /.well-known/mercure/debug/.

Subscribers are unaffected: the PWA subscribes anonymously, with no token.
The test helpers moved: ApiPlatform\Symfony\Bundle\Test\{ApiTestCase,Client}
are deprecated in favour of ApiPlatform\Test\{ApiTestCase,Client}, now shipped
by a separate api-platform/test package. The deprecated shims extend classes
from that package, so without it the test suite fatals rather than warns.

That package supports phpunit ^11.5 || ^12.2, so PHPUnit moves from 13 to 12.
It is the one dependency that cannot be on its latest major while API Platform
5.0 is.

Nothing else in the demo needed changing: PHPStan reports no error and no use
of a deprecated API, and Rector has nothing to propose.
@types/node moves to 26, the rest are patch and minor bumps.

Two majors are held back, both blocked by the ecosystem rather than by the
demo:

- typescript stays on 6.x. typescript-eslint refuses to load under TS 7 and
  tracks support for >= 7.1 in typescript-eslint/typescript-eslint#10940.
- eslint stays on 9.x. eslint-plugin-react 7.37.5, pulled in transitively by
  eslint-config-next, still uses the context API that ESLint 10 removed, and
  crashes on the first React file.

Verified in a clean container: pnpm lint reports no error, pnpm build passes.
Mercure 1.0 renamed the subscribe query parameter: "topic" is rejected with

    unknown topic matcher query parameter: "topic" (use "match" or "match_urlpattern")

Publishing is unaffected, the publish endpoint still reads "topic" form fields.

This was only caught because the CI reachability check happens to subscribe.
The PWA builds its EventSource URL the same way, so real-time updates would
have shipped broken.
API Platform 5.0 no longer appends "; charset=utf-8" to JSON media types.
Per RFC 8259 and the RFC 6839 "+json" suffix, JSON defines no charset and is
always UTF-8, so sending the parameter can break strict clients; only text/*
and application/xml keep it. That accounted for all 80 functional failures.

The Mercure debugger also moved from /.well-known/mercure/ui/ to
/.well-known/mercure/debug/.
MERCURE_PUBLISHER_JWT_ALG and MERCURE_SUBSCRIBER_JWT_ALG are set nowhere in
the project. The hub defaults a raw secret to HS256, which is what
symfony/mercure-bundle signs with, so the argument only carried an empty
placeholder.
Two deliberate changes in the major:

- An unauthenticated request now reports "Access Denied." as its description
  rather than the Symfony entry point message, mirroring "detail".
- Hydra "range" is a plain IRI string for most properties and only a list for
  those carrying an owl:equivalentClass restriction, so the documentation test
  has to accept both shapes.

Four failures remain, all on the same validation message template not being
interpolated; they are tracked separately rather than aligned to the buggy
output.
Mercure 1.0 split the old "demo" directive in two. "playground" does more than
serve the debugger UI: it also redirects the site root to it.

    if m.Playground && r.URL.Path == "/" {
        http.Redirect(w, r, defaultHubURL+"/debug/", http.StatusFound)

The hub is mounted inside the API, which owns "/", so the entrypoint answered
with the debugger HTML instead of the JSON-LD entrypoint, and every client
introspecting the API broke.

"debugger" serves the same UI at /.well-known/mercure/debug/ without touching
the root.
@vincentchalamon

vincentchalamon commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor Author

Status: waiting for api-platform/core#8546

Status: waiting for api-platform/core new release

Status: waiting for api-platform/api-doc-parser#181

Status: waiting for api-platform/api-doc-parser v0.16.11

api-platform/core#8537 widened api-platform/test to
"^11.5 || ^12.2 || ^13.0", but no release carries it yet: Packagist still
serves v5.0.0 with "^11.5 || ^12.2", which caps the demo at PHPUnit 12.

Widening the constraint here changes nothing today, the lock stays on 12.5.35.
It only means the next `composer update` picks PHPUnit 13 on its own once
api-platform/test ships the change, instead of needing another manual bump.
5.0.1 carries the two fixes this branch was waiting on:

- api-platform/core#8546 resolves the "{{ type }}" placeholder, so the four
  validation failures are gone and the suite is green.
- api-platform/core#8537 widens api-platform/test to allow phpunit ^13.0. The
  constraint here was already widened in anticipation, so composer picked
  13.3.5 on its own.

Mercure 1.0.2 also landed, and it now allows only one *unnamed* hub per
configuration. SERVER_NAME holds two addresses (the public one and php:80 for
internal calls), so Caddy builds two servers and instantiates the handler
twice, which made the container fail to boot. Naming the hub is enough; the
name only feeds the health check endpoint and the metrics.
Patch bumps across the board: next 16.3.6, better-auth 1.7.6, react-query
5.103.2, @types/node 26.6.2.

The three majors stay held back, still by the ecosystem:

- eslint 9.x and @eslint/js 9.x, because eslint-plugin-react 7.37.5 (pulled
  transitively by eslint-config-next) declares eslint "^3 || ... || ^9.7".
- typescript 6.0.x, because typescript-eslint 8.70.1 declares
  typescript ">=4.8.4 <6.1.0", which rules out 7 and even 6.1.

Verified in a clean container: pnpm lint reports no error, pnpm build passes.
@vincentchalamon vincentchalamon added the deploy Deploys Pull Request label Sep 26, 2026

This branch was successfully deployed

1 active deployment
pr-691 — 0f1bc5a7 Deployed Sep 26, 2026 by vincentchalamon via Deploy / Deploy #1893
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file deploy Deploys Pull Request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrate Mercure to modern mode (issuer blocks)

1 participant