Files
test-quarkus/scripts/build-and-push.sh
T
darroyo 57af30fdd2 fix: read_pom fallback returned both artifactId and version
When xmllint is not in PATH, the grep fallback in read_pom() captured
both <artifactId> and <version> in a single multi-line string and
returned it for every call. Concatenating it with ':' produced an
invalid Docker tag like:

    test-quarkus\n1.0.0-SNAPSHOT:test-quarkus\n1.0.0-SNAPSHOT

causing 'docker build' to fail with 'invalid reference format'.

Add a third 'tag' argument to read_pom() so the fallback extracts
only the requested element, and pass 'artifactId' / 'version' from
the call sites.

Also enrich pom.xml with <name> and <description> for Maven/IDE
metadata, without changing the image identity.

Tests cover the no-xmllint path explicitly (PATH stripped to
reproduce the vm-compilador environment) and the override path.
2026-09-09 12:10:17 -03:00

293 lines
9.7 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Build the native Quarkus image and push it to a remote Docker host
# via docker-pussh (unregistry): https://github.com/psviderski/unregistry
#
# - Image name/tag default to pom.xml properties (quarkus.application.*)
# - Builds for linux/amd64 by default (cross-compile from arm64 hosts)
# - Transfers only missing layers over SSH (no external registry required)
set -euo pipefail
IFS=$'\n\t'
readonly DOCKERFILE="src/main/docker/Dockerfile"
readonly DEFAULT_PLATFORM="linux/amd64"
readonly UNREGISTRY_PLUGIN="https://raw.githubusercontent.com/psviderski/unregistry/main/docker-pussh"
readonly PLUGIN_DIR="${HOME}/.docker/cli-plugins"
readonly PLUGIN_PATH="${PLUGIN_DIR}/docker-pussh"
readonly SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
REMOTE=""
IMAGE_NAME=""
IMAGE_TAG=""
SSH_KEY=""
SSH_CONFIG=""
PROJECT_DIR="${PROJECT_DIR:-${SCRIPT_DIR}/..}"
SKIP_BUILD=0
SKIP_VERIFY=0
# ---------- 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 USER@HOST[:PORT] [options]
Build the native Quarkus image and push it to a remote Docker host using
unregistry (docker-pussh).
Required:
--remote TARGET SSH destination. Either 'user@host[:port]'
or a bare alias resolved from ~/.ssh/config
(e.g. 'prod'). See SSH config aliases below.
Image identity (defaults read from pom.xml):
--name NAME Image repository name
--tag TAG Image tag
Working directory (defaults to the parent of the script's directory):
--project-dir PATH Project root containing pom.xml
(override with env PROJECT_DIR)
Build options:
--skip-build Skip docker build, only pussh an existing image
(platform is fixed to ${DEFAULT_PLATFORM})
SSH options forwarded to docker pussh:
--ssh-key PATH Private key file (overrides SSH config)
--ssh-config PATH SSH config file (default: ~/.ssh/config)
Post-push:
--no-verify Skip the ssh/docker images verification step
Other:
-h, --help Show this help
SSH config aliases:
When --remote is a bare alias (no '@'), it is resolved from the SSH
config: HostName, User, Port and IdentityFile are pulled from the
matching Host block, with sensible fallbacks. --ssh-key always wins
over the config IdentityFile.
Examples:
$(basename "$0") --remote deploy@prod.example.com
$(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 deploy@prod --skip-build --tag 2.0.0
/absolute/path/to/test-quarkus/scripts/$(basename "$0") --remote deploy@prod
PROJECT_DIR=/srv/proj $(basename "$0") --remote deploy@prod --skip-build
EOF
}
read_pom() {
local xpath="$1" fallback="$2" tag="${3:-}"
local value=""
if command -v xmllint >/dev/null 2>&1; then
value=$(xmllint --xpath "string($xpath)" pom.xml 2>/dev/null || true)
fi
if [[ -z "$value" && -n "$tag" ]]; then
value=$(grep -E "<${tag}>" pom.xml | head -1 \
| sed -E "s:.*<${tag}>(.*)</${tag}>.*:\1:")
fi
printf '%s' "${value:-$fallback}"
}
require() {
command -v "$1" >/dev/null 2>&1 || die "Required command '$1' not found in PATH."
}
resolve_ssh_alias() {
local alias="$1"
local config="${SSH_CONFIG:-$HOME/.ssh/config}"
if [[ "$alias" == *@* ]]; then
printf '%s|%s\n' "$alias" ""
return 0
fi
[[ -f "$config" ]] \
|| die "Cannot resolve '$alias': $config not found and value has no '@'."
local in_block=0 hostname="" user="" port="" identity="" line first p
local -a patterns
# Force whitespace-aware splitting (the script's global IFS excludes spaces).
local IFS=$' \t\n'
# SSH config directives are case-insensitive (HostName == Hostname == HOSTNAME).
# nocasematch is supported in bash 3.2 (unlike ${var,,} which needs 4+).
shopt -s nocasematch
while read -r line || [[ -n "$line" ]]; do
# First whitespace-separated token (tabs and spaces collapse uniformly).
read -r first _ <<< "$line"
[[ -z "$first" || "$first" == "#" ]] && continue
if [[ "$first" == "Host" ]]; then
if (( in_block )); then break; fi
read -r -a patterns <<< "$line"
patterns=("${patterns[@]:1}")
in_block=0
for p in "${patterns[@]}"; do
if [[ "$p" == "$alias" ]]; then
in_block=1
hostname=""; user=""; port=""; identity=""
break
fi
done
continue
fi
if (( in_block )); then
case "$first" in
HostName) read -r _ hostname <<< "$line" ;;
User) read -r _ user <<< "$line" ;;
Port) read -r _ port <<< "$line" ;;
IdentityFile) read -r _ identity <<< "$line" ;;
esac
fi
done < "$config"
shopt -u nocasematch
(( in_block )) \
|| die "No matching Host entry in $config for alias '$alias'. Use user@host[:port] or check ~/.ssh/config."
[[ -z "$hostname" ]] && die "Host '$alias' has no HostName in $config."
[[ -z "$user" ]] && user="$(whoami)"
[[ -z "$port" ]] && port="22"
identity="${identity/#\~/$HOME}"
printf '%s@%s:%s|%s\n' "$user" "$hostname" "$port" "$identity"
}
# ---------- arg parsing ----------
while [[ $# -gt 0 ]]; do
case "$1" in
--remote) REMOTE="$2"; shift 2 ;;
--name) IMAGE_NAME="$2"; shift 2 ;;
--tag) IMAGE_TAG="$2"; shift 2 ;;
--ssh-key) SSH_KEY="$2"; shift 2 ;;
--ssh-config) SSH_CONFIG="$2"; shift 2 ;;
--project-dir) PROJECT_DIR="$2"; shift 2 ;;
--skip-build) SKIP_BUILD=1; shift ;;
--no-verify) SKIP_VERIFY=1; shift ;;
-h|--help) usage; exit 0 ;;
*) die "Unknown argument: $1 (use --help)" ;;
esac
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)."
IMAGE_REF="${IMAGE_NAME}:${IMAGE_TAG}"
# ---------- resolve SSH target ----------
resolved=$(resolve_ssh_alias "$REMOTE")
REMOTE_TARGET="${resolved%|*}"
REMOTE_IDENTITY="${resolved##*|}"
if [[ "$REMOTE" != *@* ]]; then
log "Resolved alias '${REMOTE}' → ${REMOTE_TARGET}"
fi
if [[ -z "$SSH_KEY" && -n "$REMOTE_IDENTITY" ]]; then
SSH_KEY="$REMOTE_IDENTITY"
log "Using IdentityFile from SSH config: ${SSH_KEY}"
fi
# ---------- pre-flight ----------
log "Image: ${IMAGE_REF}"
log "Platform: ${DEFAULT_PLATFORM}"
log "Dockerfile: ${DOCKERFILE}"
log "Remote: ${REMOTE_TARGET}"
require docker
docker info >/dev/null 2>&1 \
|| 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 pom.xml ]] || die "pom.xml not found in current directory."
# ---------- build ----------
if [[ "$SKIP_BUILD" -eq 1 ]]; then
warn "Skipping build (--skip-build). Assuming ${IMAGE_REF} exists locally."
docker image inspect "$IMAGE_REF" >/dev/null 2>&1 \
|| die "Image ${IMAGE_REF} not present locally; remove --skip-build to build it."
else
log "Building ${IMAGE_REF} for ${DEFAULT_PLATFORM} (this may take several minutes)…"
docker build \
--platform "$DEFAULT_PLATFORM" \
-f "$DOCKERFILE" \
-t "$IMAGE_REF" \
.
ok "Build finished."
fi
# ---------- pussh ----------
log "Pusshing ${IMAGE_REF} to ${REMOTE_TARGET} via unregistry…"
pussh_args=( "$REMOTE_TARGET" )
[[ -n "$SSH_KEY" ]] && pussh_args+=( -i "$SSH_KEY" )
[[ -n "$SSH_CONFIG" ]] && pussh_args+=( -F "$SSH_CONFIG" )
docker pussh "${IMAGE_REF}" "${pussh_args[@]}"
ok "Pussh complete."
# ---------- verify ----------
if [[ "$SKIP_VERIFY" -eq 1 ]]; then
warn "Skipping post-push verification (--no-verify)."
else
log "Verifying ${IMAGE_REF} on ${REMOTE_TARGET}…"
ssh_args=( "$REMOTE_TARGET" )
[[ -n "$SSH_KEY" ]] && ssh_args=( -i "$SSH_KEY" "${ssh_args[@]}" )
[[ -n "$SSH_CONFIG" ]] && ssh_args=( -F "$SSH_CONFIG" "${ssh_args[@]}" )
if ! ssh "${ssh_args[@]}" "docker images --format '{{.Repository}}:{{.Tag}} {{.Size}}' | grep -E '^${IMAGE_NAME}:${IMAGE_TAG} '"; then
die "Image ${IMAGE_REF} not found on ${REMOTE_TARGET} after pussh."
fi
ok "Image present on remote."
fi
# ---------- next step hint ----------
printf '\n%s→ To run 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' \
"$REMOTE_TARGET" "$IMAGE_NAME" "$IMAGE_REF"