315 lines
12 KiB
Bash
Executable File
315 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# shellcheck shell=bash
|
|
#
|
|
# Interactive onboarding driver. Sequences doctor, init-env,
|
|
# check-gitea, login, check-access, and clone-orchestrator, prompting
|
|
# only when a decision requires a human.
|
|
|
|
set -euo pipefail
|
|
|
|
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
|
# shellcheck disable=SC1091
|
|
. "$here/common.sh"
|
|
|
|
root="$(repo_root)"
|
|
cd "$root"
|
|
|
|
headless=0
|
|
deploy=0
|
|
usage() {
|
|
cat <<'USAGE'
|
|
Usage: just setup [--headless|--no-browser] [--deploy] [--yes|-y]
|
|
|
|
Options:
|
|
--headless Do not open the browser during login. Prints the
|
|
--no-browser authorisation URL to stderr instead; paste it into
|
|
any browser that can reach the loopback callback
|
|
port (typically via SSH port-forward, see README).
|
|
--deploy After cloning the orchestrator, prompt [Y/n]
|
|
(default Y) to exec the orchestrator's
|
|
`just operator-setup` runner (reads
|
|
operator_setup_steps.json). Equivalent to
|
|
`just deploy`. Without --deploy, setup prints the
|
|
operator plan and stops after the clone with no
|
|
prompt and no auto-run.
|
|
--yes, -y Auto-accept every prompt (session reuse, checkout
|
|
reuse, handoff confirmation) by setting
|
|
FORGE_SETUP_YES=1 for this run. Safe only when
|
|
FORGE_GITEA_USERNAME is already set in .env. Does
|
|
not relax the --headless guard (a fresh PKCE login
|
|
still requires either a browser or a pre-populated
|
|
refresh token).
|
|
-h, --help Show this message.
|
|
|
|
Environment:
|
|
FORGE_SETUP_YES=1 Same as --yes; honoured even when no flag is given.
|
|
USAGE
|
|
}
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--headless|--no-browser) headless=1; shift;;
|
|
--deploy) deploy=1; shift;;
|
|
--yes|-y) export FORGE_SETUP_YES=1; shift;;
|
|
-h|--help) usage; exit 0;;
|
|
--) shift; break;;
|
|
-*) die "unknown option: $1 (try 'just setup --help')";;
|
|
*) die "unexpected positional argument: $1 (try 'just setup --help')";;
|
|
esac
|
|
done
|
|
|
|
# info/ok/warn/err/step/note/die: see scripts/common.sh.
|
|
|
|
# Stdout = chosen reply; stderr = prompt/echo.
|
|
prompt_choice() {
|
|
local msg="$1" default="$2" reply
|
|
if [ "${FORGE_SETUP_YES:-0}" = "1" ] || [ ! -t 0 ]; then
|
|
printf '%s %s [%s] (auto: %s)\n' \
|
|
"$(_fc_tag "$_FC_MAGENTA" '?')" "$msg" "$default" "$default" >&2
|
|
reply="$default"
|
|
else
|
|
printf '%s %s [%s] ' \
|
|
"$(_fc_tag "$_FC_MAGENTA" '?')" "$msg" "$default" >/dev/tty
|
|
IFS= read -r reply </dev/tty || reply=""
|
|
[ -z "$reply" ] && reply="$default"
|
|
fi
|
|
printf '%s' "$reply"
|
|
}
|
|
|
|
prompt_line() {
|
|
local msg="$1" reply
|
|
if [ "${FORGE_SETUP_YES:-0}" = "1" ] || [ ! -t 0 ]; then
|
|
die "non-interactive mode requested ($msg) but no default is safe; run 'just setup' in a terminal"
|
|
fi
|
|
printf '%s %s: ' "$(_fc_tag "$_FC_MAGENTA" '?')" "$msg" >/dev/tty
|
|
IFS= read -r reply </dev/tty || reply=""
|
|
printf '%s' "$reply"
|
|
}
|
|
|
|
step "1/7 checking prerequisites (just doctor)"
|
|
bash "$here/doctor.sh"
|
|
|
|
step "2/7 preparing .env"
|
|
if [ ! -f "$root/.env" ]; then
|
|
cp "$root/.env.example" "$root/.env"
|
|
ok "wrote $root/.env from template"
|
|
else
|
|
ok ".env already present"
|
|
fi
|
|
|
|
load_env
|
|
|
|
if [ -z "${FORGE_GITEA_USERNAME:-}" ]; then
|
|
note "FORGE_GITEA_USERNAME is blank. Enter the Gitea username exactly as it appears in the profile URL."
|
|
new_user="$(prompt_line "Gitea username")"
|
|
if [ -z "$new_user" ]; then
|
|
die "no username entered: aborting. Edit .env by hand and re-run 'just setup'."
|
|
fi
|
|
python3 - "$root/.env" "$new_user" <<'PY'
|
|
import pathlib, sys
|
|
path = pathlib.Path(sys.argv[1])
|
|
value = sys.argv[2]
|
|
lines = path.read_text(encoding="utf-8").splitlines()
|
|
touched = False
|
|
for i, line in enumerate(lines):
|
|
stripped = line.lstrip()
|
|
if stripped.startswith("FORGE_GITEA_USERNAME=") or stripped.startswith("export FORGE_GITEA_USERNAME="):
|
|
prefix = "export " if stripped.startswith("export ") else ""
|
|
lines[i] = f'{prefix}FORGE_GITEA_USERNAME="{value}"'
|
|
touched = True
|
|
break
|
|
if not touched:
|
|
lines.append(f'FORGE_GITEA_USERNAME="{value}"')
|
|
tmp = path.with_suffix(path.suffix + ".tmp")
|
|
tmp.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
tmp.replace(path)
|
|
PY
|
|
export FORGE_GITEA_USERNAME="$new_user"
|
|
ok "set FORGE_GITEA_USERNAME=$new_user in .env"
|
|
else
|
|
ok "FORGE_GITEA_USERNAME=$FORGE_GITEA_USERNAME"
|
|
fi
|
|
|
|
step "3/7 checking Gitea reachability (just check-gitea)"
|
|
curl -fsS --max-time 10 ${FORGE_INSECURE_TLS:+-k} \
|
|
"${FORGE_GITEA_URL}/api/v1/version" \
|
|
| python3 -c 'import json,sys; d=json.load(sys.stdin); print("[check-gitea] Gitea version:", d.get("version","?"))'
|
|
ok "reached $FORGE_GITEA_URL"
|
|
|
|
step "4/7 authenticating with Gitea (just login)"
|
|
existing_user=""
|
|
have_live=0
|
|
have_stored=0
|
|
if [ -f "$FORGE_AUTH_FILE" ]; then
|
|
have_stored=1
|
|
existing_user="$(python3 -c '
|
|
import json, sys
|
|
try:
|
|
d = json.load(open(sys.argv[1]))
|
|
print(d.get("username") or "")
|
|
except Exception:
|
|
print("")
|
|
' "$FORGE_AUTH_FILE" 2>/dev/null || true)"
|
|
if python3 "$here/forge_auth.py" status >/dev/null 2>&1; then
|
|
have_live=1
|
|
fi
|
|
fi
|
|
|
|
if [ "$have_stored" = "1" ] && [ "$have_live" = "0" ] \
|
|
&& [ -n "$existing_user" ] && [ -n "${FORGE_GITEA_USERNAME:-}" ] \
|
|
&& [ "${existing_user,,}" = "${FORGE_GITEA_USERNAME,,}" ]; then
|
|
note "stored session for '$existing_user' is not live; attempting silent refresh..."
|
|
if python3 "$here/forge_auth.py" refresh --force >/dev/null 2>&1; then
|
|
ok "refreshed stored session without a browser"
|
|
have_live=1
|
|
else
|
|
note "refresh failed (refresh token likely expired); a fresh login is required"
|
|
fi
|
|
fi
|
|
|
|
run_login=1
|
|
if [ "$have_live" = "1" ] && [ -n "$existing_user" ]; then
|
|
if [ -n "${FORGE_GITEA_USERNAME:-}" ] \
|
|
&& [ "${existing_user,,}" != "${FORGE_GITEA_USERNAME,,}" ]; then
|
|
note "stored token is for '$existing_user' but .env configures '$FORGE_GITEA_USERNAME'"
|
|
reply="$(prompt_choice "Log out and sign in as '$FORGE_GITEA_USERNAME'? [Y/n]" "Y")"
|
|
case "$reply" in
|
|
[Nn]*) run_login=0; note "keeping the existing '$existing_user' session (forge_login may still reject it)";;
|
|
*) python3 "$here/forge_auth.py" logout >/dev/null; ok "cleared stored Gitea tokens";;
|
|
esac
|
|
else
|
|
ok "stored token is live for '$existing_user'"
|
|
reply="$(prompt_choice "Reuse this session, or log out and re-authenticate? [R]euse / [L]ogout [R]" "R")"
|
|
case "$reply" in
|
|
[Ll]*) python3 "$here/forge_auth.py" logout >/dev/null; ok "cleared stored Gitea tokens";;
|
|
*) run_login=0; ok "reusing the stored session";;
|
|
esac
|
|
fi
|
|
elif [ "$have_stored" = "1" ] && [ -n "$existing_user" ]; then
|
|
note "stored session for '$existing_user' is stale and could not be refreshed silently; re-authenticating."
|
|
fi
|
|
|
|
if [ "$run_login" = "1" ]; then
|
|
if [ "$headless" = "1" ] && [ "${FORGE_SETUP_YES:-0}" = "1" ]; then
|
|
die "cannot complete a fresh login under --headless + FORGE_SETUP_YES=1:
|
|
a browser-based OAuth step is required but both flags
|
|
forbid any interaction. Recovery options:
|
|
1. Drop FORGE_SETUP_YES and re-run 'just setup --headless'
|
|
(setup prints the authorisation URL and waits for
|
|
browser completion).
|
|
2. Run 'just login' once on a machine with a browser
|
|
to populate the stored refresh token, then re-run
|
|
'just setup --headless' from the headless host.
|
|
3. Run 'just setup' (with a browser available) instead."
|
|
fi
|
|
if [ "$headless" = "1" ]; then
|
|
bash "$here/forge_login.sh" --no-browser
|
|
else
|
|
bash "$here/forge_login.sh"
|
|
fi
|
|
else
|
|
bash "$here/install-git-credential-helper.sh" >/dev/null
|
|
fi
|
|
|
|
step "5/7 confirming silent git access (just check-access)"
|
|
GIT_TERMINAL_PROMPT=0 GCM_INTERACTIVE=Never \
|
|
GIT_ASKPASS="" SSH_ASKPASS="" \
|
|
VSCODE_GIT_ASKPASS_MAIN="" VSCODE_GIT_ASKPASS_NODE="" \
|
|
VSCODE_GIT_ASKPASS_EXTRA_ARGS="" VSCODE_GIT_IPC_HANDLE="" \
|
|
DISPLAY="" WAYLAND_DISPLAY="" \
|
|
git ls-remote "$FORGE_ORCHESTRATOR_REPO_URL" HEAD >/dev/null 2>&1 \
|
|
|| die "git ls-remote failed. The Gitea account may not yet be in the org, or FORGE_ORCHESTRATOR_REPO_URL is wrong."
|
|
ok "silent clone access confirmed"
|
|
|
|
step "6/7 cloning the orchestrator (just clone-orchestrator)"
|
|
workspace_root="${FORGE_WORKSPACE_ROOT:-.}"
|
|
repo_name="$(basename "$FORGE_ORCHESTRATOR_REPO_URL" .git)"
|
|
dest="$workspace_root/$repo_name"
|
|
|
|
if [ -d "$dest/.git" ]; then
|
|
note "orchestrator already present at $dest"
|
|
reply="$(prompt_choice "Reuse existing checkout, or wipe and re-clone? [R]euse / [W]ipe [R]" "R")"
|
|
case "$reply" in
|
|
[Ww]*)
|
|
note "removing $dest"
|
|
rm -rf "$dest"
|
|
;;
|
|
*)
|
|
ok "keeping existing checkout"
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
if [ ! -d "$dest/.git" ]; then
|
|
mkdir -p "$workspace_root"
|
|
export GIT_TERMINAL_PROMPT=0 GCM_INTERACTIVE=Never
|
|
unset GIT_ASKPASS SSH_ASKPASS \
|
|
VSCODE_GIT_ASKPASS_MAIN VSCODE_GIT_ASKPASS_NODE \
|
|
VSCODE_GIT_ASKPASS_EXTRA_ARGS VSCODE_GIT_IPC_HANDLE \
|
|
DISPLAY WAYLAND_DISPLAY
|
|
if [ -n "${FORGE_ORCHESTRATOR_BRANCH:-}" ]; then
|
|
git clone --branch "$FORGE_ORCHESTRATOR_BRANCH" \
|
|
"$FORGE_ORCHESTRATOR_REPO_URL" "$dest"
|
|
else
|
|
git clone "$FORGE_ORCHESTRATOR_REPO_URL" "$dest"
|
|
fi
|
|
ok "cloned into $dest"
|
|
fi
|
|
|
|
step "7/7 next steps (from the orchestrator's operator manifest)"
|
|
|
|
# The operator scaffold is pinned to the operator manifest/recipe pair;
|
|
# `--deploy` only controls whether to prompt for an automatic handoff.
|
|
# The contributor manifest is never referenced from this scaffold.
|
|
manifest_name="operator_setup_steps.json"
|
|
recipe_name="operator-setup"
|
|
manifest_path="$dest/scripts/$manifest_name"
|
|
|
|
if [ ! -f "$manifest_path" ]; then
|
|
warn "orchestrator at $dest does not ship $manifest_name"
|
|
warn "(older checkout?). See the orchestrator's README for onboarding:"
|
|
note " $dest/README.md"
|
|
exit 0
|
|
fi
|
|
|
|
# Authoritative step list: $manifest_path in the orchestrator checkout.
|
|
# This welcome repo never hardcodes the sequence.
|
|
bash "$here/next_steps.sh" --manifest "$manifest_name" --recipe "$recipe_name" || true
|
|
|
|
forward=()
|
|
[ "$headless" = "1" ] && forward+=("--headless")
|
|
[ "${FORGE_SETUP_YES:-0}" = "1" ] && forward+=("--yes")
|
|
|
|
if [ "$deploy" != "1" ]; then
|
|
# `just setup` (no --deploy) prints the plan and stops: no prompt,
|
|
# no auto-run. The hint below is informational only.
|
|
note "skipping auto-run (no --deploy). To execute the plan later:"
|
|
if [ "${#forward[@]}" -gt 0 ]; then
|
|
note " just run-next-steps --manifest $manifest_name --recipe $recipe_name ${forward[*]}"
|
|
else
|
|
note " just run-next-steps --manifest $manifest_name --recipe $recipe_name"
|
|
fi
|
|
note "or, inside the orchestrator checkout:"
|
|
note " cd $dest && just $recipe_name"
|
|
exit 0
|
|
fi
|
|
|
|
# --deploy path: prompt [Y/n] (default Y). FORGE_SETUP_YES=1 accepts Y.
|
|
reply="$(prompt_choice "Hand off to just $recipe_name now? [Y/n]" "Y")"
|
|
case "$reply" in
|
|
[Nn]*)
|
|
note "skipping auto-run. To execute later:"
|
|
if [ "${#forward[@]}" -gt 0 ]; then
|
|
note " just run-next-steps --manifest $manifest_name --recipe $recipe_name ${forward[*]}"
|
|
else
|
|
note " just run-next-steps --manifest $manifest_name --recipe $recipe_name"
|
|
fi
|
|
note "or, inside the orchestrator checkout:"
|
|
note " cd $dest && just $recipe_name"
|
|
;;
|
|
*)
|
|
step "handing off to the orchestrator"
|
|
exec bash "$here/next_steps.sh" --run --manifest "$manifest_name" --recipe "$recipe_name" "${forward[@]}"
|
|
;;
|
|
esac
|