Agrega un script para actualizar varios repositorios git desde un archivo de rutas.
Pide usuario y contraseña una sola vez y sigue con el resto si alguno falla.
This commit is contained in:
@@ -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`.
|
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`.
|
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`.
|
||||||
|
|||||||
Executable
+290
@@ -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 <<EOF
|
||||||
|
Usage: $(basename "$0") --repos FILE
|
||||||
|
|
||||||
|
Run git pull once for every repository listed in FILE. A failure does not
|
||||||
|
stop the batch. The exit code is 1 if any repository failed.
|
||||||
|
|
||||||
|
The script asks for a username and password on the terminal and uses them
|
||||||
|
for HTTPS remotes. They are not read from the command line or the list file.
|
||||||
|
|
||||||
|
Required:
|
||||||
|
--repos FILE Repository list. One path per line. Blank lines
|
||||||
|
and lines starting with '#' are ignored. Relative
|
||||||
|
paths resolve against the directory that contains
|
||||||
|
FILE.
|
||||||
|
|
||||||
|
Other:
|
||||||
|
-h, --help Show this help
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
$(basename "$0") --repos /srv/proyecto/repos.txt
|
||||||
|
$(basename "$0") --repos ./repos.txt
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
trim() {
|
||||||
|
local value="$1"
|
||||||
|
value="${value#"${value%%[![:space:]]*}"}"
|
||||||
|
value="${value%"${value##*[![:space:]]}"}"
|
||||||
|
value="${value%$'\r'}"
|
||||||
|
printf '%s' "$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
require_value() {
|
||||||
|
[[ $# -ge 2 && -n "${2:-}" && "$2" != -* ]] \
|
||||||
|
|| die "Missing value for $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_repo() {
|
||||||
|
local entry="$1"
|
||||||
|
if [[ "$entry" == /* ]]; then
|
||||||
|
printf '%s' "$entry"
|
||||||
|
else
|
||||||
|
printf '%s' "${REPOS_DIR}/${entry}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Collapse "." and ".." without requiring the path to exist.
|
||||||
|
normalize_path() {
|
||||||
|
local path="$1"
|
||||||
|
local part joined=""
|
||||||
|
local -a stack=()
|
||||||
|
local old_ifs="$IFS"
|
||||||
|
set -f
|
||||||
|
IFS=/
|
||||||
|
# shellcheck disable=SC2206
|
||||||
|
local -a parts=($path)
|
||||||
|
IFS="$old_ifs"
|
||||||
|
set +f
|
||||||
|
for part in "${parts[@]}"; do
|
||||||
|
case "$part" in
|
||||||
|
""|.) ;;
|
||||||
|
..)
|
||||||
|
if [[ ${#stack[@]} -gt 0 ]]; then
|
||||||
|
unset 'stack[${#stack[@]}-1]'
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*) stack+=("$part") ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
if [[ ${#stack[@]} -eq 0 ]]; then
|
||||||
|
printf '/'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
for part in "${stack[@]}"; do
|
||||||
|
joined+="/${part}"
|
||||||
|
done
|
||||||
|
printf '%s' "$joined"
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt_credentials() {
|
||||||
|
local user="" pass=""
|
||||||
|
[[ -r /dev/tty && -w /dev/tty ]] \
|
||||||
|
|| die "A terminal is required to enter the username and password."
|
||||||
|
read -r -p "Usuario: " user </dev/tty
|
||||||
|
read -r -s -p "Contraseña: " pass </dev/tty
|
||||||
|
printf '\n' >/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."
|
||||||
Reference in New Issue
Block a user