Files
summercms/modules/sunrise/README.md
Jakub Zych 602e0412f3 docs: add a Role in the framework section to every module README
Each module README now says where the module sits in the dependency
graph, why it is its own module, and lists use cases for application
developers.
2026-10-11 00:58:17 +02:00

128 lines
9.0 KiB
Markdown

# 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"`
**Name:** The start of a summer day; it starts a new application, or updates one, in one command.
## Overview
`sunrise` holds `new.sh`, the script behind the one-liner served at https://summercms.io/new:
```sh
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:
```sh
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.
## Role in the framework
A distribution leaf outside the runtime: no module imports it and the application binary does not need it. It lives in a package only so the script is embedded and versioned with the framework release it installs.
Use cases:
- Start a new application with one command on a fresh machine.
- Update an existing application and its framework checkout to the newest release.
- Serve the install script from the project website, pinned to a release.
## Features
- Truncation-safe: everything is a function definition and the last line is `main "$@"` (`main "$@" --update` in `sunrise.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.Z` tag, or at `SUMMERCMS_REF` when set.
- Fast-forward-only updates: an existing checkout is fetched (tags are never force-updated) and moved only when its `HEAD` is an ancestor of the target.
- Refusals that protect local work: an existing directory that is not a git repository, whose `go.mod` is 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 `$PATH` is printed literally.
- Update mode: run inside one application, it moves the framework checkout named by the application's `go.mod` replace forward, fast-forward only (detached release checkouts to the newest release, branch checkouts with `git pull --ff-only`), reinstalls `summer` and runs `summer build` in that application only.
- Shared-checkout notice: other applications whose `go.mod` replace 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__SECRET` to 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`, `go` and `docker` are run (update mode also runs `openssl`, or `head` and `base64`, and the installed `summer`), with stdin closed and `GIT_TERMINAL_PROMPT=0` so nothing waits for input.
## Usage
Run the script from the directory that should hold the framework checkout and the application:
```sh
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:
```sh
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:
```go
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
```sh
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.
```sh
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.