Skip to content

Cut over to the framework-mode frontend (v9.0.0) - #12340

Draft
nbudin wants to merge 6 commits into
mainfrom
12260-cutover
Draft

nbudin wants to merge 6 commits into
mainfrom
12260-cutover

Conversation

@nbudin

@nbudin nbudin commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Part of #12260 (the cutover half of the plan). Draft, not to be merged until there is a safe window to release it: CI only builds images for pushes to main, so merging this stops main from being releasable as 8.x. Merge it when the next release is v9.0.0. Needs the major label (done).

What changes

The Rails image carries the frontend.

  • Dockerfile has a frontend stage (yarn build:framework) and the Rails image keeps the pristine build in build/framework/client.
  • bin/entrypoint.sh runs bin/with_static_shell (inside chamber exec, since ASSETS_HOST comes from there). It copies the build, substitutes ASSETS_HOST (or same-origin for none), puts the hashed files in public/assets and sets STATIC_SHELL_DIR. Rails then serves the whole frontend itself, so nginx or a CDN in front is optional.
  • Rails serves the assets with Access-Control-Allow-Origin: * and the 180-day cache it already used for files in public. The middleware that kept the unhashed data-mode entry files uncached (Intercode::DisableCachingForSpecificAssets, config/nocache-files.json) is removed with the build that needed it.

The assets image is intercode-assets, built from the same stage.

  • It is the assets target of the main Dockerfile (nginx, same front server config as before; docker-assets-framework/ is now docker-assets/). Dockerfile.assets, Dockerfile.assets-framework, nginx-assets.conf and genNginxNocache.js are gone.
  • CI builds it on every push, on main layered on intercode-assets:latest, so the previous release's files stay for 30 days. The BUILD_FRAMEWORK gate is gone.
  • The release tags intercode and intercode-assets (one image, not two).

Buckets.

  • upload-assets-to-bucket.yml (the ASSETS_BUCKET behind ASSETS_HOST) now uploads the framework build's assets/, with ASSETS_HOST substituted, instead of packs/. It needs a new repository variable ASSETS_HOST (set it to assets.neilhosting.net before the release; the workflow fails early without it).
  • In its publish phase it marks everything under packs/ obsolete, so the bucket's lifecycle rule expires the 8.x files after the retention period. A tab left open on an 8.x release can load its chunks until then. (This is what makes "stale tabs" a non-issue for the Rails-served domains.)
  • The shell bucket workflow only follows the image rename.

The Devise pages. SessionsController, PasswordsController and RegistrationsController rendered the data-mode shell (layout: "application", which loads /packs/application.js) for sign-in, sign-up and password reset. They now call render_spa_shell, which serves the static shell when there is one. CI's authentication system tests found this: without packs/ in the image, signing in broke.

Print reports and the CMS layout preview take their stylesheet from the shell's index.html (StaticSpaShell.stylesheet_hrefs) instead of /packs/application-styles.js. They go unstyled in development until the reports move into the SPA (#12322).

Checks. The bundle size check and the GraphQL document check read the framework build. CI's system tests run once, against the static shell, as production does. CI builds both images and smoke-tests them (see below).

Docs. The deployment guide, the compose example and a note in the staged rollout guide.

What this deliberately does not do

