Files
summercms/modules/sunrise/sunrise.go
Jakub Zych 9b3803c098 feat(sunrise): update an application in place with summercms.io/update
- 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
2026-10-08 20:15:50 +02:00

58 lines
2.2 KiB
Go

// Package sunrise embeds the SummerCMS bootstrap script, new.sh, so the
// script is versioned with the framework release that ships it.
//
// The script is what `curl -fsSL https://summercms.io/new | bash -s -- <name>`
// runs: it checks for Go 1.27 or newer, git and Docker, clones the framework
// at its newest release tag (or moves an existing release checkout forward),
// installs the summer CLI and runs `summer new <name>` next to the checkout.
// A web server hands out Script() as text/plain.
//
// The same script has an update mode, which
// `curl -fsSL https://summercms.io/update | bash` runs inside one
// application: it moves the framework checkout named by the application's
// go.mod forward, reinstalls summer and rebuilds that application.
// UpdateScript() is the script with update mode preset, for the /update URL.
package sunrise
import (
"bytes"
_ "embed"
)
//go:embed new.sh
var script []byte
// Script returns the bootstrap script. Each call returns a fresh copy, so a
// caller may modify the slice without affecting later calls.
func Script() []byte {
return bytes.Clone(script)
}
const (
mainCall = "main \"$@\"\n"
updateCall = "main \"$@\" --update\n"
)
var updateScript = deriveUpdateScript(script)
// deriveUpdateScript replaces the final `main "$@"` line of s with
// `main "$@" --update`. It panics when s does not end with that line, so a
// changed script fails every test instead of serving create mode at /update.
func deriveUpdateScript(s []byte) []byte {
if !bytes.HasSuffix(s, []byte(mainCall)) {
panic(`sunrise: new.sh must end with the line main "$@"`)
}
out := bytes.Clone(s[:len(s)-len(mainCall)])
return append(out, updateCall...)
}
// UpdateScript returns the bootstrap script with update mode preset: Script()
// with its final line `main "$@"` replaced by `main "$@" --update`, so both
// URLs serve the same logic. The flag follows "$@" so that every proper
// prefix of the last line, as a cut-short download delivers it, calls main
// without a valid --update or with no arguments, and main exits with the
// usage instead of running anything. Each call returns a fresh copy.
func UpdateScript() []byte {
return bytes.Clone(updateScript)
}