docs(11.2-02): complete the summercms.io app, site plugin and deploy plan

This commit is contained in:
Jakub Zych
2026-10-01 16:24:01 +02:00
parent a494375db7
commit 29231efab2
2 changed files with 350 additions and 0 deletions

View File

@@ -0,0 +1,337 @@
---
phase: 11.2-ready-to-share-summercms-io-website-and-newsletter-plugin
plan: 02
subsystem: infra
tags: [go-embed, static-site, nuxt, docsite, nginx, supervisor, postgres15, release]
requires:
- phase: 11.2-01
provides: vue-summercmsio-app (Nuxt landing page, terminal.json single command source, en.json copy)
- phase: 11.1
provides: summer docs:build static docs site and the docs checker
provides:
- sm-summercmsio-plugin (golem15.summercms) serving the embedded Nuxt build at / and the docs at /docs
- sm-summercmsio-app with go.work workspace, config, build (release/dev), smoke and check-deploy scripts
- framework docs say PostgreSQL 15 or newer (verified on postgres:15)
- docsite site_url/site_label keys and --site-url/--site-label flags (header link back to the main site)
- link, terminal-command, docs-header and external-link checks against the built site
- DEPLOY.md with nginx, supervisor, env template and rollback copy for rome
affects: [11.2-03, cutover, v0.1.0 tag]
actuals:
tokens: 24561
tasks: 5
commits: 2
commits_all_repos: 8
plan_head_before: 130b6ac38f9ae8feaf5e42c107a47febb804ff48
plan_head_after: a494375db74c27683c0a652e1f96556568893710
tech-stack:
added: []
patterns:
- "Site plugin embeds build output with //go:embed all:public and serves in-memory trees with strong ETags and per-path Cache-Control"
- "Release builds compile the framework from a tag export through a temporary go.work replace"
- "Env-gated build-dependent tests (SUMMERCMS_REQUIRE_BUILD, SUMMERCMS_TERMINAL_CHECK, SUMMERCMS_CHECK_EXTERNAL)"
key-files:
created:
- ../sm-summercmsio-app/plugins/golem15/summercms/plugin.go
- ../sm-summercmsio-app/plugins/golem15/summercms/static.go
- ../sm-summercmsio-app/plugins/golem15/summercms/links_test.go
- ../sm-summercmsio-app/plugins/golem15/summercms/README.md
- ../sm-summercmsio-app/scripts/build.sh
- ../sm-summercmsio-app/scripts/smoke.sh
- ../sm-summercmsio-app/scripts/check-deploy.sh
- ../sm-summercmsio-app/terminal_check_test.go
- ../sm-summercmsio-app/DEPLOY.md
- ../sm-summercmsio-app/README.md
- ../sm-summercmsio-app/deploy/nginx/summercms.io.conf
- ../sm-summercmsio-app/deploy/supervisor/summercms-io.conf
- ../sm-summercmsio-app/deploy/env.example
- ../sm-summercmsio-app/deploy/rollback/under-construction/index.html
- ../sm-summercmsio-app/deploy/rollback/under-construction/logo.png
modified:
- README.md
- docs/setup/installation.md
- internal/docsite/load.go
- internal/docsite/docsite.go
- internal/docsite/emit.go
- internal/docsite/theme/templates/header.html
- internal/docsite/theme/assets/site.css
- cmd/summer/docs.go
- docs/console/utilities.md
key-decisions:
- "D-42 resolved as defer-tag: no v0.1.0 tag created or pushed in summercms.go; the release path was proven against a scratch clone with a local tag, and creating + pushing v0.1.0 (and pushing master) is a DEPLOY.md launch step"
- "Terminal check clone override appends the page's clone directory (git clone <override> summercms) so the page's own cd summercms works for a local path override"
- "check-deploy.sh moves listen ports to unprivileged loopback ports in its temp copy because nginx -t binds the listeners"
- "DEPLOY.md installs the supervisor program on the first release (after the binary exists) and uploads with --rsync-path=\"sudo rsync\" into root-owned bin/ and config/"
patterns-established:
- "Build-dependent plugin tests skip unless SUMMERCMS_REQUIRE_BUILD=1, and fail (not skip) on a missing build when it is set"
- "Landing links are resolved through the real surf-assembled handler with at most one root-relative 301"
requirements-completed: []
coverage:
- id: D1
description: "One summercms-io binary embeds the Nuxt site and the docs and serves / and /docs with indexable responses, cache rules, ETag/304, 301s, real 404s, no /backend and 405 on POST, booted on postgres:15"
verification:
- kind: e2e
ref: "scripts/smoke.sh (release binary, postgres:15) -> smoke: ok"
status: pass
- kind: unit
ref: "plugins/golem15/summercms/smoke_test.go#TestStaticSmoke"
status: pass
human_judgment: false
- id: D2
description: "Framework database suites pass on postgres:15 and README/installation docs say PostgreSQL 15 or newer (D-25)"
verification:
- kind: integration
ref: "HEAD export with postgres:15: go test -count=1 -p 4 lagoon, lagoon/attach, cabana, beachcomber, lighthouse, bouncer, conga, docs/examples/blog"
status: pass
human_judgment: false
- id: D3
description: "docsite site_url/site_label keys and --site-url/--site-label flags add a validated header link back to the main site; unset output unchanged apart from additive CSS (D-41/D-46)"
verification:
- kind: unit
ref: "go test ./internal/docsite ./cmd/summer -run '^(TestDocsTree|TestDocsBuildRealTree|TestToolCommandNames|TestParseSite|TestSiteLink|TestDocsBuildSiteFlags)$'"
status: pass
- kind: other
ref: "scripts/check-phase11.1.sh --docs && --forbidden"
status: pass
human_judgment: false
- id: D4
description: "Release build takes docs, summer CLI and compiled framework from the v0.1.0 tag (proven on a scratch clone with a local tag; real tag deferred)"
verification:
- kind: e2e
ref: "SUMMERCMS_FRAMEWORK=<scratch clone> scripts/build.sh -> build: bin/summercms-io (release v0.1.0) ready; go version -m shows summercms v0.1.0 => <tmp>/fw"
status: pass
human_judgment: false
- id: D5
description: "Every landing link resolves against the built site, the page shows every terminal command and comment, and the docs header links back to / (SC3, D-40, D-44, D-46)"
verification:
- kind: integration
ref: "SUMMERCMS_REQUIRE_BUILD=1 plugins/golem15/summercms#TestLandingLinks,TestTerminalCommandsInPage,TestDocsHeaderSiteLink"
status: pass
- kind: e2e
ref: "SUMMERCMS_TERMINAL_CHECK=1 SUMMERCMS_CLONE_URL=../summercms.go go test -run TestTerminalCommands . (handled=true)"
status: pass
human_judgment: false
- id: D6
description: "DEPLOY.md, nginx and supervisor configs, env template and rollback copy cover launch, setup, build, upload, release, cutover, verification and rollback on rome (SC4)"
verification:
- kind: other
ref: "scripts/check-deploy.sh -> check-deploy: ok (nginx -t + supervisor parse)"
status: pass
human_judgment: true
rationale: "Bringing the site up on rome by following DEPLOY.md (and the external link / verbatim terminal checks, which need the public repository) is the cutover UAT item; it cannot run before the user pushes the repos, makes golem15/summercms public and creates the tag."
duration: 22min
completed: 2026-10-01
status: complete
---
# Phase 11.2 Plan 02: summercms.io app, site plugin, release path and deploy Summary
**One `summercms-io` binary that embeds the Nuxt landing page and the SummerCMS docs. It serves them on PostgreSQL 15 with indexable, cache-correct responses. The framework gained PG15 docs and a docs header link back to the site. The release build is proven from a (scratch) v0.1.0 tag, every landing link and terminal command is checked against the built site, and DEPLOY.md covers rome.**
## Performance
- **Duration:** 22 min (two executor sessions: tasks 1-2, then the tag checkpoint, then tasks 4-5)
- **Started:** 2026-10-01T14:00:22Z
- **Completed:** 2026-10-01T14:22:03Z
- **Tasks:** 5 (4 auto/tracer, 1 decision checkpoint)
- **Files modified:** 10 in summercms.go, 26 in sm-summercmsio-app plus plugin (excluding generated build output)
## Accomplishments
- **Site plugin** `golem15.summercms` (module `git.golem15.com/golem15/sm-summercmsio-plugin`). It embeds `public/` with `all:` and serves `/` and `/docs` from in-memory trees with these behaviours:
- strong ETags with 304 revalidation;
- `immutable` caching for `_nuxt/` (except `builds/`) and `_fonts/`, and `no-cache` for everything else;
- a 301 from extension-less docs URLs to `.html`;
- the tree's own 404 page, and dot-segment paths refused;
- no robots-blocking header, no CSP and no admin.
- **App** `sm-summercmsio-app` is a go.work workspace with the plugin and site as submodules, config without secrets, `summer build` generated sources, and these scripts:
- `scripts/build.sh`, with release (default, from the tag) and dev modes;
- `scripts/smoke.sh`, which runs postgres:15, migrate, serve and HTTP assertions, including the D-46 docs header link;
- `scripts/check-deploy.sh`.
- **Framework (summercms.go):**
- The PG15 suites pass, and README.md and docs/setup/installation.md say "PostgreSQL 15 or newer".
- `docs/site.yaml` accepts the `site_url` and `site_label` keys, and `docs:build` and `docs:serve` take `--site-url` and `--site-label`. Both are validated: `javascript:` and `//host` are rejected.
- **Checks:**
- `TestLandingLinks` checks 30 distinct links through the real `surf.Assemble` handler, including all ten D-44 docs targets and `/docs`.
- `TestTerminalCommandsInPage` is the D-40 drift guard.
- `TestDocsHeaderSiteLink` checks the docs header link back to `/`.
- `TestExternalLinks` runs at cutover.
- `TestTerminalCommands` runs the six page commands from a fresh shell to `handled=true`.
- **DEPLOY.md** covers the launch checklist, server setup, build, upload, release, cutover, verification and rollback. It comes with the nginx config (HTTP→HTTPS, a 443 www→apex block, gzip, `/backend` denied, GET/HEAD only, proxy to 127.0.0.1:8095), the supervisor program, an env template and a copy of the live "Under construction" page.
## Task Commits
| Task | Commit | Repo | Message |
|------|--------|------|---------|
| 1 (tracer) | `0ab96ed` | sm-summercmsio-plugin | feat: serve the embedded site at / and the docs at /docs |
| 1 (tracer) | `e0b9b7c` | sm-summercmsio-app | feat: wire the summercms.io app with build and smoke scripts |
| 2A | `7936234` | summercms.go | docs: require PostgreSQL 15 or newer (verified on postgres:15) |
| 2B | `a494375` | summercms.go | feat(docsite): optional site_url and site_label link back to the main site |
| 3 | — | — | decision checkpoint, resolved **defer-tag** (no commit) |
| 4 | `7774763` | sm-summercmsio-plugin | test: verify the landing links and terminal commands against the built site |
| 4 | `699785a` | sm-summercmsio-app | feat: release builds from the v0.1.0 tag and the terminal command check |
| 5 | `fe77fa7` | sm-summercmsio-plugin | docs: describe the site plugin |
| 5 | `77eef5d` | sm-summercmsio-app | docs: add DEPLOY.md with nginx, supervisor and rollback configs |
Each app commit carries the updated plugin gitlink. Nothing was pushed anywhere, and no commit has a co-author trailer.
## D-42 tag decision: defer-tag
- **What the user chose:** "defer-tag" at the blocking-human checkpoint (Task 3).
- **summercms.go is untouched:** `git tag -l v0.1.0` in summercms.go prints nothing, and nothing was pushed.
- **How the release path was proven:** against the scratch clone `/tmp/claude-1000/-media-nvme-dev-golem15-summercms-io-summercms-summercms-go/b0d2a3f5-592c-4ca7-8d0c-8f89dbb2cfac/scratchpad/fw-release`. That clone is a `git clone` of the local summercms.go with a local annotated `v0.1.0` tag at `a494375` (the Task 2 docsite commit). The build ran as `SUMMERCMS_FRAMEWORK=<clone> scripts/build.sh`.
- **Release build output:** `build: bin/summercms-io (release v0.1.0) ready`. `go version -m bin/summercms-io` shows `dep git.golem15.com/golem15/summercms v0.1.0 => /tmp/…/fw (devel)`, so the compiled framework is the tag export.
- **Without the tag:** plain `scripts/build.sh` against the real checkout stops with `build: tag v0.1.0 not found in …/summercms.go; run 'scripts/build.sh dev' or create the tag (DEPLOY.md)`.
- **Launch checklist step 3 in DEPLOY.md:**
1. Review the commit.
2. `git tag -a v0.1.0 -m "SummerCMS Alpha 0.1" <reviewed-sha>`.
3. `git push origin master`.
4. `git push origin v0.1.0`.
5. Confirm with `git ls-remote --tags origin v0.1.0`.
## Verification Evidence
**D-25, PostgreSQL 15 (previous session).**
- Setup: HEAD export, with `postgres:16-alpine` replaced by `postgres:15` in 9 test files, run as `go test -count=1 -p 4 -v`.
- Result: EXIT=0, and all 11 containers were postgres:15 (log: `scratchpad/pg15.log`).
- Packages, every line `ok`, no `FAIL`:
- modules: `lagoon` 53.2s, `lagoon/attach` 66.9s, `cabana` 65.6s, `beachcomber` 44.4s, `beachcomber/typesense` 0.2s, `lighthouse` 22.2s, `lighthouse/centrifugo` 0.1s, `bouncer` 8.0s, `conga` 19.7s;
- docs/examples: `blog` 5.5s, `blog/console`, `blog/controllers`, `blog/models`, `blog/updates`.
**Task 1 tracer gate (previous session).** `scripts/build.sh dev` and `scripts/smoke.sh` ("smoke: ok") passed, `go vet` was clean in the app and the plugin, and `TestStaticSmoke` passed. All Task 1 acceptance criteria passed.
**Task 2.**
- `go vet ./...` is clean.
- The six named tests pass: TestDocsTree, TestDocsBuildRealTree, TestToolCommandNames, TestParseSite, TestSiteLink and TestDocsBuildSiteFlags.
- `scripts/check-phase11.1.sh --docs` and `--forbidden` pass.
- Re-run at the end of this session: `go vet ./... && go test ./internal/docsite ./cmd/summer -count=1` ok, and both phase11.1 checks passed.
**Task 4.**
- Release build: see the D-42 section above.
- `scripts/smoke.sh` → `smoke: ok`, with the new D-46 `/docs/` site-link assertions.
- `SUMMERCMS_REQUIRE_BUILD=1 go test ./...` in the plugin: PASS for TestLandingLinks, TestTerminalCommandsInPage, TestDocsHeaderSiteLink and TestStaticSmoke; TestExternalLinks SKIP (it is gated).
- `SUMMERCMS_TERMINAL_CHECK=1 SUMMERCMS_CLONE_URL=…/summercms.go go test -run TestTerminalCommands`: PASS. The output included `built hello in 1.93s`, `golem15.greeter active` and `… handled=true`.
- `grep -c 'class="site-link" href="/"' public/docs/index.html` = 1.
- `go vet ./...` and `go test ./... -count=1` pass in the app (gated tests skip).
- `file bin/summercms-io`: ELF 64-bit x86-64, statically linked.
**Task 5.**
- `scripts/check-deploy.sh` → `check-deploy: ok`. It runs `nginx -t` on nginx 1.30.4 with a temp self-signed certificate, temp DH parameters and loopback ports. The only output is deprecation warnings for `listen … http2`, which is kept for rome's assumed nginx 1.22, per A3.
- All acceptance greps match. `git ls-files` lists no `.env`.
## Files Created/Modified
**summercms.go (Task 2, previous session)**
- `README.md` and `docs/setup/installation.md`: PostgreSQL 15 or newer.
- `internal/docsite/{load,docsite,emit}.go`, `theme/templates/header.html`, `theme/assets/site.css`: the `site_url`/`site_label` link.
- `cmd/summer/docs.go`: the flags. `docs/console/utilities.md`: the docs, with a neutral `acme.example` example.
- Tests in `load_test.go`, `theme_test.go` and `cmd/summer/docs_test.go`.
**sm-summercmsio-plugin**
- `plugin.go`, `static.go`, `smoke_test.go`, `go.mod`/`go.sum`, `.gitignore`, `public/README.md` (Task 1).
- `links_test.go` (Task 4) and `README.md` (Task 5).
**sm-summercmsio-app**
- Workspace, config, generated sources and submodules (Task 1).
- `scripts/build.sh`: release mode. `scripts/smoke.sh`: D-46 assertion. `terminal_check_test.go` (Task 4).
- `DEPLOY.md`, `README.md`, `deploy/**`, `scripts/check-deploy.sh` (Task 5).
## Decisions Made
- **D-42: defer-tag (user decision).** See the section above.
- **Clone override in `terminalScript`.** A non-empty `SUMMERCMS_CLONE_URL` becomes `git clone <override> summercms`, with the directory taken from `path.Base` of the page URL. The page's next command, `cd summercms`, then works for a local override such as `…/summercms.go`. With no override, the commands run verbatim.
- **`check-deploy.sh` test copy.** It rewrites the listen ports to `127.0.0.1:18480` / `[::1]:18480` and `127.0.0.1:18443` / `[::1]:18443` (overridable), because `nginx -t` binds the listeners and a non-root check cannot bind 80 or 443. The committed config keeps 80 and 443.
- **DEPLOY.md choices:**
- `bin/` and `config/` are root-owned, so the service cannot rewrite its own binary; `storage/` is owned by summercms.
- Uploads use `--rsync-path="sudo rsync"`.
- The supervisor program is installed on the first release, because autostart before the binary exists would put the program into FATAL.
- The key comes from `./bin/summercms-io key:generate`, which prints `✓ <key>`.
- **`TestLandingLinks` scope.** It also checks `src` links, `_payload.json?<uuid>` and the og:image (`https://summercms.io/og-image.png` mapped to `/og-image.png`), not only hrefs.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Bug] The terminal check's clone override changed the clone directory**
- **Found during:** Task 4.
- **Issue:** `git clone /…/summercms.go` clones into `summercms.go/`, so the page's next command, `cd summercms`, failed (`cd: summercms: No such file or directory`).
- **Fix:** with an override, `terminalScript` passes the page URL's directory name explicitly (`git clone <override> summercms`). The verbatim run (no override) is unchanged.
- **Files modified:** `sm-summercmsio-app/terminal_check_test.go`.
- **Verification:** TestTerminalCommands PASS with handled=true.
- **Committed in:** `699785a`.
**2. [Rule 3 - Blocking] `nginx -t` binds the listen ports**
- **Found during:** Task 5.
- **Issue:** As a normal user, `nginx -t` failed with `bind() to 0.0.0.0:80 failed (13: Permission denied)`.
- **Fix:** `check-deploy.sh` rewrites the listen directives in its temp copy to unprivileged loopback ports, and fails if any 80/443 listen remains unreplaced. The DH parameters are generated with `openssl dhparam -dsaparam` instead of dropping the `ssl_dhparam` line.
- **Files modified:** `sm-summercmsio-app/scripts/check-deploy.sh`.
- **Verification:** `check-deploy: ok`. Earlier, a failing run showed the script surfaces nginx errors.
- **Committed in:** `77eef5d`.
**3. [Rule 1 - Bug, previous session] `checkSiteURL` also rejects backslashes**
- **Issue:** browsers treat `/\host` as `//host`, which would make a protocol-relative link to another host.
- **Fix:** backslashes are rejected, and `--site-label` is validated as one non-empty line. The override logic lives in `load()` in `load.go`.
- **Committed in:** `a494375`.
**4. [Rule 3 - Blocking, previous session] `summer plugin:add` wrote an absolute path into the `go.work` `use` list**
- **Fix:** rewritten to `./plugins/golem15/summercms` with `go work edit`. `go mod tidy` before the first `summer build` dropped the requires: they were restored, the build ran, the module was tidied, and `toolchain go1.27.0` was re-added.
- **Follow-up:** the framework quirks are logged in `deferred-items.md`.
- **Committed in:** `e0b9b7c`.
**5. [Acceptance-check note, previous session] Task 2 byte-identical check**
- **Issue:** a literal `diff -r pre post` cannot be empty, because the content of `utilities.md` changed and the `.site-link` rules live in the shared `assets/site.css`.
- **Isolated check:** the pre-change docs source was built with the new binary. Only `assets/site.css` differs, by 17 additive lines. All 72 HTML pages, the `.md` copies, `llms*.txt` and `search-index.json` are byte-identical. An unset build has 0 `site-link` in `index.html` and `404.html`.
**6. [Acceptance-check note] The `location ^~ /backend` grep**
- **Issue:** the plan's literal `grep -n 'location ^~ /backend'` cannot match, because GNU BRE treats the mid-pattern `^` as an anchor.
- **Check:** `grep -nF 'location ^~ /backend'` matches line 50 of `deploy/nginx/summercms.io.conf`.
---
**Total deviations:** 4 auto-fixed (2 bugs, 2 blocking) and 2 acceptance-check notes.
**Impact on plan:** All fixes were needed for the checks to run as specified. There was no scope creep, and no new Go module or npm dependency.
## Issues Encountered
- **HEAD on `master`.** The executor's protected-branch assertion reports `master` as protected. This project commits directly on master (branching_strategy `none`, use_worktrees `false`), and the sequential orchestrator dispatch said to use normal commits on the main working tree, as the previous session did for `7936234` and `a494375`. Commits were made on master, and none were pushed.
- **Generated build output.** `bin/summercms-io` and `plugins/golem15/summercms/public/{site,docs}` now hold a release build from the scratch tag. These paths are gitignored.
## Known Stubs
None. The gated tests (`TestExternalLinks`, and `TestTerminalCommands` without an override) are designed to run at cutover, not stubs.
## Threat Flags
None beyond the plan's threat model. T-11.2-04/05/06/07/08/09/11/12/14 are mitigated as planned. T-11.2-13 (the tag) is deferred to the user at cutover.
## User Setup Required
These are the cutover steps from the DEPLOY.md launch checklist, all for the user:
1. Push `sm-summercmsio-plugin`, `vue-summercmsio-app`, then `sm-summercmsio-app`. The origins are already set.
2. Make `golem15/summercms` public (D-38).
3. Create `v0.1.0` of `git.golem15.com/golem15/summercms` at the reviewed commit, push it, and push summercms.go `master` (D-42, deferred).
4. Run `TestExternalLinks` and the verbatim `TestTerminalCommands`.
5. On rome: check that port 8095 is free, do the one-time setup, save the existing summercms.io server block into `deploy/rollback/nginx-under-construction.conf`, then deploy and cut over.
## Next Phase Readiness
- Plan 11.2-03 (unit test coverage) can build on the stable interfaces: `Plugin`, `newHandlers`, `tree`, `newTree`, `contentType`, `siteImmutable`, `redirectTo`, `pageLinks`, `resolve`, `requireBuild`, `terminalScript`, `terminalEnv`, `loadTerminal`, `checkSiteURL` and `siteLabel`.
- The cutover is blocked only on the user steps above.
## Self-Check: PASSED
- Files exist: plugin.go, static.go, links_test.go and README.md in the plugin; build.sh, smoke.sh, check-deploy.sh, terminal_check_test.go, DEPLOY.md, README.md, the deploy/nginx and deploy/supervisor configs, deploy/env.example and deploy/rollback/under-construction/{index.html,logo.png} in the app.
- Commits found: `0ab96ed`, `7774763` and `fe77fa7` (plugin); `e0b9b7c`, `699785a` and `77eef5d` (app); `7936234` and `a494375` (summercms.go).
- No `v0.1.0` tag in summercms.go, and nothing pushed.
---
*Phase: 11.2-ready-to-share-summercms-io-website-and-newsletter-plugin*
*Completed: 2026-10-01*

