- new.sh gains --update, run inside one application directory (go.mod and summer.yaml required); it changes only that application - the framework checkout is read from the app's go.mod replace (single-line or block form); module-version replaces, missing targets and foreign or dirty checkouts are refused through the shared check_framework_dir - a detached checkout moves fast-forward only to the newest release (or SUMMERCMS_REF) through the shared move_detached; a branch checkout is pulled with git pull --no-rebase --ff-only, refusing a missing upstream or a diverged branch with the checkout unchanged - other sm-*-app directories on the same checkout are named on one "not rebuilt" line and never built or written - summer is reinstalled, a missing SUMMER_ADMIN__JWT__SECRET is appended to this app's .env (openssl, else head and base64; mode kept, never printed), then summer build runs and the script ends with the migrate and restart reminder and the shared path_hint - sunrise.UpdateScript() serves the same script with its last line changed to main "$@" --update, so truncated downloads still run nothing - check-phase14.3.sh --forbidden now allows only the literal SUMMER_ADMIN__JWT__SECRET in new.sh and still fails on any other SUMMER_ name or on a read or export of one; TestUpdateScript and TestUpdateScriptRoute are required tests
8.5 KiB
sunrise
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.
import "git.golem15.com/golem15/summercms/modules/sunrise"
Overview
sunrise holds new.sh, the script behind the one-liner served at https://summercms.io/new:
curl -fsSL https://summercms.io/new | bash -s -- blog
The script checks for Go 1.27 or newer, git and Docker, clones the framework into ./summercms at its newest release tag (or moves an existing release checkout forward to it), installs the summer CLI with go install ./cmd/summer and runs summer new blog --framework <checkout> next to the checkout, which writes ./sm-blog-app. summer new prints the next steps; the script adds the cd line and, when the Go install directory is not on PATH, a hint for the login shell named by SHELL: fish_add_path <dir> for fish (which keeps it for new sessions), an export PATH=... line naming ~/.zshrc or ~/.bashrc for zsh and bash, and the generic export PATH=... line with "your shell profile" for any other shell.
The same script updates an existing application. Run inside the application directory, served at https://summercms.io/update:
cd sm-blog-app && curl -fsSL https://summercms.io/update | bash
curl -fsSL https://summercms.io/new | bash -s -- --update does the same. Update mode needs a go.mod and a summer.yaml in the current directory and reads the framework checkout from the go.mod replace of the framework module, as summer new writes it; a replace with a module version instead of a local path is refused. A detached checkout is fetched and moved fast-forward only to the newest release tag (or SUMMERCMS_REF); a branch checkout with an upstream is pulled with git pull --ff-only. A checkout with changes to tracked files, a branch without an upstream and a branch that has diverged stop the script with the checkout unchanged, and nothing is installed or built. Other sm-*-app directories next to the application or next to the checkout whose replace points at the same checkout are named on one not rebuilt line and left untouched. The script then reinstalls summer, appends a SUMMER_ADMIN__JWT__SECRET line to this application's .env when no active line sets it (once, keeping the file mode, never printing the value), runs summer build in the application and ends with a reminder to run summer migrate and restart summer serve, followed by the same PATH hint.
The script lives inside this package so //go:embed can reach it, and a web server serves the bytes of the framework release it was built against. sunrise.Script returns them; sunrise.UpdateScript returns the same script with update mode preset, for the update URL.
Features
- Truncation-safe: everything is a function definition and the last line is
main "$@"(main "$@" --updateinsunrise.UpdateScript), so a download cut short runs nothing. - Prerequisites first: Go older than 1.27 or missing, or a missing git, stops the script with an install hint before anything is cloned; a missing Docker or compose plugin is only a warning.
- Release tags: a fresh clone is checked out detached at the newest
vX.Y.Ztag, or atSUMMERCMS_REFwhen set. - Fast-forward-only updates: an existing checkout is fetched (tags are never force-updated) and moved only when its
HEADis an ancestor of the target. - Refusals that protect local work: an existing directory that is not a git repository, whose
go.modis not the framework module, that has changes to tracked files, or that is on a branch is left untouched and the script stops with a message. - Early name check: a name that is not lower-case letters and digits starting with a letter, or an existing
sm-<name>-app, stops the script before any clone or install. - Login-shell-aware PATH hint: the closing hint is one correct line for fish, zsh or bash, with a generic
export PATH=...line for any other shell; the directory is escaped for the target shell's double quotes and$PATHis printed literally. - Update mode: run inside one application, it moves the framework checkout named by the application's
go.modreplace forward, fast-forward only (detached release checkouts to the newest release, branch checkouts withgit pull --ff-only), reinstallssummerand runssummer buildin that application only. - Shared-checkout notice: other applications whose
go.modreplace resolves to the same checkout are named on one line and never built or written. - Admin token secret: update mode appends a missing
SUMMER_ADMIN__JWT__SECRETto the application's.env, 32 random bytes as base64, without printing it; an existing line, even an empty one, is left alone. - No privilege escalation and no dynamic code; only bash builtins plus
git,goanddockerare run (update mode also runsopenssl, orheadandbase64, and the installedsummer), with stdin closed andGIT_TERMINAL_PROMPT=0so nothing waits for input.
Usage
Run the script from the directory that should hold the framework checkout and the application:
curl -fsSL https://summercms.io/new | bash -s -- blog
bash -s -- <name> passes the application name to the piped script; -h or --help prints the usage. A local copy runs the same way with bash new.sh blog.
Update an existing application from inside its directory:
cd sm-blog-app
curl -fsSL https://summercms.io/update | bash
Update mode takes no other argument. It works the same from bash, zsh and fish, because the piped script always runs in bash.
Serving the script from a Go handler:
package acme
import (
"net/http"
"git.golem15.com/golem15/summercms/modules/sunrise"
)
func newScript(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
_, _ = w.Write(sunrise.Script())
}
A second handler serves sunrise.UpdateScript() the same way at the update path.
API reference
| Identifier | Description |
|---|---|
sunrise.Script |
Returns a fresh copy of the embedded new.sh bytes on every call. |
sunrise.UpdateScript |
Returns a fresh copy of the script with its last line main "$@" replaced by main "$@" --update, so it runs update mode. |
Configuration
The script reads three environment variables, plus SHELL, which only picks the form of the PATH hint. It never reads or exports SUMMER_-prefixed variables, which the framework reads as configuration. The only such name it writes is SUMMER_ADMIN__JWT__SECRET, which update mode appends to the application's .env.
| Variable | Default | Purpose |
|---|---|---|
SUMMERCMS_REPO |
https://git.golem15.com/golem15/summercms |
Repository to clone; used only when cloning. |
SUMMERCMS_DIR |
summercms (relative to the current directory) |
Framework checkout directory; the application is created next to it. Update mode ignores it with a warning, because the checkout comes from go.mod. |
SUMMERCMS_REF |
newest vX.Y.Z tag |
Tag or commit to check out instead of the newest release. It applies to detached checkouts; update mode ignores it, with a warning, on a branch. |
Dependencies
- SummerCMS modules: none.
- Third-party: none.
- Standard library:
bytes,embed.
Testing
go test ./modules/sunrise/...
The default run checks the embedded bytes, the script's shape and syntax (bash -n), that sunrise.UpdateScript differs from it only in the last line, and runs every line-boundary prefix and every byte prefix of the final line of both scripts against stub commands to prove a truncated download invokes nothing. A behaviour harness runs the script against a local framework repository and stub go, docker, summer and openssl commands, covering create mode and update mode: detached and branch checkouts and their refusals, the shared-checkout notice, the .env admin secret and the PATH hint.
SUMMERCMS_BOOTSTRAP_CHECK=1 go test ./modules/sunrise/ -run TestBootstrapEndToEnd -timeout 20m
The gated run serves the script from a local HTTP server, pipes it through curl | bash with SUMMERCMS_REPO set to this repository and SUMMERCMS_REF to its HEAD, and builds the generated application. It then removes the admin token secret from the application's .env and updates the application through the update script, which must restore the secret without printing it and rebuild. It needs bash, curl, git, Go and access to the Go module proxy.