diff --git a/scripts/setup/officer-setup.sh b/scripts/setup/officer-setup.sh index 3f876e6f..edb23428 100755 --- a/scripts/setup/officer-setup.sh +++ b/scripts/setup/officer-setup.sh @@ -40,6 +40,8 @@ done source "$SCRIPT_DIR/officer-setup/lib/base.sh" # shellcheck source=officer-setup/lib/preflight.sh source "$SCRIPT_DIR/officer-setup/lib/preflight.sh" +# shellcheck source=officer-setup/lib/repo.sh +source "$SCRIPT_DIR/officer-setup/lib/repo.sh" trap 'echo ""; echo -e "${RED}╔══════════════════════════════════════════════════╗${NC}"; echo -e "${RED}║ OFFICER SETUP FAILED${NC}"; echo -e "${RED}║ Step: ${CURRENT_STEP:-unknown}${NC}"; echo -e "${RED}║ Line: $LINENO${NC}"; echo -e "${RED}║ Command: $BASH_COMMAND${NC}"; echo -e "${RED}╚══════════════════════════════════════════════════╝${NC}"' ERR @@ -153,11 +155,113 @@ else echo " steps are remembered and skipped." fi +# ============================================================================= +# 2. Repository +# ============================================================================= + +step "Repository" +if ! skip; then + PLATFORM_DIR="$(platform_dir)" + + echo "" + info "Repository — where the platform's code lives" + echo " path: ${PLATFORM_DIR}" + + if repo_exists; then + echo " remote: $(repo_remote)" + echo " branch: $(repo_branch)" + echo " working: $(repo_is_dirty && echo 'has uncommitted changes' || echo 'clean')" + + # Reported, never silently corrected. Repointing somebody's remote is a + # decision about where their work goes, and this script is not entitled to + # make it quietly. + if [[ -n "$(repo_remote)" && "$(repo_remote)" != "$OFFICER_REPO" ]]; then + echo "" + warn "this checkout points somewhere other than ${OFFICER_REPO}" + echo " Left alone. To move it:" + echo " git -C ${PLATFORM_DIR} remote set-url origin ${OFFICER_REPO}" + fi + + if repo_is_dirty; then + echo "" + echo " not pulling — there are uncommitted changes here, and a pull" + echo " would either fail or bury them" + SUMMARY+=("Repository: present at ${PLATFORM_DIR}, left alone (uncommitted changes)") + elif confirm "Pull the latest changes?"; then + if pull_repo; then + ok "up to date on $(repo_branch)" + SUMMARY+=("Repository: pulled, on $(repo_branch)") + else + # --ff-only, so this means the branch has diverged rather than that the + # network failed. Saying which matters. + warn "could not fast-forward — the local branch has diverged from the remote" + ERRORS+=("Repository: pull refused, branch diverged") + SUMMARY+=("Repository: present, pull refused (diverged)") + fi + else + SUMMARY+=("Repository: present at ${PLATFORM_DIR}") + fi + + else + echo " nothing there yet" + echo "" + info "Clone from ${OFFICER_REPO}?" + echo " Cloned as ${USERNAME}, not as root — a repository owned by root is" + echo " one you cannot pull, commit in, or install into." + + CLONE_URL="$OFFICER_REPO" + + # Checked before cloning. An ssh URL with no usable key does not fail + # cleanly: git prompts for a password nobody is there to type, or stops on + # host-key verification. + if [[ "$CLONE_URL" == ssh://* ]] && ! repo_ssh_ok "$CLONE_URL"; then + echo "" + warn "${USERNAME} cannot authenticate to $(repo_ssh_host "$CLONE_URL") over ssh" + echo " Either add that account's public key to the git server, or clone" + echo " over https instead — which works without a key if the repository" + echo " is readable anonymously." + echo "" + echo " [1] https — $(repo_https_url "$CLONE_URL")" + echo " [2] ssh anyway — will fail if the key is genuinely missing" + echo " [3] stop here, and add the key first" + echo "" + REPO_PICK="" + while [[ -z "$REPO_PICK" ]]; do + if ! read -rp " Which one? (1/2/3) [1]: " REPO_CHOICE; then + echo "" + fail "No answer." + fi + case "${REPO_CHOICE:-1}" in + 1) + CLONE_URL="$(repo_https_url "$CLONE_URL")" + REPO_PICK=go + ;; + 2) REPO_PICK=go ;; + 3) fail "Stopped. Add ${USERNAME}'s public key to the git server and run this again." ;; + *) warn "Pick 1, 2 or 3." ;; + esac + done + fi + + if confirm "Clone it now?"; then + if clone_repo "$CLONE_URL"; then + ok "cloned to ${PLATFORM_DIR} on $(repo_branch)" + SUMMARY+=("Repository: cloned from ${CLONE_URL}") + else + warn "the clone did not complete" + ERRORS+=("Repository: clone failed") + fail "Nothing below can run without the repository." + fi + else + fail "Nothing below can run without the repository." + fi + fi + step_ok +fi + # ============================================================================= # NOT BUILT YET # ============================================================================= -# -# 2 Repository clone or locate the platform at $OFFICER_ROOT/platform # 3 Dependencies bun install # 4 Database Postgres in docker, or one you already run # 5 Environment .env diff --git a/scripts/setup/officer-setup/lib/repo.sh b/scripts/setup/officer-setup/lib/repo.sh new file mode 100644 index 00000000..163a147b --- /dev/null +++ b/scripts/setup/officer-setup/lib/repo.sh @@ -0,0 +1,62 @@ +#!/bin/bash +# ============================================================================= +# officer-setup — the repository +# ============================================================================= +# +# Definitions only. +# +# ── Cloned as the owner, never as root ── +# +# A repository cloned by root is one the owner cannot pull, cannot commit in, and +# whose node_modules they cannot write. Every git operation here runs as the +# account, from a directory that account can stat. + +[[ -n "${OFFICER_SETUP_REPO_LOADED:-}" ]] && return 0 +OFFICER_SETUP_REPO_LOADED=1 + +OFFICER_REPO="${OFFICER_REPO:-ssh://git@gitea.officer.dev:2222/officerdev/platform.git}" + +platform_dir() { echo "${OFFICER_ROOT}/platform"; } + +repo_exists() { [[ -d "$(platform_dir)/.git" ]]; } + +repo_remote() { (cd "$(platform_dir)" 2>/dev/null && git remote get-url origin 2>/dev/null) || true; } +repo_branch() { (cd "$(platform_dir)" 2>/dev/null && git branch --show-current 2>/dev/null) || true; } +repo_is_dirty() { [[ -n "$(cd "$(platform_dir)" 2>/dev/null && git status --porcelain 2>/dev/null)" ]]; } + +# Split an ssh:// URL into host and port, for the reachability check below. +repo_ssh_host() { sed -E 's|^ssh://[^@]*@([^:/]+).*|\1|' <<<"$1"; } +repo_ssh_port() { sed -nE 's|^ssh://[^@]*@[^:]+:([0-9]+)/.*|\1|p' <<<"$1"; } + +# Can this account actually clone it? +# +# Checked before the clone rather than after, because an ssh URL with no usable +# key does not fail cleanly — git either prompts for a password nobody is there +# to type, or hangs on host-key verification. BatchMode turns both into an +# immediate non-zero. +# +# Gitea answers a successful auth with a message and exit 1, so the test is +# whether the SERVER recognised us, not the exit code. +repo_ssh_ok() { + local url="$1" host port out + host="$(repo_ssh_host "$url")" + port="$(repo_ssh_port "$url")" + [[ -n "$host" ]] || return 1 + + out="$(as_owner "ssh -T -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=8 ${port:+-p $port} git@${host} 2>&1" || true)" + grep -qiE "authenticated|successfully|welcome|does not provide shell access" <<<"$out" +} + +# The https form of the same repository, for a machine with no key. +repo_https_url() { + sed -E 's|^ssh://[^@]*@([^:/]+)(:[0-9]+)?/|https://\1/|' <<<"$1" +} + +clone_repo() { + local url="$1" dest + dest="$(platform_dir)" + install -d -m 0755 -o "$USERNAME" -g "$(user_group)" "$OFFICER_ROOT" + as_owner "git clone '${url}' '${dest}'" / +} + +pull_repo() { as_owner "git -C '$(platform_dir)' pull --ff-only" /; }