diff --git a/scripts/README.md b/scripts/README.md index 57e35d2..911e5b7 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -224,3 +224,25 @@ Formato del archivo: una ruta por línea y, si hace falta, un segundo campo `jav Se reenvían a cada servicio: `--ssh-key`, `--ssh-config`, `--skip-build`, `--no-verify` y `--cleanup`, más el `--type` de esa línea. No se aceptan `--name`, `--tag`, `--project-dir` ni `--type`. Ctrl+C interrumpe el lote. Ayuda: `./scripts/deploy-services.sh --help`. + +## `update-repos.sh` + +Actualiza un repositorio git por cada ruta de un archivo de lista. Ejecuta `git pull` en cada uno. Si alguno falla, sigue con el resto y al terminar indica cuáles fallaron. El código de salida es `1` si alguno falló. + +No hay lista por defecto. `--repos` es obligatorio. + +```shell +./scripts/update-repos.sh --repos /srv/proyecto/repos.txt +``` + +Al arrancar pide `Usuario:` y `Contraseña:` en la terminal, una sola vez para todo el lote. Esas credenciales se usan en los remotos HTTPS. No se leen desde la línea de comandos ni desde el archivo, y no se guardan. Un remoto SSH se actualiza con su acceso habitual (clave), sin esa contraseña. + +Formato del archivo: una ruta por línea. Se ignoran las líneas vacías y las que empiezan por `#`. Una ruta relativa se resuelve desde el directorio del archivo de lista, no desde el directorio donde lanzas el script. Cada ruta debe ser la raíz de un repositorio git. + +```text +# /srv/proyecto/repos.txt +/srv/repos/api +./portal +``` + +Ctrl+C interrumpe el lote. Ayuda: `./scripts/update-repos.sh --help`. diff --git a/scripts/update-repos.sh b/scripts/update-repos.sh new file mode 100755 index 0000000..e5bcfc8 --- /dev/null +++ b/scripts/update-repos.sh @@ -0,0 +1,290 @@ +#!/usr/bin/env bash +# +# Update one git repository per line in a list file. +# Prompts once for a username and password and uses them for HTTPS remotes. +# Continues after a failure and prints a summary when the batch finishes. +# +# The list path is required (--repos). Relative paths are resolved against +# the directory that contains that file, not the caller's cwd. + +set -euo pipefail +IFS=$'\n\t' + +REPOS_FILE="" +GIT_USER="" +GIT_PASS="" +ASKPASS_FILE="" + +# ---------- logging ---------- +if [[ -t 1 ]]; then + readonly C_INFO=$'\033[0;36m' + readonly C_OK=$'\033[0;32m' + readonly C_WARN=$'\033[0;33m' + readonly C_ERR=$'\033[0;31m' + readonly C_RESET=$'\033[0m' +else + readonly C_INFO="" C_OK="" C_WARN="" C_ERR="" C_RESET="" +fi + +log() { printf '%s[INFO]%s %s\n' "$C_INFO" "$C_RESET" "$*"; } +ok() { printf '%s[OK]%s %s\n' "$C_OK" "$C_RESET" "$*"; } +warn() { printf '%s[WARN]%s %s\n' "$C_WARN" "$C_RESET" "$*" >&2; } +err() { printf '%s[ERROR]%s %s\n' "$C_ERR" "$C_RESET" "$*" >&2; } +die() { err "$*"; exit 1; } + +cleanup() { + GIT_USER="" + GIT_PASS="" + unset GIT_USER GIT_PASS + if [[ -n "${ASKPASS_FILE:-}" && -f "$ASKPASS_FILE" ]]; then + rm -f "$ASKPASS_FILE" + fi + ASKPASS_FILE="" +} + +on_error() { + local exit_code=$? + err "Script aborted on line ${BASH_LINENO[0]} (exit ${exit_code})." + exit "$exit_code" +} +trap cleanup EXIT +trap on_error ERR + +# ---------- helpers ---------- +usage() { + cat </dev/tty + [[ -n "$user" && -n "$pass" ]] || die "Username and password are required." + GIT_USER="$user" + GIT_PASS="$pass" +} + +write_askpass() { + ASKPASS_FILE="$(mktemp "${TMPDIR:-/tmp}/git-update-askpass.XXXXXX")" + cat >"$ASKPASS_FILE" <<'EOF' +#!/bin/sh +prompt=$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]') +case "$prompt" in + *username*) printf '%s\n' "${GIT_UPDATE_USER:-}" ;; + *password*) printf '%s\n' "${GIT_UPDATE_PASS:-}" ;; + *) exit 1 ;; +esac +EOF + chmod 700 "$ASKPASS_FILE" +} + +remote_uses_http() { + local repo="$1" + local upstream="" remote="" url="" + upstream="$(git -C "$repo" rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' 2>/dev/null || true)" + [[ -n "$upstream" && "$upstream" == */* ]] || return 1 + remote="${upstream%%/*}" + url="$(git -C "$repo" remote get-url "$remote" 2>/dev/null || true)" + [[ "$url" == http://* || "$url" == https://* ]] +} + +pull_repo() { + local repo="$1" + if remote_uses_http "$repo"; then + GIT_ASKPASS="$ASKPASS_FILE" \ + GIT_TERMINAL_PROMPT=0 \ + GIT_UPDATE_USER="$GIT_USER" \ + GIT_UPDATE_PASS="$GIT_PASS" \ + git -C "$repo" -c credential.helper= pull + else + git -C "$repo" pull + fi +} + +# ---------- arg parsing ---------- +while [[ $# -gt 0 ]]; do + case "$1" in + --repos) + require_value "$@" + REPOS_FILE="$2" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + die "Unknown argument: $1 (use --help)" + ;; + esac +done + +[[ -n "$REPOS_FILE" ]] || die "Missing --repos FILE (use --help)." +[[ -f "$REPOS_FILE" ]] || die "Repository list not found: ${REPOS_FILE}" + +REPOS_FILE="$(readlink -f "$REPOS_FILE")" +REPOS_DIR="$(cd "$(dirname "$REPOS_FILE")" && pwd)" + +repos=() +while IFS= read -r raw_line || [[ -n "$raw_line" ]]; do + line="$(trim "$raw_line")" + [[ -z "$line" || "$line" == \#* ]] && continue + line="${line/#\~/$HOME}" + repos+=("$line") +done < "$REPOS_FILE" + +[[ ${#repos[@]} -gt 0 ]] || die "Repository list is empty: ${REPOS_FILE}" + +prompt_credentials +write_askpass + +total="${#repos[@]}" +index=0 +ok_count=0 +fail_count=0 +result_paths=() +result_notes=() + +log "Repository list: ${REPOS_FILE}" +log "Repositories: ${total}" + +for entry in "${repos[@]}"; do + index=$((index + 1)) + resolved="$(normalize_path "$(resolve_repo "$entry")")" + if [[ -d "$resolved" ]]; then + resolved="$(cd "$resolved" && pwd)" + fi + + printf '\n%s========== [%s/%s] %s ==========%s\n' \ + "$C_INFO" "$index" "$total" "$resolved" "$C_RESET" + + if [[ ! -d "$resolved" ]]; then + err "Directory not found: ${resolved}" + result_paths+=("$resolved") + result_notes+=("not found") + fail_count=$((fail_count + 1)) + continue + fi + + toplevel="$(git -C "$resolved" rev-parse --show-toplevel 2>/dev/null || true)" + if [[ -z "$toplevel" || "$(cd "$resolved" && pwd -P)" != "$(cd "$toplevel" && pwd -P)" ]]; then + err "Not a git work tree: ${resolved}" + result_paths+=("$resolved") + result_notes+=("not a git work tree") + fail_count=$((fail_count + 1)) + continue + fi + + exit_code=0 + if pull_repo "$resolved"; then + ok "Updated ${resolved}" + result_paths+=("$resolved") + result_notes+=("0") + ok_count=$((ok_count + 1)) + else + exit_code=$? + err "Failed ${resolved} (exit ${exit_code})" + result_paths+=("$resolved") + result_notes+=("exit ${exit_code}") + fail_count=$((fail_count + 1)) + fi +done + +printf '\n%s========== Summary ==========%s\n' "$C_INFO" "$C_RESET" +summary_index=0 +while [[ "$summary_index" -lt "$total" ]]; do + note="${result_notes[$summary_index]}" + path="${result_paths[$summary_index]}" + if [[ "$note" == "0" ]]; then + ok "${path}" + else + err "${path} (${note})" + fi + summary_index=$((summary_index + 1)) +done + +if [[ "$fail_count" -gt 0 ]]; then + err "${ok_count} updated, ${fail_count} failed." + exit 1 +fi + +ok "${ok_count} updated, 0 failed."