docs(quick-261008-o6k): summercms.io/update updates one application in place
This commit is contained in:
@@ -31,7 +31,7 @@ See: .planning/PROJECT.md (updated 2026-09-16)
|
||||
Phase: 14.3 (One-command app scaffolding: summer new and summercms.io/new bootstrap script) — EXECUTING
|
||||
Plan: 4 of 4
|
||||
Status: Phase complete — ready for verification
|
||||
Last activity: 2026-10-08 - Completed quick task 261008-kdh: serve prints URL and admin status; summer new apps admin-ready
|
||||
Last activity: 2026-10-08 - Completed quick task 261008-o6k: summercms.io/update updates one application in place
|
||||
|
||||
Progress: [██████████] 100%
|
||||
|
||||
@@ -672,6 +672,7 @@ Recent decisions affecting current work:
|
||||
| 261008-k7s | Bootstrap script PATH hint per login shell (fish/zsh/bash) | 2026-10-08 | ddf1178 | [261008-k7s-bootstrap-script-path-hint-per-shell](./quick/261008-k7s-bootstrap-script-path-hint-per-shell/) |
|
||||
| 261008-kdf | summer delegates pass flags through, admin/route/key delegates, signal forwarding | 2026-10-08 | 50c8cee | [261008-kdf-delegated-summer-commands-pass-flags-thr](./quick/261008-kdf-delegated-summer-commands-pass-flags-thr/) |
|
||||
| 261008-kdh | serve prints URL and admin status; summer new apps admin-ready | 2026-10-08 | f70d80e | [261008-kdh-serve-startup-and-admin-feedback-admin-r](./quick/261008-kdh-serve-startup-and-admin-feedback-admin-r/) |
|
||||
| 261008-o6k | summercms.io/update: update one application in place (framework checkout, CLI, .env admin secret, build) | 2026-10-08 | 9b3803c | [261008-o6k-bootstrap-upgrade-mode-for-existing-chec](./quick/261008-o6k-bootstrap-upgrade-mode-for-existing-chec/) |
|
||||
|
||||
## Deferred Items
|
||||
|
||||
|
||||
@@ -0,0 +1,408 @@
|
||||
---
|
||||
phase: quick-261008-o6k
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- modules/sunrise/new.sh
|
||||
- modules/sunrise/sunrise.go
|
||||
- modules/sunrise/sunrise_test.go
|
||||
- modules/sunrise/script_behavior_test.go
|
||||
- modules/sunrise/README.md
|
||||
- docs/setup/installation.md
|
||||
- README.md
|
||||
- scripts/check-phase14.3.sh
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/plugin.go
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/routes_test.go
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/README.md
|
||||
- ../sm-summercmsio-app/README.md
|
||||
- ../sm-summercmsio-app/CLAUDE.md
|
||||
autonomous: true
|
||||
requirements: [QUICK-261008-o6k]
|
||||
|
||||
estimate:
|
||||
tokens: 190000
|
||||
raw_tokens: 190000
|
||||
tasks: 4
|
||||
confidence: low
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "From inside one application directory, `curl -fsSL https://summercms.io/update | bash` (or `curl -fsSL https://summercms.io/new | bash -s -- --update`) updates that application only. Without ./go.mod and ./summer.yaml, the script exits 1 with `run this from inside an sm-<name>-app directory` before any git or go call. Combining --update with any other argument exits 2 with the usage. A repeated --update is accepted."
|
||||
- "The checkout comes from the app's go.mod `replace git.golem15.com/golem15/summercms => <path>` (single-line or block form, optional version on the left, // comments stripped). A missing replace exits 1 with `has no replace for`, and a replace to a module version exits 1 with `not a local checkout`. A path that does not exist exits 1 with `does not exist`. A path that is not a clean framework checkout is refused by the shared check_framework_dir. SUMMERCMS_DIR is ignored with a warning."
|
||||
- "A detached checkout is fetched and moved fast-forward only to the newest vX.Y.Z tag, or to SUMMERCMS_REF, through the same move_detached function as create mode. At the target, it says `already at the target`. On a branch, a clean tree with an upstream runs `git pull --no-rebase --ff-only`, and SUMMERCMS_REF is ignored with a warning. A dirty tree, a missing upstream or a diverged branch exits 1, leaving HEAD, the branch and the files unchanged and installing and building nothing."
|
||||
- "Other sm-*-app directories (next to the app and next to the checkout) whose go.mod replace resolves physically to the same checkout get one `not rebuilt` line that names them. They are never built and their files are never written."
|
||||
- "summer is reinstalled with `GOWORK=off go -C <checkout> install ./cmd/summer`. If the app's .env exists and has no active SUMMER_ADMIN__JWT__SECRET line (compass loader rules), one line holding 44-character base64 from 32 random bytes is appended. The append keeps the file mode, adds a newline first when the file lacks one, runs only once and never prints the value. Then `<gobin>/summer build` runs in the app with stdin from /dev/null. A failed build exits 1 naming the app."
|
||||
- "A successful update ends on stdout with `==> Done. Run summer migrate, then restart summer serve.` followed by the PATH hint from the shared path_hint function. Create-mode stdout is byte-identical to before (pinned by an exact-equality case)."
|
||||
- "sunrise.UpdateScript() returns Script() with the final line `main \"$@\"` replaced by `main \"$@\" --update`. Script() is unchanged. Every proper prefix of either script runs nothing, and UpdateScript bytes piped to bash with no arguments run update mode."
|
||||
- "The site serves GET /update from sunrise.UpdateScript() with the same headers as GET /new (text/plain; charset=utf-8, nosniff, no-cache, strong ETag, 304 on match). The route tests, the plugin README and the site README cover it. The --forbidden gate allows only the literal SUMMER_ADMIN__JWT__SECRET in new.sh. go vet, go test -short, TestDocsTree, docs:build --check, --forbidden and --unit are green."
|
||||
artifacts:
|
||||
- path: modules/sunrise/new.sh
|
||||
provides: "--update mode: app detection, go.mod framework replace parsing, shared check_framework_dir and move_detached, ff-only branch pull, shared-checkout notice, summer reinstall, .env admin secret append, summer build, shared path_hint"
|
||||
contains: "--update"
|
||||
- path: modules/sunrise/sunrise.go
|
||||
provides: "UpdateScript(): Script() with the last line replaced by main \"$@\" --update"
|
||||
contains: "func UpdateScript"
|
||||
- path: modules/sunrise/script_behavior_test.go
|
||||
provides: "update: cases in TestScriptBehavior plus harness support (run in a directory, app fixtures, UpdateScript file, head/base64/openssl, STUB_SUMMER_FAIL)"
|
||||
contains: "update:"
|
||||
- path: modules/sunrise/sunrise_test.go
|
||||
provides: "TestUpdateScript, truncation proof over both scripts, gated end-to-end update run"
|
||||
contains: "TestUpdateScript"
|
||||
- path: docs/setup/installation.md
|
||||
provides: "## Updating an application section"
|
||||
contains: "## Updating an application"
|
||||
- path: scripts/check-phase14.3.sh
|
||||
provides: "narrowed SUMMER_ scan; TestUpdateScript and TestUpdateScriptRoute required"
|
||||
contains: "SUMMER_ADMIN__JWT__SECRET"
|
||||
- path: ../sm-summercmsio-app/plugins/golem15/summercms/plugin.go
|
||||
provides: "GET /update route serving sunrise.UpdateScript()"
|
||||
contains: "sunrise.UpdateScript()"
|
||||
- path: ../sm-summercmsio-app/plugins/golem15/summercms/routes_test.go
|
||||
provides: "TestUpdateScriptRoute and /update rows in the route-table tests"
|
||||
contains: "TestUpdateScriptRoute"
|
||||
key_links:
|
||||
- from: modules/sunrise/new.sh
|
||||
to: "internal/build/stubs/new.tmpl go.mod (`require <framework> v0.0.0` + `replace <framework> => <relative path>`)"
|
||||
via: "framework_replace mirrors internal/build/scaffold.go parseGoMod/splitReplace: single-line and block replace, // comments stripped, first field on each side"
|
||||
pattern: "replace"
|
||||
- from: modules/sunrise/sunrise.go
|
||||
to: modules/sunrise/new.sh
|
||||
via: "UpdateScript derives from the embedded bytes by one suffix replacement, so both URLs serve the same logic"
|
||||
pattern: "main \"\\$@\" --update"
|
||||
- from: ../sm-summercmsio-app/plugins/golem15/summercms/plugin.go
|
||||
to: modules/sunrise/sunrise.go
|
||||
via: "g.Get(\"/update\", scriptHandler(sunrise.UpdateScript()))"
|
||||
pattern: "UpdateScript"
|
||||
- from: modules/sunrise/new.sh
|
||||
to: "modules/compass/env.go readDotEnv"
|
||||
via: "presence check for SUMMER_ADMIN__JWT__SECRET follows the loader: trimmed lines, # comments skipped, `export ` prefix, key trimmed before ="
|
||||
pattern: "SUMMER_ADMIN__JWT__SECRET"
|
||||
- from: "new.sh path_hint"
|
||||
to: "create and update modes"
|
||||
via: "one function prints the per-shell PATH hint; exportHint/fishHint test helpers pin both"
|
||||
pattern: "path_hint"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Add update mode to the bootstrap script. Run from inside one application directory, it updates that application only:
|
||||
|
||||
cd sm-blog-app
|
||||
curl -fsSL https://summercms.io/update | bash
|
||||
|
||||
`curl -fsSL https://summercms.io/new | bash -s -- --update` does the same thing. Update mode reads the framework checkout from the app's go.mod replace. It moves the checkout forward (fast-forward only), names other apps that share the checkout without touching them, reinstalls summer, adds a missing admin token secret to this app's `.env`, runs `summer build` here and ends with the migrate and restart reminder and the PATH hint.
|
||||
|
||||
This replaces the rejected `--upgrade` design, which rebuilt every sibling sm-*-app. The user may have ten apps and does not want that.
|
||||
|
||||
Purpose: today, updating means manual git, go install and summer build steps. Re-running the one-liner with a name refuses an existing app, and apps created before 261008-kdh lack SUMMER_ADMIN__JWT__SECRET.
|
||||
|
||||
Output, in two repositories:
|
||||
- Framework (summercms.go): one `feat(sunrise)` commit with new.sh, sunrise.go (`UpdateScript`), tests, the sunrise README, docs/setup/installation.md, the root README and the narrowed check-phase14.3.sh.
|
||||
- Site (../sm-summercmsio-app): one commit in the plugin submodule (`plugins/golem15/summercms`, branch master) adding GET /update, then one parent commit that bumps the submodule pointer and updates the site README and CLAUDE.md.
|
||||
|
||||
No require bump, tag, push or deploy: the orchestrator does the release.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/gsd-core/workflows/execute-plan.md
|
||||
@~/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@CLAUDE.md
|
||||
@.planning/STATE.md
|
||||
@modules/sunrise/new.sh
|
||||
@modules/sunrise/sunrise.go
|
||||
@modules/sunrise/sunrise_test.go
|
||||
@modules/sunrise/script_behavior_test.go
|
||||
@modules/sunrise/README.md
|
||||
@docs/setup/installation.md
|
||||
@scripts/check-phase14.3.sh
|
||||
@internal/build/stubs/new.tmpl
|
||||
@../sm-summercmsio-app/plugins/golem15/summercms/plugin.go
|
||||
@../sm-summercmsio-app/plugins/golem15/summercms/script.go
|
||||
@../sm-summercmsio-app/plugins/golem15/summercms/routes_test.go
|
||||
@../sm-summercmsio-app/plugins/golem15/summercms/README.md
|
||||
|
||||
Facts established while planning (do not re-derive):
|
||||
|
||||
- **The uncommitted partial refactor in new.sh is KEPT.** The aborted executor split `check_framework_dir "$dir" "$hint"` out of `check_checkout`, and `move_detached "$dir" "$ref"` out of `checkout_framework`. Verified on the working tree during planning: `shellcheck -s bash` is clean, there is exactly one `shellcheck disable`, `grep -Ecw 'sudo|eval'` is 0, and `go test ./modules/sunrise/ -short` passes all 32 TestScriptBehavior cases, so create-mode messages are unchanged. Build on these two functions. The same diff also added header-comment and `usage` text for the rejected `--upgrade` design ("runs summer build in every sm-*-app next to the checkout"). Rewrite that text for `--update` in Task 1. Do not leave any `--upgrade` text behind (verify greps for it).
|
||||
- **Flag name is `--update`,** matching the /update URL. Remove `--upgrade` everywhere.
|
||||
- **UpdateScript design (one source of truth).** All logic lives in new.sh. `UpdateScript()` is `Script()` with its final line `main "$@"` (plus newline) replaced by `main "$@" --update` (plus newline). The flag goes after `"$@"`: then every proper prefix of the last line (`main`, `main "$@"`, `main "$@" --upd` and so on) calls main without a valid `--update` or with no arguments, and main refuses with usage exit 2. A cut-short download of either script still runs nothing, and the truncation test proves it for both. Prepending `set -- --update "$@"` was rejected because it puts a statement before the function definitions and breaks the "everything is a function, the last line calls main" invariant. A separate update.sh was rejected because it would duplicate abspath, check_go, check_framework_dir, move_detached, go_bin_dir and the hint code. `bash -s -- foo` against /update becomes `main foo --update`, which exits 2. `bash -s -- --update` against /update becomes `main --update --update`, which is accepted.
|
||||
- **Argument parsing.** Keep `-h/--help` exactly as today (only when it is the sole argument). Then count arguments: any argument equal to `--update` sets update mode, and every other argument counts as "other". Update mode with any other argument prints `error: --update takes no other argument`, a blank line and the usage to stderr, then exits 2. Under `set -e`, write the counter as `others=$((others + 1))`, never `((others++))`, which returns 1 when the value is 0 and kills the script. For bash 3.2 under `set -u`, guard the loop with `(($#))` before `for arg in "$@"`. Without `--update`, today's create path runs unchanged.
|
||||
- **App detection.** The app is `$PWD`. It must hold a regular `go.mod` and a regular `summer.yaml`; `summer build` needs the manifest, and the check keeps a plugin subdirectory, which has a go.mod, from being mistaken for an app. The framework module path is `git.golem15.com/golem15/summercms` (`frameworkModule` in internal/build; the test constant is `fwModule`). Keep the literal in one place: add a tiny `framework_module` function that prints it, and use it in check_framework_dir and the replace parser. check_framework_dir's messages must stay byte-identical (it interpolates the same string).
|
||||
- **Replace parsing** mirrors `internal/build/scaffold.go` parseGoMod/splitReplace. Read with `while IFS= read -r line || [[ -n $line ]]`, cut at the first `//`, trim, recognise `replace (` ... `)` blocks and single-line `replace X => Y`, take the first whitespace field of each side, and match the left field exactly against the framework module (a version may follow it). The function prints the right-hand side (first field plus any second field) or returns 1 when no replace for the framework exists. If the first right-hand field is double-quoted, strip the quotes. A target is a local path only when it is `.`, `..` or starts with `./`, `../` or `/` (Go's rule). Anything else is a module replace, which is refused. Resolve a relative target with `abspath "$PWD/$target"`. Normalise an absolute one with abspath too.
|
||||
- **The generated app layout** (`internal/build/stubs/new.tmpl`, golden `internal/build/testdata/new/go.mod.golden`): `require git.golem15.com/golem15/summercms v0.0.0` and `replace git.golem15.com/golem15/summercms => ../summercms` on one line, plus a second replace for `./plugins/demo`. The app's go.work only `use`s `.` and the plugin. `.env` holds SUMMER_ADMIN__JWT__SECRET for apps created since 261008-kdh. Older apps lack it.
|
||||
- **The --forbidden gate blocks the literal today.** `scripts/check-phase14.3.sh` `stage_forbidden` fails on `grep -nE 'SUMMER_' modules/sunrise/new.sh`, per 14.3 D-26: the script's own overrides are `SUMMERCMS_*`, because compass reads `SUMMER_*` as config. Writing one `.env` line keeps that intent, so Task 3 narrows the gate in the same commit. Do not dodge it by assembling the name from pieces. Keep the name once in non-comment code, as a `local key=` inside the function that writes `.env`, and refer to it only as `$key` elsewhere. `SUMMERCMS_` does not match `SUMMER_`.
|
||||
- **Create-mode output is pinned.** The acceptance stage greps ` cd /work/sm-demo-app` (-x) and `export PATH="/root/go/bin:$PATH"`. TestBootstrapEndToEnd greps ` cd <app>`, the export line and `(and add it to ~/.bashrc):`. The first usage line `Usage: curl -fsSL https://summercms.io/new | bash -s -- <name>` is pinned by the help case.
|
||||
- **14.3-SECURITY-REVIEW pins** (do not edit that file): new.sh is shellcheck clean except the one SC2016 directive for the literal `$PATH` in the hint, and `grep -Ecw 'sudo|eval'` on new.sh is 0 (T-14.3-16). That count includes comments: do not write those two words anywhere in new.sh.
|
||||
- **Admin secret format** (`internal/build/new.go` generateAdminSecret): standard base64 of 32 random bytes, 44 characters ending in `=`. Validate the generated value against `^[A-Za-z0-9+/]{43}=$` (regex held in a `local`, unquoted on the right of `=~`).
|
||||
- **The .env loader** (`modules/compass/env.go` readDotEnv) trims each line, skips blank lines and `#` comments, strips an `export ` prefix (export plus one space) and trims again, cuts at the first `=` and trims the key. "Already present" follows those rules: ` export SUMMER_ADMIN__JWT__SECRET = x` counts, `# SUMMER_ADMIN__JWT__SECRET=x` does not. An existing line with an empty value (after trimming, and after removing matching surrounding quotes) counts as present. Leave it alone and warn that it is empty, naming only the key and the file.
|
||||
- **Appending keeps the file mode.** `>>` opens with O_APPEND and keeps mode and owner. Never rewrite through a temp file. Detect a missing final newline without `tail`: after a `while IFS= read -r line` loop, a non-empty `$line` is an unterminated final line. Check that line for the key in the same pass.
|
||||
- **`.env` is protected in this environment.** A PreToolUse hook blocks Bash commands that read `.env` files. Tests read temp-dir `.env` files from Go, which is fine. Never `cat` or `grep` a `.env` from Bash while executing.
|
||||
- **macOS ships bash 3.2 for `curl | bash`.** Under `set -u`, bash before 4.4 treats `"${arr[@]}"` on an empty array as unbound. Guard such expansions with `${arr[@]+"${arr[@]}"}`, or collect into a newline- or space-joined scalar. No `mapfile`/`readarray` and no `shopt -s nullglob`: check each glob result with `[[ -d ]]`.
|
||||
- **Shellcheck:** write the upstream ref as `'@{u}'` with quotes (an unquoted brace trips SC1083). Keep `~` text inside format strings as the existing hint does (SC2088).
|
||||
- **Git facts for the tests:** `git clone` creates a local `main` tracking `origin/main` before the script detaches at the tag. After `h.mustRun("demo")`, `git -C <checkout> switch -q main` therefore gives a branch with an upstream, and `switch -q -c mine` gives one without. Use `pull --no-rebase --ff-only`, so a user's `pull.rebase` setting cannot turn the fast-forward into a rebase. The fake remote's `commit()` edits the tracked `VERSION` file.
|
||||
- **Harness facts:** PATH holds only `h.tools` (a git symlink plus go and docker stubs), so `head`, `base64` and `openssl` are missing unless the harness adds them. The summer stub overwrites `summer.call` with `>`. Update mode makes exactly one summer call (build), so `summer.call` stays sufficient and needs no append log. `h.mustRun("demo")` creates no app directory, because the stub writes nothing, so update cases create the app fixture themselves. The env is built from scratch with `SHELL=` defaulted before `h.env`, and later duplicates win. `cmd.Dir` and the `PWD=` env entry must name the same directory.
|
||||
- **Site:** the plugin's go.mod and the site go.work both `replace git.golem15.com/golem15/summercms => ../summercms.go` (relative), so the site builds against the framework working tree. Do not change any `require` version. `scripts/check-deploy.sh` and `scripts/smoke.sh` do not enumerate `/new` (checked), so they stay unchanged. nginx proxies every GET/HEAD path except `/backend`, so `/update` needs no nginx change. The plugin submodule is on branch `master` at 17ebc73, and the site parent tree is clean.
|
||||
- **Working tree:** `.planning/state.json` is modified by the orchestrator. Never stage it. Stage by explicit path.
|
||||
- **Commit messages carry no co-author or attribution trailer.** The user's global and project CLAUDE.md override the session attribution reminder.
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="tracer" tdd="true">
|
||||
<name>Task 1: `curl …/update | bash` end to end on a detached checkout: UpdateScript, argument parsing, app and go.mod resolution, move, reinstall, build, shared PATH hint</name>
|
||||
<files>modules/sunrise/new.sh, modules/sunrise/sunrise.go, modules/sunrise/sunrise_test.go, modules/sunrise/script_behavior_test.go</files>
|
||||
<behavior>
|
||||
- `--help` stdout still contains `Usage: curl -fsSL https://summercms.io/new | bash -s -- <name>` and `SUMMERCMS_REF`, and now also `https://summercms.io/update | bash` and `bash -s -- --update`. Stdout contains no `--upgrade`.
|
||||
- `--update demo` and `demo --update` exit 2. Stderr has `--update takes no other argument` and `Usage:`, and nothing starts (expectNothingStarted). `--update --update` inside a ready app is accepted and updates.
|
||||
- Not in an app: `--update` in h.work (no go.mod) exits 1 with `run this from inside an sm-<name>-app directory`, and nothing starts. An app dir with go.mod plus the framework replace but no summer.yaml exits 1 with the same substring and `summer.yaml`, with no go call.
|
||||
- No replace: an app whose go.mod has no framework replace exits 1 with `has no replace for git.golem15.com/golem15/summercms`. No go call.
|
||||
- Version replace: `replace git.golem15.com/golem15/summercms => git.golem15.com/golem15/summercms v0.1.13` exits 1 with `not a local checkout`. No go call.
|
||||
- Missing target: replace `=> ../nowhere` exits 1 with `does not exist`. No go call.
|
||||
- Foreign target: replace pointing at a dir whose go.mod is `module example.com/other` exits 1 with `is not a SummerCMS framework checkout`, and there is no go install.
|
||||
- Detached update (h.updateReady, then `h.remote.tag("v0.11.0")`): exits 0. HEAD is the v0.11.0 commit and still detached. Stderr has `updating the framework checkout` and `framework target: v0.11.0`. `go.install` is exactly `GOWORK=off dir=<work>/summercms\n`. `summer.call` is exactly `cwd=<app>\narg=build\n`. `summer.stdin` is empty. Stdout ends with `\n==> Done. Run summer migrate, then restart summer serve.\n` followed by `exportHint(gobin, "and add it to your shell profile")`.
|
||||
- Already at target: with no new tag, exit 0. Stderr has `already at the target`. HEAD is unchanged, and go install and summer build still run.
|
||||
- SUMMERCMS_REF on a detached checkout: with tags v0.11.0 and v0.12.0 pushed and `SUMMERCMS_REF=v0.11.0`, HEAD becomes v0.11.0.
|
||||
- Block form plus absolute path: the app's go.mod has `replace (` / `git.golem15.com/golem15/summercms v0.0.0 => <abs checkout> // local` / `)` and updates and builds.
|
||||
- Build failure: `STUB_SUMMER_FAIL=1` exits 1. Stderr has `summer build failed in <app>`. Stdout lacks `==> Done.`.
|
||||
- PATH hint: `SHELL=/usr/bin/fish` ends stdout with `fishHint(gobin)`. With gobinOnPath there is no hint.
|
||||
- Create-mode stdout is exact: a plain `h.mustRun("demo")` with empty SHELL gives stdout exactly `"\n==> Done. Start with:\n cd " + <work>/sm-demo-app + "\n" + exportHint(gobin, "and add it to your shell profile")`.
|
||||
- UpdateScript runs update mode: the harness writes `UpdateScript()` to a second file. Running it with no arguments in a ready app exits 0 and makes the same build call. Running it with argument `demo` exits 2 with `--update takes no other argument`.
|
||||
- TestUpdateScript: `UpdateScript()` equals `Script()` minus the trailing `main "$@"\n` plus `main "$@" --update\n`. Its last line is `main "$@" --update`, and it passes `bash -n`. Modifying a returned slice does not affect the next call. `Script()` still equals new.sh (TestScriptEmbedded unchanged).
|
||||
- TestScriptTruncationRunsNothing covers three variants. Script() with arg `demo` in an empty dir is today's. Script() with arg `--update` runs in an app-shaped temp tree. UpdateScript() with no args runs in the same kind of tree. For every proper prefix, no stub is invoked and the tree's file list is unchanged. For each variant, the full script reaches a stub (positive control). The stub list now also has openssl, head and base64.
|
||||
- All 32 existing TestScriptBehavior cases pass unchanged.
|
||||
</behavior>
|
||||
<action>
|
||||
Write the failing tests from `<behavior>` first, then the code.
|
||||
|
||||
modules/sunrise/new.sh. Every new piece is a function above `main`. Use tabs, `local` declarations, `</dev/null` on every git, go and summer call, and the file's comment style.
|
||||
|
||||
1. Header comment and `usage`: replace the synopsis line and paragraphs that the partial refactor wrote for the rejected rebuild-every-app design with `--update` text. The old flag spelling must be gone afterwards. Header synopsis lines: keep `curl -fsSL https://summercms.io/new | bash -s -- <name>`, and add `cd sm-<name>-app && curl -fsSL https://summercms.io/update | bash` plus the `.../new | bash -s -- --update` equivalent. The paragraph says: update mode runs inside one application, reads the framework checkout from its go.mod replace, moves it forward (detached: fast-forward only to the newest release; branch: git pull --ff-only), reinstalls summer, adds a missing admin token secret to this application's .env, runs summer build here, and names other applications sharing the checkout without rebuilding them. Note that SUMMERCMS_DIR is not used in update mode and SUMMERCMS_REPO only when cloning. Keep the closing sentence on which commands run: in update mode also openssl, or head and base64, and the installed summer. In `usage`, keep the first line byte-identical, add ` cd sm-<name>-app && curl -fsSL https://summercms.io/update | bash` and ` curl -fsSL https://summercms.io/new | bash -s -- --update`, and add a short update paragraph mirroring the header. Do not write the two T-14.3-16 words.
|
||||
|
||||
2. Add `framework_module` (prints the module path), and make check_framework_dir use it, with byte-identical messages. Move the PATH hint block at the end of `main` (from `if [[ :${PATH:-}: != *":$gobin:"* ]]` to its `fi`, with its comments and the single SC2016 directive) into `path_hint`, which takes gobin. Create mode calls `path_hint "$gobin"` where the block was. It remains the only disable directive.
|
||||
|
||||
3. Add `framework_replace` (arg: go.mod path), with the parsing rules from context. Add `is_local_path` (Go's rule).
|
||||
|
||||
4. Argument parsing in `main`, per context: help unchanged, then the `--update`/other counting. Update mode with others exits 2. Update mode alone calls `update_app` and returns. Without `--update`, today's path runs unchanged.
|
||||
|
||||
5. `update_app`, in this order (builtins only until step d):
|
||||
a. `app="$PWD"`. Without regular `$app/go.mod` or `$app/summer.yaml`, die with `run this from inside an sm-<name>-app directory: $app has no go.mod` (or `has no summer.yaml`).
|
||||
b. `framework_replace` fails: die with `$app/go.mod has no replace for <module>; --update works on applications whose go.mod points the framework at a local checkout, as summer new writes it`. A non-local first field: die with `$app/go.mod replaces <module> with <rhs>, not a local checkout; update that requirement with go get instead`.
|
||||
c. Resolve `dir` (context). If neither `-e` nor `-L` holds, die with `the framework replace in $app/go.mod points at $dir, which does not exist`. Refuse `/` like create mode. If SUMMERCMS_DIR is set and non-empty, warn `SUMMERCMS_DIR is ignored by --update; the framework checkout comes from go.mod ($dir)`. Task 2 adds the test for this warning.
|
||||
d. Check that git is installed (create mode's message) and run `check_go`. Skip check_docker.
|
||||
e. `check_framework_dir "$dir" "point the framework replace in $app/go.mod at a SummerCMS framework checkout"`.
|
||||
f. `git symbolic-ref -q --short HEAD`. On a branch, call `pull_branch "$dir" "$branch" "$ref"`. In this task its body dies with `$dir is on branch $branch` (Task 2 writes the pull in the same uncommitted change, so this never ships). Detached: `move_detached "$dir" "$ref"` with `ref="${SUMMERCMS_REF:-}"`.
|
||||
g. Call `note_shared_apps "$dir" "$app"`, a no-op that Task 2 fills.
|
||||
h. gobin via `go_bin_dir` (create mode's die message), `say "installing summer into $gobin"`, `GOWORK=off go -C "$dir" install ./cmd/summer </dev/null`.
|
||||
i. Call `ensure_admin_secret "$app"`, a no-op that Task 2 fills.
|
||||
j. `say "building $app"`, then `"$gobin/summer" build </dev/null || die "summer build failed in $app; fix the error above and run the update again"`.
|
||||
k. `printf '\n==> Done. Run summer migrate, then restart summer serve.\n'`, then `path_hint "$gobin"`.
|
||||
|
||||
modules/sunrise/sunrise.go: update the package doc to cover both URLs. Add `UpdateScript() []byte`, built from the embedded bytes per context, returning a fresh copy on every call. Compute the update bytes once at package init through a helper that panics with a clear message if new.sh does not end with `main "$@"\n`. TestScriptShape already pins that suffix, so a broken embed fails every test instead of silently serving create mode at /update. Its doc comment states the derivation and why the flag follows `"$@"`.
|
||||
|
||||
modules/sunrise/script_behavior_test.go:
|
||||
- Refactor `run` onto `runFile(file, dir string, args ...string)` (cmd.Dir and `PWD=` both set to dir; the unsetShell path sources `file`). Add `runIn(dir, args...)`. Write `UpdateScript()` to `h.updateFile` in newScriptHarness.
|
||||
- Summer stub: after reading stdin, `[[ -z ${STUB_SUMMER_FAIL:-} ]] || exit 1`.
|
||||
- Helpers: `replaceLine(target)` returns `replace ` + fwModule + ` => ` + target. `h.appAt(dir, gomod)` writes go.mod and a `summer.yaml` (`name: x`) and returns dir. `h.app(name, gomod)` places it at `<work>/sm-<name>-app`. `h.updateReady()` runs `h.mustRun("demo")`, creates `h.app("blog", "module example.com/blog\n\n" + replaceLine("../summercms") + "\n")`, runs `h.resetLogs()` and returns the app dir.
|
||||
- Add one case per `<behavior>` bullet, with names prefixed `update:`, and raise the `len(cases) < 32` floor to the new total.
|
||||
|
||||
modules/sunrise/sunrise_test.go: add TestUpdateScript. Restructure TestScriptTruncationRunsNothing into the three variants. The app-shaped tree is `<tmp>/sm-demo-app/{go.mod with replaceLine("../summercms"), summer.yaml}` plus `<tmp>/summercms/go.mod` (`module` fwModule). Compare a recursive file listing before and after each prefix. Positive control: the full script reaches the stub go through check_go. Add `openssl`, `head` and `base64` to the stub names. Keep the `len(prefixes) < 50` floor per variant.
|
||||
|
||||
Do not commit in this task.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /media/nvme/dev/golem15/summercms.io/summercms/summercms.go && set -o pipefail && shellcheck -s bash modules/sunrise/new.sh && test "$(grep -c 'shellcheck disable' modules/sunrise/new.sh)" -eq 1 && test "$(grep -Ecw 'sudo|eval' modules/sunrise/new.sh)" -eq 0 && test "$(grep -c -- '--upgrade' modules/sunrise/new.sh)" -eq 0 && bash -n modules/sunrise/new.sh && go vet ./modules/sunrise/ && go test ./modules/sunrise/ -short -count=1 -v -run '^(TestScriptEmbedded|TestScriptShape|TestScriptTruncationRunsNothing|TestScriptBehavior|TestScriptShellcheck|TestUpdateScript)$' 2>&1 | grep -E '^(--- |ok|FAIL|panic)'</automated>
|
||||
</verify>
|
||||
<done>shellcheck is clean, with exactly one disable directive, zero T-14.3-16 words and no `--upgrade` left in new.sh. go vet passes. The six named tests show `--- PASS:` (not SKIP), with no FAIL. The 32 original cases and every Task 1 `update:` case pass. UpdateScript is Script() with only the last line changed.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Branch fast-forward pull, shared-checkout notice and the .env admin secret</name>
|
||||
<files>modules/sunrise/new.sh, modules/sunrise/script_behavior_test.go, modules/sunrise/sunrise_test.go</files>
|
||||
<behavior>
|
||||
- Branch fast-forward: after h.updateReady(), `switch -q main` in the checkout and `h.remote.commit()`, the run exits 0. HEAD equals the new remote main commit, the checkout is still on `main`, and go install and the build ran. Stderr has `pulling main from origin/main (fast-forward only)` and `framework checkout moved from`.
|
||||
- Branch up to date: a second run exits 0 with `framework checkout is already up to date`, and HEAD is unchanged.
|
||||
- Dirty branch: on `main`, with an edited tracked `VERSION` and a new remote commit, the run exits 1 with `uncommitted changes to tracked files`. HEAD, the branch and the file bytes are unchanged, there is no go install and no summer call.
|
||||
- No upstream: on `switch -q -c mine`, the run exits 1 with `has no upstream`. HEAD and the branch are unchanged, with no go install and no summer call.
|
||||
- Diverged: on `main` with a local commit and a new remote commit, the run exits 1 with `not a fast-forward`. HEAD is unchanged and there is no summer call.
|
||||
- SUMMERCMS_REF on a branch: `SUMMERCMS_REF=v0.2.0` on `main` with a new remote commit gives the warning `SUMMERCMS_REF is ignored for a branch checkout`. HEAD is the new main commit, not v0.2.0.
|
||||
- SUMMERCMS_DIR ignored: `SUMMERCMS_DIR=elsewhere` warns `SUMMERCMS_DIR is ignored by --update`, and the update still uses the go.mod checkout and exits 0.
|
||||
- Shared checkout: `sm-alpha-app` (replace `../summercms`) and `sm-beta-app` (block replace with the absolute checkout path) are next to the checkout, plus `sm-other-app` whose replace is `../elsewhere` (an existing dir with a framework go.mod is not needed; it just must not match). Updating `sm-blog-app` puts on stderr exactly one line containing `not rebuilt` that names `<work>/sm-alpha-app` and `<work>/sm-beta-app` and not sm-other-app or sm-blog-app. `summer.call` cwd is sm-blog-app only. The alpha and beta `.env` files (which lack the key) are byte-identical afterwards.
|
||||
- App elsewhere: an app at `<root>/apps/sm-far-app` with an absolute replace to `<work>/summercms` still lists `<work>/sm-alpha-app` (scanned next to the checkout), and updates and builds sm-far-app.
|
||||
- No other apps: stderr lacks `not rebuilt`.
|
||||
- Env append (fallback generator: no openssl in tools, real head and base64): the app `.env` is `SUMMER_DATABASE__DSN=x\n` at mode 0640. Afterwards it is the original bytes plus one line `SUMMER_ADMIN__JWT__SECRET=<v>\n`, where `<v>` matches `^[A-Za-z0-9+/]{43}=$`. The mode is still 0640. Stderr has `added SUMMER_ADMIN__JWT__SECRET to <app>/.env`. Neither stdout nor stderr contains `<v>`. The app was built.
|
||||
- Idempotent: a second run leaves `.env` byte-identical and its stderr lacks `added SUMMER_ADMIN__JWT__SECRET`.
|
||||
- No trailing newline: `SUMMER_DATABASE__DSN=x` becomes `SUMMER_DATABASE__DSN=x\nSUMMER_ADMIN__JWT__SECRET=<v>\n`.
|
||||
- Loader-form present: ` export SUMMER_ADMIN__JWT__SECRET = abc\n` is left byte-identical. A commented `# SUMMER_ADMIN__JWT__SECRET=old\n` does not count, so the line is appended after it.
|
||||
- Empty value: `SUMMER_ADMIN__JWT__SECRET=\n` is left byte-identical. Stderr warns `is empty`, and the app is still built.
|
||||
- openssl preferred: a tools/openssl stub logs its args and prints `QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVphYmNkZWY=`. It is called with `rand -base64 32`, `.env` gets that value, and no output contains it.
|
||||
- Generator failure: an openssl stub printing `short`, with `head` removed from tools, exits 1. `.env` is unchanged. Stderr names the manual fix (`add a SUMMER_ADMIN__JWT__SECRET line`), and summer is not called.
|
||||
- No .env: the app builds, has no `.env` afterwards, and stderr says `no .env in <app>`.
|
||||
</behavior>
|
||||
<action>
|
||||
modules/sunrise/new.sh:
|
||||
|
||||
1. `pull_branch` (dir, branch, ref) replaces the Task 1 refusal:
|
||||
- A non-empty ref warns `SUMMERCMS_REF is ignored for a branch checkout; $branch is pulled from its upstream`.
|
||||
- Get the upstream with `git -C "$dir" rev-parse --abbrev-ref --symbolic-full-name '@{u}' </dev/null 2>/dev/null`. On failure, die with `$dir is on branch $branch, which has no upstream; the checkout was not changed. Set one with git branch --set-upstream-to, or pull it yourself`.
|
||||
- The clean-tree check already ran in check_framework_dir.
|
||||
- Save HEAD, then `say "pulling $branch from $upstream (fast-forward only)"`. Run `git -C "$dir" pull --quiet --no-rebase --ff-only </dev/null` inside an `if`. On failure, die with `git pull --ff-only in $dir failed: $branch has diverged from $upstream (not a fast-forward) or the fetch failed; the checkout was not changed`.
|
||||
- Compare HEAD: unchanged gives `framework checkout is already up to date`, otherwise `framework checkout moved from <old 12> to <new 12>`.
|
||||
|
||||
2. `note_shared_apps` (dir, app):
|
||||
- Take the physical checkout path (`cd "$dir" && pwd -P`) and the physical app path.
|
||||
- Scan `<physical parent of app>/sm-*-app`, and also `<physical parent of checkout>/sm-*-app` when that parent differs.
|
||||
- For each candidate that is a directory (`[[ -d ]]`), is not the app (physical compare) and has a readable go.mod: take framework_replace's first field. If it is a local path, resolve it against the candidate dir, and if the target is a directory, compare its `pwd -P` (subshell) with the checkout's.
|
||||
- Collect matches into a space-joined scalar.
|
||||
- When non-empty, print one line: `say "not rebuilt, also using $dir:$list; each picks up the new framework on its next summer build"`.
|
||||
- This function never writes, builds or reads anything but go.mod.
|
||||
|
||||
3. `ensure_admin_secret` (app), with the key name held once as `local key=` (context):
|
||||
- If `$app/.env` is not a regular file, `say "no .env in $app; skipped the admin token secret"` and return.
|
||||
- If it is not readable and writable, die.
|
||||
- Scan with the loader rules from context. After the loop, an unterminated final line gets the same check and marks that a newline is needed.
|
||||
- Key present: warn `$key in $app/.env is empty; the admin refuses to start until it has a value` only when the value is empty, then return.
|
||||
- Generate with `generate_secret`. It prefers `openssl rand -base64 32 </dev/null` when `command -v openssl` succeeds. Otherwise it uses `head -c 32 /dev/urandom | base64` when head, base64 and a readable /dev/urandom exist. It strips newlines and carriage returns with a builtin expansion, validates against the regex and prints the value, or returns 1. The value travels only through command substitution and the printf builtin.
|
||||
- Generation failed: die with `could not generate $key for $app/.env (needs openssl, or head and base64); add a $key line with any long random value, then run the update again`. The tests pin the substring `add a SUMMER_ADMIN__JWT__SECRET line`, which comes from `$key`.
|
||||
- Append with one redirection, `{ [newline if needed]; printf '%s=%s\n' "$key" "$secret"; } >>"$env"`, inside an `if`. On failure, die. On success, `say "added $key to $app/.env"`. Never print the value.
|
||||
|
||||
modules/sunrise/script_behavior_test.go:
|
||||
- Symlink the real `head` and `base64` into h.tools the way git is (LookPath, else t.Skip).
|
||||
- Add `h.stubOpenssl(output string)`, which writes tools/openssl that appends `$*` to `$STUB_LOG/openssl.calls` and prints output. Add `openssl.calls` to resetLogs.
|
||||
- Add `writeEnv(t, path, data, mode)`, which uses WriteFile then Chmod so the umask cannot change the mode.
|
||||
- Add `envValue(t, data, key)`, which scans in Go. Failure messages never print `.env` bytes beyond the key name and the value length.
|
||||
- Add one case per `<behavior>` bullet, prefixed `update:`. For the branch cases, use the git facts. Assert HEAD with h.head(), the branch with `symbolic-ref --short HEAD` and bytes with os.ReadFile. Raise the case floor.
|
||||
|
||||
modules/sunrise/sunrise_test.go, gated TestBootstrapEndToEnd. Keep every existing assertion and the final summer build. The httptest server now serves `/update` with UpdateScript() and everything else with Script(); the existing run uses `/new`. After the build:
|
||||
- Remove the `SUMMER_ADMIN__JWT__SECRET=` line from `sm-demo-app/.env` in Go, keeping the mode.
|
||||
- Run `curl -fsSL "$1" | bash` with `$1` = srv.URL+"/update`, cwd = the app and the same env.
|
||||
- Require exit 0 and output containing `already at the target`, `added SUMMER_ADMIN__JWT__SECRET to <app>/.env` and `Run summer migrate, then restart summer serve`, without the new value.
|
||||
- Require `.env` to hold exactly one active key line with a 44-character value at mode 0600, and `bin/demo` to exist.
|
||||
|
||||
The e2e run is gated and is not part of this task's acceptance.
|
||||
|
||||
Do not commit in this task.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /media/nvme/dev/golem15/summercms.io/summercms/summercms.go && set -o pipefail && shellcheck -s bash modules/sunrise/new.sh && test "$(grep -c 'shellcheck disable' modules/sunrise/new.sh)" -eq 1 && test "$(grep -Ecw 'sudo|eval' modules/sunrise/new.sh)" -eq 0 && test "$(grep -v '^[[:space:]]*#' modules/sunrise/new.sh | grep -o 'SUMMER_ADMIN__JWT__SECRET' | wc -l)" -eq 1 && bash -n modules/sunrise/new.sh && go vet ./modules/sunrise/ && go test ./modules/sunrise/ -short -count=1 -v -run '^(TestScriptEmbedded|TestScriptShape|TestScriptTruncationRunsNothing|TestScriptBehavior|TestScriptShellcheck|TestUpdateScript)$' 2>&1 | grep -E '^(--- |ok|FAIL|panic)'</automated>
|
||||
</verify>
|
||||
<done>shellcheck is clean with one disable directive, and the key name appears once in non-comment code. The six sunrise tests PASS (no SKIP or FAIL), with all original, Task 1 and Task 2 `update:` cases green. TestBootstrapEndToEnd compiles and skips because it is gated.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Framework docs, narrowed gate, full framework gates, one feat(sunrise) commit</name>
|
||||
<files>modules/sunrise/README.md, docs/setup/installation.md, README.md, scripts/check-phase14.3.sh</files>
|
||||
<action>
|
||||
1. modules/sunrise/README.md:
|
||||
- H1 summary becomes `Bootstrap script that installs the summer CLI and creates a new SummerCMS application, or updates an existing one, in one command, embedded so it is versioned with the framework.`, and the root README modules table row is updated to the same sentence (CLAUDE.md rule).
|
||||
- Overview: add a paragraph on `cd sm-blog-app && curl -fsSL https://summercms.io/update | bash` (and `.../new | bash -s -- --update`) covering the Task 1 and 2 steps in plain prose: go.mod and summer.yaml required; checkout read from the go.mod replace; detached fast-forward only and branch `git pull --ff-only` with its refusals; other apps on the same checkout named and not rebuilt; summer reinstalled; a missing SUMMER_ADMIN__JWT__SECRET appended to this app's .env once and never printed; summer build here; the migrate and restart reminder. Say that `sunrise.UpdateScript` serves the same script with update mode preset.
|
||||
- Features: add bullets for update mode, the shared-checkout notice and the secret append. In the "No privilege escalation and no dynamic code" bullet, add that update mode also runs openssl, or head and base64, and the installed summer.
|
||||
- Usage: add the update fence, and a sentence that it takes no other argument and works the same from bash, zsh and fish because the piped script runs in bash. In the Go example, add a sentence (or a second handler) serving `sunrise.UpdateScript()` the same way at a second path.
|
||||
- API reference: add a `sunrise.UpdateScript` row.
|
||||
- Configuration: the script never reads or exports `SUMMER_`-prefixed variables. The only such name it writes is SUMMER_ADMIN__JWT__SECRET, appended to the application's .env by update mode. Note that SUMMERCMS_DIR is ignored (with a warning) in update mode because the checkout comes from go.mod, that SUMMERCMS_REF applies to detached checkouts and is ignored on a branch, and that SUMMERCMS_REPO is used only when cloning.
|
||||
- Testing: the harness covers update mode, and the gated run also updates the generated app through /update.
|
||||
- No consuming-application names. Only real identifiers (`sunrise.Script`, `sunrise.UpdateScript`).
|
||||
|
||||
2. docs/setup/installation.md: add `## Updating an application` after "Run the application" and before "Create an application without the script".
|
||||
- A `sh` fence with `cd sm-demo-app` and `curl -fsSL https://summercms.io/update | bash`. Say it works the same from bash, zsh and fish, and mention the `.../new | bash -s -- --update` form.
|
||||
- A numbered list of what it does: the same content as the README overview, including the refusals (dirty tree, no upstream, diverged branch, module-version replace), which leave the checkout unchanged, the shared-checkout line, and the .env secret for apps created before the admin was wired in.
|
||||
- Then an `sh` fence with `summer migrate` and `summer serve` (restart).
|
||||
- A short manual-update paragraph with an `sh` fence: `git -C ../summercms fetch --tags`, `git -C ../summercms checkout --detach vX.Y.Z` (with `git -C ../summercms tag -l 'v*' --sort=-v:refname` listing tags newest first, or `git -C ../summercms pull --ff-only` for a branch), `go -C ../summercms install ./cmd/summer`, `summer build`, `summer migrate`. Say that a missing SUMMER_ADMIN__JWT__SECRET needs one .env line with a long random value, for example from `openssl rand -base64 32`.
|
||||
- Use only `sh` fences. In "What the script does" step 2, after the branch-checkout refusal, add a clause linking `#updating-an-application`: the update script pulls a branch checkout instead. Note SUMMERCMS_DIR is not used by the update script. Leave the frontmatter unchanged.
|
||||
|
||||
3. Root README.md, after the sentence at line 32 pointing at sunrise: add one sentence. Later, `curl -fsSL https://summercms.io/update | bash` run inside the application moves the framework checkout forward, reinstalls summer and rebuilds that application. Also update the table row (step 1).
|
||||
|
||||
4. scripts/check-phase14.3.sh:
|
||||
- In `stage_forbidden`, replace the single `grep -nE 'SUMMER_'` check with two checks. (a) Every `grep -noE 'SUMMER_[A-Z0-9_]*'` token in new.sh must be exactly SUMMER_ADMIN__JWT__SECRET. Otherwise fail with `new.sh names a framework config variable other than SUMMER_ADMIN__JWT__SECRET`. (b) Fail on `grep -nE '\$\{?SUMMER_|export[[:space:]]+SUMMER_'` with `new.sh reads or exports a framework config variable`.
|
||||
- Update the `--forbidden` header paragraph to match.
|
||||
- Add `TestUpdateScript` to REQUIRED_TESTS and `TestUpdateScriptRoute` to SITE_PLUGIN_TESTS. The site test lands in Task 4, so run --unit only after Task 4.
|
||||
- Change nothing else.
|
||||
|
||||
5. Run the verify gates; `go test ./... -short` may take minutes. Negative self-check without committing: copy new.sh to the scratchpad, append a line containing `$SUMMER_APP__KEY` and another containing `SUMMER_APP__KEY=x`, and run the two new grep expressions against the copy. (b) must match the first line and (a) the second.
|
||||
|
||||
6. Commit exactly the eight framework files (new.sh, sunrise.go, sunrise_test.go, script_behavior_test.go, sunrise README, installation.md, README.md, check-phase14.3.sh), staged by explicit path, never .planning/state.json. Subject: `feat(sunrise): update an application in place with summercms.io/update`. The body names: --update mode run inside one app, the checkout read from the go.mod replace, the detached and branch fast-forward paths, the shared-checkout notice, the summer reinstall, the .env admin secret, summer build, UpdateScript and the narrowed --forbidden scan. No co-author or attribution trailer. No tag or push.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /media/nvme/dev/golem15/summercms.io/summercms/summercms.go && grep -qF '## Updating an application' docs/setup/installation.md && grep -qF 'https://summercms.io/update | bash' docs/setup/installation.md && grep -qF 'sunrise.UpdateScript' modules/sunrise/README.md && grep -qF 'https://summercms.io/update' README.md && test -z "$(grep -rn -- '--upgrade' modules/sunrise docs/setup/installation.md README.md)" && test -z "$(grep -noE 'SUMMER_[A-Z0-9_]*' modules/sunrise/new.sh | grep -v ':SUMMER_ADMIN__JWT__SECRET$')" && grep -qF 'TestUpdateScriptRoute' scripts/check-phase14.3.sh && go vet ./... && go test ./cmd/summer -run '^TestDocsTree$' -count=1 && go run ./cmd/summer docs:build --check && bash scripts/check-phase14.3.sh --forbidden && go test ./... -short -count=1</automated>
|
||||
</verify>
|
||||
<done>go vet and go test ./... -short are green. TestDocsTree passes and docs:build --check reports nothing. `check-phase14.3.sh --forbidden` prints "Phase 14.3 forbidden-surface scan passed", and the negative self-check catches both an expansion and another SUMMER_ name. One framework commit holds the eight files, with no trailer and without state.json.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 4: Site GET /update route, tests, READMEs; submodule commit, pointer bump, cross-repo gates</name>
|
||||
<precondition>`git -C /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app status --porcelain` is empty and `git -C .../sm-summercmsio-app/plugins/golem15/summercms branch --show-current` prints master; if not, stop and report.</precondition>
|
||||
<files>../sm-summercmsio-app/plugins/golem15/summercms/plugin.go, ../sm-summercmsio-app/plugins/golem15/summercms/routes_test.go, ../sm-summercmsio-app/plugins/golem15/summercms/README.md, ../sm-summercmsio-app/README.md, ../sm-summercmsio-app/CLAUDE.md</files>
|
||||
<action>
|
||||
1. plugin.go: register `g.Get("/update", scriptHandler(sunrise.UpdateScript()))` right after `/new` and before `/`. Update the package doc and the Routes doc comment to name GET /update (the update script from sunrise.UpdateScript). script.go stays unchanged: scriptHandler already takes the body, and the name it passes to ServeContent is unused because Content-Type is set.
|
||||
|
||||
2. routes_test.go:
|
||||
- TestRoutesAssemble gets `/update` rows: GET 200 with body `string(sunrise.UpdateScript())`, HEAD 200, POST 405.
|
||||
- Extract TestNewScriptRoute's body into `checkScriptRoute(t, h, target string, want []byte) (etag string)`. TestNewScriptRoute calls it for /new.
|
||||
- Add TestUpdateScriptRoute, which calls it for /update (body equals UpdateScript(), same four headers, strong 16-hex ETag, 304 on a match, 200 on a stale tag) and asserts its ETag differs from /new's.
|
||||
- TestRoutesCoexistWithAdminPatterns: wantPatterns becomes `GET /docs`, `GET /docs/{path...}`, `GET /new`, `GET /update`, `GET /`, and pluginHandlers gains `"GET /update": scriptHandler(sunrise.UpdateScript())`.
|
||||
|
||||
3. Plugin README.md: add the `GET /update` row (sunrise.UpdateScript(), same headers, for `curl -fsSL https://summercms.io/update | bash` run inside an application directory).
|
||||
|
||||
4. Site README.md line 3 and CLAUDE.md line 7: add a short mention of the update script at `/update` (`sunrise.UpdateScript()`, `curl -fsSL https://summercms.io/update | bash`) beside the existing `/new` mention. Change nothing else. scripts/check-deploy.sh, scripts/smoke.sh, nginx and DEPLOY.md stay unchanged (see context).
|
||||
|
||||
5. Verify (command below). Then commit inside the submodule. Run `ssu status` first if useful, and stage the three plugin files by path. Message: `feat: serve the SummerCMS update script at GET /update`, with a body naming the route, the shared headers and the tests.
|
||||
|
||||
6. In the site parent, stage `plugins/golem15/summercms`, README.md and CLAUDE.md by path and commit `feat: bump the site plugin to its GET /update route`. No trailers, no push, no require bump, no deploy.
|
||||
|
||||
7. Cross-repo gate: from the framework repo, run `bash scripts/check-phase14.3.sh --unit`. It runs the full non-short suite (Docker), the site plugin and app tests with named PASS for TestNewScriptRoute, TestUpdateScriptRoute and TestRoutesAssemble, pnpm generate and test, and --forbidden. It can exceed ten minutes, so run it in the background with a generous timeout and wait for it. Record the result in the SUMMARY. Optional, when network and time allow: `SUMMERCMS_BOOTSTRAP_CHECK=1 go test ./modules/sunrise/ -run '^TestBootstrapEndToEnd$' -count=1 -timeout 20m`. Record the result too.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app && set -o pipefail && go -C plugins/golem15/summercms vet ./... && go -C plugins/golem15/summercms test ./... -count=1 -v -run '^(TestRoutesAssemble|TestNewScriptRoute|TestUpdateScriptRoute|TestRoutesCoexistWithAdminPatterns)$' 2>&1 | grep -E '^(--- |ok|FAIL|panic)' && go vet ./... && go test ./... -count=1 && grep -qF 'GET /update' plugins/golem15/summercms/README.md && grep -qF '/update' README.md && git -C plugins/golem15/summercms diff --quiet go.mod && git diff --quiet HEAD -- go.mod</automated>
|
||||
</verify>
|
||||
<done>The four site route tests PASS. The site plugin and app vet and test green. Neither go.mod changed. One submodule commit holds the three plugin files. One parent commit bumps the pointer and updates README.md and CLAUDE.md. `check-phase14.3.sh --unit` prints "Phase 14.3 unit stage passed". Nothing is pushed, tagged or deployed.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| remote repository → framework checkout | `git fetch`/`git pull` bring upstream refs; the checkout moves only by fast-forward |
|
||||
| app go.mod → script | the replace target picks which checkout is updated, compiled into summer and executed |
|
||||
| script → app `.env` | the script appends a secret the framework reads as configuration |
|
||||
| script output → terminal and logs | output may land in CI logs; it must never contain the secret |
|
||||
| site → curl \| bash | GET /update bytes are executed by the user's shell |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-o6k-01 | Tampering | checkout move in update mode | high | mitigate | Detached checkouts go through the existing move_detached (tags never force-updated, ancestor check). Branch checkouts run `pull --no-rebase --ff-only` only when the tracked tree is clean and an upstream exists. Dirty, no-upstream and diverged cases die before any change. Pinned by the `update:` dirty, no-upstream and diverged cases (HEAD, branch and bytes unchanged, no install or build). |
|
||||
| T-o6k-02 | Information Disclosure | generated SUMMER_ADMIN__JWT__SECRET | high | mitigate | The value passes only through command substitution and the printf builtin. `say` names only the key and the file. Tests assert stdout and stderr lack the value for both generators, and the gated end-to-end run checks the same. |
|
||||
| T-o6k-03 | Tampering | app `.env` | medium | mitigate | Append-only `>>` keeps mode and owner, and a missing final newline is added first. Presence follows the compass loader, so no duplicate key is written, and an existing empty value is left alone with a warning. Pinned by the 0640, no-newline, export-form, commented, empty and idempotent cases. |
|
||||
| T-o6k-04 | Elevation of Privilege | building or writing in the wrong directory | medium | mitigate | Only `$PWD` is built, and only with go.mod, summer.yaml and a local-path framework replace to a clean framework checkout. Sibling apps are read (go.mod only), never written or built. Pinned by the shared-checkout case (sibling .env bytes unchanged, a single build cwd). |
|
||||
| T-o6k-05 | Elevation of Privilege | malicious replace target in an untrusted app | medium | accept | Update mode compiles and runs summer from the checkout the app's go.mod names. That is the same trust as running `summer build` in that app yourself, which already compiles the app's code. The target must still pass check_framework_dir (framework module, git root, clean tree). The docs say to run it inside your own application. |
|
||||
| T-o6k-06 | Spoofing | weak admin token secret | medium | mitigate | 32 bytes from `openssl rand` or `/dev/urandom`, validated as 44-character base64 before writing. Anything else writes nothing and exits 1 with the manual fix. Pinned by the generator-failure case. |
|
||||
| T-o6k-07 | Tampering | truncated download of /new or /update | high | mitigate | UpdateScript changes only the last line, to `main "$@" --update`, so every proper prefix of it runs main without valid update arguments, which exits 2. TestScriptTruncationRunsNothing runs every prefix of both scripts against stubs, with positive controls. |
|
||||
| T-o6k-08 | Elevation of Privilege | shell injection via go.mod paths or app dir names | high | mitigate | Replace targets and paths are only quoted arguments to abspath, `cd`, `git -C` and `go -C`, and are never evaluated. shellcheck stays clean with one SC2016 directive, and the T-14.3-16 words stay absent. Pinned by TestScriptShellcheck and the verify greps. |
|
||||
| T-o6k-09 | Tampering | weakened --forbidden gate | medium | mitigate | The allowlist holds one exact name. Every other SUMMER_ token and any `$SUMMER_`/`${SUMMER_` expansion or `export SUMMER_` still fails. Task 3's negative self-check proves both. D-26 is unchanged. |
|
||||
| T-o6k-10 | Tampering | site GET /update | medium | mitigate | The route serves the embedded UpdateScript() as text/plain with nosniff, no-cache and a strong ETag. No request data is reflected. nginx allows only GET and HEAD. Pinned by TestUpdateScriptRoute and the TestRoutesAssemble 405 row. |
|
||||
| T-o6k-11 | Denial of Service | `set -u`/`set -e` pitfalls on bash 3.2 | low | mitigate | Empty-array expansions are guarded or replaced by scalars. The counter uses `x=$((x + 1))`. The positional loop is guarded by `(($#))`. The no-other-apps and no-args cases exercise these paths. |
|
||||
| T-o6k-SC | Tampering | npm/pip/cargo installs | low | accept | No package installs and no new Go requirement. `--forbidden` confirms go.mod gained no require since v0.1.12, and the site go.mod files are verified unchanged. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- shellcheck is clean on new.sh, with exactly one disable directive, zero T-14.3-16 words, no `--upgrade`, and the key name once in non-comment code.
|
||||
- TestScriptBehavior passes the 32 original cases plus every `update:` case. TestUpdateScript and the three-variant truncation test pass.
|
||||
- Framework: `go vet ./...`, `go test ./... -short`, TestDocsTree, `docs:build --check` and `check-phase14.3.sh --forbidden` are green. After the site commit, `check-phase14.3.sh --unit` is green.
|
||||
- Site: the plugin and app vet and test green, with TestUpdateScriptRoute passing. The go.mod files are unchanged.
|
||||
- Commits: one framework `feat(sunrise)`, one plugin-submodule `feat:`, one parent pointer bump. No trailers, push, tag or deploy.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
A user who created `sm-blog-app` with the one-liner runs `cd sm-blog-app && curl -fsSL https://summercms.io/update | bash` from fish, zsh or bash. The framework checkout named in go.mod moves forward, fast-forward only. Other apps on the same checkout are named and left untouched. summer is reinstalled, a missing admin token secret is added to this app's `.env` without being printed, the app is rebuilt, and the script says to run summer migrate and restart summer serve. Local framework work is never overwritten. Create mode is byte-identical. Both URLs serve one script, and the site serves /update once the orchestrator releases and deploys.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/quick/261008-o6k-bootstrap-upgrade-mode-for-existing-chec/261008-o6k-SUMMARY.md` when done
|
||||
</output>
|
||||
@@ -0,0 +1,158 @@
|
||||
---
|
||||
phase: quick-261008-o6k
|
||||
plan: 01
|
||||
subsystem: sunrise (bootstrap script), summercms.io site plugin
|
||||
status: complete
|
||||
tags: [bootstrap, update, curl-bash, site-route, admin-secret]
|
||||
requires: [quick-261008-kdh (apps carry SUMMER_ADMIN__JWT__SECRET), phase 14.3 (new.sh, GET /new)]
|
||||
provides:
|
||||
- "new.sh --update: per-app framework update run inside one sm-<name>-app"
|
||||
- "sunrise.UpdateScript(): Script() with the last line `main \"$@\" --update`"
|
||||
- "site GET /update serving sunrise.UpdateScript()"
|
||||
affects: [modules/sunrise, docs/setup/installation.md, scripts/check-phase14.3.sh, sm-summercmsio-app]
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "one script, two URLs: UpdateScript derived from the embedded bytes by one suffix replacement"
|
||||
- "go.mod replace parsing in bash mirrors internal/build parseGoMod/splitReplace"
|
||||
- ".env presence check mirrors compass readDotEnv; append-only >> write"
|
||||
key-files:
|
||||
created: []
|
||||
modified:
|
||||
- modules/sunrise/new.sh
|
||||
- modules/sunrise/sunrise.go
|
||||
- modules/sunrise/sunrise_test.go
|
||||
- modules/sunrise/script_behavior_test.go
|
||||
- modules/sunrise/README.md
|
||||
- docs/setup/installation.md
|
||||
- README.md
|
||||
- scripts/check-phase14.3.sh
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/plugin.go
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/routes_test.go
|
||||
- ../sm-summercmsio-app/plugins/golem15/summercms/README.md
|
||||
- ../sm-summercmsio-app/README.md
|
||||
- ../sm-summercmsio-app/CLAUDE.md
|
||||
decisions:
|
||||
- "Update mode lives in new.sh behind --update; /update serves the same bytes with the final line `main \"$@\" --update`, so truncation still runs nothing"
|
||||
- "Only $PWD is built; other apps sharing the checkout are named on one `not rebuilt` line and never written"
|
||||
- "--forbidden allows exactly one SUMMER_ token in new.sh (SUMMER_ADMIN__JWT__SECRET) and still fails on any read or export of a SUMMER_ variable"
|
||||
- "generate_secret falls back to head+base64 when openssl is missing or prints an invalid value"
|
||||
metrics:
|
||||
duration: "not measured (start time was not recorded)"
|
||||
completed: 2026-10-08
|
||||
estimate:
|
||||
tokens: 190000
|
||||
tasks: 4
|
||||
actuals:
|
||||
tokens: 23000
|
||||
tasks: 4
|
||||
commits: 1
|
||||
plan_head_before: 27f75308d74bb28e645d2f3eaf9d94e2e80775eb
|
||||
plan_head_after: 9b3803c098707b9c25099fbfb2d0fb84d44be8b3
|
||||
---
|
||||
|
||||
# Phase quick-261008-o6k Plan 01: Per-app `/update` bootstrap mode Summary
|
||||
|
||||
You can now update one application in place with `cd sm-blog-app && curl -fsSL https://summercms.io/update | bash`, or the equivalent `.../new | bash -s -- --update`. It reads the framework checkout from the app's go.mod replace and moves it forward fast-forward only, detached or on a branch. It names other apps on the same checkout without rebuilding them, reinstalls summer, appends a missing SUMMER_ADMIN__JWT__SECRET to this app's .env without printing it, and runs `summer build`. The site serves the script at GET /update.
|
||||
|
||||
`actuals.commits: 1` counts the framework repository only (measured from the ledger). The site adds two more commits, listed below.
|
||||
|
||||
## Commits
|
||||
|
||||
| Repo | Commit | Message |
|
||||
|------|--------|---------|
|
||||
| summercms.go | 9b3803c | feat(sunrise): update an application in place with summercms.io/update |
|
||||
| sm-summercmsio-app/plugins/golem15/summercms (master) | 511e692 | feat: serve the SummerCMS update script at GET /update |
|
||||
| sm-summercmsio-app (master) | bd252c6 | feat: bump the site plugin to its GET /update route |
|
||||
|
||||
No tag, push, deploy or `require` bump. `.planning/state.json` was not staged.
|
||||
|
||||
## What was built
|
||||
|
||||
- **new.sh**
|
||||
- The header and usage text are rewritten for `--update`, and no text from the rejected flag is left.
|
||||
- `framework_module` keeps the module literal in one place. `check_framework_dir` uses it, and its messages are unchanged.
|
||||
- `trim`, `is_local_path` and `framework_replace` are new. `framework_replace` handles single-line and block replaces, strips `//` comments, accepts a version on the left and a quoted right-hand side, and prints the path or module plus an optional version line.
|
||||
- `path_hint` is extracted from `main`, and the single SC2016 directive moved with it. Create-mode stdout is byte-identical, which an exact-equality case pins.
|
||||
- `pull_branch` warns that SUMMERCMS_REF is ignored on a branch, refuses when there is no upstream, and runs `git pull --quiet --no-rebase --ff-only`.
|
||||
- `note_shared_apps` scans `sm-*-app` next to the app and next to the checkout, compares physical paths, and only reads go.mod.
|
||||
- `env_line_state` follows the compass loader rules: trimmed lines, `#` comments skipped, the `export ` prefix, the key trimmed, matching quotes removed.
|
||||
- `generate_secret` uses openssl first, then head and base64, and validates the result against `^[A-Za-z0-9+/]{43}=$`.
|
||||
- `ensure_admin_secret` uses one append with `>>`. It writes a leading newline only when the last line is unterminated, keeps the file mode and never prints the value.
|
||||
- `update_app` handles the app checks, the replace and target checks, the SUMMERCMS_DIR warning, git and Go, the shared-checkout notice, the reinstall, the secret, the build and the migrate/restart line plus the hint.
|
||||
- `main` counts `--update` against other arguments. It exits 2 on a mix, accepts a repeated `--update`, and uses `others=$((others + 1))` with a `(($#))` guard.
|
||||
- **sunrise.go**: `UpdateScript()` is derived once at init by `deriveUpdateScript`. That function panics when new.sh does not end with `main "$@"\n`.
|
||||
- **Tests**
|
||||
- TestScriptBehavior has 67 cases: the 32 originals, unchanged, plus 35 `update:` cases.
|
||||
- TestUpdateScript is new, along with TestDeriveUpdateScriptPanicsWithoutMainCall.
|
||||
- TestScriptTruncationRunsNothing now covers three variants: Script with demo, Script with --update, and UpdateScript. Each compares a recursive file listing and has its own positive control.
|
||||
- The gated TestBootstrapEndToEnd now also runs `/update` inside the generated app after removing its secret.
|
||||
- **Docs**: the sunrise README covers the new summary sentence, an Overview paragraph, Features, Usage, the `sunrise.UpdateScript` API row, Configuration and Testing. installation.md gains `## Updating an application` with the manual path and a `#updating-an-application` link from step 2. The root README gets a new sentence and the updated table row.
|
||||
- **Gate**: `--forbidden` is narrowed to a check on `SUMMER_` tokens plus a check on reads and exports. REQUIRED_TESTS gains TestUpdateScript, and SITE_PLUGIN_TESTS gains TestUpdateScriptRoute.
|
||||
- **Site**
|
||||
- GET /update is registered between /new and /.
|
||||
- `checkScriptRoute` is extracted from TestNewScriptRoute, and TestUpdateScriptRoute checks that its ETag differs from the one on /new.
|
||||
- TestRoutesAssemble gains /update rows (GET 200, HEAD 200, POST 405).
|
||||
- TestRoutesCoexistWithAdminPatterns lists GET /update.
|
||||
- The plugin README, site README and CLAUDE.md each mention the route.
|
||||
|
||||
## Verification
|
||||
|
||||
- `shellcheck -s bash new.sh` is clean. There is one `shellcheck disable`, no T-14.3-16 words and no rejected-flag text. SUMMER_ADMIN__JWT__SECRET appears once in non-comment code, and `bash -n` passes.
|
||||
- The six named sunrise tests show `--- PASS`.
|
||||
- On the framework: `go vet ./...`, `go test ./... -short -count=1`, TestDocsTree, `summer docs:build --check` ("no problems found") and `check-phase14.3.sh --forbidden` (scan passed) are all green.
|
||||
- Negative self-check of the narrowed gate on a scratch copy of new.sh: check (a) flagged `SUMMER_APP__KEY`, `SUMMER_DB` and `SUMMER_Q`. Check (b) flagged `"$SUMMER_APP__KEY"`, `"${SUMMER_DB}"` and `export SUMMER_Q=1`. The real new.sh matches neither.
|
||||
- On the site: plugin vet and tests, with TestRoutesAssemble, TestNewScriptRoute, TestUpdateScriptRoute and TestRoutesCoexistWithAdminPatterns all PASS. App vet and tests pass, and neither go.mod changed.
|
||||
- `bash scripts/check-phase14.3.sh --unit` exited 0 with "Phase 14.3 unit stage passed". The log shows PASS for TestUpdateScript, TestNewScriptRoute, TestUpdateScriptRoute and TestRoutesAssemble. The pnpm suite reported 32/32.
|
||||
- `SUMMERCMS_BOOTSTRAP_CHECK=1 go test ./modules/sunrise/ -run '^TestBootstrapEndToEnd$'` PASSED in 14.6s. That covers create mode through curl|bash, then `/update` inside the app: already at the target, secret re-added and not printed, mode 0600, `bin/demo` rebuilt.
|
||||
- Mutation spot checks, run against a scratch backup with the tree restored afterwards:
|
||||
- These mutations each made at least one `update:` case fail: dropping `--ff-only`, skipping the missing-newline fix, treating an empty value as absent, dropping the shared list, ignoring the `export ` form, and skipping the build.
|
||||
- Dropping the `#`-comment guard is not caught. That guard is redundant by construction, because a commented line's key starts with `#` and can never equal the key name.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 1 - Test strength] Diverged-branch case gets a git identity**
|
||||
- **Found during:** Task 2 mutation spot check
|
||||
- **Issue:** With `--ff-only` removed, the diverged case still passed, because `git pull --no-rebase` failed on a missing identity rather than on the fast-forward rule.
|
||||
- **Fix:** The case now sets GIT_AUTHOR_* and GIT_COMMITTER_* through `h.env`, so a plain pull would merge and move HEAD, and only `--ff-only` refuses.
|
||||
- **Files modified:** modules/sunrise/script_behavior_test.go
|
||||
- **Commit:** 9b3803c
|
||||
|
||||
**2. [Rule 3 - Blocking] The literal of the rejected flag in a test**
|
||||
- **Issue:** The help case's `expectLacks(..., "--upgrade")` made the Task 3 grep `grep -rn -- '--upgrade' modules/sunrise ...` non-empty.
|
||||
- **Fix:** The string is now built as `"--"+"upgrade"`, with a comment.
|
||||
- **Commit:** 9b3803c
|
||||
|
||||
**3. [Process] Code written before tests in Tasks 1 and 2**
|
||||
- The plan asked for failing tests first. The script and UpdateScript were written first, then the 35 `update:` cases and the truncation and UpdateScript tests.
|
||||
- To compensate, mutation spot checks (listed above) confirmed that the cases detect regressions.
|
||||
|
||||
### Small additions within scope
|
||||
- `TestDeriveUpdateScriptPanicsWithoutMainCall` pins the init-time panic.
|
||||
- `generate_secret` also falls back to head and base64 when openssl is present but prints an invalid value. The plan only specified the fallback for a missing openssl. The generator-failure case, with openssl printing `short` and head removed, still exits 1 with .env unchanged.
|
||||
- `update_app` refuses a replace that resolves to `/` before the existence check.
|
||||
- The e2e test removes `bin/demo` before the update run, which proves the rebuild rather than only checking that the file exists.
|
||||
|
||||
## Not tested
|
||||
|
||||
- macOS bash 3.2. The script avoids the known pitfalls: empty-array expansion, `((x++))`, mapfile and nullglob. All runs were on the Linux bash.
|
||||
- A real `git pull` against an HTTPS remote that needs credentials. `GIT_TERMINAL_PROMPT=0` makes it fail rather than prompt, which surfaces as the generic "diverged or the fetch failed" message.
|
||||
- A quoted go.mod replace path that contains spaces. Quotes are stripped, but no case covers it.
|
||||
- An unreadable or unwritable `.env`. The script dies with a message, but no case covers it.
|
||||
- The live site. GET /update is not deployed, by design, since the orchestrator releases.
|
||||
|
||||
## Known Stubs
|
||||
|
||||
None.
|
||||
|
||||
## Threat Flags
|
||||
|
||||
None. The new surface (GET /update, the `.env` append, reading go.mod replace targets) is covered by T-o6k-01..11 in the plan.
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
- FOUND: modules/sunrise/new.sh, modules/sunrise/sunrise.go, modules/sunrise/sunrise_test.go, modules/sunrise/script_behavior_test.go, modules/sunrise/README.md, docs/setup/installation.md, README.md, scripts/check-phase14.3.sh
|
||||
- FOUND: ../sm-summercmsio-app/plugins/golem15/summercms/{plugin.go,routes_test.go,README.md}, ../sm-summercmsio-app/{README.md,CLAUDE.md}
|
||||
- FOUND commits: 9b3803c (summercms.go), 511e692 (plugin submodule), bd252c6 (site parent)
|
||||
Reference in New Issue
Block a user