- modules/sunrise/new.sh: curl | bash installer wrapped in main(), checks Go 1.27+/git/Docker, clones or fast-forwards the framework at the newest release tag (SUMMERCMS_REPO/DIR/REF overrides), installs summer and runs summer new next to the checkout, prints the cd line and a PATH hint - sunrise.Script() returns a fresh copy of the embedded script - smoke, shape, truncation and gated end-to-end tests; README and modules row
258 lines
9.4 KiB
Bash
Executable File
258 lines
9.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# SummerCMS bootstrap: create a new SummerCMS application in one command.
|
|
#
|
|
# curl -fsSL https://summercms.io/new | bash -s -- <name>
|
|
#
|
|
# The script checks for Go 1.27 or newer, git and (optionally) 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` and runs `summer new <name>` next to the checkout, which
|
|
# writes ./sm-<name>-app.
|
|
#
|
|
# Environment overrides:
|
|
# SUMMERCMS_REPO framework repository to clone
|
|
# (default https://git.golem15.com/golem15/summercms)
|
|
# SUMMERCMS_DIR framework checkout directory (default ./summercms); the
|
|
# application is created next to it
|
|
# SUMMERCMS_REF tag or commit to check out instead of the newest release
|
|
#
|
|
# Everything below is a function definition and the last line calls main, so
|
|
# a download cut short runs nothing. Only bash builtins and the git, go and
|
|
# docker commands are used.
|
|
|
|
say() {
|
|
printf '==> %s\n' "$*" >&2
|
|
}
|
|
|
|
warn() {
|
|
printf 'warning: %s\n' "$*" >&2
|
|
}
|
|
|
|
die() {
|
|
printf 'error: %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
usage() {
|
|
printf '%s\n' \
|
|
'Usage: curl -fsSL https://summercms.io/new | bash -s -- <name>' \
|
|
' bash new.sh <name>' \
|
|
'' \
|
|
'Creates sm-<name>-app next to a SummerCMS framework checkout. <name> is' \
|
|
'lower-case letters and digits, starting with a letter.' \
|
|
'' \
|
|
'Steps: check Go 1.27+, git and Docker; clone the framework (or move an' \
|
|
'existing release checkout forward) at its newest release tag; install' \
|
|
'the summer CLI with go install; run summer new <name>.' \
|
|
'' \
|
|
'Environment:' \
|
|
' SUMMERCMS_REPO repository to clone (default https://git.golem15.com/golem15/summercms)' \
|
|
' SUMMERCMS_DIR framework checkout directory (default ./summercms)' \
|
|
' SUMMERCMS_REF tag or commit to check out instead of the newest release'
|
|
}
|
|
|
|
# abspath prints $1 as an absolute path against $PWD with ".", ".." and
|
|
# repeated slashes removed lexically.
|
|
abspath() {
|
|
local p="$1" out="" seg
|
|
local -a parts
|
|
[[ $p == /* ]] || p="$PWD/$p"
|
|
IFS=/ read -r -a parts <<<"$p"
|
|
for seg in "${parts[@]}"; do
|
|
case "$seg" in
|
|
'' | .) ;;
|
|
..) out="${out%/*}" ;;
|
|
*) out="$out/$seg" ;;
|
|
esac
|
|
done
|
|
printf '%s\n' "${out:-/}"
|
|
}
|
|
|
|
# check_go requires go 1.27 or newer.
|
|
check_go() {
|
|
command -v go >/dev/null 2>&1 ||
|
|
die "Go is not installed. SummerCMS needs Go 1.27 or newer: https://go.dev/dl/"
|
|
local v
|
|
# Read the version from / so a go.mod in the current directory cannot
|
|
# make the go command switch toolchains for the check.
|
|
v="$(cd / && go env GOVERSION </dev/null 2>/dev/null)" || v=""
|
|
if [[ $v =~ ^go([0-9]+)\.([0-9]+) ]]; then
|
|
local major="${BASH_REMATCH[1]}" minor="${BASH_REMATCH[2]}"
|
|
if ((major < 1 || (major == 1 && minor < 27))); then
|
|
die "SummerCMS needs Go 1.27 or newer, found $v. Install a newer Go from https://go.dev/dl/"
|
|
fi
|
|
else
|
|
warn "could not read the Go version (go env GOVERSION printed '${v}'); SummerCMS needs Go 1.27 or newer"
|
|
fi
|
|
}
|
|
|
|
# check_docker only warns: the application builds without Docker, the
|
|
# generated compose.yaml just needs it for the development database.
|
|
check_docker() {
|
|
if ! command -v docker >/dev/null 2>&1; then
|
|
warn "Docker is not installed; the generated compose.yaml needs it to run PostgreSQL (https://docs.docker.com/engine/install/)"
|
|
elif ! docker compose version </dev/null >/dev/null 2>&1; then
|
|
warn "the Docker compose plugin is missing; the generated compose.yaml needs 'docker compose'"
|
|
fi
|
|
}
|
|
|
|
# latest_tag prints the newest vX.Y.Z tag of the checkout in $1.
|
|
latest_tag() {
|
|
local dir="$1" tags tag
|
|
# Capture the whole list first: piping into head under pipefail can
|
|
# fail with SIGPIPE.
|
|
tags="$(git -C "$dir" tag -l 'v*' --sort=-v:refname </dev/null)" || return 1
|
|
while IFS= read -r tag; do
|
|
if [[ $tag =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
printf '%s\n' "$tag"
|
|
return 0
|
|
fi
|
|
done <<<"$tags"
|
|
return 1
|
|
}
|
|
|
|
# target_commit prints the commit to check out in $1: SUMMERCMS_REF ($2)
|
|
# when set, otherwise the newest release tag.
|
|
target_commit() {
|
|
local dir="$1" ref="$2" label commit
|
|
if [[ -n $ref ]]; then
|
|
[[ $ref != -* ]] || die "SUMMERCMS_REF must be a tag or commit, not '$ref'"
|
|
label="$ref"
|
|
else
|
|
label="$(latest_tag "$dir")" ||
|
|
die "no release tag (vX.Y.Z) found in $dir; set SUMMERCMS_REF to a tag or commit"
|
|
ref="refs/tags/$label"
|
|
fi
|
|
commit="$(git -C "$dir" rev-parse --verify --quiet "${ref}^{commit}" </dev/null)" ||
|
|
die "$label is not a tag or commit in $dir"
|
|
say "framework target: $label (${commit:0:12})"
|
|
printf '%s\n' "$commit"
|
|
}
|
|
|
|
# check_checkout refuses an existing directory that is not a clean,
|
|
# detached SummerCMS framework checkout. It never changes the directory.
|
|
check_checkout() {
|
|
local dir="$1" hint top real line module="" status branch
|
|
hint="use SUMMERCMS_DIR for a separate clone, or run 'summer new --framework <checkout>' yourself"
|
|
[[ -d $dir ]] || die "$dir exists and is not a directory; $hint"
|
|
[[ -f $dir/go.mod ]] || die "$dir is not a SummerCMS framework checkout (no go.mod); $hint"
|
|
while IFS= read -r line || [[ -n $line ]]; do
|
|
if [[ $line =~ ^[[:space:]]*module[[:space:]]+\"?([^[:space:]\"]+) ]]; then
|
|
module="${BASH_REMATCH[1]}"
|
|
break
|
|
fi
|
|
done <"$dir/go.mod"
|
|
[[ $module == git.golem15.com/golem15/summercms ]] ||
|
|
die "$dir is not a SummerCMS framework checkout (go.mod module is '${module}'); $hint"
|
|
top="$(git -C "$dir" rev-parse --show-toplevel </dev/null 2>/dev/null)" ||
|
|
die "$dir is not a git repository; $hint"
|
|
real="$(cd "$dir" && pwd -P)"
|
|
[[ $top == "$real" ]] || die "$dir is not the root of a git repository; $hint"
|
|
status="$(git -C "$dir" status --porcelain --untracked-files=no </dev/null)" ||
|
|
die "git status failed in $dir"
|
|
[[ -z $status ]] || die "$dir has uncommitted changes to tracked files; commit or stash them, or $hint"
|
|
if branch="$(git -C "$dir" symbolic-ref -q --short HEAD </dev/null)"; then
|
|
die "$dir is on branch $branch; this installer only moves a detached release checkout. To keep your branch, $hint"
|
|
fi
|
|
}
|
|
|
|
# checkout_framework clones the framework into $2 or moves an existing
|
|
# detached checkout forward, fast-forward only.
|
|
checkout_framework() {
|
|
local repo="$1" dir="$2" ref="$3" target head
|
|
if [[ ! -e $dir && ! -L $dir ]]; then
|
|
say "cloning $repo into $dir"
|
|
git clone --quiet -- "$repo" "$dir" </dev/null || die "git clone of $repo failed"
|
|
target="$(target_commit "$dir" "$ref")" ||
|
|
die "the new clone in $dir has no usable target; remove $dir and run again"
|
|
git -C "$dir" -c advice.detachedHead=false checkout --quiet --detach "$target" </dev/null
|
|
return 0
|
|
fi
|
|
check_checkout "$dir"
|
|
say "updating the framework checkout in $dir"
|
|
# No --force: a tag that moved upstream is never overwritten locally,
|
|
# git refuses and the script stops.
|
|
git -C "$dir" fetch --quiet --tags origin </dev/null ||
|
|
die "git fetch in $dir failed (a release tag may have moved upstream); the checkout was not changed"
|
|
target="$(target_commit "$dir" "$ref")" || exit 1
|
|
head="$(git -C "$dir" rev-parse HEAD </dev/null)"
|
|
if [[ $head == "$target" ]]; then
|
|
say "framework checkout is already at the target"
|
|
elif git -C "$dir" merge-base --is-ancestor "$head" "$target" </dev/null; then
|
|
git -C "$dir" -c advice.detachedHead=false checkout --quiet --detach "$target" </dev/null
|
|
else
|
|
die "moving $dir from ${head:0:12} to ${target:0:12} is not a fast-forward; the checkout was not changed. $dir must be an ancestor of the target; use SUMMERCMS_DIR for a fresh clone"
|
|
fi
|
|
}
|
|
|
|
# go_bin_dir prints the directory go install writes to: GOBIN, else the
|
|
# first GOPATH entry plus /bin.
|
|
go_bin_dir() {
|
|
local gobin gopath
|
|
gobin="$(cd / && go env GOBIN </dev/null)" || return 1
|
|
if [[ -z $gobin ]]; then
|
|
gopath="$(cd / && go env GOPATH </dev/null)" || return 1
|
|
gopath="${gopath%%:*}"
|
|
[[ -n $gopath ]] || return 1
|
|
gobin="$gopath/bin"
|
|
fi
|
|
printf '%s\n' "$gobin"
|
|
}
|
|
|
|
main() {
|
|
set -euo pipefail
|
|
export GIT_TERMINAL_PROMPT=0
|
|
|
|
if (($# == 1)) && [[ $1 == -h || $1 == --help ]]; then
|
|
usage
|
|
exit 0
|
|
fi
|
|
if (($# != 1)); then
|
|
usage >&2
|
|
exit 2
|
|
fi
|
|
local name="$1"
|
|
if [[ ! $name =~ ^[a-z][a-z0-9]*$ ]]; then
|
|
printf 'error: invalid application name %q: use lower-case letters and digits, starting with a letter\n\n' "$name" >&2
|
|
usage >&2
|
|
exit 2
|
|
fi
|
|
|
|
local repo="${SUMMERCMS_REPO:-https://git.golem15.com/golem15/summercms}"
|
|
local ref="${SUMMERCMS_REF:-}"
|
|
local dir parent app
|
|
dir="$(abspath "${SUMMERCMS_DIR:-summercms}")"
|
|
[[ $dir != / ]] || die "SUMMERCMS_DIR must not be /"
|
|
parent="${dir%/*}"
|
|
parent="${parent:-/}"
|
|
app="${parent%/}/sm-$name-app"
|
|
[[ -d $parent ]] || die "$parent does not exist"
|
|
if [[ -e $app || -L $app ]]; then
|
|
die "$app already exists; choose another name or remove it"
|
|
fi
|
|
|
|
command -v git >/dev/null 2>&1 || die "git is not installed; install git and run again"
|
|
check_go
|
|
check_docker
|
|
|
|
checkout_framework "$repo" "$dir" "$ref"
|
|
|
|
local gobin
|
|
gobin="$(go_bin_dir)" || die "could not determine the Go install directory (go env GOBIN / GOPATH)"
|
|
say "installing summer into $gobin"
|
|
GOWORK=off go -C "$dir" install ./cmd/summer </dev/null
|
|
|
|
say "creating $app"
|
|
(cd "$parent" && "$gobin/summer" new "$name" --framework "$dir" </dev/null)
|
|
|
|
printf '\n==> Done. Start with:\n cd %s\n' "$app"
|
|
if [[ :${PATH:-}: != *":$gobin:"* ]]; then
|
|
printf '\n==> summer is installed in %s, which is not on your PATH. Run this (and add it to your shell profile):\n' "$gobin"
|
|
# $PATH stays literal: the user's shell expands it.
|
|
# shellcheck disable=SC2016
|
|
printf ' export PATH="%s:$PATH"\n' "$gobin"
|
|
fi
|
|
}
|
|
|
|
main "$@"
|