View File

@@ -0,0 +1,13 @@
## Deferred Items
- `summer plugin:add` writes an absolute path into the app's `go.work` `use` list
status: open
**Found during:** 11.2-02 Task 1 (wiring sm-summercmsio-app).
**What:** `ensureGoWork` in `internal/build/scaffold.go` adds the plugin directory to `go.work` as an absolute path instead of the relative `./plugins/<vendor>/<name>`, so a committed `go.work` only works on the machine that ran the command. The app's `go.work` was corrected by hand with `go work edit` to `./plugins/golem15/summercms`.
**Why deferred:** framework behaviour outside this plan's scope (the plan changes only docsite, the docs CLI flags and two docs pages in summercms.go).
- `go mod tidy` in an app whose generated sources do not exist yet drops the plugin and framework requires
status: open
**Found during:** 11.2-02 Task 1.
**What:** running `go mod tidy` before the first `summer build` (no `main.go` / `plugins.gen.go` yet, so no Go sources import the plugin) removes the `require` lines that `summer plugin:add` added, and the `toolchain` line. Workaround used: restore the requires, run `summer build`, then tidy and re-add `toolchain go1.27.0`. The scaffolding docs or `plugin:add` could warn about the order.
**Why deferred:** framework/CLI ergonomics, out of scope for the site.