#!/usr/bin/env bash set -euo pipefail ######################################## # Forgejo Updater # curl | bash safe ######################################## TTY="/dev/tty" BOLD="\033[1m" GREEN="\033[32m" YELLOW="\033[33m" RED="\033[31m" BLUE="\033[34m" RESET="\033[0m" section() { printf "\n${BLUE}${BOLD}==> %s${RESET}\n" "$*"; } say() { printf "${GREEN}✔ %s${RESET}\n" "$*"; } warn() { printf "${YELLOW}⚠ %s${RESET}\n" "$*"; } err() { printf "${RED}✖ %s${RESET}\n" "$*"; } have() { command -v "$1" >/dev/null 2>&1; } need_tty() { if [[ ! -r "$TTY" || ! -w "$TTY" ]]; then # No TTY (e.g. non-interactive CI) — that's fine as long as -y was passed. return 1 fi return 0 } prompt() { local msg="$1" out printf "${BOLD}%s${RESET}" "$msg" >"$TTY" IFS= read -r out <"$TTY" printf "%s" "$out" } confirm_default_no() { local msg="$1" ans if $ASSUME_YES; then return 0 fi if ! need_tty; then err "No TTY and -y not given — refusing to guess on: $msg" exit 1 fi ans="$(prompt "$msg [y/N]: ")" [[ "${ans,,}" == "y" || "${ans,,}" == "yes" ]] } require_root_or_sudo() { if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then if have sudo; then SUDO="sudo" else err "Root or sudo required." exit 1 fi else SUDO="" fi } run_with_spinner() { local label="$1"; shift ("$@") & local pid=$! spin='|/-\' i=0 printf "\033[?25l" >"$TTY" 2>/dev/null || true while kill -0 "$pid" 2>/dev/null; do i=$(( (i + 1) % 4 )) printf "\r${BLUE}${BOLD}..${RESET} %s %s" "${spin:$i:1}" "$label" >"$TTY" 2>/dev/null || true sleep 0.12 done wait "$pid"; local rc=$? printf "\r\033[K" >"$TTY" 2>/dev/null || true printf "\033[?25h" >"$TTY" 2>/dev/null || true return "$rc" } ######################################## # Args ######################################## ASSUME_YES=false INSTALL_PATH="/usr/local/bin/forgejo" SERVICE_NAME="forgejo" VERSION="" usage() { cat < -y Assume yes to all prompts (non-interactive) -p PATH Install path (default: /usr/local/bin/forgejo) -s NAME systemd service name (default: forgejo) Example: $0 11.0.10 EOF } while getopts ":yp:s:h" opt; do case "$opt" in y) ASSUME_YES=true ;; p) INSTALL_PATH="$OPTARG" ;; s) SERVICE_NAME="$OPTARG" ;; h) usage; exit 0 ;; *) usage; exit 1 ;; esac done shift $((OPTIND - 1)) VERSION="${1:-}" if [[ -z "$VERSION" ]]; then usage exit 1 fi # Strip an accidental leading "v" so both "11.0.10" and "v11.0.10" work. VERSION="${VERSION#v}" need_tty || true require_root_or_sudo ######################################## # Dependency check ######################################## section "Checking dependencies" for bin in curl systemctl sha256sum install; do have "$bin" || { err "Required tool '$bin' not found."; exit 1; } done say "All required tools present" ######################################## # Arch detection ######################################## case "$(uname -m)" in x86_64|amd64) ARCH="amd64" ;; aarch64|arm64) ARCH="arm64" ;; armv7l|armv6l) ARCH="arm-6" ;; *) err "Unsupported architecture: $(uname -m)" exit 1 ;; esac BINARY="forgejo-${VERSION}-linux-${ARCH}" BASE_URL="https://codeberg.org/forgejo/forgejo/releases/download/v${VERSION}" ######################################## # Sanity checks before touching anything ######################################## if ! systemctl list-unit-files "${SERVICE_NAME}.service" >/dev/null 2>&1; then warn "systemd unit '${SERVICE_NAME}.service' not found — will still attempt but stop/start may fail." fi WORKDIR="$(mktemp -d)" cleanup() { rm -rf "$WORKDIR"; } trap cleanup EXIT ######################################## # Download ######################################## section "Downloading Forgejo ${VERSION} (${ARCH})" if ! run_with_spinner "Downloading binary..." \ curl -fL --retry 3 --retry-delay 2 -o "${WORKDIR}/${BINARY}" "${BASE_URL}/${BINARY}"; then err "Download failed. Check the version number and your network connection." exit 1 fi say "Downloaded ${BINARY}" ######################################## # Checksum verification (best-effort; Forgejo publishes .sha256 sidecars) ######################################## section "Verifying checksum" if curl -fsSL -o "${WORKDIR}/${BINARY}.sha256" "${BASE_URL}/${BINARY}.sha256" 2>/dev/null; then ( cd "$WORKDIR" && sha256sum -c "${BINARY}.sha256" ) || { err "Checksum verification FAILED. Refusing to install a corrupted/tampered binary." exit 1 } say "Checksum verified" else warn "No checksum file found upstream — skipping verification." if ! confirm_default_no "Continue without checksum verification?"; then err "Aborted by user." exit 1 fi fi chmod +x "${WORKDIR}/${BINARY}" ######################################## # Validate the binary actually runs before going anywhere near the service ######################################## section "Validating binary" if ! "${WORKDIR}/${BINARY}" --version >/dev/null 2>&1; then err "Downloaded binary failed to execute (--version check). Aborting before touching the running service." exit 1 fi say "Binary runs OK: $("${WORKDIR}/${BINARY}" --version 2>&1 | head -n1)" ######################################## # Backup current binary ######################################## BACKUP="" if [[ -f "$INSTALL_PATH" ]]; then BACKUP="${INSTALL_PATH}.bak.$(date +%Y%m%d%H%M%S)" $SUDO cp -a "$INSTALL_PATH" "$BACKUP" say "Backed up existing binary to ${BACKUP}" fi ######################################## # Confirm before disrupting the service ######################################## section "Ready to install" echo " Version: ${VERSION}" echo " Path: ${INSTALL_PATH}" echo " Service: ${SERVICE_NAME}" if ! confirm_default_no "Stop '${SERVICE_NAME}', replace the binary, and restart?"; then err "Aborted by user. Nothing was changed." exit 1 fi ######################################## # Stop, swap (atomically), start ######################################## section "Installing" $SUDO systemctl stop "${SERVICE_NAME}" || warn "Failed to stop ${SERVICE_NAME} (continuing anyway)" # `install` does an atomic rename into place rather than an in-place copy, # so there's never a half-written binary on disk. $SUDO install -m 0755 "${WORKDIR}/${BINARY}" "$INSTALL_PATH" say "Installed new binary to ${INSTALL_PATH}" $SUDO systemctl start "${SERVICE_NAME}" || true section "Verifying service" sleep 2 if $SUDO systemctl is-active --quiet "${SERVICE_NAME}"; then say "${SERVICE_NAME} is active and running" else err "${SERVICE_NAME} failed to start after update!" if [[ -n "$BACKUP" ]]; then warn "Rolling back to previous binary..." $SUDO install -m 0755 "$BACKUP" "$INSTALL_PATH" $SUDO systemctl restart "${SERVICE_NAME}" || true if $SUDO systemctl is-active --quiet "${SERVICE_NAME}"; then say "Rollback successful — service restored to previous version" else err "Rollback also failed — manual intervention required. Backup at: ${BACKUP}" fi else err "No backup was available to roll back to. Check 'systemctl status ${SERVICE_NAME}' and journalctl." fi exit 1 fi section "Done" say "Forgejo updated to ${VERSION}"