The plan also had this PR delete AppRouter.tsx, lazyRoute, the route generator and yarn routes:check, and make yarn start run the framework dev server. I left that out and it can follow in a separate PR (or go with #12261):

  • Production doesn't need it, now that the Rails-rendered pages that used the data-mode entry serve the framework shell instead (below). CI used to build the data-mode bundle on the host and it ended up in the image, which hid how much still depended on it; it no longer builds it, so the image has no packs/.
  • Local development does: the framework dev server proxies Rails with one hard-coded Host (intercode.test), so convention subdomains wouldn't work, and the seeded OAuth redirect URIs point at the data-mode dev server (:3135). That deserves its own design, and keeping it out of this diff keeps the release-critical part small.

Also still there for #12261: cdn_spa_shell, the old application_entry_script_tag, checkAppEntrypointHeadersMatch.ts (it already does nothing in framework mode), the dev-only GraphiQL entry.

Tested

  • Built both images locally (arm64) and ran them: ASSETS_HOST substituted into the shell and public/assets (no placeholder left), with and without a host; Rails served /assets/<hash>.css with CORS and the long cache; the full deploy compose example (schema load, migrate, Rails, nginx) on those images: Rails serves the shell directly, crawlers get the Open Graph document, nginx proxies /client_configuration.
  • Running the images caught a bug (production config still required the deleted middleware, so Rails would not have booted with static files on), and CI's authentication system tests caught the Devise pages (above). CI now also runs a container with RAILS_SERVE_STATIC_FILES and fetches an asset, and tests the Devise pages with and without a static shell. All of CI is green.
  • ts:check (typegen couldn't run in my environment, so it used the existing generated types), the new Ruby tests, check-graphql-documents and the bundle size check (348 KB against the 700 KB budget) on the framework build.

Not tested

  • The release workflow changes (they only run on a release): the assets bucket upload with the substituted host, and the packs/ marking. The packs/ marking changes real objects in the production bucket on the first release.
  • Anything on Fly itself: with_static_shell under chamber exec with the real ASSETS_HOST. That is what the canary is for.

Canary (temporary fly.canary.toml, delete before merging)

A second Fly app running this branch's web process against production, for w.interconlarp.org.

  1. IAM, one time: the intercode-chamber role only trusts new-england-interactive-literature:intercode:* (the app name is part of the OIDC subject), so the canary can't read its secrets until new-england-interactive-literature:intercode-canary:* is added to the trust policy's sub condition.
  2. The canary gets ASSETS_HOST=assets.neilhosting.net from chamber, so its shell points at the assets bucket: upload this branch's assets/ there first (additive; it touches nothing under packs/). Build with the same arguments as the Fly build (fly.canary.toml has them) so the hashed names match, then verify (below).
  3. fly apps create intercode-canary, then fly deploy -c fly.canary.toml --remote-only.
  4. Add the cert (fly certs add w.interconlarp.org -a intercode-canary), lower the DNS TTL beforehand, and point w at the canary. Roll back by pointing it back.
  5. Verify every /assets/ file the canary's shell names exists on the assets host, then watch Sentry, Rollbar and the logs.

Rebase this branch on the released main before deploying the canary, so it never runs ahead of the production schema.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UKBUsy46smVMUGFsNFhy1B

@nbudin nbudin added enhancement major Bump the major version number labels Oct 31, 2026
nbudin and others added 6 commits October 31, 2026 13:46
The Dockerfile builds the frontend in its own stage and the Rails image keeps the pristine build. bin/with_static_shell,
which the entrypoint now runs (inside chamber, because ASSETS_HOST comes from there), makes the copy Rails serves with
ASSETS_HOST substituted in, puts the hashed files in public/assets, and sets STATIC_SHELL_DIR. Rails can then serve the
whole frontend without nginx or a CDN in front of it.

The middleware that kept the unhashed data-mode entry files uncached, and the list it read, go with the build that
needed them.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…12260)

The nginx image is now the assets stage of the main Dockerfile, built from the same frontend stage as the Rails image so
the two always carry the same build, and it replaces the data-mode assets image under the same name. CI builds it on
every push (on main it layers on the last release's image, so the previous release's files stay available for the
retention period), smoke-tests both images with and without ASSETS_HOST, and checks that Rails serves the hashed files.

The release tags one image instead of two. The assets bucket workflow now uploads the framework build's assets/ (with
ASSETS_HOST substituted in) instead of packs/, and, in its publish phase, marks the files under packs/ obsolete so that
tabs open on an 8.x release can load them until the bucket's retention runs out. The bundle size and GraphQL document
checks read the framework build, and CI keeps checking the generated routes in the TypeScript job.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…12260)

The printable reports and the CMS layout preview loaded /packs/application-styles.js, which only the data-mode build
makes. They now link the stylesheets the shell's index.html links, which StaticSpaShell reads. Without a shell (in
development) they go unstyled, until the reports move into the SPA (#12322).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The deployment guide now says that Rails serves the frontend itself and nginx is optional, that intercode-assets is the
new name of the framework-mode image (and what it was before 9.0.0), and that both containers use ASSETS_HOST to build
what they serve. The staged rollout guide gets a note on what 9.0.0 changes for it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Temporary: delete this file before merging the branch.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…part of #12260)

SessionsController, PasswordsController and RegistrationsController rendered the data-mode shell for the sign-in,
sign-up and password pages, which loads packs/application.js. CI used to hide that by also building the data-mode
bundle into the image; with that gone, sign-in broke. They now serve the static shell when there is one (render_spa_shell).

The upload script tests drop the deleted packs script and cover UPLOAD_INDEX=false and marking packs/ obsolete, and the
CI check of Rails serving assets waits for the container to make public/assets before it looks.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UKBUsy46smVMUGFsNFhy1B
@github-actions

Copy link
Copy Markdown
Contributor

Code Coverage Report: Only Changed Files listed

Package Base Coverage New Coverage Difference
app/controllers/application_controller.rb 🟢 82.05% 🟢 82.79% 🟢 0.74%
app/controllers/registrations_controller.rb 🟢 85% 🟢 90% 🟢 5%
app/javascript/setupI18Next.ts 🟢 82.76% 🟢 89.66% 🟢 6.9%
Overall Coverage 🟢 67.34% 🟢 67.4% 🟢 0.06%

Minimum allowed coverage is 0%, this run produced 67.4%

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement major Bump the major version number

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant