- 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
619 lines
23 KiB
Bash
Executable File
619 lines
23 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# SummerCMS bootstrap: create a new SummerCMS application in one command, or
|
|
# update an existing one.
|
|
#
|
|
# curl -fsSL https://summercms.io/new | bash -s -- <name>
|
|
# cd sm-<name>-app && curl -fsSL https://summercms.io/update | bash
|
|
# cd sm-<name>-app && curl -fsSL https://summercms.io/new | bash -s -- --update
|
|
#
|
|
# 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.
|
|
#
|
|
# Update mode (--update, which /update presets) runs inside one application
|
|
# and changes only that application. It reads the framework checkout from the
|
|
# application's go.mod replace and moves it forward: a detached checkout fast-
|
|
# forward only to the newest release (or SUMMERCMS_REF), a branch checkout
|
|
# with git pull --ff-only. It then reinstalls summer, adds a missing admin
|
|
# token secret to this application's .env, runs summer build here, and names
|
|
# other applications that share the checkout without rebuilding them.
|
|
#
|
|
# Environment overrides:
|
|
# SUMMERCMS_REPO framework repository to clone
|
|
# (default https://git.golem15.com/golem15/summercms);
|
|
# used only when cloning
|
|
# SUMMERCMS_DIR framework checkout directory (default ./summercms); the
|
|
# application is created next to it. Not used in update
|
|
# mode, where the checkout comes from go.mod
|
|
# SUMMERCMS_REF tag or commit to check out instead of the newest release
|
|
# (detached checkouts only)
|
|
#
|
|
# The login shell named by SHELL only picks the form of the PATH hint printed
|
|
# at the end (fish, zsh, bash, or a generic export line).
|
|
#
|
|
# 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, plus, in update mode, openssl (or head and
|
|
# base64) to make the admin token secret and the installed summer.
|
|
|
|
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>' \
|
|
' cd sm-<name>-app && curl -fsSL https://summercms.io/update | bash' \
|
|
' curl -fsSL https://summercms.io/new | bash -s -- --update' \
|
|
' 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>.' \
|
|
'' \
|
|
'--update takes no other argument and runs inside one application (a' \
|
|
'directory with go.mod and summer.yaml). It reads the framework checkout' \
|
|
'from the go.mod replace and 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 the .env and runs summer build there.' \
|
|
'Other applications sharing the checkout are named, not rebuilt.' \
|
|
'' \
|
|
'Environment:' \
|
|
' SUMMERCMS_REPO repository to clone (default https://git.golem15.com/golem15/summercms)' \
|
|
' SUMMERCMS_DIR framework checkout directory (default ./summercms); not used by --update' \
|
|
' SUMMERCMS_REF tag or commit to check out instead of the newest release'
|
|
}
|
|
|
|
# framework_module prints the module path of the SummerCMS framework.
|
|
framework_module() {
|
|
printf '%s\n' git.golem15.com/golem15/summercms
|
|
}
|
|
|
|
# trim prints $1 without leading and trailing whitespace.
|
|
trim() {
|
|
local s="$1"
|
|
s="${s#"${s%%[![:space:]]*}"}"
|
|
s="${s%"${s##*[![:space:]]}"}"
|
|
printf '%s\n' "$s"
|
|
}
|
|
|
|
# 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_framework_dir refuses an existing directory $1 that is not a clean
|
|
# SummerCMS framework checkout at the root of a git repository; $2 is the
|
|
# hint appended to every refusal. It never changes the directory.
|
|
check_framework_dir() {
|
|
local dir="$1" hint="$2" top real line module="" status fw
|
|
fw="$(framework_module)"
|
|
[[ -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 == "$fw" ]] ||
|
|
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"
|
|
}
|
|
|
|
# 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 branch
|
|
hint="use SUMMERCMS_DIR for a separate clone, or run 'summer new --framework <checkout>' yourself"
|
|
check_framework_dir "$dir" "$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
|
|
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"
|
|
move_detached "$dir" "$ref"
|
|
}
|
|
|
|
# move_detached fetches tags into the detached checkout $1 and moves it to
|
|
# SUMMERCMS_REF ($2) or the newest release tag, fast-forward only.
|
|
move_detached() {
|
|
local dir="$1" ref="$2" target head
|
|
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"
|
|
}
|
|
|
|
# dq_escape prints $1 escaped for the double quotes of the shell named by $2:
|
|
# backslash, double quote and dollar always; for shells other than fish also
|
|
# backtick, and ! moved into single quotes against history expansion.
|
|
dq_escape() {
|
|
local s="$1" sh="$2" out="" c i
|
|
for ((i = 0; i < ${#s}; i++)); do
|
|
c="${s:i:1}"
|
|
case "$c" in
|
|
\\ | \" | \$) out+="\\$c" ;;
|
|
\`) if [[ $sh == fish ]]; then out+="$c"; else out+="\\$c"; fi ;;
|
|
!) if [[ $sh == fish ]]; then out+="$c"; else out+="\"'!'\""; fi ;;
|
|
*) out+="$c" ;;
|
|
esac
|
|
done
|
|
printf '%s\n' "$out"
|
|
}
|
|
|
|
# path_hint prints how to put the Go bin directory $1 on PATH, in the form
|
|
# of the login shell named by SHELL, when it is not on PATH yet.
|
|
path_hint() {
|
|
local gobin="$1"
|
|
if [[ :${PATH:-}: != *":$gobin:"* ]]; then
|
|
# Under curl | bash this is always bash; SHELL still names the
|
|
# user's login shell, which picks the hint form.
|
|
local login="${SHELL:-}" note
|
|
login="${login##*/}"
|
|
case "$login" in
|
|
fish) note='fish_add_path also keeps it for new fish sessions' ;;
|
|
zsh) note='and add it to ~/.zshrc' ;;
|
|
bash) note='and add it to ~/.bashrc' ;;
|
|
*) note='and add it to your shell profile' ;;
|
|
esac
|
|
printf '\n==> summer is installed in %s, which is not on your PATH. Run this (%s):\n' "$gobin" "$note"
|
|
if [[ $login == fish ]]; then
|
|
printf ' fish_add_path "%s"\n' "$(dq_escape "$gobin" fish)"
|
|
else
|
|
# $PATH stays literal: the user's shell expands it.
|
|
# shellcheck disable=SC2016
|
|
printf ' export PATH="%s:$PATH"\n' "$(dq_escape "$gobin" "$login")"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# is_local_path tells whether the replace target $1 is a directory path
|
|
# rather than a module path, by Go's rule: ".", ".." or a leading "./",
|
|
# "../" or "/".
|
|
is_local_path() {
|
|
case "$1" in
|
|
. | .. | ./* | ../* | /*) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
# framework_replace reads the go.mod file $1 the way summer build does
|
|
# (// comments cut, single-line and block replace directives, the first
|
|
# field of each side, the left side matched exactly against the framework
|
|
# module, a version may follow it) and prints the replacement: its path or
|
|
# module on the first line and its version, if any, on a second line. It
|
|
# returns 1 when go.mod has no replace for the framework.
|
|
framework_replace() {
|
|
local file="$1" fw line rest lhs rhs first ver="" inblock=0
|
|
local block_re='^replace[[:space:]]*[(]$' single_re='^replace[[:space:]]+(.+)$'
|
|
local quoted_re='^"([^"]*)"(.*)$'
|
|
fw="$(framework_module)"
|
|
while IFS= read -r line || [[ -n $line ]]; do
|
|
line="$(trim "${line%%//*}")"
|
|
[[ -n $line ]] || continue
|
|
if ((inblock)); then
|
|
if [[ $line == ')' ]]; then
|
|
inblock=0
|
|
continue
|
|
fi
|
|
rest="$line"
|
|
elif [[ $line =~ $block_re ]]; then
|
|
inblock=1
|
|
continue
|
|
elif [[ $line =~ $single_re ]]; then
|
|
rest="${BASH_REMATCH[1]}"
|
|
else
|
|
continue
|
|
fi
|
|
[[ $rest == *'=>'* ]] || continue
|
|
read -r lhs _ <<<"${rest%%=>*}"
|
|
[[ ${lhs:-} == "$fw" ]] || continue
|
|
rhs="$(trim "${rest#*=>}")"
|
|
if [[ $rhs =~ $quoted_re ]]; then
|
|
first="${BASH_REMATCH[1]}"
|
|
read -r ver _ <<<"${BASH_REMATCH[2]}"
|
|
else
|
|
read -r first ver _ <<<"$rhs"
|
|
fi
|
|
[[ -n ${first:-} ]] || continue
|
|
printf '%s\n' "$first"
|
|
if [[ -n ${ver:-} ]]; then
|
|
printf '%s\n' "$ver"
|
|
fi
|
|
return 0
|
|
done <"$file"
|
|
return 1
|
|
}
|
|
|
|
# pull_branch fast-forwards the branch $2 checked out in $1 from its
|
|
# upstream. SUMMERCMS_REF ($3) does not apply to a branch. A missing
|
|
# upstream or a diverged branch stops the script with the checkout
|
|
# unchanged; check_framework_dir has already refused a dirty tree.
|
|
pull_branch() {
|
|
local dir="$1" branch="$2" ref="$3" upstream before after
|
|
if [[ -n $ref ]]; then
|
|
warn "SUMMERCMS_REF is ignored for a branch checkout; $branch is pulled from its upstream"
|
|
fi
|
|
upstream="$(git -C "$dir" rev-parse --abbrev-ref --symbolic-full-name '@{u}' </dev/null 2>/dev/null)" ||
|
|
die "$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"
|
|
before="$(git -C "$dir" rev-parse HEAD </dev/null)"
|
|
say "pulling $branch from $upstream (fast-forward only)"
|
|
# --no-rebase: a pull.rebase setting must not turn this into a rebase.
|
|
if ! git -C "$dir" pull --quiet --no-rebase --ff-only </dev/null; then
|
|
die "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"
|
|
fi
|
|
after="$(git -C "$dir" rev-parse HEAD </dev/null)"
|
|
if [[ $before == "$after" ]]; then
|
|
say "framework checkout is already up to date"
|
|
else
|
|
say "framework checkout moved from ${before:0:12} to ${after:0:12}"
|
|
fi
|
|
}
|
|
|
|
# note_shared_apps names the other sm-*-app directories, next to the
|
|
# application $2 and next to the checkout $1, whose go.mod replace resolves
|
|
# to the same checkout. It only reads their go.mod; they are not rebuilt.
|
|
note_shared_apps() {
|
|
local dir="$1" app="$2" real self parent fwparent scan last="" cand target list=""
|
|
real="$(cd "$dir" && pwd -P)"
|
|
self="$(cd "$app" && pwd -P)"
|
|
parent="${self%/*}"
|
|
fwparent="${real%/*}"
|
|
for scan in "${parent:-/}" "${fwparent:-/}"; do
|
|
[[ $scan != "$last" ]] || continue
|
|
last="$scan"
|
|
for cand in "${scan%/}"/sm-*-app; do
|
|
[[ -d $cand && -f $cand/go.mod && -r $cand/go.mod ]] || continue
|
|
[[ "$(cd "$cand" 2>/dev/null && pwd -P)" != "$self" ]] || continue
|
|
target="$(framework_replace "$cand/go.mod")" || continue
|
|
target="${target%%$'\n'*}"
|
|
is_local_path "$target" || continue
|
|
[[ $target == /* ]] || target="$cand/$target"
|
|
[[ -d $target ]] || continue
|
|
[[ "$(cd "$target" 2>/dev/null && pwd -P)" == "$real" ]] || continue
|
|
list+=" $cand"
|
|
done
|
|
done
|
|
if [[ -n $list ]]; then
|
|
say "not rebuilt, also using $dir:$list; each picks up the new framework on its next summer build"
|
|
fi
|
|
}
|
|
|
|
# env_line_state tells whether the .env line $2 sets the key $1, read the
|
|
# way the framework's .env loader reads it (trimmed, # comments skipped, an
|
|
# optional "export " prefix, the key trimmed before the first =, matching
|
|
# surrounding quotes removed from the value): 0 with a value, 2 with an
|
|
# empty value, 1 when the line does not set the key.
|
|
env_line_state() {
|
|
local key="$1" line k v
|
|
line="$(trim "$2")"
|
|
[[ -n $line && $line != '#'* ]] || return 1
|
|
if [[ $line == 'export '* ]]; then
|
|
line="$(trim "${line#export }")"
|
|
fi
|
|
[[ $line == *=* ]] || return 1
|
|
k="$(trim "${line%%=*}")"
|
|
[[ $k == "$key" ]] || return 1
|
|
v="$(trim "${line#*=}")"
|
|
if ((${#v} >= 2)) && [[ $v == \"*\" || $v == \'*\' ]]; then
|
|
v="${v:1:${#v}-2}"
|
|
fi
|
|
[[ -n $v ]] || return 2
|
|
return 0
|
|
}
|
|
|
|
# generate_secret prints 32 random bytes as standard base64 (44 characters),
|
|
# from openssl when it is installed, else from head and base64 over
|
|
# /dev/urandom. It returns 1 when neither produces a valid value. The value
|
|
# only travels through command substitution and printf.
|
|
generate_secret() {
|
|
local s="" re='^[A-Za-z0-9+/]{43}=$'
|
|
if command -v openssl >/dev/null 2>&1; then
|
|
s="$(openssl rand -base64 32 </dev/null 2>/dev/null)" || s=""
|
|
s="${s//$'\n'/}"
|
|
s="${s//$'\r'/}"
|
|
fi
|
|
if [[ ! $s =~ $re ]] && command -v head >/dev/null 2>&1 &&
|
|
command -v base64 >/dev/null 2>&1 && [[ -r /dev/urandom ]]; then
|
|
s="$(head -c 32 /dev/urandom | base64 2>/dev/null)" || s=""
|
|
s="${s//$'\n'/}"
|
|
s="${s//$'\r'/}"
|
|
fi
|
|
[[ $s =~ $re ]] || return 1
|
|
printf '%s\n' "$s"
|
|
}
|
|
|
|
# ensure_admin_secret appends the admin token secret to the .env of the
|
|
# application $1 when no active line sets it (applications created before
|
|
# the admin was wired in lack it). The append keeps the file mode, adds a
|
|
# newline first when the file lacks a final one, and never prints the value.
|
|
ensure_admin_secret() {
|
|
local app="$1" key=SUMMER_ADMIN__JWT__SECRET env line state found=0 empty=0 newline="" secret
|
|
env="$app/.env"
|
|
if [[ ! -f $env ]]; then
|
|
say "no .env in $app; skipped the admin token secret"
|
|
return 0
|
|
fi
|
|
[[ -r $env && -w $env ]] || die "$env is not readable and writable; add a $key line with any long random value, then run the update again"
|
|
while IFS= read -r line; do
|
|
state=0
|
|
env_line_state "$key" "$line" || state=$?
|
|
if ((state != 1)); then
|
|
found=1
|
|
empty=$((state == 2 ? 1 : 0))
|
|
fi
|
|
done <"$env"
|
|
# A non-empty $line here is a final line without a newline.
|
|
if [[ -n $line ]]; then
|
|
newline=$'\n'
|
|
state=0
|
|
env_line_state "$key" "$line" || state=$?
|
|
if ((state != 1)); then
|
|
found=1
|
|
empty=$((state == 2 ? 1 : 0))
|
|
fi
|
|
fi
|
|
if ((found)); then
|
|
if ((empty)); then
|
|
warn "$key in $env is empty; the admin refuses to start until it has a value"
|
|
fi
|
|
return 0
|
|
fi
|
|
secret="$(generate_secret)" ||
|
|
die "could not generate $key for $env (needs openssl, or head and base64); add a $key line with any long random value, then run the update again"
|
|
if ! printf '%s%s=%s\n' "$newline" "$key" "$secret" >>"$env"; then
|
|
die "could not append $key to $env"
|
|
fi
|
|
say "added $key to $env"
|
|
}
|
|
|
|
# update_app updates the application in the current directory: the
|
|
# framework checkout its go.mod replace names, summer, the admin token
|
|
# secret in its .env and its build. Nothing else is built or written.
|
|
update_app() {
|
|
local app="$PWD" fw rhs target dir ref="${SUMMERCMS_REF:-}" branch gobin
|
|
fw="$(framework_module)"
|
|
[[ -f $app/go.mod ]] || die "run this from inside an sm-<name>-app directory: $app has no go.mod"
|
|
[[ -f $app/summer.yaml ]] || die "run this from inside an sm-<name>-app directory: $app has no summer.yaml"
|
|
rhs="$(framework_replace "$app/go.mod")" ||
|
|
die "$app/go.mod has no replace for $fw; --update works on applications whose go.mod points the framework at a local checkout, as summer new writes it"
|
|
target="${rhs%%$'\n'*}"
|
|
is_local_path "$target" ||
|
|
die "$app/go.mod replaces $fw with ${rhs//$'\n'/ }, not a local checkout; update that requirement with go get instead"
|
|
if [[ $target == /* ]]; then
|
|
dir="$(abspath "$target")"
|
|
else
|
|
dir="$(abspath "$app/$target")"
|
|
fi
|
|
[[ $dir != / ]] || die "the framework replace in $app/go.mod must not point at /"
|
|
[[ -e $dir || -L $dir ]] || die "the framework replace in $app/go.mod points at $dir, which does not exist"
|
|
if [[ -n ${SUMMERCMS_DIR:-} ]]; then
|
|
warn "SUMMERCMS_DIR is ignored by --update; the framework checkout comes from go.mod ($dir)"
|
|
fi
|
|
|
|
command -v git >/dev/null 2>&1 || die "git is not installed; install git and run again"
|
|
check_go
|
|
|
|
check_framework_dir "$dir" "point the framework replace in $app/go.mod at a SummerCMS framework checkout"
|
|
if branch="$(git -C "$dir" symbolic-ref -q --short HEAD </dev/null)"; then
|
|
pull_branch "$dir" "$branch" "$ref"
|
|
else
|
|
move_detached "$dir" "$ref"
|
|
fi
|
|
note_shared_apps "$dir" "$app"
|
|
|
|
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
|
|
|
|
ensure_admin_secret "$app"
|
|
|
|
say "building $app"
|
|
"$gobin/summer" build </dev/null || die "summer build failed in $app; fix the error above and run the update again"
|
|
|
|
printf '\n==> Done. Run summer migrate, then restart summer serve.\n'
|
|
path_hint "$gobin"
|
|
}
|
|
|
|
main() {
|
|
set -euo pipefail
|
|
export GIT_TERMINAL_PROMPT=0
|
|
|
|
if (($# == 1)) && [[ $1 == -h || $1 == --help ]]; then
|
|
usage
|
|
exit 0
|
|
fi
|
|
local arg update=0 others=0
|
|
if (($#)); then
|
|
for arg in "$@"; do
|
|
if [[ $arg == --update ]]; then
|
|
update=1
|
|
else
|
|
others=$((others + 1))
|
|
fi
|
|
done
|
|
fi
|
|
if ((update)); then
|
|
if ((others)); then
|
|
printf 'error: --update takes no other argument\n\n' >&2
|
|
usage >&2
|
|
exit 2
|
|
fi
|
|
update_app
|
|
return 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"
|
|
path_hint "$gobin"
|
|
}
|
|
|
|
main "$@"
|