490 lines
69 KiB
Markdown
490 lines
69 KiB
Markdown
---
|
|
phase: 11.2-ready-to-share-summercms-io-website-and-newsletter-plugin
|
|
plan: 02
|
|
type: execute
|
|
wave: 2
|
|
depends_on: ["11.2-01"]
|
|
files_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
|
|
- internal/docsite/load_test.go
|
|
- internal/docsite/theme_test.go
|
|
- cmd/summer/docs.go
|
|
- cmd/summer/docs_test.go
|
|
- docs/console/utilities.md
|
|
- ../sm-summercmsio-app/plugins/golem15/summercms/go.mod
|
|
- ../sm-summercmsio-app/plugins/golem15/summercms/go.sum
|
|
- ../sm-summercmsio-app/plugins/golem15/summercms/plugin.go
|
|
- ../sm-summercmsio-app/plugins/golem15/summercms/static.go
|
|
- ../sm-summercmsio-app/plugins/golem15/summercms/smoke_test.go
|
|
- ../sm-summercmsio-app/plugins/golem15/summercms/links_test.go
|
|
- ../sm-summercmsio-app/plugins/golem15/summercms/public/README.md
|
|
- ../sm-summercmsio-app/plugins/golem15/summercms/.gitignore
|
|
- ../sm-summercmsio-app/plugins/golem15/summercms/README.md
|
|
- ../sm-summercmsio-app/go.mod
|
|
- ../sm-summercmsio-app/go.sum
|
|
- ../sm-summercmsio-app/go.work
|
|
- ../sm-summercmsio-app/summer.yaml
|
|
- ../sm-summercmsio-app/main.go
|
|
- ../sm-summercmsio-app/plugins.gen.go
|
|
- ../sm-summercmsio-app/config/app.yaml
|
|
- ../sm-summercmsio-app/config/http.yaml
|
|
- ../sm-summercmsio-app/config/storage.yaml
|
|
- ../sm-summercmsio-app/config/queue.yaml
|
|
- ../sm-summercmsio-app/config/database.yaml
|
|
- ../sm-summercmsio-app/.gitignore
|
|
- ../sm-summercmsio-app/.gitmodules
|
|
- ../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/README.md
|
|
- ../sm-summercmsio-app/DEPLOY.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
|
|
autonomous: false
|
|
requirements: []
|
|
assumption_delta_decision: no-change
|
|
specless_probe_fallback: "skipped: phase has no requirement IDs to probe (visible skip); ROADMAP SC1-SC5 and the D-IDs are the acceptance contract"
|
|
user_setup: []
|
|
|
|
estimate:
|
|
tokens: 200000
|
|
raw_tokens: 200000
|
|
tasks: 5
|
|
confidence: low
|
|
|
|
must_haves:
|
|
truths:
|
|
- "Per D-01, D-02 and D-43, `sm-summercmsio-app` (module `git.golem15.com/golem15/sm-summercmsio-app`) is a git repository next to summercms.go, holding `vue-summercmsio-app` and `plugins/golem15/summercms` (module `git.golem15.com/golem15/sm-summercmsio-plugin`, plugin ID `golem15.summercms`) as submodules whose URLs are `git@git.golem15.com:golem15/<repo>.git`; creating those remotes is a listed user step."
|
|
- "Per D-03, the app is a go.work workspace (`use . ./plugins/golem15/summercms`) that requires the framework with `replace git.golem15.com/golem15/summercms => ../summercms.go`, the plugin requires it with `=> ../../../../summercms.go`, and `go list ./...` in the app lists only the app's own package."
|
|
- "Per D-05 and SC2, one `summercms-io` binary embeds the Nuxt output and the docs build through `//go:embed all:public` in the site plugin, and `scripts/build.sh` runs nuxt generate, then `summer docs:build --base-url /docs --site-url / --site-label summercms.io`, then the plugin tests, then `summer build` and a linux/amd64 `go build`."
|
|
- "Per D-07 and SC2, responses for `/` and `/docs/` carry no robots-blocking header and no Content-Security-Policy; `/_nuxt/*` (except `/_nuxt/builds/*`) and `/_fonts/*` get `public, max-age=31536000, immutable`; every other file, including `/docs/assets/*`, gets `no-cache` with a strong ETag, and a matching If-None-Match returns 304."
|
|
- "Per D-47, `GET /docs/<p>` answers 301 to `/docs/<p>.html` when that page exists, built only from the cleaned path; `GET /docs` answers 301 to `/docs/`; an unknown path gets its tree's `404.html` with status 404, and any dot-segment path (for example `/docs/.summer-docs`) is 404."
|
|
- "Per D-24, D-27 and D-29, the binary boots through the stock `serve` command against PostgreSQL 15 with `queue.work_in_serve: false`, numeric `http.body_limits`, a file uploads bucket and secrets only from a server-side `.env`; the site plugin declares no admin controllers, so `/backend` is a 404 and `POST /` is a 405 (smoke script)."
|
|
- "Per D-25, the framework's database suites pass against `postgres:15` (output recorded in the SUMMARY) before README.md and docs/setup/installation.md say `PostgreSQL 15 or newer`; if any suite fails on 15 the plan stops and asks the user."
|
|
- "Per D-41 and D-46, `docs/site.yaml` accepts optional `site_url` and `site_label`, `summer docs:build` and `summer docs:serve` accept `--site-url` and `--site-label` overrides, the docs header shows a link back to the main site only when a site URL is set (label: explicit, else the URL host, else `Home`), the framework's own docs output is byte-identical when nothing is set, and `docs/console/utilities.md` documents both keys and flags with a neutral example."
|
|
- "Per D-42, the framework is tagged `v0.1.0` only after the user confirms at a blocking-human checkpoint, at a commit where the D-25 and D-41/D-46 changes are green; the app and the plugin require `git.golem15.com/golem15/summercms v0.1.0`, and the release build takes the docs and the compiled framework from the tag export."
|
|
- "Per SC3 and D-44, `TestLandingLinks` resolves every href and src in the built index.html through the real assembled handler (one 301 followed, final 200), checks every in-page anchor, and requires all ten handoff `/docs/...` targets plus `/docs`; external links are checked anonymously by `TestExternalLinks` at cutover."
|
|
- "Per D-40, `TestTerminalCommands` runs the six commands from `vue-summercmsio-app/app/data/terminal.json` in a temp directory with a temp GOBIN first on PATH and no SUMMER_* or GOWORK variables, and sees `handled=true`; only the clone URL may be overridden (SUMMERCMS_CLONE_URL) until D-38 makes the repository public, and a drift test proves every command and comment appears in the built page."
|
|
- "Per SC4 and D-27 to D-32, DEPLOY.md and the committed nginx and supervisor configs cover the launch checklist, one-time server setup (system user, Postgres role `summercms` owning `summercms_io`, a 0600 `.env`), local build, rsync upload, migrate before restart, nginx with certbot TLS, HTTP to HTTPS and www to apex redirects, gzip, proxy headers and denied admin paths, the supervisor program on 127.0.0.1:8095, verification and rollback; `scripts/check-deploy.sh` passes `nginx -t`."
|
|
- statement: "Following DEPLOY.md on rome brings the site up at https://summercms.io (manual, at cutover)."
|
|
verification: backstop
|
|
artifacts:
|
|
- path: "../sm-summercmsio-app/plugins/golem15/summercms/plugin.go"
|
|
provides: "site plugin: ID, embed, routes for /, /docs and /docs/{path...}"
|
|
contains: "go:embed all:public"
|
|
- path: "../sm-summercmsio-app/plugins/golem15/summercms/static.go"
|
|
provides: "embedded static handler: resolution, MIME table, cache rules, ETag, 301s, 404s"
|
|
contains: "http.ServeContent"
|
|
- path: "../sm-summercmsio-app/plugins/golem15/summercms/links_test.go"
|
|
provides: "SC3 link check, D-40 drift guard, D-46 header check, external link check"
|
|
contains: "TestLandingLinks"
|
|
- path: "../sm-summercmsio-app/scripts/build.sh"
|
|
provides: "D-05/D-28 scripted build, release from the tag and dev"
|
|
contains: "docs:build --base-url /docs --site-url / --site-label summercms.io"
|
|
- path: "../sm-summercmsio-app/scripts/smoke.sh"
|
|
provides: "boot on postgres:15 and HTTP assertions"
|
|
contains: "postgres:15"
|
|
- path: "../sm-summercmsio-app/terminal_check_test.go"
|
|
provides: "D-40 verbatim terminal command check"
|
|
contains: "TestTerminalCommands"
|
|
- path: "../sm-summercmsio-app/DEPLOY.md"
|
|
provides: "SC4 deploy, launch checklist and rollback"
|
|
contains: "supervisorctl restart summercms-io"
|
|
- path: "../sm-summercmsio-app/deploy/nginx/summercms.io.conf"
|
|
provides: "nginx server blocks"
|
|
contains: "proxy_pass http://127.0.0.1:8095"
|
|
- path: "../sm-summercmsio-app/deploy/supervisor/summercms-io.conf"
|
|
provides: "supervisor program"
|
|
contains: "user=summercms"
|
|
- path: "internal/docsite/load.go"
|
|
provides: "Site.SiteURL, Site.SiteLabel, checkSiteURL, siteLabel"
|
|
contains: "yaml:\"site_url\""
|
|
- path: "internal/docsite/theme/templates/header.html"
|
|
provides: "conditional link back to the main site"
|
|
contains: "site-link"
|
|
- path: "cmd/summer/docs.go"
|
|
provides: "--site-url and --site-label on docs:build and docs:serve"
|
|
contains: "site-label"
|
|
key_links:
|
|
- from: "../sm-summercmsio-app/plugins.gen.go"
|
|
to: "../sm-summercmsio-app/plugins/golem15/summercms/plugin.go"
|
|
via: "generated blank import registers the plugin through its init"
|
|
pattern: "sm-summercmsio-plugin"
|
|
- from: "../sm-summercmsio-app/plugins/golem15/summercms/plugin.go"
|
|
to: "modules/surf router"
|
|
via: "Routes registers a raw group with GET /docs, GET /docs/{path...} and GET /"
|
|
pattern: "GroupRaw\\("
|
|
- from: "../sm-summercmsio-app/scripts/build.sh"
|
|
to: "../sm-summercmsio-app/plugins/golem15/summercms/public"
|
|
via: "rsync of .output/public into public/site and docs:build --out public/docs before go build"
|
|
pattern: "public/(site|docs)"
|
|
- from: "../sm-summercmsio-app/scripts/build.sh"
|
|
to: "summercms.go tag v0.1.0"
|
|
via: "git archive of the required tag feeds docs:build and a temporary go.work replace"
|
|
pattern: "git -C \"\\$FW\" archive"
|
|
- from: "cmd/summer/docs.go"
|
|
to: "internal/docsite/docsite.go"
|
|
via: "docsOptions copies --site-url and --site-label into Options"
|
|
pattern: "SiteURL"
|
|
- from: "../sm-summercmsio-app/terminal_check_test.go"
|
|
to: "../sm-summercmsio-app/vue-summercmsio-app/app/data/terminal.json"
|
|
via: "reads the single command list the page renders"
|
|
pattern: "app/data/terminal\\.json"
|
|
---
|
|
|
|
<objective>
|
|
Make summercms.io shippable: the small framework changes the site needs (D-25 PostgreSQL 15, D-41/D-46 docs link back to the site), the `v0.1.0` tag behind a user confirmation (D-42), the `sm-summercmsio-plugin` site plugin that serves the embedded Nuxt build at `/` and the docs at `/docs` with indexable responses and correct cache headers (D-01, D-05, D-07, D-47), the `sm-summercmsio-app` root app with its workspace, config, build and smoke scripts (D-02, D-03, D-24, D-28), the link and terminal-command checks (SC3, D-40, D-44), and DEPLOY.md with nginx, supervisor and rollback (SC4, D-27 to D-32, D-38, D-43).
|
|
|
|
Purpose: SC2, SC3 and SC4. Plan 11.2-03 adds the full unit-test coverage on top of the interfaces fixed here.
|
|
|
|
Output: framework commits in summercms.go (two, then the tag), and two new repositories, `sm-summercmsio-app` and its submodule `sm-summercmsio-plugin` (the plan 11.2-01 repository becomes the second submodule).
|
|
|
|
Task count: five tasks including the tag checkpoint, above the usual three, because the user fixed this phase at three plans (CLAUDE.md lean rule) and placed all of this scope in plan 02.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@~/.claude/gsd-core/workflows/execute-plan.md
|
|
@~/.claude/gsd-core/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@CLAUDE.md
|
|
@.planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-CONTEXT.md
|
|
@.planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-RESEARCH.md
|
|
@.planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-PATTERNS.md
|
|
@.planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-01-SUMMARY.md
|
|
@modules/boardwalk/boardwalk.go
|
|
@internal/docsite/serve.go
|
|
@internal/docsite/load.go
|
|
@internal/docsite/emit.go
|
|
@internal/docsite/theme/templates/header.html
|
|
@cmd/summer/docs.go
|
|
@examples/hello/plugins/base/plugin.go
|
|
@../fonoteka.go/go.work
|
|
@../fonoteka.go/summer.yaml
|
|
|
|
**Paths.** Commands run from the summercms.go root (`FW`). `APP` = `../sm-summercmsio-app` (absolute `/media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app`), `PLUG` = `APP/plugins/golem15/summercms`, `SITE` = `APP/vue-summercmsio-app` (created by plan 11.2-01; this plan only reads it). Each task names the repository it commits to. Never `git add` in the meta repository `summercms/`.
|
|
|
|
**Commits.** One logical change per commit, conventional messages, never a co-author tag. In summercms.go, stage only the files a task lists (never `git add -A`); planning docs are committed separately by the orchestrator.
|
|
|
|
**Tools.** A `summer` binary for the app is installed into a temporary GOBIN, for example `GOBIN=$(mktemp -d) go -C <framework tree> install ./cmd/summer`; never rely on a `summer` already on PATH.
|
|
|
|
<interfaces>
|
|
Site plugin, package `summercms`, module `git.golem15.com/golem15/sm-summercmsio-plugin` (plan 11.2-03 tests these names; keep them stable):
|
|
|
|
- `//go:embed all:public` into `var publicFS embed.FS` (the `all:` prefix is required: Nuxt writes `_nuxt/`, `_fonts/`, `_i18n/` and `_payload.json`).
|
|
- `type Plugin struct { fsys fs.FS }`; a nil `fsys` means `fs.Sub(publicFS, "public")`. Methods `ID() string` (returns the literal `"golem15.summercms"`), `Requires() []string` (nil), `Register(*backpack.App) error`, `Boot(*backpack.App) error`, `Routes(r pact.Router) error`. Compile-time assertion `var _ pact.HasRoutes = (*Plugin)(nil)`. `func init() { party.Register(&Plugin{}) }`.
|
|
- `func newHandlers(public fs.FS) (site, docs http.Handler, err error)`: subtrees `site` and `docs` of `public`; error text `summercms: public/site/index.html missing; run scripts/build.sh` (or `public/docs/...`) when a tree has no `index.html`.
|
|
- `type tree struct` with fields `files map[string]*file`, `mount string` (`"/"` or `"/docs/"`), `immutable func(name string) bool` (nil means never), `htmlRedirect bool` (true for docs); `type file struct { body []byte; etag, ctype string }`.
|
|
- `func newTree(fsys fs.FS, mount string, immutable func(string) bool, htmlRedirect bool) (*tree, error)`: loads every regular file whose path has no dot-segment, precomputes `ctype` and a strong ETag `"` + first 16 hex chars of sha256 + `"`; returns `errMissingIndex` when `index.html` is absent.
|
|
- `func (t *tree) ServeHTTP(w http.ResponseWriter, r *http.Request)`: rel = `r.URL.Path` without `t.mount`; name = `path.Clean("/"+rel)` without the leading `/` (empty means `index.html`); any segment starting with `.` → not found; exact file → serve; `name + "/index.html"` → serve; `htmlRedirect` and `name + ".html"` exists → 301 to `t.mount + name + ".html"`; otherwise not found = the tree's `404.html` with status 404 (or `http.NotFound` without one). Serving sets `Content-Type` (from `ctype`), `Cache-Control` (`cacheImmutable` when `t.immutable(name)`, else `cacheNoCache`), `ETag`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`, then calls `http.ServeContent(w, r, name, time.Time{}, bytes.NewReader(body))`.
|
|
- `const cacheImmutable = "public, max-age=31536000, immutable"`, `const cacheNoCache = "no-cache"`.
|
|
- `func siteImmutable(name string) bool`: true for `_nuxt/` paths except `_nuxt/builds/`, and for `_fonts/` paths.
|
|
- `var contentTypes map[string]string` and `func contentType(name string) string`: own table first (`.html` `text/html; charset=utf-8`, `.css` `text/css; charset=utf-8`, `.js` and `.mjs` `text/javascript; charset=utf-8`, `.json` `application/json`, `.md` `text/markdown; charset=utf-8`, `.txt` `text/plain; charset=utf-8`, `.xml` and `.xsl` `application/xml; charset=utf-8`, `.svg` `image/svg+xml`, `.png` `image/png`, `.webp` `image/webp`, `.ico` `image/x-icon`, `.woff2` `font/woff2`, `.woff` `font/woff`, `.webmanifest` `application/manifest+json`), then `mime.TypeByExtension`, then `application/octet-stream`.
|
|
- `func redirectTo(location string) http.HandlerFunc`: 301 to a constant location.
|
|
- Test helpers in `links_test.go`: `func requireBuild(t *testing.T)` (skips unless `SUMMERCMS_REQUIRE_BUILD=1`; when set, fails if either tree has no index.html), `func pageLinks(html []byte) []string`, `func resolve(t *testing.T, h http.Handler, target string) (status int, final string)`.
|
|
|
|
Framework, package `internal/docsite` and `cmd/summer`:
|
|
|
|
- `Site.SiteURL string` (`yaml:"site_url"`) and `Site.SiteLabel string` (`yaml:"site_label"`); `Options.SiteURL` and `Options.SiteLabel` override them when non-empty, as `Options.BaseURL` overrides `base_url`.
|
|
- `func checkSiteURL(raw string) error`: accepts `http://` or `https://` URLs with a host and no user info, or a path starting with exactly one `/`; rejects every other value (`javascript:`, `data:`, `//host`, relative paths, whitespace or control characters).
|
|
- `func siteLabel(siteURL, label string) string`: `label` (trimmed) when set, else the host of an absolute URL (the parsed URL's `Host` field, port included when present), else `Home`.
|
|
- Unexported `site` fields `siteURL`, `siteLabel`; `pageView.SiteURL`, `pageView.SiteLabel` filled in `baseView` (so the 404 page has the link too), never passed through `s.url()`.
|
|
- CLI flags `site-url` and `site-label` on `docs:build` and `docs:serve`, read in `docsOptions`.
|
|
|
|
App, package `main` in `sm-summercmsio-app` (test file only, never shipped in the binary):
|
|
|
|
- `type terminalGroup struct { Comment string \`json:"comment"\`; Commands []string \`json:"commands"\` }`, `func loadTerminal(path string) ([]terminalGroup, error)`, `const terminalCloneURL = "https://git.golem15.com/golem15/summercms"`, `func terminalScript(groups []terminalGroup, cloneURL string) string` (commands joined by `\n`; a non-empty cloneURL replaces only `terminalCloneURL` inside the `git clone` command), `func terminalEnv(base []string, gobin string) []string` (drops every `SUMMER_*` and `GOWORK` entry, sets `GOBIN=gobin`, prefixes `PATH` with gobin).
|
|
</interfaces>
|
|
|
|
<assumption_delta_decision>
|
|
Detector signal: pluralization ("a SummerCMS binary that also serves the Phase 11.1 docs"). Primary noun: the docs site stays a self-contained static tree addressed by `base_url`. Decision: no-change. The site plugin serves two independent embedded trees by path prefix, and `site_url` is an optional outbound link, not a second identity for the docs; no data model, primary key or contract changes.
|
|
</assumption_delta_decision>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="tracer">
|
|
<name>Task 1: Tracer: one `summercms-io` binary, booted on PostgreSQL 15, serves the Nuxt site at / and the docs at /docs</name>
|
|
<precondition>`docker info` exits 0 and `docker image inspect postgres:15` succeeds; `test -f ../sm-summercmsio-app/vue-summercmsio-app/app/data/terminal.json` succeeds (plan 11.2-01 is complete).</precondition>
|
|
<reversibility rating="costly">D-02/D-01 module paths and repo names are referenced by go.mod, go.work, summer.yaml, the generated imports and the submodule URLs; nothing is published in this task (remotes are created at cutover), so a rename now touches many files but needs no migration. They become one-way when pushed at cutover, a step the user already chose in CONTEXT.</reversibility>
|
|
<files>../sm-summercmsio-app/plugins/golem15/summercms/go.mod, ../sm-summercmsio-app/plugins/golem15/summercms/go.sum, ../sm-summercmsio-app/plugins/golem15/summercms/plugin.go, ../sm-summercmsio-app/plugins/golem15/summercms/static.go, ../sm-summercmsio-app/plugins/golem15/summercms/smoke_test.go, ../sm-summercmsio-app/plugins/golem15/summercms/public/README.md, ../sm-summercmsio-app/plugins/golem15/summercms/.gitignore, ../sm-summercmsio-app/go.mod, ../sm-summercmsio-app/go.sum, ../sm-summercmsio-app/go.work, ../sm-summercmsio-app/summer.yaml, ../sm-summercmsio-app/main.go, ../sm-summercmsio-app/plugins.gen.go, ../sm-summercmsio-app/config/app.yaml, ../sm-summercmsio-app/config/http.yaml, ../sm-summercmsio-app/config/storage.yaml, ../sm-summercmsio-app/config/queue.yaml, ../sm-summercmsio-app/config/database.yaml, ../sm-summercmsio-app/.gitignore, ../sm-summercmsio-app/.gitmodules, ../sm-summercmsio-app/scripts/build.sh, ../sm-summercmsio-app/scripts/smoke.sh</files>
|
|
<read_first>
|
|
- modules/boardwalk/boardwalk.go lines 24-48 and 132-173 (embed, fs.Sub, content-type table, cache by prefix; the security headers and SPA fallback there are NOT to be copied, D-07)
|
|
- internal/docsite/serve.go lines 236-290 (public static semantics: dot-segment refusal, dir index.html, 404.html with status 404)
|
|
- examples/hello/plugins/base/plugin.go (plugin skeleton, init registration) and examples/hello/plugins/greeter/plugin.go lines 59-76 (Routes)
|
|
- modules/pact/capabilities.go lines 60-80 (Router, GroupRaw, HasRoutes) and modules/surf/router.go lines 350-430 and 460-480 (route registration, recoverBare, body limits)
|
|
- modules/cabana/http.go lines 64-74 (no admin controllers means no admin routes)
|
|
- internal/build/scaffold.go lines 104-170 and 318-370 (plugin:add edits go.mod, go.work and summer.yaml; ID read from plugin.go)
|
|
- ../fonoteka.go/go.work, ../fonoteka.go/go.mod (header and replaces), ../fonoteka.go/summer.yaml, ../fonoteka.go/.gitignore, ../fonoteka.go/config/{app,http,storage,queue,database}.yaml
|
|
- modules/surf/clientip.go lines 45-70 (http.trusted_proxies format)
|
|
- .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-RESEARCH.md sections "Framework Surface", "Postgres-at-boot config", "Docs Build", "Pattern 2", "Cache-Control by path", "MIME table", "Build script outline", Pitfalls 7-12
|
|
</read_first>
|
|
<action>
|
|
Wire one request path through every layer: embedded files → static handler → surf raw group → generated app binary → stock `serve` on Postgres 15 → HTTP.
|
|
|
|
1. Plugin repository (writes to sm-summercmsio-plugin): `mkdir -p` PLUG and `git init -b master` inside it. `go.mod`: `module git.golem15.com/golem15/sm-summercmsio-plugin`, `go 1.27.0`, `require git.golem15.com/golem15/summercms v0.1.0` (D-42; the directory replace resolves it before the tag exists), `replace git.golem15.com/golem15/summercms => ../../../../summercms.go` (same depth as fonoteka's plugins). `.gitignore`: `/public/site/` and `/public/docs/`. `public/README.md`: build.sh fills `site/` (Nuxt output) and `docs/` (docs build); nothing else may be placed in `public/docs`, because docs:build refuses to clean a directory without its marker file; the file exists so the embed pattern always matches.
|
|
2. `plugin.go` exactly as the interfaces block defines it. The plugin implements only the party lifecycle and `pact.HasRoutes`: it must not implement the pact admin-controllers capability or any other capability, so cabana never activates and no `/backend` route exists (D-29, T-11.2-07). `Routes` builds both handlers via `newHandlers` (returning its error, so `serve` fails closed with `summercms: public/site/index.html missing; run scripts/build.sh` on an unbuilt tree) and registers, inside `r.GroupRaw("", nil, …)`: `GET /docs` → `redirectTo("/docs/")` (a permanent 301 instead of ServeMux's 307), `GET /docs/{path...}` → docs tree, `GET /` → site tree (the least specific pattern, verified conflict-free alongside cabana's patterns).
|
|
3. `static.go` exactly as the interfaces block defines it (D-07, D-47; Pitfalls 8-11). Copy the ideas of the admin SPA embed handler (fs.FS, own content-type table, cache by path, ServeContent) and of the docsite preview handler (dot refusal, dir index, 404 page), but add no robots-blocking header, no CSP, no frame-deny header, no index-token rewrite and no SPA fallback: the public site wants indexable pages and real 404s. Build the 301 Location only from `t.mount` plus the cleaned name plus `.html`, and only when that file is in the map (T-11.2-05). Stdlib only.
|
|
4. `smoke_test.go` (smoke level; coverage is plan 11.2-03): `TestStaticSmoke` builds handlers from a `fstest.MapFS` with `site/index.html`, `site/404.html`, `docs/index.html`, `docs/404.html`, `docs/setup/installation.html` and asserts `GET /` 200 `text/html; charset=utf-8`, docs `GET /docs/setup/installation` 301 to `/docs/setup/installation.html`, and `GET /nope` 404 with the 404 body. Run `go -C PLUG mod tidy`, then commit in PLUG as `feat: serve the embedded site at / and the docs at /docs`.
|
|
5. App repository (writes to sm-summercmsio-app): `git init -b master` in APP. Register the two existing repositories as submodules without cloning (git reports "Adding existing repo"): `git submodule add git@git.golem15.com:golem15/vue-summercmsio-app.git vue-summercmsio-app` and `git submodule add git@git.golem15.com:golem15/sm-summercmsio-plugin.git plugins/golem15/summercms` (D-43; the remotes are created by the user later, see DEPLOY.md). `.gitignore`: `/bin/`, `/tmp/`, `*.exe`, `go.work.sum`, `.env`, `/storage/`.
|
|
6. `go.mod`: `module git.golem15.com/golem15/sm-summercmsio-app`, `go 1.27.0`, `toolchain go1.27.0`, `replace git.golem15.com/golem15/summercms => ../summercms.go`, `require git.golem15.com/golem15/summercms v0.1.0`. `summer.yaml`: `module: git.golem15.com/golem15/sm-summercmsio-app`, `binary: summercms-io`, `plugins:` with `id: golem15.summercms`, `module: git.golem15.com/golem15/sm-summercmsio-plugin`. Then, with a summer binary installed from summercms.go into a temp GOBIN, run `summer plugin:add plugins/golem15/summercms` in APP (it adds the plugin require and `replace … => ./plugins/golem15/summercms`, and writes `go.work` with `use ( . ./plugins/golem15/summercms )`; confirm `go 1.27.0` and `toolchain go1.27.0` as in fonoteka.go/go.work, and that go.work does not `use` the framework), then `go -C APP mod tidy`.
|
|
7. `config/` (no secrets; Pitfall 12, D-24, D-27): `app.yaml` (`name: summercms-io`, `debug: false`, `locale: en`, `fallback_locale: en`, `key: ""` with the comment "Set SUMMER_APP__KEY to a 32-byte base64 value (key:generate)"); `http.yaml` (`body_limits.default_bytes: 1048576`, `body_limits.upload_bytes: 1048576`, `trusted_proxies` holding `127.0.0.1/32` because nginx proxies on loopback, using the CIDR list format clientip.go reads); `storage.yaml` (`uploads.bucket_url: "file://./storage/app/uploads"`, `uploads.public_path_prefix: "/storage/uploads"`); `queue.yaml` (`work_in_serve: false` with a comment that the site has no jobs, plus fonoteka's `max_attempts`, `job_timeout` and `queues.default: 1`); `database.yaml` (`dsn: ""` with the comment "Set SUMMER_DATABASE__DSN").
|
|
8. `scripts/build.sh` (bash, `set -euo pipefail`, `ROOT` from `BASH_SOURCE` as in fonoteka.go/scripts/check-openapi.sh, explicit guards that print `build: …` to stderr and exit 1). This task implements the `dev` mode; Task 4 adds `release`, which needs the tag, so until then any argument other than `dev` exits 2 with a usage line. Steps: check `pnpm`, `go`, `rsync`, `git`, `tar` exist; `FW="${SUMMERCMS_FRAMEWORK:-$ROOT/../summercms.go}"`; (a) in SITE `pnpm install --frozen-lockfile` and `pnpm run generate`, require `.output/public/index.html`, then `rsync -a --delete "$SITE/.output/public/" "$PLUG/public/site/"` (never the `dist` symlink, which embed refuses); (b) `git -C "$FW" archive HEAD | tar -x -C "$TMP/fw"` (TMP from mktemp with an EXIT trap), install summer from the export into `$TMP/bin`, run `docs:build --base-url /docs --out "$PLUG/public/docs"` inside the export, require `public/docs/index.html` and `public/docs/.summer-docs`; (c) `SUMMERCMS_REQUIRE_BUILD=1 go -C "$PLUG" test ./...`; (d) in APP run `"$TMP/bin/summer" build` (writes main.go and plugins.gen.go), then `CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go -C "$ROOT" build -trimpath -o bin/summercms-io .`; print `build: bin/summercms-io (dev) ready`.
|
|
9. `scripts/smoke.sh` (bash, `set -euo pipefail`, trap cleanup of the container, the serve process and the temp dir; binary path argument defaulting to `bin/summercms-io`): start `postgres:15` with `docker run -d --rm` (user `summercms`, password `smoke`, database `summercms_io`, port `127.0.0.1::5432`, read back with `docker port`), wait for `pg_isready`; copy `config/` into a temp work dir and write a `.env` there with `SUMMER_DATABASE__DSN` and `SUMMER_APP__KEY` (32 random bytes, base64); from the work dir run the binary's `migrate`, then `serve --addr 127.0.0.1:${SMOKE_PORT:-18095}` in the background and wait for it. Assert with curl: `/` 200, `Content-Type` starting `text/html`, `Cache-Control: no-cache`, an `ETag`, and neither an `X-Robots-Tag` nor a `Content-Security-Policy` header; the same `/` with `If-None-Match: <etag>` is 304; `/docs` 301 with `Location: /docs/`; `/docs/` 200 and no `X-Robots-Tag`; `/docs/setup/installation` 301 to `/docs/setup/installation.html`, which is 200; the first `/_nuxt/*.js` referenced by index.html has `immutable` and `text/javascript`; the first file under `public/site/_fonts/` is `font/woff2` and `immutable`; `/missing-page` 404; `/backend` 404; `/docs/.summer-docs` 404; `POST /` 405. Print `smoke: ok`.
|
|
10. Run `scripts/build.sh dev` and `scripts/smoke.sh`, then commit in APP (including `.gitmodules` and both gitlinks) as `feat: wire the summercms.io app with build and smoke scripts`.
|
|
</action>
|
|
<verify>
|
|
<automated>../sm-summercmsio-app/scripts/build.sh dev && ../sm-summercmsio-app/scripts/smoke.sh</automated>
|
|
<fails_when>non-zero exit, a line starting "build:" on stderr, or no "smoke: ok" line</fails_when>
|
|
<automated>go -C ../sm-summercmsio-app vet ./... && go -C ../sm-summercmsio-app/plugins/golem15/summercms vet ./... && go -C ../sm-summercmsio-app/plugins/golem15/summercms test ./... -run '^TestStaticSmoke$' -count=1 -v</automated>
|
|
<fails_when>non-zero exit, a "--- FAIL" line, or no "--- PASS: TestStaticSmoke" line</fails_when>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `go -C ../sm-summercmsio-app list ./...` prints exactly `git.golem15.com/golem15/sm-summercmsio-app` (pnpm's dot-directory layout keeps node_modules out of the module).
|
|
- `git -C ../sm-summercmsio-app submodule status` lists `plugins/golem15/summercms` and `vue-summercmsio-app`, and `git -C ../sm-summercmsio-app config -f .gitmodules --get-regexp url` prints `git@git.golem15.com:golem15/sm-summercmsio-plugin.git` and `git@git.golem15.com:golem15/vue-summercmsio-app.git`.
|
|
- `grep -n 'go:embed all:public' ../sm-summercmsio-app/plugins/golem15/summercms/plugin.go` and `grep -n 'return "golem15.summercms"' ../sm-summercmsio-app/plugins/golem15/summercms/plugin.go` both find a match.
|
|
- `grep -c 'HasAdminControllers' ../sm-summercmsio-app/plugins/golem15/summercms/plugin.go` prints 0.
|
|
- Every import path printed by `go -C ../sm-summercmsio-app/plugins/golem15/summercms list -f '{{join .Imports "\n"}}' .` is either a standard-library path or starts with `git.golem15.com/golem15/summercms/modules/` (no new third-party dependency).
|
|
- `grep -n 'work_in_serve: false' ../sm-summercmsio-app/config/queue.yaml` finds a match, and `grep -n 'key: ""' ../sm-summercmsio-app/config/app.yaml` finds a match.
|
|
- `file ../sm-summercmsio-app/bin/summercms-io` reports an ELF 64-bit x86-64 executable.
|
|
</acceptance_criteria>
|
|
<done>The app and plugin repositories exist with the submodule layout, the binary embeds both trees and boots on postgres:15 through the stock serve command, and the smoke script proves status codes, content types, cache headers, 301s, 404s and the absence of robots-blocking and CSP headers.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Framework for v0.1.0: verified and documented on PostgreSQL 15, and the docs header links back to the main site</name>
|
|
<files>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, internal/docsite/load_test.go, internal/docsite/theme_test.go, cmd/summer/docs.go, cmd/summer/docs_test.go, docs/console/utilities.md</files>
|
|
<read_first>
|
|
- .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-RESEARCH.md sections "PostgreSQL 15 (question 3)", "Docs Build (question 2)", "D-41 header change", Open Question 1
|
|
- .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-CONTEXT.md D-25, D-41, D-46
|
|
- README.md lines 10-40, docs/setup/installation.md lines 1-20
|
|
- internal/docsite/load.go (Site, ParseSite, load and the base_url override at lines 217-221), internal/docsite/docsite.go (Options, normalize), internal/docsite/emit.go lines 60-95 (pageView, baseView)
|
|
- internal/docsite/theme/templates/header.html (whole file), internal/docsite/theme/templates/icons.html line 8 (icon-chevron-left), internal/docsite/theme/assets/site.css lines 250-285 (.wordmark, .header-spacer) and its mobile media query
|
|
- cmd/summer/docs.go (flag lists of docs:build and docs:serve, docsOptions), cmd/summer/docs_test.go (TestDocsTree, TestDocsBuildRealTree)
|
|
- internal/docsite/load_test.go lines 11-50 (TestParseSite table) and internal/docsite/theme_test.go lines 1-80 (themeTree, buildTheme)
|
|
- docs/console/utilities.md lines 28-50 (Documentation commands table)
|
|
</read_first>
|
|
<action>
|
|
Two commits in summercms.go, in this order. Stage only the listed files.
|
|
|
|
A. D-25, PostgreSQL 15 (no repository edits until the run passes). Export HEAD to a scratch directory, retarget the test image and run the database suites exactly as RESEARCH "How to run on 15 without permanent edits" does: `S=$(mktemp -d)`, `git archive HEAD | tar -x -C "$S"`, replace `postgres:16-alpine` with `postgres:15` in every `*.go` file under `$S` with sed, then `go -C "$S" test -count=1 -p 4 ./modules/lagoon/... ./modules/cabana/... ./modules/beachcomber/... ./modules/lighthouse/... ./modules/bouncer/... ./modules/conga/... ./docs/examples/blog/...`, and confirm with `-v` output or the testcontainers log that the image was `postgres:15`. Record the package result lines in the SUMMARY. If any package fails on 15, stop here: do not edit the docs, do not continue to the tag, and return a `checkpoint:decision` (`gate="blocking-human"`) to the user with the failing output (D-25 says stop and ask; do not work around it and do not upgrade rome). When every package passes, change `README.md` line 15 to "PostgreSQL 15 or newer for any application that uses the data layer ([lagoon](modules/lagoon/README.md))." and line 35 to "Create a database on PostgreSQL 15 or newer:", and `docs/setup/installation.md` line 14 to "PostgreSQL 15 or newer for any application that uses the data layer." Leave `docs/plugins/testing.md` and the lagoon and conga READMEs unchanged: they name the test image, not a requirement. Commit as `docs: require PostgreSQL 15 or newer (verified on postgres:15)`.
|
|
|
|
B. D-41/D-46, the link back to the main site. Before editing, build the real tree from a HEAD export into a scratch directory `pre` (for the byte-identical check).
|
|
1. `load.go`: add `SiteURL` (`yaml:"site_url"`) and `SiteLabel` (`yaml:"site_label"`) to `Site` with doc comments (strict decoding needs the fields). In `ParseSite`, when `site_url` is set validate it with `checkSiteURL` and report `docsite: site config: site_url must be an http(s) URL with a host or a path starting with a single /`; when `site_label` is set without `site_url` report `docsite: site config: site_label needs site_url`; a label that is blank after trimming or holds a line break is `docsite: site config: site_label must be one non-empty line`. Add `checkSiteURL` and `siteLabel` exactly as the interfaces block defines them (`net/url` for absolute URLs; T-11.2-09).
|
|
2. `docsite.go`: add `Options.SiteURL` ("overrides site.yaml site_url when non-empty") and `Options.SiteLabel` ("overrides site.yaml site_label when non-empty"). In `load`, next to the base_url override: start from the config values, apply the option overrides, validate an overriding URL with `checkSiteURL` (error `docsite: --site-url: must be an http(s) URL with a host or a path starting with a single /`), refuse a label without any site URL (`docsite: --site-label needs --site-url or site.yaml site_url`), then store `s.siteURL` and `s.siteLabel = siteLabel(s.siteURL, label)`.
|
|
3. `emit.go`: add `SiteURL` and `SiteLabel` to `pageView` and set them in `baseView` from the site fields, not through `s.url()` (they point at another site).
|
|
4. `header.html`: append to the end of the wordmark anchor's line a conditional anchor `{{if .SiteURL}}<a class="site-link" href="{{.SiteURL}}" aria-label="{{.SiteLabel}}">{{template "icon-chevron-left"}}<span class="site-link-label">{{.SiteLabel}}</span></a>{{end}}`, on the same line so the unset output keeps the exact bytes it has today. html/template escapes the attribute.
|
|
5. `site.css`: a `.site-link` rule next to `.wordmark` (inline-flex, centered, small gap, a left margin, the theme's muted text color and the existing accent on hover, 14px), and in the theme's existing narrow-screen media query hide `.site-link-label` so the search trigger keeps its room (the aria-label keeps the link named).
|
|
6. `cmd/summer/docs.go`: add `{Name: "site-url", Description: "Main site URL linked from the docs header (overrides site.yaml site_url)"}` and `{Name: "site-label", Description: "Label of the main site link (overrides site.yaml site_label; default: the URL host, or Home)"}` to both `docs:build` and `docs:serve`, and read both in `docsOptions`.
|
|
7. `docs/console/utilities.md`: add `--site-url` and `--site-label` to the Flags cells of the `docs:build` and `docs:serve` rows, and below the table a short paragraph: `docs/site.yaml` accepts two optional keys, `site_url` and `site_label`; with `site_url: https://acme.example/` every page header links back to the main site as "acme.example"; without `site_label` the label is the URL's host, or Home for a path such as `/`; the two flags override the keys the way `--base-url` overrides `base_url`. Use only the neutral `acme.example` example (CLAUDE.md: framework docs never name a consuming application). `docs/site.yaml` itself stays unchanged (D-46).
|
|
8. Smoke tests (coverage is plan 11.2-03): `TestParseSite` rows for an accepted `https://acme.example/`, an accepted `/`, a rejected `javascript:alert(1)`, a rejected `//acme.example`, and a rejected `site_label` without `site_url`; `TestSiteLink` in `theme_test.go` builds `themeTree` three ways and checks `index.html` and `404.html`: no options → no `site-link`; `Options{SiteURL: "/", SiteLabel: "acme.example"}` → `<a class="site-link" href="/"` and `acme.example`; `Options{SiteURL: "https://acme.example/docs"}` → label `acme.example`; `Options{SiteURL: "/"}` → label `Home`. `TestDocsBuildSiteFlags` in `cmd/summer/docs_test.go`: `docs:build --root ../.. --out <tmp> --site-url / --site-label example.org` writes `class="site-link" href="/"` into `index.html`, and `--site-url javascript:alert(1)` returns an error containing `--site-url`.
|
|
9. Byte-identical check: build the working tree with no site flags into a scratch directory `post` and require `diff -r pre post` to print nothing; record that in the SUMMARY.
|
|
Commit as `feat(docsite): optional site_url and site_label link back to the main site`.
|
|
</action>
|
|
<verify>
|
|
<automated>go vet ./internal/docsite/... ./cmd/summer && go test ./internal/docsite ./cmd/summer -run '^(TestDocsTree|TestDocsBuildRealTree|TestToolCommandNames|TestParseSite|TestSiteLink|TestDocsBuildSiteFlags)$' -count=1 -v</automated>
|
|
<fails_when>non-zero exit, a "--- FAIL" line, "no tests to run", or fewer than six "--- PASS" lines for the named tests</fails_when>
|
|
<automated>go vet ./... && go test ./internal/docsite ./cmd/summer -count=1</automated>
|
|
<fails_when>non-zero exit or a "FAIL" line</fails_when>
|
|
<automated>scripts/check-phase11.1.sh --docs && scripts/check-phase11.1.sh --forbidden</automated>
|
|
<fails_when>non-zero exit or a line starting "refuse:"</fails_when>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- The SUMMARY lists an `ok` line for each of lagoon, lagoon/attach, cabana, beachcomber, lighthouse, bouncer, conga and docs/examples/blog run against `postgres:15`, and no `FAIL` line.
|
|
- `grep -c 'PostgreSQL 15 or newer' README.md` prints 2 and `grep -c 'PostgreSQL 15 or newer' docs/setup/installation.md` prints 1.
|
|
- `grep -n 'yaml:"site_url"' internal/docsite/load.go` and `grep -n 'yaml:"site_label"' internal/docsite/load.go` both find a match.
|
|
- `grep -c 'site-url\|site-label' cmd/summer/docs.go` prints at least 6 (two flags on two commands plus two reads).
|
|
- `grep -n 'acme.example' docs/console/utilities.md` finds a match, and `git diff --stat "$D25_SHA^..$SITE_SHA" -- docs/site.yaml` prints nothing, where `D25_SHA` and `SITE_SHA` hold the two commit shas of this task recorded in the SUMMARY (the framework's own site.yaml is unchanged, D-46).
|
|
- `diff -r` of the pre-change and post-change real-tree builds without site flags prints nothing (recorded in the SUMMARY).
|
|
- `git log --format=%s "$D25_SHA^..$SITE_SHA"` in summercms.go prints exactly the two commit subjects above, and `git log --format='%(trailers:key=Co-authored-by,valueonly)' "$D25_SHA^..$SITE_SHA"` prints only empty lines.
|
|
</acceptance_criteria>
|
|
<done>The database suites are proven on postgres:15 and the docs say 15 or newer; site_url and site_label (keys and flags) add a validated link back to the main site in every docs page header, leaving the framework's own output unchanged; the docs checker stays green.</done>
|
|
</task>
|
|
|
|
<task type="checkpoint:decision" gate="blocking-human">
|
|
<name>Task 3: Confirm the v0.1.0 tag (one-way, D-42)</name>
|
|
<decision>Create and push the `v0.1.0` tag of `git.golem15.com/golem15/summercms` now, at the commit produced by Task 2?</decision>
|
|
<context>D-42 requires the user to confirm before the tag is created and pushed. A pushed Go module version tag is cached by module proxies and cannot be reused, so a wrong commit means burning v0.1.0. Before asking, show: the summercms.go HEAD sha and subject (it must be the Task 2 docsite commit, with the D-25 docs commit before it), `git status --porcelain` (must be empty), the Task 2 test results, the D-25 postgres:15 result lines, and `git log --oneline` since the last pushed commit. The release build in Task 4 takes the docs and the compiled framework from this tag, so the page, the docs and the code agree. Plan 11.2-03 adds framework test commits after the tag, which is acceptable because tests do not change v0.1.0 behaviour.</context>
|
|
<options>
|
|
<option id="tag-now">
|
|
<name>Tag and push v0.1.0 now</name>
|
|
<pros>The release build, the embedded docs and DEPLOY.md all reference a real published tag; nothing is left for cutover.</pros>
|
|
<cons>One-way: if a defect is found before launch, the fix ships as v0.1.1.</cons>
|
|
</option>
|
|
<option id="defer-tag">
|
|
<name>Defer the tag to cutover</name>
|
|
<pros>Keeps v0.1.0 unspent until the site is reviewed; Task 4 still proves the release path against a scratch clone carrying a local tag.</pros>
|
|
<cons>The real release build waits for the tag; DEPLOY.md lists creating and pushing it as a launch step.</cons>
|
|
</option>
|
|
</options>
|
|
<resume-signal>Type "tag-now" or "defer-tag".</resume-signal>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 4: Release build from the v0.1.0 tag, with every landing link and every terminal command proven against the built site</name>
|
|
<precondition>Task 3 returned "tag-now" or "defer-tag". For "tag-now": `git ls-remote origin` in summercms.go succeeds (SSH access to git.golem15.com). For "defer-tag": `SUMMERCMS_FRAMEWORK` is exported to the scratch clone (step 1) before any verify command below runs, so the same commands exercise the release path.</precondition>
|
|
<reversibility rating="one-way">Pushing v0.1.0 publishes a module version that proxies cache forever (D-42); done only after the Task 3 confirmation.</reversibility>
|
|
<files>../sm-summercmsio-app/scripts/build.sh, ../sm-summercmsio-app/scripts/smoke.sh, ../sm-summercmsio-app/terminal_check_test.go, ../sm-summercmsio-app/plugins/golem15/summercms/links_test.go</files>
|
|
<read_first>
|
|
- ../sm-summercmsio-app/scripts/build.sh and scripts/smoke.sh (as written in Task 1)
|
|
- ../sm-summercmsio-app/plugins/golem15/summercms/plugin.go and static.go (as written in Task 1)
|
|
- ../sm-summercmsio-app/vue-summercmsio-app/app/data/terminal.json and i18n/locales/en.json (the command list and comment texts)
|
|
- modules/surf/example_test.go lines 114-140 (surf.Assemble with backpack.New(nil))
|
|
- modules/bonfire/prompts.go lines 25-35 (non-interactive Confirm returns the default)
|
|
- .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-RESEARCH.md "Pattern 3", "Every link on the page", "D-40: does ./bin/hello greeter:hello need a database? No.", "Build script outline" (the temporary go.work note)
|
|
</read_first>
|
|
<action>
|
|
1. Tag (summercms.go). For "tag-now": confirm `git status --porcelain` is empty and HEAD is the Task 2 docsite commit, run `git tag -a v0.1.0 -m "SummerCMS Alpha 0.1"` and `git push origin v0.1.0`, then confirm `git ls-remote --tags origin v0.1.0` prints it. If the push fails on authentication, return a `checkpoint:human-action` asking the user to run the same push. For "defer-tag": create nothing in summercms.go; make a scratch clone (`git clone` of the local summercms.go into a temp dir), add a local `v0.1.0` tag there, and use it through `SUMMERCMS_FRAMEWORK=<clone>` for every release-mode run below. Record which path was taken in the SUMMARY.
|
|
2. `scripts/build.sh` release mode (now the default when no argument is given; `dev` stays): read the framework version the app requires (`go -C "$ROOT" list -m -f '{{.Version}}' git.golem15.com/golem15/summercms`, which prints `v0.1.0`); require `git -C "$FW" rev-parse -q --verify "refs/tags/$TAG^{commit}"`, else exit 1 with `build: tag $TAG not found in $FW; run 'scripts/build.sh dev' or create the tag (DEPLOY.md)`; `git -C "$FW" archive "$TAG"` into `$TMP/fw`; install summer from that export; run docs:build there with `--base-url /docs --site-url / --site-label summercms.io --out "$PLUG/public/docs"` (D-46; dev mode now passes the same two site flags); run the plugin tests with `SUMMERCMS_REQUIRE_BUILD=1`; run `summer build` in APP, then fail if `git -C "$ROOT" status --porcelain -- main.go plugins.gen.go` shows drift (generated sources must be committed); write `$TMP/go.work` (`go 1.27.0`, `use` the absolute APP and PLUG directories, `replace git.golem15.com/golem15/summercms => $TMP/fw`) and build with `GOWORK="$TMP/go.work" CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go -C "$ROOT" build -trimpath -o bin/summercms-io .` so the compiled framework is the tag (go.work replaces override module replaces); print `build: bin/summercms-io (release v0.1.0) ready`.
|
|
3. `scripts/smoke.sh`: add the D-46 assertion that `/docs/` contains `class="site-link" href="/"` and the text `summercms.io`.
|
|
4. `links_test.go` in PLUG (package `summercms`, stdlib only) with the helpers from the interfaces block:
|
|
- `TestLandingLinks` (`requireBuild`): assemble the real plugin with `surf.Assemble(backpack.New(nil), []party.Plugin{&Plugin{}})`; collect every `href` and `src` value from `public/site/index.html`, plus the `og:image` content; for `#id` require `id="id"` in the page; for a root-relative path, or an absolute `https://summercms.io/` URL mapped to its path, request it through the handler, follow at most one 301 whose Location must start with `/`, and require a final 200; collect other `http(s)` URLs for the external test. Require that the hrefs include `/docs` and the ten D-44 targets: `/docs/backend/admin-spa`, `/docs/database/models`, `/docs/services/routing`, `/docs/services/jobs`, `/docs/services/realtime`, `/docs/services/search`, `/docs/services/mail`, `/docs/console/introduction`, `/docs/setup/coming-from-wintercms`, `/docs/setup/installation`.
|
|
- `TestTerminalCommandsInPage` (`requireBuild`; the D-40 drift guard): read `../../../vue-summercmsio-app/app/data/terminal.json` and `../../../vue-summercmsio-app/i18n/locales/en.json` (overridable with `SUMMERCMS_SITE_DIR`) and require every command and every resolved comment text in the built index.html.
|
|
- `TestDocsHeaderSiteLink` (`requireBuild`): `public/docs/index.html` contains `class="site-link" href="/"` and `summercms.io`.
|
|
- `TestExternalLinks` (skips unless `SUMMERCMS_CHECK_EXTERNAL=1`): GET each external URL with a fresh client (no cookies, 15 s timeout, redirects followed) and require 200; log each URL with its status. Before D-38 the Source link is expected to fail (private repository); this test runs at cutover.
|
|
Commit in PLUG as `test: verify the landing links and terminal commands against the built site`.
|
|
5. `terminal_check_test.go` in APP (package `main`) with the helpers from the interfaces block. `TestTerminalCommands` skips unless `SUMMERCMS_TERMINAL_CHECK=1`; it loads `vue-summercmsio-app/app/data/terminal.json`, creates a temp work dir and a temp GOBIN, runs `bash -euo pipefail -c <terminalScript(groups, os.Getenv("SUMMERCMS_CLONE_URL"))>` in the work dir with stdin from /dev/null, `terminalEnv(os.Environ(), gobin)` and a 10-minute context, then requires exit 0 and `handled=true` in the combined output (logged on failure). With `SUMMERCMS_CLONE_URL` unset the commands run verbatim (cutover, after D-38). If a command needs setup that the check cannot provide without changing the page copy, stop and ask the user (D-40) instead of editing terminal.json.
|
|
6. Run the release build (for "defer-tag", with `SUMMERCMS_FRAMEWORK` pointing at the scratch clone), then `scripts/smoke.sh`, then `SUMMERCMS_TERMINAL_CHECK=1 SUMMERCMS_CLONE_URL=/media/nvme/dev/golem15/summercms.io/summercms/summercms.go go -C ../sm-summercmsio-app test -run '^TestTerminalCommands$' -count=1 -v .`. Commit in APP (including the updated plugin gitlink) as `feat: release builds from the v0.1.0 tag and the terminal command check`.
|
|
</action>
|
|
<verify>
|
|
<automated>../sm-summercmsio-app/scripts/build.sh && ../sm-summercmsio-app/scripts/smoke.sh</automated>
|
|
<fails_when>non-zero exit, a line starting "build:" on stderr, no "(release v0.1.0) ready" line, or no "smoke: ok" line</fails_when>
|
|
<automated>SUMMERCMS_REQUIRE_BUILD=1 go -C ../sm-summercmsio-app/plugins/golem15/summercms test ./... -run '^(TestLandingLinks|TestTerminalCommandsInPage|TestDocsHeaderSiteLink)$' -count=1 -v</automated>
|
|
<fails_when>non-zero exit, a "--- FAIL" or "--- SKIP" line, or fewer than three "--- PASS" lines</fails_when>
|
|
<automated>SUMMERCMS_TERMINAL_CHECK=1 SUMMERCMS_CLONE_URL=/media/nvme/dev/golem15/summercms.io/summercms/summercms.go go -C ../sm-summercmsio-app test -run '^TestTerminalCommands$' -count=1 -v .</automated>
|
|
<fails_when>non-zero exit, a "--- SKIP" line, or no "--- PASS: TestTerminalCommands" line</fails_when>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- For "tag-now": `git ls-remote --tags origin v0.1.0` in summercms.go prints one line ending in `refs/tags/v0.1.0`. For "defer-tag": `git tag -l v0.1.0` in summercms.go prints nothing and the SUMMARY names the scratch clone used.
|
|
- `go version -m ../sm-summercmsio-app/bin/summercms-io | grep 'git.golem15.com/golem15/summercms'` shows version `v0.1.0`.
|
|
- `grep -n 'docs:build --base-url /docs --site-url / --site-label summercms.io' ../sm-summercmsio-app/scripts/build.sh` finds a match.
|
|
- `grep -c 'class="site-link" href="/"' ../sm-summercmsio-app/plugins/golem15/summercms/public/docs/index.html` prints 1.
|
|
- `go -C ../sm-summercmsio-app test ./... -count=1` passes with the gated tests skipped (no env vars set), and `go -C ../sm-summercmsio-app vet ./...` is clean.
|
|
</acceptance_criteria>
|
|
<done>The release binary is built from the confirmed v0.1.0 tag (or a scratch tag when deferred), every landing link resolves against the built site with redirects followed, the page and the terminal check share one command list, and the six commands run from a fresh shell to handled=true.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 5: An operator can deploy, verify and roll back summercms.io on rome by following DEPLOY.md</name>
|
|
<files>../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, ../sm-summercmsio-app/scripts/check-deploy.sh, ../sm-summercmsio-app/plugins/golem15/summercms/README.md</files>
|
|
<read_first>
|
|
- .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-RESEARCH.md section "Deploy (question 6)" items 1-9, "Live site observations", Assumptions A1-A3, "Security Domain"
|
|
- .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-CONTEXT.md D-27 to D-32, D-38, D-42, D-43
|
|
- modules/surf/serve.go lines 20-80 (`serve --addr`, 10 s shutdown on SIGTERM), modules/lagoon/keygen.go (key:generate prints a key), modules/compass/README.md lines 85-100 (config dir and the dotenv file next to it)
|
|
- ../sm-summercmsio-app/scripts/build.sh and scripts/smoke.sh (as written in Tasks 1 and 4)
|
|
</read_first>
|
|
<action>
|
|
Write the deploy contract for rome (D-28 to D-32) and the user's launch steps (D-38, D-42, D-43). Values marked [ASSUMED] in RESEARCH are written as defaults with a "check on rome" note.
|
|
|
|
1. `deploy/nginx/summercms.io.conf` (D-29, D-30, D-31): a port-80 server for `summercms.io` and `www.summercms.io` answering `301 https://summercms.io$request_uri`; a 443 server for `www.summercms.io` answering 301 to the apex (rome has no 443 www block today, so https://www.summercms.io fails its TLS handshake); the 443 apex server with `listen 443 ssl http2`, `ssl_certificate /etc/letsencrypt/live/summercms.io/fullchain.pem`, `ssl_certificate_key /etc/letsencrypt/live/summercms.io/privkey.pem`, `include /etc/letsencrypt/options-ssl-nginx.conf`, `ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem` (comment: copy the lineage lines from the existing block if they differ), `gzip on; gzip_vary on; gzip_proxied any;` with types `text/css text/plain text/xml application/xml application/json text/javascript application/javascript image/svg+xml text/markdown`, `location ^~ /backend { return 404; }`, and `location /` with `limit_except GET HEAD { deny all; }`, `proxy_pass http://127.0.0.1:8095`, `proxy_http_version 1.1` and the `Host`, `X-Real-IP`, `X-Forwarded-For` and `X-Forwarded-Proto` headers.
|
|
2. `deploy/supervisor/summercms-io.conf` (D-32): `[program:summercms-io]` with `command=/srv/summercms-io/bin/summercms-io serve --addr 127.0.0.1:8095`, `directory=/srv/summercms-io` (config is read relative to the working directory), `user=summercms`, `environment=SUMMER_ENV="production"`, `autostart=true`, `autorestart=true`, `startsecs=3`, `stopsignal=TERM`, `stopwaitsecs=15`, `redirect_stderr=true`, `stdout_logfile=/var/log/supervisor/summercms-io.log` with size and backup limits.
|
|
3. `deploy/env.example`: the two variable names with no secret values (`SUMMER_DATABASE__DSN=postgres://summercms:<password>@127.0.0.1:5432/summercms_io?sslmode=disable`, `SUMMER_APP__KEY=`) and a comment that the real file lives at `/srv/summercms-io/.env`, owner summercms, mode 0600, never in git (T-11.2-08).
|
|
4. Rollback copy of today's site (D-31): download `https://summercms.io/` and `https://summercms.io/logo.png` with `curl -fsSL` into `deploy/rollback/under-construction/index.html` and `logo.png` (the live "Under construction" docroot is one HTML file plus the logo).
|
|
5. `DEPLOY.md` with these sections:
|
|
- Overview: what runs where (nginx :443 → 127.0.0.1:8095 summercms-io → Postgres 15 `summercms_io`), the user, port and paths (`summercms`, `127.0.0.1:8095`, `/srv/summercms-io/{bin,config,storage,.env}`), marked "check on rome" where assumed.
|
|
- Launch checklist (once, before cutover, done by the user): create the three repositories `golem15/sm-summercmsio-app`, `golem15/vue-summercmsio-app` and `golem15/sm-summercmsio-plugin` on git.golem15.com, then in each local repository `git remote add origin git@git.golem15.com:golem15/<name>.git` and `git push -u origin master`, submodules first (`ssu` can push submodules that are ahead) (D-43); make `golem15/summercms` public (D-38); confirm `git ls-remote --tags origin v0.1.0` in summercms.go, or create and push the tag now if it was deferred (D-42); run `SUMMERCMS_CHECK_EXTERNAL=1 go -C plugins/golem15/summercms test -run TestExternalLinks -count=1 -v ./...` and `SUMMERCMS_TERMINAL_CHECK=1 go test -run TestTerminalCommands -count=1 -v .` with no clone override; on rome check that port 8095 is free with `ss -ltnp`.
|
|
- One-time server setup: `adduser --system --group --home /srv/summercms-io --shell /usr/sbin/nologin summercms`; `sudo -u postgres createuser --pwprompt summercms`; `sudo -u postgres createdb -O summercms -E UTF8 summercms_io` (D-27); create the directories; write `/srv/summercms-io/.env` from `deploy/env.example` with the DSN and a key printed by `./bin/summercms-io key:generate`, `chown summercms:summercms` and `chmod 0600`; install the supervisor program and run `supervisorctl reread && supervisorctl update`.
|
|
- Build (local, D-28): `scripts/build.sh` (release from v0.1.0; `scripts/build.sh dev` builds from the framework working tree for previews); the server needs neither Go nor Node.
|
|
- Upload: `rsync -av --chmod=F644,D755 config/ rome:/srv/summercms-io/config/` and `rsync -av bin/summercms-io rome:/srv/summercms-io/bin/summercms-io.new`; `.env` is never rsynced.
|
|
- Release (every deploy): `cd /srv/summercms-io && sudo -u summercms ./bin/summercms-io.new migrate` (D-27, before restart; cwd matters), keep the old binary as `bin/summercms-io.prev`, move `.new` into place, `supervisorctl restart summercms-io`, `curl -sI http://127.0.0.1:8095/`.
|
|
- Cutover (D-31): first save the existing summercms.io server block from rome into `deploy/rollback/nginx-under-construction.conf` in this repository and commit it (it cannot be read from the build machine), keep the old docroot, then replace the block in place with `deploy/nginx/summercms.io.conf` and run `nginx -t && systemctl reload nginx`.
|
|
- Verify after cutover: curl checks for `https://summercms.io/` 200 with `Cache-Control: no-cache` and no robots-blocking header, a `/_nuxt/` asset with `immutable`, `/docs/` 200, `/docs/setup/installation` 301 to `.html`, `/backend` 404, `POST /` 403, `http://summercms.io/` 301 to https, `https://www.summercms.io/` 301 to the apex; then the external link check against the live site.
|
|
- Rollback: app (`mv bin/summercms-io.prev bin/summercms-io && supervisorctl restart summercms-io`; 11.2 adds no plugin migrations) and cutover (restore `deploy/rollback/nginx-under-construction.conf` and the docroot from `deploy/rollback/under-construction/`, then `nginx -t && systemctl reload nginx`).
|
|
6. `scripts/check-deploy.sh` (bash, `set -euo pipefail`): in a temp prefix, generate a self-signed certificate with openssl, copy the nginx site config with the certbot paths, the options include and the dhparam line replaced by temp equivalents, wrap it in a minimal `nginx.conf` (events block, an http block with temp `*_temp_path` entries and `pid` in the prefix), and run `nginx -t -p "$TMP" -c "$TMP/nginx.conf" -e "$TMP/error.log"`; parse the supervisor file with `python3` configparser and require `[program:summercms-io]` with `command`, `directory`, `user`, `autostart`, `autorestart`, `stopsignal`; print `check-deploy: ok`.
|
|
7. `README.md` in APP: what the app is, the submodule layout and `git submodule update --init`, `scripts/build.sh` and `dev`, the test commands and their environment gates (`SUMMERCMS_REQUIRE_BUILD`, `SUMMERCMS_TERMINAL_CHECK`, `SUMMERCMS_CLONE_URL`, `SUMMERCMS_CHECK_EXTERNAL`), and a pointer to DEPLOY.md. `README.md` in PLUG: what it serves (`/`, `/docs`, `/docs/{path...}`), the cache and content-type rules, the 301 for extension-less docs URLs, why it declares no admin controllers, and how `public/` is filled.
|
|
Commit in PLUG as `docs: describe the site plugin`, then in APP (with the updated plugin gitlink) as `docs: add DEPLOY.md with nginx, supervisor and rollback configs`.
|
|
</action>
|
|
<verify>
|
|
<automated>../sm-summercmsio-app/scripts/check-deploy.sh</automated>
|
|
<fails_when>non-zero exit, "test failed" or "[emerg]" in the nginx output, or no "check-deploy: ok" line</fails_when>
|
|
<human-check>At cutover, follow DEPLOY.md on rome from the launch checklist to "Verify after cutover": the site answers at https://summercms.io with the landing page, /docs serves the docs with the "summercms.io" header link, /backend is 404, and https://www.summercms.io redirects to the apex.</human-check>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `grep -n 'location ^~ /backend' ../sm-summercmsio-app/deploy/nginx/summercms.io.conf` and `grep -n 'limit_except GET HEAD' ../sm-summercmsio-app/deploy/nginx/summercms.io.conf` both find a match.
|
|
- `grep -n 'user=summercms' ../sm-summercmsio-app/deploy/supervisor/summercms-io.conf` and `grep -n '127.0.0.1:8095' ../sm-summercmsio-app/deploy/supervisor/summercms-io.conf` both find a match.
|
|
- `grep -c 'migrate' ../sm-summercmsio-app/DEPLOY.md` prints at least 1, and `grep -n 'createdb -O summercms' ../sm-summercmsio-app/DEPLOY.md`, `grep -n 'chmod 0600' ../sm-summercmsio-app/DEPLOY.md` and `grep -n 'git remote add origin' ../sm-summercmsio-app/DEPLOY.md` each find a match.
|
|
- `grep -n 'Rollback' ../sm-summercmsio-app/DEPLOY.md` finds a match and `test -s ../sm-summercmsio-app/deploy/rollback/under-construction/index.html` succeeds.
|
|
- `git -C ../sm-summercmsio-app ls-files -- .env deploy/.env` prints nothing (secrets are never committed).
|
|
</acceptance_criteria>
|
|
<done>DEPLOY.md, the nginx and supervisor configs, the env template and the rollback copy cover build, upload, release, cutover, verification and rollback on rome; the nginx config passes `nginx -t`; the user's launch steps (remotes, public repository, tag, external checks) are listed.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<threat_model>
|
|
## Trust Boundaries
|
|
|
|
| Boundary | Description |
|
|
|----------|-------------|
|
|
| internet → nginx (rome) | Untrusted HTTP requests; TLS terminates here |
|
|
| nginx → summercms-io on 127.0.0.1:8095 | Proxied requests; path and method filtered by nginx |
|
|
| request path → embedded file map | Untrusted path selects a file and may trigger a redirect |
|
|
| site.yaml / CLI flags → docs header HTML | Configured URL is written into every docs page |
|
|
| operator machine → rome | Binary and config upload; secrets stay on the server |
|
|
| summercms.go → module proxy | A pushed version tag becomes immutable public state |
|
|
|
|
## STRIDE Threat Register
|
|
|
|
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
|
|-----------|----------|-----------|----------|-------------|-----------------|
|
|
| T-11.2-04 | Information disclosure | PLUG static.go path resolution | high | mitigate | `path.Clean` on a fixed root, lookups only in the in-memory map built from the embedded tree, dot-segment paths refused (hides `.summer-docs`); smoke asserts `/docs/.summer-docs` 404 |
|
|
| T-11.2-05 | Spoofing (open redirect) | `/docs` and extension-less docs redirects | medium | mitigate | Location is a constant (`/docs/`) or `t.mount` + cleaned name + `.html`, only when that file exists; never from the raw URL |
|
|
| T-11.2-06 | Tampering (MIME sniffing) | static.go headers | low | mitigate | Own content-type table before `mime`, `X-Content-Type-Options: nosniff`; only build output is served |
|
|
| T-11.2-07 | Elevation of privilege | admin exposure | high | mitigate | The plugin declares no admin controllers, so cabana never mounts `/backend`; nginx `location ^~ /backend { return 404; }`; smoke asserts `/backend` 404 |
|
|
| T-11.2-08 | Information disclosure | DB password and app key | high | mitigate | Config YAML keeps `key` and `dsn` empty; secrets only in `/srv/summercms-io/.env` (0600, owner summercms), gitignored and never rsynced; `deploy/env.example` holds names only |
|
|
| T-11.2-09 | Tampering (XSS) | docs header `site_url` | medium | mitigate | `checkSiteURL` allow-list (http/https with host, or a single-slash path) for both the key and the flag; html/template escaping; TestParseSite rejects `javascript:` and `//host` |
|
|
| T-11.2-10 | Tampering (clickjacking) | public pages | low | accept | Static marketing and docs pages with no state-changing actions; the admin CSP is deliberately not reused because it blocks Nuxt's inline scripts (D-07) |
|
|
| T-11.2-11 | Information disclosure (transport) | nginx | high | mitigate | HTTPS only with certbot certificates, HTTP→HTTPS 301, a 443 www→apex block; the binary listens on loopback only |
|
|
| T-11.2-12 | Denial of service | nginx and binary | medium | mitigate | `limit_except GET HEAD` at nginx, GET-only routes (405 otherwise) and in-memory static files in the binary; supervisor restarts on exit |
|
|
| T-11.2-13 | Tampering / Repudiation | v0.1.0 tag | medium | mitigate | Blocking-human checkpoint before creation and push; annotated tag at a clean, green commit whose sha is shown to the user |
|
|
| T-11.2-14 | Elevation of privilege | runtime account | medium | mitigate | Dedicated nologin system user, dedicated Postgres role owning only `summercms_io` (D-27), app on 127.0.0.1 |
|
|
| T-11.2-SC | Tampering | dependency installs | high | mitigate | No new Go module (plugin and app import only stdlib and framework modules, checked by an acceptance criterion); build.sh installs npm packages only with `--frozen-lockfile` from plan 11.2-01's lockfile |
|
|
</threat_model>
|
|
|
|
<verification>
|
|
- summercms.go: `go vet ./... && go test ./internal/docsite ./cmd/summer -count=1` green; `scripts/check-phase11.1.sh --docs && scripts/check-phase11.1.sh --forbidden` green.
|
|
- APP: `scripts/build.sh` (release) or the scratch-clone equivalent, `scripts/smoke.sh`, `scripts/check-deploy.sh` green; `go -C ../sm-summercmsio-app vet ./... && go -C ../sm-summercmsio-app test ./... -count=1` green.
|
|
- PLUG: `SUMMERCMS_REQUIRE_BUILD=1 go -C ../sm-summercmsio-app/plugins/golem15/summercms test ./... -count=1` green.
|
|
- D-40: `TestTerminalCommands` green with the local clone override.
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- SC2: one binary embeds and serves `/` and `/docs` with indexable responses and the cache rules (smoke and plugin tests).
|
|
- SC3: every link on the page resolves against the built docs (TestLandingLinks); external links are checked at cutover.
|
|
- SC4: scripted build and DEPLOY.md with supervisor and nginx configs (TLS, proxy, gzip); the clean-server bring-up is the cutover UAT item.
|
|
- D-25, D-41, D-42, D-46 framework changes landed before the tag; the tag is created only after confirmation.
|
|
</success_criteria>
|
|
|
|
## User steps at cutover (D-38, D-42, D-43)
|
|
|
|
Creating the three remotes and pushing, making `golem15/summercms` public, pushing `v0.1.0` if it was deferred, running the external-link and verbatim terminal checks, saving rome's current server block, and the rome deploy itself. DEPLOY.md's launch checklist lists them; the SUMMARY repeats them.
|
|
|
|
## Artifacts this phase produces
|
|
|
|
- Repositories: `sm-summercmsio-app` (module `git.golem15.com/golem15/sm-summercmsio-app`, binary `summercms-io`) and `sm-summercmsio-plugin` (module `git.golem15.com/golem15/sm-summercmsio-plugin`, package `summercms`, plugin ID `golem15.summercms`) at `/media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app` and its `plugins/golem15/summercms`; submodule URLs `git@git.golem15.com:golem15/vue-summercmsio-app.git` and `git@git.golem15.com:golem15/sm-summercmsio-plugin.git`.
|
|
- Plugin symbols: `Plugin` (field `fsys`; methods `ID`, `Requires`, `Register`, `Boot`, `Routes`), `publicFS`, `newHandlers`, `tree` (fields `files`, `mount`, `immutable`, `htmlRedirect`; method `ServeHTTP`), `file` (fields `body`, `etag`, `ctype`), `newTree`, `errMissingIndex`, `siteImmutable`, `contentTypes`, `contentType`, `redirectTo`, `cacheImmutable`, `cacheNoCache`. Routes `GET /docs`, `GET /docs/{path...}`, `GET /`.
|
|
- Plugin tests: `TestStaticSmoke`, `TestLandingLinks`, `TestTerminalCommandsInPage`, `TestDocsHeaderSiteLink`, `TestExternalLinks`; helpers `requireBuild`, `pageLinks`, `resolve`.
|
|
- App test symbols: `terminalGroup`, `loadTerminal`, `terminalCloneURL`, `terminalScript`, `terminalEnv`, `TestTerminalCommands`.
|
|
- Environment variables: `SUMMERCMS_FRAMEWORK`, `SUMMERCMS_REQUIRE_BUILD`, `SUMMERCMS_TERMINAL_CHECK`, `SUMMERCMS_CLONE_URL`, `SUMMERCMS_CHECK_EXTERNAL`, `SUMMERCMS_SITE_DIR`, `SMOKE_PORT`; runtime `SUMMER_DATABASE__DSN`, `SUMMER_APP__KEY`, `SUMMER_ENV`.
|
|
- App config keys: `app.key`, `http.body_limits.default_bytes`, `http.body_limits.upload_bytes`, `http.trusted_proxies`, `storage.uploads.bucket_url`, `storage.uploads.public_path_prefix`, `queue.work_in_serve`, `database.dsn`.
|
|
- Scripts and deploy files: `scripts/build.sh` (`release`, `dev`), `scripts/smoke.sh`, `scripts/check-deploy.sh`, `DEPLOY.md`, `deploy/nginx/summercms.io.conf`, `deploy/supervisor/summercms-io.conf`, `deploy/env.example`, `deploy/rollback/under-construction/`.
|
|
- Framework: `docsite.Site.SiteURL`, `docsite.Site.SiteLabel`, `docsite.Options.SiteURL`, `docsite.Options.SiteLabel`, unexported `checkSiteURL`, `siteLabel`, `pageView.SiteURL`, `pageView.SiteLabel`; `site.yaml` keys `site_url`, `site_label`; CLI flags `--site-url`, `--site-label` on `summer docs:build` and `summer docs:serve`; CSS classes `.site-link`, `.site-link-label`; tests `TestSiteLink`, `TestDocsBuildSiteFlags` and new `TestParseSite` rows; git tag `v0.1.0` (after confirmation).
|
|
|
|
<output>
|
|
Create `.planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-02-SUMMARY.md` when done
|
|
</output>
|