From d2c07300681085439db9eed9291c0566bf3a4099 Mon Sep 17 00:00:00 2001 From: S Date: Thu, 26 Feb 2026 15:01:28 -0600 Subject: [PATCH] feat: add shared library (lib/common.sh) 17 functions: logging (info/warn/error/success/step/phase_header), env management (load_env/save_env_var/require_vars), SSH wrappers (ssh_exec/ssh_check/scp_to), API wrappers (gitea_api/gitea_backup_api/ github_api), template rendering, and polling (wait_for_http/wait_for_ssh). All logs go to stderr, JSON data to stdout. Shellcheck clean. Co-Authored-By: Claude Opus 4.6 --- lib/common.sh | 290 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 lib/common.sh diff --git a/lib/common.sh b/lib/common.sh new file mode 100644 index 0000000..fec4cea --- /dev/null +++ b/lib/common.sh @@ -0,0 +1,290 @@ +#!/usr/bin/env bash +# ============================================================================= +# lib/common.sh — Shared functions for Gitea Migration Toolkit +# Source this file in every script: source "$(dirname "$0")/lib/common.sh" +# ============================================================================= +# This file defines functions only. No side effects at source time. + +# --------------------------------------------------------------------------- +# Colors (only if stderr is a terminal) +# --------------------------------------------------------------------------- +if [[ -t 2 ]]; then + _C_RESET='\033[0m' + _C_RED='\033[0;31m' + _C_GREEN='\033[0;32m' + _C_YELLOW='\033[0;33m' + _C_BLUE='\033[0;34m' + _C_BOLD='\033[1m' +else + _C_RESET='' _C_RED='' _C_GREEN='' _C_YELLOW='' _C_BLUE='' _C_BOLD='' +fi + +# --------------------------------------------------------------------------- +# Logging — all output to stderr so stdout stays clean for data (JSON, etc.) +# --------------------------------------------------------------------------- + +log_info() { + printf '%b[INFO]%b %s\n' "$_C_BLUE" "$_C_RESET" "$*" >&2 +} + +log_warn() { + printf '%b[WARN]%b %s\n' "$_C_YELLOW" "$_C_RESET" "$*" >&2 +} + +log_error() { + printf '%b[ERROR]%b %s\n' "$_C_RED" "$_C_RESET" "$*" >&2 +} + +log_success() { + printf '%b[OK]%b %s\n' "$_C_GREEN" "$_C_RESET" "$*" >&2 +} + +log_step() { + local step_num="$1"; shift + printf ' %b[%s]%b %s\n' "$_C_BOLD" "$step_num" "$_C_RESET" "$*" >&2 +} + +phase_header() { + local num="$1" name="$2" + printf '\n%b=== Phase %s: %s ===%b\n\n' "$_C_BOLD" "$num" "$name" "$_C_RESET" >&2 +} + +# --------------------------------------------------------------------------- +# Environment +# --------------------------------------------------------------------------- + +# Resolve the project root (directory containing .env / .env.example) +_project_root() { + local dir + dir="$(cd "$(dirname "${BASH_SOURCE[1]:-${BASH_SOURCE[0]}}")/.." && pwd)" + # Walk up until we find .env or .env.example + while [[ "$dir" != "/" ]]; do + if [[ -f "$dir/.env" ]] || [[ -f "$dir/.env.example" ]]; then + printf '%s' "$dir" + return 0 + fi + dir="$(dirname "$dir")" + done + # Fallback: script's parent dir + cd "$(dirname "${BASH_SOURCE[1]:-${BASH_SOURCE[0]}}")/.." && pwd +} + +load_env() { + local env_file + env_file="$(_project_root)/.env" + if [[ ! -f "$env_file" ]]; then + log_error ".env file not found at $env_file" + log_error "Copy .env.example to .env and populate values." + return 1 + fi + set -a + # shellcheck source=/dev/null + source "$env_file" + set +a +} + +save_env_var() { + local key="$1" value="$2" + local env_file + env_file="$(_project_root)/.env" + + if [[ ! -f "$env_file" ]]; then + log_error ".env file not found at $env_file" + return 1 + fi + + # Escape special characters in value for sed + local escaped_value + escaped_value=$(printf '%s' "$value" | sed 's/[&/\]/\\&/g') + + if grep -q "^${key}=" "$env_file"; then + # Replace existing line — match KEY= followed by anything (value + optional comment) + sed -i.bak "s|^${key}=.*|${key}=${escaped_value}|" "$env_file" + rm -f "${env_file}.bak" + else + # Append new line + printf '%s=%s\n' "$key" "$value" >> "$env_file" + fi + + # Also export it in current shell + export "${key}=${value}" +} + +require_vars() { + local var + for var in "$@"; do + if [[ -z "${!var:-}" ]]; then + log_error "Missing required var: $var" + return 1 + fi + done +} + +# --------------------------------------------------------------------------- +# SSH +# --------------------------------------------------------------------------- + +ssh_exec() { + local host_key="$1"; shift + local cmd="$*" + + local ip_var="${host_key}_IP" + local user_var="${host_key}_SSH_USER" + local port_var="${host_key}_SSH_PORT" + + local ip="${!ip_var:-}" + local user="${!user_var:-}" + local port="${!port_var:-22}" + + if [[ -z "$ip" ]] || [[ -z "$user" ]]; then + log_error "SSH config incomplete for $host_key: ${ip_var}=${ip:-}, ${user_var}=${user:-}" + return 1 + fi + + ssh -o ConnectTimeout=10 \ + -o StrictHostKeyChecking=accept-new \ + -o BatchMode=yes \ + -p "$port" \ + "${user}@${ip}" \ + "$cmd" +} + +ssh_check() { + local host_key="$1" + ssh_exec "$host_key" "true" 2>/dev/null +} + +scp_to() { + local host_key="$1" src="$2" dst="$3" + + local ip_var="${host_key}_IP" + local user_var="${host_key}_SSH_USER" + local port_var="${host_key}_SSH_PORT" + + local ip="${!ip_var:-}" + local user="${!user_var:-}" + local port="${!port_var:-22}" + + if [[ -z "$ip" ]] || [[ -z "$user" ]]; then + log_error "SCP config incomplete for $host_key" + return 1 + fi + + scp -o ConnectTimeout=10 \ + -o StrictHostKeyChecking=accept-new \ + -o BatchMode=yes \ + -P "$port" \ + "$src" "${user}@${ip}:${dst}" +} + +# --------------------------------------------------------------------------- +# API wrappers — return JSON on stdout, logs go to stderr +# --------------------------------------------------------------------------- + +_api_call() { + local base_url="$1" token="$2" method="$3" path="$4" data="${5:-}" + + local http_code + local tmpfile + tmpfile=$(mktemp) + + local -a curl_args=( + -s + -X "$method" + -H "Authorization: token ${token}" + -H "Content-Type: application/json" + -H "Accept: application/json" + -o "$tmpfile" + -w "%{http_code}" + ) + + if [[ -n "$data" ]]; then + curl_args+=(-d "$data") + fi + + http_code=$(curl "${curl_args[@]}" "${base_url}${path}") || { + log_error "curl failed for $method ${base_url}${path}" + rm -f "$tmpfile" + return 1 + } + + if [[ "$http_code" -ge 400 ]]; then + log_error "API $method ${path} returned HTTP ${http_code}" + log_error "Response: $(cat "$tmpfile")" + rm -f "$tmpfile" + return 1 + fi + + cat "$tmpfile" + rm -f "$tmpfile" +} + +gitea_api() { + local method="$1" path="$2" data="${3:-}" + _api_call "${GITEA_INTERNAL_URL}/api/v1" "${GITEA_ADMIN_TOKEN}" "$method" "$path" "$data" +} + +gitea_backup_api() { + local method="$1" path="$2" data="${3:-}" + _api_call "${GITEA_BACKUP_INTERNAL_URL}/api/v1" "${GITEA_BACKUP_ADMIN_TOKEN}" "$method" "$path" "$data" +} + +github_api() { + local method="$1" path="$2" data="${3:-}" + _api_call "https://api.github.com" "${GITHUB_TOKEN}" "$method" "$path" "$data" +} + +# --------------------------------------------------------------------------- +# Templates +# --------------------------------------------------------------------------- + +render_template() { + local src="$1" dest="$2" + + if [[ ! -f "$src" ]]; then + log_error "Template not found: $src" + return 1 + fi + + envsubst < "$src" > "$dest" +} + +# --------------------------------------------------------------------------- +# Polling / waiting +# --------------------------------------------------------------------------- + +wait_for_http() { + local url="$1" max_secs="${2:-60}" + local elapsed=0 + + log_info "Waiting for HTTP 200 at ${url} (timeout: ${max_secs}s)..." + while [[ $elapsed -lt $max_secs ]]; do + if curl -sf -o /dev/null "$url" 2>/dev/null; then + log_success "HTTP 200 at ${url} (after ${elapsed}s)" + return 0 + fi + sleep 2 + elapsed=$((elapsed + 2)) + done + + log_error "Timeout waiting for ${url} after ${max_secs}s" + return 1 +} + +wait_for_ssh() { + local host_key="$1" max_secs="${2:-60}" + local elapsed=0 + + log_info "Waiting for SSH to ${host_key} (timeout: ${max_secs}s)..." + while [[ $elapsed -lt $max_secs ]]; do + if ssh_check "$host_key"; then + log_success "SSH to ${host_key} connected (after ${elapsed}s)" + return 0 + fi + sleep 2 + elapsed=$((elapsed + 2)) + done + + log_error "Timeout waiting for SSH to ${host_key} after ${max_secs}s" + return 1 +}