Compare commits

...
13 Commits
Author SHA1 Message Date
darroyo 2c727244df Pasa el nombre de la imagen a minúsculas para que Docker acepte la carpeta del servicio.
Un directorio como SALE-HITES-BFF deja de fallar el build por el formato del repositorio.
2026-10-06 19:35:39 -03:00
darroyo fa125ed616 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.
2026-09-29 10:17:46 -03:00
darroyo b65730c45f El nombre de la imagen sale de la carpeta del servicio y el Dockerfile depende de si es java o web.
Así se puede desplegar sin pom.xml y ubicar el Dockerfile en la raíz cuando el servicio es web.
2026-09-25 12:55:43 -03:00
darroyo 45f7663258 version nueva 2026-09-25 09:42:01 -03:00
darroyo fc4cb86f24 log final 2026-09-25 09:32:33 -03:00
darroyo 782a130bb9 build: deploy several Quarkus services from one project list
Add deploy-services.sh so a single command runs build-and-push.sh for every path in a project file, keeps going after a failure, and reports which services failed.
2026-09-24 22:54:45 -03:00
darroyo f7bcad0b38 cambios 2026-09-24 18:50:08 -03:00
darroyo 4417645639 version 2026-09-24 18:33:21 -03:00
darroyo 803f05a60b tag con fecha y hora 2026-09-24 18:21:35 -03:00
darroyo faabf0b82b cambios cleanup del registry remoto y tags 2026-09-24 17:18:05 -03:00
darroyo 5b4bbce5c8 cambios en script y version 2026-09-24 16:41:36 -03:00
darroyo bc5ffaf1d2 fix: use Mandrel jdk-25 builder for Quarkus 3.39 native image
Quarkus 3.39 requires GraalVM/Mandrel 25.0; the jdk-21 builder ships
Mandrel 23.1 and fails native-image compilation in Docker.
2026-09-24 13:11:06 -03:00
darroyo bd196ec9e0 docs: add Spanish guide for build-and-push.sh
Document flags, SSH aliases, and troubleshooting for invalid Docker tags
when xmllint is missing on build hosts such as vm-compilador.
2026-09-24 12:58:11 -03:00
8 changed files with 1224 additions and 70 deletions
+3
View File
@@ -41,3 +41,6 @@ nb-configuration.xml
# Plugin directory # Plugin directory
/.quarkus/cli/plugins/ /.quarkus/cli/plugins/
# deploy-services.sh summary logs
deploy-*.log
+35 -5
View File
@@ -7,16 +7,46 @@ If you want to learn more about Quarkus, please visit its website: https://quark
## Building and deploying natively ## Building and deploying natively
Build the native image with the Mandrel-based multi-stage Dockerfile and push it Build the native image with the Mandrel-based multi-stage Dockerfile and push it
directly to a remote Docker host via [unregistry](https://github.com/psviderski/unregistry) to a remote Docker host over an SSH tunnel. The script starts a temporary
(no external registry required; only missing layers are transferred over SSH): `registry:2` container on the remote (localhost only), forwards it to the build
machine, pushes the image through that tunnel, then pulls and retags it on the
remote Docker daemon. No external registry is required.
```shell script ```shell script
./scripts/build-and-push.sh --remote deploy@prod.example.com ./scripts/build-and-push.sh --remote deploy@prod.example.com
``` ```
The script reads `artifactId` and `version` from `pom.xml` for the image name To push every service in one project, pass that project's list file to
and tag, builds for `linux/amd64`, and verifies the image is present on the `./scripts/deploy-services.sh`. It runs `build-and-push.sh` once per path and
remote after pushing. See `./scripts/build-and-push.sh --help` for all flags. reports any failures at the end. See `scripts/README.md` for the list format.
The script uses the last directory of the service location, lowercased, as the image name
and reads `version` from `pom.xml` for the local build tag (`1.0.0-SNAPSHOT`
if there is no pom). It builds for `linux/amd64` and verifies the deploy on
the remote.
On the remote, durable tags are only:
- `<name>:latest` — always the most recent successful push
- `<name>:vYYYY.MM.DD-HH.MM` — UTC timestamp of that push (for rollback)
Older releases stay tagged by date only: the script keeps the current image (with
`latest` and its date tag) and the previous date tag, and removes other tags and
images (including old `1.0.x`, `sha256-*`, and third copies). Run
`--cleanup` on the remote to apply the same pruning without a new push.
To roll back on the remote, run a previous `v…` tag from `docker images` instead of `:latest`.
The temporary registry container and SSH tunnel are removed when the script
finishes. See `./scripts/build-and-push.sh --help` for all flags.
To remove leftover temporary registry containers and `localhost:<port>/...` image
tags on the remote (for example after an interrupted push or manual tests):
```shell script
./scripts/build-and-push.sh --remote deploy@prod.example.com --cleanup
```
This does not rebuild or push; it only needs SSH access to the remote host.
It can be invoked from any directory: by default it resolves the project root It can be invoked from any directory: by default it resolves the project root
from the script's own location. Override with `--project-dir PATH` or the from the script's own location. Override with `--project-dir PATH` or the
+1 -1
View File
@@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId> <groupId>com.example</groupId>
<artifactId>test-quarkus</artifactId> <artifactId>test-quarkus</artifactId>
<version>1.0.0-SNAPSHOT</version> <version>1.0.4</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>Test Quarkus</name> <name>Test Quarkus</name>
+248
View File
@@ -0,0 +1,248 @@
# Scripts de despliegue
Guía de uso de los scripts de este directorio.
## `build-and-push.sh`
Construye la imagen nativa de Quarkus y la envía al daemon Docker de un host remoto por SSH, sin usar un registry externo ([unregistry](https://github.com/psviderski/unregistry) / `docker pussh`). Solo se transfieren las capas que el remoto aún no tiene.
### Qué hace el flujo
1. Toma el nombre de la imagen de la última carpeta de la ubicación del servicio, en minúsculas, y lee `version` de [`pom.xml`](../pom.xml) para el tag, salvo que uses `--name` o `--tag`. Si no hay `pom.xml`, el tag es `1.0.0-SNAPSHOT`. `--name` también se pasa a minúsculas.
2. Ejecuta `docker build` para la plataforma fija `linux/amd64` (útil si construyes desde un Mac arm64). Con `--type java` (el valor por defecto) usa [`src/main/docker/Dockerfile`](../src/main/docker/Dockerfile). Con `--type web` usa `Dockerfile` en la raíz del servicio.
3. Ejecuta `docker pussh` hacia el destino SSH indicado.
4. Comprueba por SSH que la imagen existe en el remoto (salvo `--no-verify`).
5. Muestra un comando `docker run` sugerido para arrancar el contenedor en el remoto.
### Requisitos
| Requisito | Descripción |
|-----------|-------------|
| Docker local | Daemon en marcha (`docker info` debe responder). |
| SSH | Acceso al host remoto; en el remoto debe estar instalado y usable Docker. |
| Plugin `docker pussh` | CLI plugin de unregistry. |
Si el plugin no está instalado, el script indica cómo añadirlo:
```shell
mkdir -p ~/.docker/cli-plugins
curl -fsSL https://raw.githubusercontent.com/psviderski/unregistry/main/docker-pussh \
-o ~/.docker/cli-plugins/docker-pussh
chmod +x ~/.docker/cli-plugins/docker-pussh
```
`xmllint` es opcional: si no está en el PATH y existe `pom.xml`, el script obtiene `version` del POM con `grep`/`sed`.
### Invocación
Puedes ejecutar el script desde cualquier directorio. Por defecto usa como raíz del proyecto el directorio padre de `scripts/` (donde está `pom.xml`).
```shell
./scripts/build-and-push.sh --remote USER@HOST
```
Alternativas para la raíz del proyecto:
- `--project-dir /ruta/al/proyecto`
- variable de entorno `PROJECT_DIR`
Ayuda integrada (en inglés):
```shell
./scripts/build-and-push.sh --help
```
### Opciones
**Destino (obligatorio)**
| Opción | Descripción |
|--------|-------------|
| `--remote TARGET` | Destino SSH: `usuario@host[:puerto]` o un alias definido en `~/.ssh/config` (sin `@`). |
**Identidad de la imagen** (el nombre es la última carpeta del servicio, en minúsculas; el tag sale de `pom.xml` si existe)
| Opción | Descripción |
|--------|-------------|
| `--name NAME` | Nombre del repositorio de la imagen. Se guarda en minúsculas. |
| `--tag TAG` | Tag de la imagen. |
**Directorio de trabajo**
| Opción | Descripción |
|--------|-------------|
| `--project-dir PATH` | Raíz del servicio. El nombre de la imagen es su última carpeta, en minúsculas. |
| `PROJECT_DIR` | Misma función vía entorno. |
**Build**
| Opción | Descripción |
|--------|-------------|
| `--type` | Dónde está el Dockerfile. `java` (por defecto): `src/main/docker/Dockerfile`. `web`: `Dockerfile` en la raíz del servicio. |
| `--skip-build` | No construye; solo hace `pussh` de una imagen que ya exista en local. La plataforma de build sigue siendo `linux/amd64` cuando no se omite el build. |
**SSH** (se reenvían a `docker pussh` y, en la verificación, a `ssh`)
| Opción | Descripción |
|--------|-------------|
| `--ssh-key PATH` | Clave privada; tiene prioridad sobre `IdentityFile` del config SSH. |
| `--ssh-config PATH` | Archivo de configuración SSH (por defecto `~/.ssh/config`). |
**Post-push**
| Opción | Descripción |
|--------|-------------|
| `--no-verify` | Omite la comprobación `docker images` en el remoto. |
**Otros**
| Opción | Descripción |
|--------|-------------|
| `-h`, `--help` | Muestra la ayuda del script. |
### Alias en `~/.ssh/config`
Si `--remote` es un alias sin `@`, el script resuelve `HostName`, `User`, `Port` e `IdentityFile` del bloque `Host` correspondiente. `--ssh-key` siempre anula `IdentityFile`.
Ejemplo de configuración:
```text
# ~/.ssh/config
Host prod
HostName prod.example.com
User deploy
Port 22
IdentityFile ~/.ssh/deploy_ed25519
```
Uso:
```shell
./scripts/build-and-push.sh --remote prod
# Equivale a deploy@prod.example.com:22 con la clave del bloque Host prod
```
### Ejemplos
Destino explícito:
```shell
./scripts/build-and-push.sh --remote deploy@prod.example.com
```
Alias SSH:
```shell
./scripts/build-and-push.sh --remote prod
```
Clave explícita:
```shell
./scripts/build-and-push.sh --remote ubuntu@1.2.3.4 --ssh-key ~/.ssh/deploy_ed25519
```
Solo push (imagen ya construida) con otro tag:
```shell
./scripts/build-and-push.sh --remote deploy@prod --skip-build --tag 2.0.0
```
Ruta absoluta al script:
```shell
/absolute/path/to/test-quarkus/scripts/build-and-push.sh --remote deploy@prod
```
Proyecto en otra ruta:
```shell
PROJECT_DIR=/srv/proj ./scripts/build-and-push.sh --remote deploy@prod --skip-build
```
### Después del push
Al terminar con éxito, el script sugiere algo equivalente a:
```shell
ssh deploy@prod.example.com \
"docker run -d --name <carpeta> --restart unless-stopped -p 8080:8080 <carpeta>:<version>"
```
Sustituye `<carpeta>` por la última carpeta de la ubicación del servicio en minúsculas (o el valor de `--name`, también en minúsculas) y `<version>` por el tag del build (el de `pom.xml`, `1.0.0-SNAPSHOT` si no hay POM, o el de `--tag`).
### Solución de problemas
#### `invalid reference format` con saltos de línea en el tag
Si `docker build` falla con un tag parecido a `test-quarkus\n1.0.0-SNAPSHOT:test-quarkus\n1.0.0-SNAPSHOT`, el checkout del script es anterior al commit `57af30f`. En hosts sin `xmllint` (por ejemplo vm-compilador), el fallback antiguo de `read_pom` mezclaba `artifactId` y `version` en una sola cadena.
En la máquina donde compilas (`~/repo` u otra ruta del proyecto):
```shell
git rev-parse --short HEAD
grep -n 'head -2' scripts/build-and-push.sh || echo "script actualizado"
git pull
git rev-parse --short HEAD # debe ser 57af30f o posterior
./scripts/build-and-push.sh --remote TU_DESTINO
```
Comprueba que la primera línea del log sea `Image: test-quarkus:1.0.0-SNAPSHOT` (sin saltos de línea).
**Atajo** si aún no puedes actualizar el repo: fija nombre y tag explícitos (no usa el fallback roto):
```shell
./scripts/build-and-push.sh --remote TU_DESTINO \
--name test-quarkus --tag 1.0.0-SNAPSHOT
```
Opcional: instala `xmllint` para que el script lea el POM con XPath (`libxml2-utils` en Debian/Ubuntu).
## `deploy-services.sh`
Script independiente, en el mismo directorio que `build-and-push.sh`. No lo modifica: lo ejecuta una vez por cada servicio de un archivo de lista. Si un servicio falla, sigue con el resto y al terminar indica cuáles fallaron. El código de salida es `1` si alguno falló.
Al terminar el lote, ese resumen queda también en `deploy-YYYYMMDD-HHMMSS.log` en el directorio desde el que se lanzó el script (hora local de inicio). Si ese nombre ya existe, se añade el PID. Si no se puede escribir el archivo, el script avisa y conserva el código de salida del lote.
No hay lista por defecto. `--services` es obligatorio, así cada proyecto puede tener su propio archivo.
```shell
./scripts/deploy-services.sh --remote prod --services /srv/proyecto-a/services.txt
./scripts/deploy-services.sh --remote prod --services /srv/proyecto-b/services.txt --skip-build
```
Formato del archivo: una ruta por línea y, si hace falta, un segundo campo `java` o `web`. Si el tipo falta, es `java`. 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.
```text
# /srv/proyecto-a/services.txt
./api
./portal web
```
`java` usa `src/main/docker/Dockerfile`. `web` usa `Dockerfile` en la raíz del servicio. El nombre de la imagen es la última carpeta de esa ruta, en minúsculas. El tag sale de `pom.xml` si existe; si no, es `1.0.0-SNAPSHOT`.
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`.
+310 -61
View File
@@ -1,20 +1,19 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# #
# Build the native Quarkus image and push it to a remote Docker host # Build the native Quarkus image and push it to a remote Docker host
# via docker-pussh (unregistry): https://github.com/psviderski/unregistry # via a temporary registry:2 on the remote, reached through an SSH tunnel.
# #
# - Image name/tag default to pom.xml properties (quarkus.application.*) # - Image name defaults to the last directory of the service location
# - Image tag defaults to the pom.xml version (1.0.0-SNAPSHOT if there is no pom)
# - Builds for linux/amd64 by default (cross-compile from arm64 hosts) # - Builds for linux/amd64 by default (cross-compile from arm64 hosts)
# - Transfers only missing layers over SSH (no external registry required) # - Transfers layers over the tunnel (no external registry required)
# - On the remote, durable tags are :latest and vYYYY.MM.DD-HH.MM (UTC) only
set -euo pipefail set -euo pipefail
IFS=$'\n\t' IFS=$'\n\t'
readonly DOCKERFILE="src/main/docker/Dockerfile"
readonly DEFAULT_PLATFORM="linux/amd64" readonly DEFAULT_PLATFORM="linux/amd64"
readonly UNREGISTRY_PLUGIN="https://raw.githubusercontent.com/psviderski/unregistry/main/docker-pussh" readonly TEMP_REGISTRY_IMAGE="registry:2"
readonly PLUGIN_DIR="${HOME}/.docker/cli-plugins"
readonly PLUGIN_PATH="${PLUGIN_DIR}/docker-pussh"
readonly SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)" readonly SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
REMOTE="" REMOTE=""
@@ -23,8 +22,11 @@ IMAGE_TAG=""
SSH_KEY="" SSH_KEY=""
SSH_CONFIG="" SSH_CONFIG=""
PROJECT_DIR="${PROJECT_DIR:-${SCRIPT_DIR}/..}" PROJECT_DIR="${PROJECT_DIR:-${SCRIPT_DIR}/..}"
APP_TYPE="java"
DOCKERFILE=""
SKIP_BUILD=0 SKIP_BUILD=0
SKIP_VERIFY=0 SKIP_VERIFY=0
CLEANUP=0
# ---------- logging ---------- # ---------- logging ----------
if [[ -t 1 ]]; then if [[ -t 1 ]]; then
@@ -56,32 +58,43 @@ usage() {
Usage: $(basename "$0") --remote USER@HOST[:PORT] [options] Usage: $(basename "$0") --remote USER@HOST[:PORT] [options]
Build the native Quarkus image and push it to a remote Docker host using Build the native Quarkus image and push it to a remote Docker host using
unregistry (docker-pussh). a temporary Docker registry on the remote, reached through an SSH tunnel.
Required: Required:
--remote TARGET SSH destination. Either 'user@host[:port]' --remote TARGET SSH destination. Either 'user@host[:port]'
or a bare alias resolved from ~/.ssh/config or a bare alias resolved from ~/.ssh/config
(e.g. 'prod'). See SSH config aliases below. (e.g. 'prod'). See SSH config aliases below.
Image identity (defaults read from pom.xml): Image identity:
--name NAME Image repository name --name NAME Image repository name (default: last directory of
--tag TAG Image tag the service location, lowercased)
--tag TAG Local build tag (pom.xml version by default, or
1.0.0-SNAPSHOT if there is no pom.xml). On the
remote only :latest and vYYYY.MM.DD-HH.MM (UTC).
Working directory (defaults to the parent of the script's directory): Working directory (defaults to the parent of the script's directory):
--project-dir PATH Project root containing pom.xml --project-dir PATH Service root. The image name is its last directory,
(override with env PROJECT_DIR) lowercased (override with env PROJECT_DIR)
Build options: Build options:
--skip-build Skip docker build, only pussh an existing image --type java|web Dockerfile location. java (default):
src/main/docker/Dockerfile
web: Dockerfile at the service root
--skip-build Skip docker build, only push an existing image
(platform is fixed to ${DEFAULT_PLATFORM}) (platform is fixed to ${DEFAULT_PLATFORM})
SSH options forwarded to docker pussh: SSH options:
--ssh-key PATH Private key file (overrides SSH config) --ssh-key PATH Private key file (overrides SSH config)
--ssh-config PATH SSH config file (default: ~/.ssh/config) --ssh-config PATH SSH config file (default: ~/.ssh/config)
Post-push: Post-push:
--no-verify Skip the ssh/docker images verification step --no-verify Skip the ssh/docker images verification step
Maintenance:
--cleanup Remove temporary registry leftovers on the remote,
drop stale tags (version, sha256-*, etc.), and keep
at most two app images (current + previous release)
Other: Other:
-h, --help Show this help -h, --help Show this help
@@ -96,6 +109,8 @@ Examples:
$(basename "$0") --remote prod # uses 'Host prod' in ~/.ssh/config $(basename "$0") --remote prod # uses 'Host prod' in ~/.ssh/config
$(basename "$0") --remote ubuntu@1.2.3.4 --ssh-key ~/.ssh/deploy_ed25519 $(basename "$0") --remote ubuntu@1.2.3.4 --ssh-key ~/.ssh/deploy_ed25519
$(basename "$0") --remote deploy@prod --skip-build --tag 2.0.0 $(basename "$0") --remote deploy@prod --skip-build --tag 2.0.0
$(basename "$0") --remote prod --type web
$(basename "$0") --remote prod --cleanup
/absolute/path/to/test-quarkus/scripts/$(basename "$0") --remote deploy@prod /absolute/path/to/test-quarkus/scripts/$(basename "$0") --remote deploy@prod
PROJECT_DIR=/srv/proj $(basename "$0") --remote deploy@prod --skip-build PROJECT_DIR=/srv/proj $(basename "$0") --remote deploy@prod --skip-build
EOF EOF
@@ -104,16 +119,44 @@ EOF
read_pom() { read_pom() {
local xpath="$1" fallback="$2" tag="${3:-}" local xpath="$1" fallback="$2" tag="${3:-}"
local value="" local value=""
if [[ ! -f pom.xml ]]; then
printf '%s' "$fallback"
return 0
fi
if command -v xmllint >/dev/null 2>&1; then if command -v xmllint >/dev/null 2>&1; then
value=$(xmllint --xpath "string($xpath)" pom.xml 2>/dev/null || true) value=$(xmllint --xpath "string($xpath)" pom.xml 2>/dev/null || true)
fi fi
if [[ -z "$value" && -n "$tag" ]]; then if [[ -z "$value" && -n "$tag" ]]; then
value=$(grep -E "<${tag}>" pom.xml | head -1 \ value=$(grep -E "<${tag}>" pom.xml | head -1 \
| sed -E "s:.*<${tag}>(.*)</${tag}>.*:\1:") | sed -E "s:.*<${tag}>(.*)</${tag}>.*:\1:" || true)
fi fi
printf '%s' "${value:-$fallback}" printf '%s' "${value:-$fallback}"
} }
# java: src/main/docker/Dockerfile; web: Dockerfile at the service root.
resolve_dockerfile() {
case "$APP_TYPE" in
java) DOCKERFILE="src/main/docker/Dockerfile" ;;
web) DOCKERFILE="Dockerfile" ;;
*) die "Unknown --type '$APP_TYPE' (use java or web)." ;;
esac
}
# Docker repository names must be lowercase. ${var,,} needs bash 4+.
lowercase() {
printf '%s' "$1" | tr '[:upper:]' '[:lower:]'
}
# Last directory of the service location (cwd must already be that directory).
image_name_from_location() {
local dir
dir="$(pwd)"
dir="${dir%/}"
[[ -n "$dir" && "$dir" != "/" ]] \
|| die "Cannot derive image name from project directory."
lowercase "${dir##*/}"
}
require() { require() {
command -v "$1" >/dev/null 2>&1 || die "Required command '$1' not found in PATH." command -v "$1" >/dev/null 2>&1 || die "Required command '$1' not found in PATH."
} }
@@ -178,34 +221,214 @@ resolve_ssh_alias() {
printf '%s@%s:%s|%s\n' "$user" "$hostname" "$port" "$identity" printf '%s@%s:%s|%s\n' "$user" "$hostname" "$port" "$identity"
} }
random_port() {
echo $((55000 + RANDOM % 10536))
}
# REMOTE_TARGET is user@host:port; OpenSSH needs -p and user@host separately.
parse_ssh_target() {
if [[ "$REMOTE_TARGET" == *:* ]]; then
SSH_PORT="${REMOTE_TARGET##*:}"
SSH_HOST_USER="${REMOTE_TARGET%:*}"
else
SSH_PORT="22"
SSH_HOST_USER="$REMOTE_TARGET"
fi
}
# Populates global ssh_args for ssh(1).
build_ssh_args() {
ssh_args=()
[[ -n "$SSH_KEY" ]] && ssh_args+=(-i "$SSH_KEY")
[[ -n "$SSH_CONFIG" ]] && ssh_args+=(-F "$SSH_CONFIG")
[[ "$SSH_PORT" != "22" ]] && ssh_args+=(-p "$SSH_PORT")
ssh_args+=("$SSH_HOST_USER")
}
run_remote() {
ssh "${ssh_args[@]}" "$@"
}
# Sets REMOTE_DOCKER to "docker" or "sudo -n docker".
detect_remote_docker() {
if run_remote "docker info" >/dev/null 2>&1; then
REMOTE_DOCKER="docker"
return 0
fi
if run_remote "sudo -n docker info" >/dev/null 2>&1; then
REMOTE_DOCKER="sudo -n docker"
return 0
fi
die "Cannot run docker on ${REMOTE_TARGET}. Ensure Docker is installed and the SSH user may run docker (or passwordless sudo docker)."
}
cleanup_remote_registry() {
log "Cleaning temporary registry leftovers on ${REMOTE_TARGET}…"
run_remote "${REMOTE_DOCKER} ps -aq --filter name=reg-push- | xargs -r ${REMOTE_DOCKER} rm -f" \
>/dev/null 2>&1 || true
run_remote "${REMOTE_DOCKER} rm -f regtest" >/dev/null 2>&1 || true
run_remote "${REMOTE_DOCKER} images --format '{{.Repository}}:{{.Tag}}' \
| grep -E '^localhost:[0-9]+/' | xargs -r ${REMOTE_DOCKER} rmi" \
>/dev/null 2>&1 || true
ok "Remote cleanup complete."
}
# Keep :latest plus the two newest vYYYY.MM.DD-HH.MM releases; drop other tags/IDs.
prune_remote_image_history() {
local repo="${1:-$IMAGE_NAME}"
[[ -n "$repo" ]] || return 0
log "Pruning ${repo} on remote (keep latest and previous release)…"
run_remote "bash -s" <<EOS
set -euo pipefail
RD='${REMOTE_DOCKER}'
REPO='${repo}'
release_re='^v[0-9]{4}\\.[0-9]{2}\\.[0-9]{2}-[0-9]{2}\\.[0-9]{2}\$'
rmi_ref() {
\${RD} rmi "\$1" 2>/dev/null || true
}
while IFS= read -r ref; do
tag="\${ref#*:}"
[[ "\$tag" == latest ]] && continue
if [[ "\$tag" =~ \${release_re} ]]; then continue; fi
rmi_ref "\$ref"
done < <(\${RD} images --format '{{.Repository}}:{{.Tag}}' | grep -E "^\${REPO}:" || true)
mapfile -t release_refs < <(\${RD} images --format '{{.Repository}}:{{.Tag}}' | grep -E "^\${REPO}:v[0-9]{4}\\." | sort -t: -k2)
declare -A keep_ids=()
n=\${#release_refs[@]}
if (( n == 0 )); then exit 0; fi
for (( i=n-1; i>=0 && i>=n-2; i-- )); do
id=\$(\${RD} images --format '{{.ID}}' "\${release_refs[i]}" | head -1)
[[ -n "\$id" ]] && keep_ids["\$id"]=1
done
while IFS= read -r ref; do
id=\$(\${RD} images --format '{{.ID}}' "\$ref" | head -1)
[[ -n "\${keep_ids[\$id]:-}" ]] && continue
rmi_ref "\$ref"
done < <(\${RD} images --format '{{.Repository}}:{{.Tag}}' | grep -E "^\${REPO}:" || true)
EOS
ok "Remote ${repo} pruned."
}
REG_PUSH_CONTAINER=""
REG_REMOTE_PORT=""
REG_LOCAL_PORT=""
TUNNEL_PID=""
LOCAL_REGISTRY_REF=""
RELEASE_REF=""
cleanup_push_resources() {
[[ -n "${LOCAL_REGISTRY_REF:-}" ]] \
&& docker rmi "$LOCAL_REGISTRY_REF" >/dev/null 2>&1 || true
if [[ -n "${TUNNEL_PID:-}" ]]; then
kill "$TUNNEL_PID" >/dev/null 2>&1 || true
wait "$TUNNEL_PID" 2>/dev/null || true
fi
if [[ -n "${REG_PUSH_CONTAINER:-}" && -n "${REMOTE_DOCKER:-}" && -n "${REMOTE_TARGET:-}" ]]; then
parse_ssh_target
build_ssh_args
run_remote "${REMOTE_DOCKER} rm -f ${REG_PUSH_CONTAINER}" >/dev/null 2>&1 || true
fi
}
start_remote_registry() {
local attempt output
run_remote "${REMOTE_DOCKER} image inspect ${TEMP_REGISTRY_IMAGE}" >/dev/null 2>&1 \
|| run_remote "${REMOTE_DOCKER} pull ${TEMP_REGISTRY_IMAGE}"
for attempt in $(seq 1 10); do
REG_REMOTE_PORT=$(random_port)
REG_PUSH_CONTAINER="reg-push-$$-${REG_REMOTE_PORT}"
if output=$(run_remote "${REMOTE_DOCKER} run -d \
--name ${REG_PUSH_CONTAINER} \
-p 127.0.0.1:${REG_REMOTE_PORT}:5000 \
${TEMP_REGISTRY_IMAGE}" 2>&1); then
return 0
fi
run_remote "${REMOTE_DOCKER} rm -f ${REG_PUSH_CONTAINER}" >/dev/null 2>&1 || true
if ! echo "$output" | grep -qi bind; then
die "Failed to start temporary registry on remote:\n${output}"
fi
done
die "Failed to start temporary registry on remote (port binding)."
}
start_ssh_tunnel() {
local attempt
for attempt in $(seq 1 10); do
REG_LOCAL_PORT=$(random_port)
if command -v nc >/dev/null 2>&1 && nc -z 127.0.0.1 "$REG_LOCAL_PORT" 2>/dev/null; then
continue
fi
ssh "${ssh_args[@]}" \
-o ExitOnForwardFailure=yes \
-N \
-L "127.0.0.1:${REG_LOCAL_PORT}:127.0.0.1:${REG_REMOTE_PORT}" &
TUNNEL_PID=$!
sleep 1
if kill -0 "$TUNNEL_PID" 2>/dev/null; then
return 0
fi
TUNNEL_PID=""
done
die "Failed to establish SSH tunnel to remote registry."
}
build_release_tag() {
printf 'v%s' "$(TZ=UTC date +%Y.%m.%d-%H.%M)"
}
push_via_registry_tunnel() {
LOCAL_REGISTRY_REF="localhost:${REG_LOCAL_PORT}/${IMAGE_NAME}:${IMAGE_TAG}"
local remote_registry_ref="localhost:${REG_REMOTE_PORT}/${IMAGE_NAME}:${IMAGE_TAG}"
local latest_ref="${IMAGE_NAME}:latest"
local release_tag
release_tag=$(build_release_tag)
RELEASE_REF="${IMAGE_NAME}:${release_tag}"
log "Tagging ${IMAGE_REF} as ${LOCAL_REGISTRY_REF}…"
docker tag "$IMAGE_REF" "$LOCAL_REGISTRY_REF"
log "Pushing ${LOCAL_REGISTRY_REF} through SSH tunnel…"
docker push "$LOCAL_REGISTRY_REF"
log "Tagging local image as ${latest_ref} and ${RELEASE_REF}…"
docker tag "$IMAGE_REF" "$latest_ref"
docker tag "$IMAGE_REF" "$RELEASE_REF"
log "Pulling image on remote and tagging ${latest_ref}, ${RELEASE_REF}…"
run_remote "${REMOTE_DOCKER} pull ${remote_registry_ref}"
run_remote "${REMOTE_DOCKER} tag ${remote_registry_ref} ${latest_ref}"
run_remote "${REMOTE_DOCKER} tag ${remote_registry_ref} ${RELEASE_REF}"
run_remote "${REMOTE_DOCKER} rmi ${remote_registry_ref}" >/dev/null || true
prune_remote_image_history "${IMAGE_NAME}"
ok "Remote tags: ${latest_ref}, ${RELEASE_REF}."
}
# ---------- arg parsing ---------- # ---------- arg parsing ----------
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--remote) REMOTE="$2"; shift 2 ;; --remote) REMOTE="$2"; shift 2 ;;
--name) IMAGE_NAME="$2"; shift 2 ;; --name) IMAGE_NAME="$(lowercase "$2")"; shift 2 ;;
--tag) IMAGE_TAG="$2"; shift 2 ;; --tag) IMAGE_TAG="$2"; shift 2 ;;
--ssh-key) SSH_KEY="$2"; shift 2 ;; --ssh-key) SSH_KEY="$2"; shift 2 ;;
--ssh-config) SSH_CONFIG="$2"; shift 2 ;; --ssh-config) SSH_CONFIG="$2"; shift 2 ;;
--project-dir) PROJECT_DIR="$2"; shift 2 ;; --project-dir) PROJECT_DIR="$2"; shift 2 ;;
--type) APP_TYPE="$2"; shift 2 ;;
--skip-build) SKIP_BUILD=1; shift ;; --skip-build) SKIP_BUILD=1; shift ;;
--no-verify) SKIP_VERIFY=1; shift ;; --no-verify) SKIP_VERIFY=1; shift ;;
--cleanup) CLEANUP=1; shift ;;
-h|--help) usage; exit 0 ;; -h|--help) usage; exit 0 ;;
*) die "Unknown argument: $1 (use --help)" ;; *) die "Unknown argument: $1 (use --help)" ;;
esac esac
done done
# ---------- resolve project root ----------
if ! cd "${PROJECT_DIR}" 2>/dev/null; then
die "Invalid --project-dir: ${PROJECT_DIR}"
fi
PROJECT_DIR="$(pwd)"
# ---------- defaults ----------
[[ -z "$IMAGE_NAME" ]] && IMAGE_NAME=$(read_pom "/*[local-name()='project']/*[local-name()='artifactId']" "test-quarkus" "artifactId")
[[ -z "$IMAGE_TAG" ]] && IMAGE_TAG=$(read_pom "/*[local-name()='project']/*[local-name()='version']" "1.0.0-SNAPSHOT" "version")
[[ -z "$REMOTE" ]] && die "Missing --remote USER@HOST[:PORT] (use --help)." [[ -z "$REMOTE" ]] && die "Missing --remote USER@HOST[:PORT] (use --help)."
resolve_dockerfile
IMAGE_REF="${IMAGE_NAME}:${IMAGE_TAG}"
# ---------- resolve SSH target ---------- # ---------- resolve SSH target ----------
resolved=$(resolve_ssh_alias "$REMOTE") resolved=$(resolve_ssh_alias "$REMOTE")
@@ -219,6 +442,34 @@ if [[ -z "$SSH_KEY" && -n "$REMOTE_IDENTITY" ]]; then
log "Using IdentityFile from SSH config: ${SSH_KEY}" log "Using IdentityFile from SSH config: ${SSH_KEY}"
fi fi
# ---------- remote cleanup only ----------
if [[ "$CLEANUP" -eq 1 ]]; then
log "Remote: ${REMOTE_TARGET}"
require ssh
if ! cd "${PROJECT_DIR}" 2>/dev/null; then
die "Invalid --project-dir: ${PROJECT_DIR}"
fi
[[ -z "$IMAGE_NAME" ]] && IMAGE_NAME="$(image_name_from_location)"
parse_ssh_target
build_ssh_args
detect_remote_docker
cleanup_remote_registry
prune_remote_image_history "${IMAGE_NAME}"
exit 0
fi
# ---------- resolve project root ----------
if ! cd "${PROJECT_DIR}" 2>/dev/null; then
die "Invalid --project-dir: ${PROJECT_DIR}"
fi
PROJECT_DIR="$(pwd)"
# ---------- defaults ----------
[[ -z "$IMAGE_NAME" ]] && IMAGE_NAME="$(image_name_from_location)"
[[ -z "$IMAGE_TAG" ]] && IMAGE_TAG=$(read_pom "/*[local-name()='project']/*[local-name()='version']" "1.0.0-SNAPSHOT" "version")
IMAGE_REF="${IMAGE_NAME}:${IMAGE_TAG}"
# ---------- pre-flight ---------- # ---------- pre-flight ----------
log "Image: ${IMAGE_REF}" log "Image: ${IMAGE_REF}"
log "Platform: ${DEFAULT_PLATFORM}" log "Platform: ${DEFAULT_PLATFORM}"
@@ -226,25 +477,15 @@ log "Dockerfile: ${DOCKERFILE}"
log "Remote: ${REMOTE_TARGET}" log "Remote: ${REMOTE_TARGET}"
require docker require docker
require ssh
docker info >/dev/null 2>&1 \ docker info >/dev/null 2>&1 \
|| die "Docker daemon is not reachable. Start Docker Desktop (or your daemon) and retry." || die "Docker daemon is not reachable. Start Docker Desktop (or your daemon) and retry."
if ! docker pussh --help >/dev/null 2>&1; then
cat >&2 <<EOF
'docker pussh' plugin not found. Install it with:
mkdir -p ${PLUGIN_DIR}
curl -fsSL ${UNREGISTRY_PLUGIN} -o ${PLUGIN_PATH}
chmod +x ${PLUGIN_PATH}
Then rerun this script.
EOF
exit 1
fi
[[ -f "$DOCKERFILE" ]] || die "Dockerfile not found at ${DOCKERFILE}." [[ -f "$DOCKERFILE" ]] || die "Dockerfile not found at ${DOCKERFILE}."
[[ -f pom.xml ]] || die "pom.xml not found in current directory."
parse_ssh_target
build_ssh_args
detect_remote_docker
# ---------- build ---------- # ---------- build ----------
if [[ "$SKIP_BUILD" -eq 1 ]]; then if [[ "$SKIP_BUILD" -eq 1 ]]; then
@@ -261,32 +502,40 @@ else
ok "Build finished." ok "Build finished."
fi fi
# ---------- pussh ---------- # ---------- push via temporary registry ----------
log "Pusshing ${IMAGE_REF} to ${REMOTE_TARGET} via unregistry…" trap cleanup_push_resources EXIT
pussh_args=( "$REMOTE_TARGET" ) log "Pushing ${IMAGE_REF} to ${REMOTE_TARGET} via temporary registry…"
[[ -n "$SSH_KEY" ]] && pussh_args+=( -i "$SSH_KEY" )
[[ -n "$SSH_CONFIG" ]] && pussh_args+=( -F "$SSH_CONFIG" )
docker pussh "${IMAGE_REF}" "${pussh_args[@]}" start_remote_registry
ok "Pussh complete." ok "Temporary registry listening on remote 127.0.0.1:${REG_REMOTE_PORT}."
start_ssh_tunnel
ok "SSH tunnel localhost:${REG_LOCAL_PORT} → remote:${REG_REMOTE_PORT}."
push_via_registry_tunnel
ok "Push complete."
# ---------- verify ---------- # ---------- verify ----------
if [[ "$SKIP_VERIFY" -eq 1 ]]; then if [[ "$SKIP_VERIFY" -eq 1 ]]; then
warn "Skipping post-push verification (--no-verify)." warn "Skipping post-push verification (--no-verify)."
else else
log "Verifying ${IMAGE_REF} on ${REMOTE_TARGET}…" log "Verifying tags on ${REMOTE_TARGET}…"
ssh_args=( "$REMOTE_TARGET" ) remote_images=$(run_remote "${REMOTE_DOCKER} images --format '{{.Repository}}:{{.Tag}}'")
[[ -n "$SSH_KEY" ]] && ssh_args=( -i "$SSH_KEY" "${ssh_args[@]}" ) missing=0
[[ -n "$SSH_CONFIG" ]] && ssh_args=( -F "$SSH_CONFIG" "${ssh_args[@]}" ) for ref in "${IMAGE_NAME}:latest" "${RELEASE_REF}"; do
if ! printf '%s\n' "$remote_images" | grep -Fxq "$ref"; then
if ! ssh "${ssh_args[@]}" "docker images --format '{{.Repository}}:{{.Tag}} {{.Size}}' | grep -E '^${IMAGE_NAME}:${IMAGE_TAG} '"; then err "Missing on remote: ${ref}"
die "Image ${IMAGE_REF} not found on ${REMOTE_TARGET} after pussh." missing=1
fi fi
ok "Image present on remote." done
[[ "$missing" -eq 0 ]] || die "Expected tags were not found on ${REMOTE_TARGET} after push."
ok "Remote has ${IMAGE_NAME}:latest and ${RELEASE_REF}."
fi fi
# ---------- next step hint ---------- # ---------- next step hint ----------
printf '\n%s→ To run on the remote:%s\n' "$C_OK" "$C_RESET" printf '\n%s→ To run the latest image on the remote:%s\n' "$C_OK" "$C_RESET"
printf ' ssh %s "docker run -d --name %s --restart unless-stopped -p 8080:8080 %s"\n\n' \ printf ' ssh %s "docker run -d --name %s --restart unless-stopped -p 8080:8080 %s:latest"\n' \
"$REMOTE_TARGET" "$IMAGE_NAME" "$IMAGE_REF" "$REMOTE_TARGET" "$IMAGE_NAME" "$IMAGE_NAME"
printf '%s→ Rollback: list tags with docker images %s, then run a previous vYYYY.MM.DD-HH.MM tag.%s\n\n' \
"$C_OK" "$IMAGE_NAME" "$C_RESET"
+334
View File
@@ -0,0 +1,334 @@
#!/usr/bin/env bash
#
# Run scripts/build-and-push.sh once per service listed in a project file.
# Continues after a failure and prints a summary when the batch finishes.
# That summary is also written to deploy-YYYYMMDD-HHMMSS.log in the directory
# from which the script was launched.
#
# The list path is required (--services). Relative service paths are resolved
# against the directory that contains that file, not the caller's cwd.
# An optional second field, java or web, selects the Dockerfile location.
set -euo pipefail
IFS=$'\n\t'
readonly SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
readonly BUILD_SCRIPT="${SCRIPT_DIR}/build-and-push.sh"
SERVICES_FILE=""
REMOTE=""
forward_args=()
# ---------- 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; }
on_error() {
local exit_code=$?
err "Script aborted on line ${BASH_LINENO[0]} (exit ${exit_code})."
exit "$exit_code"
}
trap on_error ERR
# ---------- helpers ----------
usage() {
cat <<EOF
Usage: $(basename "$0") --remote TARGET --services FILE [options]
Run build-and-push.sh once for every service listed in FILE. One list file
per project: pass a different --services path to deploy another project.
A failure does not stop the batch. The exit code is 1 if any service failed.
When the batch finishes, that summary is written to deploy-YYYYMMDD-HHMMSS.log
in the current directory (local start time). If that name already exists, the
process id is appended.
Required:
--remote TARGET SSH destination forwarded to build-and-push.sh
('user@host[:port]' or a ~/.ssh/config alias)
--services FILE Service list for one project. One path per line,
with an optional second field: java or web
(default java). Blank lines and lines starting
with '#' are ignored. Relative paths resolve
against the directory that contains FILE.
Forwarded to each service (build-and-push.sh):
--ssh-key PATH Private key file
--ssh-config PATH SSH config file
--skip-build Push an existing local image
--no-verify Skip the remote image check
--cleanup Prune the remote without building or pushing
Not accepted (each service has its own directory; --type comes from the
list, and --project-dir is set from that path):
--name, --tag, --project-dir, --type
Other:
-h, --help Show this help
Examples:
$(basename "$0") --remote prod --services /srv/proyecto-a/services.txt
$(basename "$0") --remote prod --services /srv/proyecto-b/services.txt --skip-build
EOF
}
trim() {
local value="$1"
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}"
printf '%s' "$value"
}
require_value() {
[[ $# -ge 2 && -n "${2:-}" && "$2" != -* ]] \
|| die "Missing value for $1"
}
# ---------- arg parsing ----------
while [[ $# -gt 0 ]]; do
case "$1" in
--services)
require_value "$@"
SERVICES_FILE="$2"
shift 2
;;
--remote)
require_value "$@"
REMOTE="$2"
shift 2
;;
--ssh-key|--ssh-config)
require_value "$@"
forward_args+=("$1" "$2")
shift 2
;;
--skip-build|--no-verify|--cleanup)
forward_args+=("$1")
shift
;;
--name|--tag|--project-dir|--type)
die "$1 is not accepted. Each service uses its own directory; --type comes from the services file."
;;
-h|--help)
usage
exit 0
;;
*)
die "Unknown argument: $1 (use --help)"
;;
esac
done
[[ -n "$REMOTE" ]] || die "Missing --remote TARGET (use --help)."
[[ -n "$SERVICES_FILE" ]] || die "Missing --services FILE (use --help)."
[[ -f "$BUILD_SCRIPT" ]] || die "build-and-push.sh not found next to this script: ${BUILD_SCRIPT}"
[[ -f "$SERVICES_FILE" ]] || die "Services file not found: ${SERVICES_FILE}"
SERVICES_FILE="$(readlink -f "$SERVICES_FILE")"
SERVICES_DIR="$(cd "$(dirname "$SERVICES_FILE")" && pwd)"
service_paths=()
service_types=()
while IFS= read -r raw_line || [[ -n "$raw_line" ]]; do
line="$(trim "$raw_line")"
[[ -z "$line" || "$line" == \#* ]] && continue
svc_path=""
svc_type=""
svc_extra=""
IFS=$' \t' read -r svc_path svc_type svc_extra <<< "$line"
[[ -n "$svc_extra" ]] && die "Service line has an extra field: ${line}"
[[ -n "$svc_path" ]] || die "Service line is missing a path: ${line}"
svc_path="${svc_path/#\~/$HOME}"
[[ -z "$svc_type" ]] && svc_type="java"
case "$svc_type" in
java|web) ;;
*) die "Unknown service type '${svc_type}' in ${SERVICES_FILE} (use java or web)." ;;
esac
service_paths+=("$svc_path")
service_types+=("$svc_type")
done < "$SERVICES_FILE"
[[ ${#service_paths[@]} -gt 0 ]] || die "Services file is empty: ${SERVICES_FILE}"
run_build() {
local project_dir="$1"
local app_type="$2"
if [[ ${#forward_args[@]} -eq 0 ]]; then
"$BUILD_SCRIPT" --project-dir "$project_dir" --remote "$REMOTE" --type "$app_type"
else
"$BUILD_SCRIPT" --project-dir "$project_dir" --remote "$REMOTE" --type "$app_type" "${forward_args[@]}"
fi
}
resolve_service() {
local entry="$1"
if [[ "$entry" == /* ]]; then
printf '%s' "$entry"
else
printf '%s' "${SERVICES_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"
}
# Plain-text summary in the directory from which the batch was launched.
# A write failure is reported and does not change the batch exit code.
write_summary_log() {
local finished="" summary_index=0 note="" path="" status=""
local -a lines=()
finished="$(date '+%Y-%m-%d %H:%M:%S %z')" || {
warn "Could not write log file: ${log_path}"
return 0
}
lines+=("started: ${run_started}")
lines+=("finished: ${finished}")
lines+=("remote: ${REMOTE}")
lines+=("services_file: ${SERVICES_FILE}")
lines+=("")
while [[ "$summary_index" -lt "$total" ]]; do
note="${result_notes[$summary_index]}"
path="${result_paths[$summary_index]}"
if [[ "$note" == "0" ]]; then
status="ok"
else
status="$note"
fi
lines+=("${path} ${status}")
summary_index=$((summary_index + 1))
done
lines+=("")
lines+=("succeeded: ${ok_count}")
lines+=("failed: ${fail_count}")
if ! printf '%s\n' "${lines[@]}" >"$log_path"; then
warn "Could not write log file: ${log_path}"
return 0
fi
log "Log file: ${log_path}"
}
total="${#service_paths[@]}"
index=0
ok_count=0
fail_count=0
result_paths=()
result_notes=()
launch_dir="$(pwd)"
run_started="$(date '+%Y-%m-%d %H:%M:%S %z')"
log_stamp="$(date '+%Y%m%d-%H%M%S')"
log_path="${launch_dir}/deploy-${log_stamp}.log"
if [[ -e "$log_path" ]]; then
log_path="${launch_dir}/deploy-${log_stamp}-$$.log"
fi
log "Services file: ${SERVICES_FILE}"
log "Remote: ${REMOTE}"
log "Services: ${total}"
while [[ "$index" -lt "$total" ]]; do
entry="${service_paths[$index]}"
app_type="${service_types[$index]}"
index=$((index + 1))
resolved="$(normalize_path "$(resolve_service "$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 "Service directory not found: ${resolved}"
result_paths+=("$resolved")
result_notes+=("not found")
fail_count=$((fail_count + 1))
continue
fi
exit_code=0
if run_build "$resolved" "$app_type"; then
ok "Finished ${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
batch_exit=0
if [[ "$fail_count" -gt 0 ]]; then
err "${ok_count} succeeded, ${fail_count} failed."
batch_exit=1
else
ok "${ok_count} succeeded, 0 failed."
fi
write_summary_log
exit "$batch_exit"
+290
View File
@@ -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."
+1 -1
View File
@@ -1,5 +1,5 @@
## Stage 1 : build with maven builder image with native capabilities ## Stage 1 : build with maven builder image with native capabilities
FROM quay.io/quarkus/ubi-quarkus-mandrel-builder-image:jdk-21 AS build FROM quay.io/quarkus/ubi-quarkus-mandrel-builder-image:jdk-25 AS build
COPY --chown=quarkus:quarkus mvnw /code/mvnw COPY --chown=quarkus:quarkus mvnw /code/mvnw
COPY --chown=quarkus:quarkus .mvn /code/.mvn COPY --chown=quarkus:quarkus .mvn /code/.mvn
COPY --chown=quarkus:quarkus pom.xml /code/ COPY --chown=quarkus:quarkus pom.xml /code/