#!/usr/bin/env bash # Publica los artefactos de dist/release/ en el registro de paquetes GENÉRICOS de Gitea. # # Gitea calcula y expone el sha256 de cada archivo, así que se convierte en la fuente de # verdad de los binarios: el PANEL descubre las versiones leyendo su API y verifica la # integridad contra ese hash. Aquí, tras subir, se compara lo que reporta Gitea contra # release.json para no dejar una publicación a medias pasando por buena. # # Uso: # GITEA_TOKEN=... ./packaging/scripts/publish-release.sh # ... --dry-run # lista qué subiría, sin subir nada # ... --force # reemplaza archivos ya publicados de esta versión # ... --notify-panel # avisa al PANEL para que sincronice de inmediato # # Variables: # GITEA_TOKEN (requerida) PAT de Gitea con scope write:package # GITEA_BASE_URL default https://git.aduanasoft.com # GITEA_OWNER default ADUANASOFT # CRAS_PACKAGE default cloudrestoreas # PANEL_API_URL (solo --notify-panel) URL base del PANEL # CLOUDRESTORE_API_TOKEN (solo --notify-panel) token de servicio del PANEL set -euo pipefail ROOT="$(cd "$(dirname "$0")/../.." && pwd)" RELEASE="$ROOT/dist/release" MANIFEST="$RELEASE/release.json" GITEA_BASE_URL="${GITEA_BASE_URL:-https://git.aduanasoft.com}" GITEA_OWNER="${GITEA_OWNER:-ADUANASOFT}" CRAS_PACKAGE="${CRAS_PACKAGE:-cloudrestoreas}" DRY_RUN=0 FORCE=0 NOTIFY_PANEL=0 for arg in "$@"; do case "$arg" in --dry-run) DRY_RUN=1 ;; --force) FORCE=1 ;; --notify-panel) NOTIFY_PANEL=1 ;; -h|--help) grep '^#' "$0" | grep -v '^#!' | sed 's/^# \{0,1\}//'; exit 0 ;; *) echo "Opción desconocida: $arg" >&2; exit 2 ;; esac done BLUE='\033[36m'; GREEN='\033[32m'; YELLOW='\033[33m'; RED='\033[31m'; NC='\033[0m' say() { echo -e "${BLUE}==>${NC} $*"; } ok() { echo -e "${GREEN}OK:${NC} $*"; } warn() { echo -e "${YELLOW}AVISO:${NC} $*" >&2; } err() { echo -e "${RED}ERROR:${NC} $*" >&2; } if [[ ! -f "$MANIFEST" ]]; then err "no existe $MANIFEST; corre packaging/scripts/package-release.sh primero" exit 1 fi VERSION="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["version"])' "$MANIFEST")" # Los paquetes del manifiesto más los archivos de verificación, que se publican para # poder auditar una versión sin depender del panel. mapfile -t FILES < <( python3 - "$MANIFEST" <<'PY' import json, sys data = json.load(open(sys.argv[1], encoding="utf-8")) for artifact in data["artifacts"]: print(artifact["file_name"]) print("SHA256SUMS") print("release.json") PY ) if [[ "${#FILES[@]}" -le 2 ]]; then err "release.json no lista artefactos; ¿falló el build de ambas plataformas?" exit 1 fi PKG_BASE="$GITEA_BASE_URL/api/packages/$GITEA_OWNER/generic/$CRAS_PACKAGE/$VERSION" API_BASE="$GITEA_BASE_URL/api/v1/packages/$GITEA_OWNER/generic/$CRAS_PACKAGE/$VERSION" say "Publicando CloudRestoreAS $VERSION" echo " Destino : $PKG_BASE" echo " Archivos: ${#FILES[@]}" if [[ "$DRY_RUN" -eq 1 ]]; then for f in "${FILES[@]}"; do if [[ -f "$RELEASE/$f" ]]; then printf ' [dry-run] PUT %s (%s)\n' "$f" "$(du -h "$RELEASE/$f" | cut -f1)" else warn "[dry-run] falta $f" fi done ok "dry-run completado; no se subió nada" exit 0 fi if [[ -z "${GITEA_TOKEN:-}" ]]; then err "falta GITEA_TOKEN (PAT de Gitea con scope write:package)" echo " Ejemplo: GITEA_TOKEN=xxxx $0" >&2 exit 1 fi # --- Subida ------------------------------------------------------------------------ for f in "${FILES[@]}"; do path="$RELEASE/$f" if [[ ! -f "$path" ]]; then warn "no existe $f; se omite" continue fi if [[ "$FORCE" -eq 1 ]]; then # Los paquetes genéricos son inmutables: para reemplazar hay que borrar primero. curl -sS -o /dev/null -X DELETE \ -H "Authorization: token $GITEA_TOKEN" \ "$PKG_BASE/$f" || true fi say "Subiendo $f ($(du -h "$path" | cut -f1))" # -# manda la barra de progreso a stderr y -w el código HTTP a stdout, así se puede # ver el avance de una subida de cientos de MB y a la vez capturar el resultado. code="$( curl -S -# -o /dev/null -w '%{http_code}' \ --retry 2 --retry-delay 3 --retry-connrefused \ -X PUT \ -H "Authorization: token $GITEA_TOKEN" \ --upload-file "$path" \ "$PKG_BASE/$f" )" case "$code" in 201|200) ok "$f publicado" ;; 409) err "$f ya está publicado en la versión $VERSION" echo " Los paquetes genéricos son inmutables. Sube una versión nueva (recomendado)" >&2 echo " o reemplaza esta a propósito con --force." >&2 exit 1 ;; 401|403) err "Gitea rechazó el token ($code); revisa GITEA_TOKEN y su scope write:package" exit 1 ;; *) err "fallo subiendo $f (HTTP $code)" exit 1 ;; esac done # --- Verificación contra la API que consume el PANEL --------------------------------- say "Verificando en Gitea (misma API que lee el PANEL)" files_json="$( curl -sS -H "Authorization: token $GITEA_TOKEN" -H 'Accept: application/json' \ "$API_BASE/files" )" if ! MANIFEST="$MANIFEST" FILES_JSON="$files_json" python3 <<'PY' import json, os, sys manifest = json.load(open(os.environ["MANIFEST"], encoding="utf-8")) try: remote = json.loads(os.environ["FILES_JSON"]) except json.JSONDecodeError: sys.exit("Gitea no devolvió JSON al listar los archivos del paquete") if not isinstance(remote, list): sys.exit(f"Respuesta inesperada de Gitea: {remote!r}") by_name = {entry.get("name"): entry for entry in remote} problems = [] for artifact in manifest["artifacts"]: name = artifact["file_name"] entry = by_name.get(name) if entry is None: problems.append(f"{name}: no aparece en Gitea") continue # Gitea calcula el sha256 del lado servidor: si coincide con el local, los bytes # llegaron completos y el PANEL podrá verificar la descarga con ese mismo hash. remote_sha = (entry.get("sha256") or "").lower() if remote_sha != artifact["sha256"].lower(): problems.append(f"{name}: sha256 local {artifact['sha256'][:12]}… != remoto {remote_sha[:12] or '(vacío)'}…") elif int(entry.get("size") or -1) != artifact["size"]: problems.append(f"{name}: tamaño local {artifact['size']} != remoto {entry.get('size')}") else: print(f" {name}: sha256 y tamaño coinciden") if problems: print("\n".join(f" {p}" for p in problems), file=sys.stderr) sys.exit(1) PY then err "la verificación contra Gitea falló; la publicación quedó incompleta" echo " Revisa el paquete y reintenta con --force." >&2 exit 1 fi ok "verificación correcta" # --- Aviso al PANEL ------------------------------------------------------------------ if [[ "$NOTIFY_PANEL" -eq 1 ]]; then if [[ -z "${PANEL_API_URL:-}" || -z "${CLOUDRESTORE_API_TOKEN:-}" ]]; then warn "--notify-panel requiere PANEL_API_URL y CLOUDRESTORE_API_TOKEN; se omite el aviso" warn "El PANEL igual descubrirá la versión al sincronizar desde su UI." else say "Avisando al PANEL para que sincronice" base="${PANEL_API_URL%/}" code="$( curl -sS -o /dev/null -w '%{http_code}' -X POST \ -H "Authorization: Bearer $CLOUDRESTORE_API_TOKEN" \ -H 'Content-Type: application/json' \ -d '{}' \ "$base/api/restore/agent-sync" )" if [[ "$code" == "200" ]]; then ok "PANEL sincronizado" else # No es fatal: la publicación en Gitea ya está firme y el panel puede sincronizar # después desde su UI. Solo se informa. warn "el PANEL respondió $code al sincronizar; hazlo desde /versiones-cras" fi fi fi echo "" ok "CloudRestoreAS $VERSION publicado en Gitea" echo " Paquete: $GITEA_BASE_URL/-/packages/$GITEA_OWNER/generic/$CRAS_PACKAGE/$VERSION" echo " Siguiente: en el PANEL, /versiones-cras → Sincronizar con Gitea → Activar"