506 lines
17 KiB
Bash
Executable File
506 lines
17 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Build the native Quarkus image and push it to a remote Docker host
|
|
# via a temporary registry:2 on the remote, reached through an SSH tunnel.
|
|
#
|
|
# - Image name/tag default to pom.xml properties (artifactId and version)
|
|
# - Builds for linux/amd64 by default (cross-compile from arm64 hosts)
|
|
# - 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
|
|
IFS=$'\n\t'
|
|
|
|
readonly DOCKERFILE="src/main/docker/Dockerfile"
|
|
readonly DEFAULT_PLATFORM="linux/amd64"
|
|
readonly TEMP_REGISTRY_IMAGE="registry:2"
|
|
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
|
|
CLEANUP=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
|
|
a temporary Docker registry on the remote, reached through an SSH tunnel.
|
|
|
|
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 Local build tag (from pom.xml by default). On the
|
|
remote only :latest and vYYYY.MM.DD-HH.MM (UTC).
|
|
|
|
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 push an existing image
|
|
(platform is fixed to ${DEFAULT_PLATFORM})
|
|
|
|
SSH options:
|
|
--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
|
|
|
|
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:
|
|
-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
|
|
$(basename "$0") --remote prod --cleanup
|
|
/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"
|
|
}
|
|
|
|
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 ----------
|
|
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 ;;
|
|
--cleanup) CLEANUP=1; shift ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
*) die "Unknown argument: $1 (use --help)" ;;
|
|
esac
|
|
done
|
|
|
|
[[ -z "$REMOTE" ]] && die "Missing --remote USER@HOST[:PORT] (use --help)."
|
|
|
|
# ---------- 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
|
|
|
|
# ---------- 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=$(read_pom "/*[local-name()='project']/*[local-name()='artifactId']" "test-quarkus" "artifactId")
|
|
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=$(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")
|
|
|
|
IMAGE_REF="${IMAGE_NAME}:${IMAGE_TAG}"
|
|
|
|
# ---------- pre-flight ----------
|
|
log "Image: ${IMAGE_REF}"
|
|
log "Platform: ${DEFAULT_PLATFORM}"
|
|
log "Dockerfile: ${DOCKERFILE}"
|
|
log "Remote: ${REMOTE_TARGET}"
|
|
|
|
require docker
|
|
require ssh
|
|
docker info >/dev/null 2>&1 \
|
|
|| die "Docker daemon is not reachable. Start Docker Desktop (or your daemon) and retry."
|
|
|
|
[[ -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 ----------
|
|
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
|
|
|
|
# ---------- push via temporary registry ----------
|
|
trap cleanup_push_resources EXIT
|
|
|
|
log "Pushing ${IMAGE_REF} to ${REMOTE_TARGET} via temporary registry…"
|
|
|
|
start_remote_registry
|
|
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 ----------
|
|
if [[ "$SKIP_VERIFY" -eq 1 ]]; then
|
|
warn "Skipping post-push verification (--no-verify)."
|
|
else
|
|
log "Verifying tags on ${REMOTE_TARGET}…"
|
|
remote_images=$(run_remote "${REMOTE_DOCKER} images --format '{{.Repository}}:{{.Tag}}'")
|
|
missing=0
|
|
for ref in "${IMAGE_NAME}:latest" "${RELEASE_REF}"; do
|
|
if ! printf '%s\n' "$remote_images" | grep -Fxq "$ref"; then
|
|
err "Missing on remote: ${ref}"
|
|
missing=1
|
|
fi
|
|
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
|
|
|
|
# ---------- next step hint ----------
|
|
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:latest"\n' \
|
|
"$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"
|