Files
docker-release/build-and-push.sh
2026-09-06 09:40:35 +12:00

281 lines
10 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Build every Fennec Hub image and push it to Docker Hub.
#
# ./build-and-push.sh all # build + push all three images
# ./build-and-push.sh build # build locally, don't push
# ./build-and-push.sh all -s admin # just the admin UI
# ./build-and-push.sh all -t 2026.09.05 # explicit tag
#
# Configuration lives in .env (see .env.example); the image catalogue lives in
# images.conf. Run --help for the full option list.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
CATALOGUE="$SCRIPT_DIR/images.conf"
# ---------------------------------------------------------------- output ----
if [[ -t 1 ]]; then
C_RESET=$'\033[0m'; C_BOLD=$'\033[1m'; C_DIM=$'\033[2m'
C_RED=$'\033[31m'; C_GREEN=$'\033[32m'; C_YELLOW=$'\033[33m'; C_BLUE=$'\033[34m'
else
C_RESET=; C_BOLD=; C_DIM=; C_RED=; C_GREEN=; C_YELLOW=; C_BLUE=
fi
log() { printf '%s\n' "${C_BLUE}==>${C_RESET} $*"; }
info() { printf '%s\n' " ${C_DIM}$*${C_RESET}"; }
ok() { printf '%s\n' "${C_GREEN}${C_RESET} $*"; }
warn() { printf '%s\n' "${C_YELLOW} !${C_RESET} $*" >&2; }
die() { printf '%s\n' "${C_RED}error:${C_RESET} $*" >&2; exit 1; }
# ------------------------------------------------------------ config ----
[[ -f "$SCRIPT_DIR/.env" ]] && { set -a; . "$SCRIPT_DIR/.env"; set +a; }
DOCKERHUB_NAMESPACE="${DOCKERHUB_NAMESPACE:-fennecsm}"
DOCKERHUB_USERNAME="${DOCKERHUB_USERNAME:-$DOCKERHUB_NAMESPACE}"
DOCKERHUB_TOKEN="${DOCKERHUB_TOKEN:-}"
PLATFORMS="${PLATFORMS:-linux/amd64}"
PUSH_LATEST="${PUSH_LATEST:-true}"
TAG="${TAG:-}"
# Angular build memory. Empty => use the per-Dockerfile defaults. Set these to
# override both frontends at once without editing the Dockerfiles.
NODE_MAX_OLD_SPACE="${NODE_MAX_OLD_SPACE:-}"
NG_BUILD_MAX_WORKERS="${NG_BUILD_MAX_WORKERS:-}"
SERVICES=""
NO_CACHE=""
DRY_RUN=false
BUILDER_NAME="fennec-release"
usage() {
cat <<EOF
${C_BOLD}Usage:${C_RESET} $(basename "$0") <command> [options]
${C_BOLD}Commands${C_RESET}
build Build images locally (no push). Single platform only.
push Build and push images to Docker Hub.
all Alias for push (login + build + push).
login Log in to Docker Hub and exit.
list Show the image catalogue and the tags that would be used.
${C_BOLD}Options${C_RESET}
-s, --service <k> Only this image; repeatable. Keys: $(cut -d'|' -f1 "$CATALOGUE" 2>/dev/null | grep -v '^#' | tr -d ' ' | grep . | paste -sd, -)
-t, --tag <tag> Tag to apply (default: auto <git-sha>-<timestamp>)
-n, --namespace <n> Docker Hub namespace (default: $DOCKERHUB_NAMESPACE)
-p, --platforms <p> Target platforms (default: $PLATFORMS)
--no-latest Do not also tag/push :latest
--no-cache Build without the Docker layer cache
--dry-run Print the docker commands instead of running them
-h, --help This message
${C_BOLD}Examples${C_RESET}
$(basename "$0") list
$(basename "$0") build -s admin
$(basename "$0") all -t 2026.09.05 -p linux/amd64,linux/arm64
EOF
}
# -------------------------------------------------------------- args ----
[[ $# -eq 0 ]] && { usage; exit 1; }
COMMAND="$1"; shift
while [[ $# -gt 0 ]]; do
case "$1" in
-s|--service) SERVICES="$SERVICES $2"; shift 2 ;;
-t|--tag) TAG="$2"; shift 2 ;;
-n|--namespace) DOCKERHUB_NAMESPACE="$2"; shift 2 ;;
-p|--platforms) PLATFORMS="$2"; shift 2 ;;
--no-latest) PUSH_LATEST=false; shift ;;
--no-cache) NO_CACHE="--no-cache"; shift ;;
--dry-run) DRY_RUN=true; shift ;;
-h|--help) usage; exit 0 ;;
*) die "unknown option: $1 (try --help)" ;;
esac
done
# ------------------------------------------------------------- helpers ----
run() {
if $DRY_RUN; then
printf '%s\n' " ${C_DIM}[dry-run]${C_RESET} $*"
else
"$@"
fi
}
# Auto tag: <short sha of the project's own repo>-<UTC timestamp>.
# Each project is its own git repo; the repo root is not, so fall back
# gracefully when no sha is available.
make_tag() {
local sha
sha="$(git -C "$REPO_ROOT/fennec-hub-multi-tenant-shared-db" rev-parse --short=7 HEAD 2>/dev/null || true)"
local stamp
stamp="$(date -u +%Y%m%dT%H%MZ)"
if [[ -n "$sha" ]]; then
printf '%s-%s' "$sha" "$stamp"
else
printf '%s' "$stamp"
fi
}
# Emits "key|image|context|dockerfile|description" for each selected image.
selected_images() {
[[ -f "$CATALOGUE" ]] || die "catalogue not found: $CATALOGUE"
local line key image context dockerfile description
while IFS='|' read -r key image context dockerfile description; do
key="$(printf '%s' "$key" | tr -d '[:space:]')"
[[ -z "$key" || "$key" == \#* ]] && continue
if [[ -n "${SERVICES// /}" ]]; then
[[ " $SERVICES " == *" $key "* ]] || continue
fi
image="$(printf '%s' "$image" | tr -d '[:space:]')"
context="$(printf '%s' "$context" | tr -d '[:space:]')"
dockerfile="$(printf '%s' "$dockerfile" | tr -d '[:space:]')"
description="$(printf '%s' "$description" | sed 's/^ *//; s/ *$//')"
printf '%s|%s|%s|%s|%s\n' "$key" "$image" "$context" "$dockerfile" "$description"
done < "$CATALOGUE"
}
validate_selection() {
local found=false k
while IFS='|' read -r k _; do found=true; done < <(selected_images)
$found || die "no images matched --service '${SERVICES# }'. Run '$(basename "$0") list'."
}
docker_login() {
if [[ -n "$DOCKERHUB_TOKEN" ]]; then
log "Logging in to Docker Hub as $DOCKERHUB_USERNAME"
if $DRY_RUN; then
info "[dry-run] docker login -u $DOCKERHUB_USERNAME --password-stdin"
else
printf '%s' "$DOCKERHUB_TOKEN" \
| docker login -u "$DOCKERHUB_USERNAME" --password-stdin >/dev/null \
|| die "docker login failed"
ok "authenticated"
fi
elif docker system info 2>/dev/null | grep -q "Username:"; then
ok "already logged in to Docker Hub"
else
log "Logging in to Docker Hub as $DOCKERHUB_USERNAME"
warn "DOCKERHUB_TOKEN is not set — docker will prompt for a password/token"
$DRY_RUN || docker login -u "$DOCKERHUB_USERNAME" || die "docker login failed"
fi
}
ensure_builder() {
# A multi-platform build needs a buildx builder backed by the
# docker-container driver; the default "docker" driver cannot do it.
if [[ "$PLATFORMS" != *,* ]]; then
return 0
fi
if docker buildx inspect "$BUILDER_NAME" >/dev/null 2>&1; then
info "using buildx builder '$BUILDER_NAME'"
else
log "Creating buildx builder '$BUILDER_NAME' (multi-platform)"
run docker buildx create --name "$BUILDER_NAME" --driver docker-container --bootstrap
fi
run docker buildx use "$BUILDER_NAME"
}
build_image() {
local key="$1" image="$2" context="$3" dockerfile="$4" push="$5"
local repo="$DOCKERHUB_NAMESPACE/$image"
local -a args=()
[[ -f "$REPO_ROOT/$dockerfile" ]] || die "$key: dockerfile not found: $dockerfile"
[[ -d "$REPO_ROOT/$context" ]] || die "$key: build context not found: $context"
args+=(buildx build)
args+=(--file "$REPO_ROOT/$dockerfile")
args+=(--platform "$PLATFORMS")
args+=(--tag "$repo:$TAG")
[[ "$PUSH_LATEST" == "true" ]] && args+=(--tag "$repo:latest")
args+=(--label "org.opencontainers.image.title=$image")
args+=(--label "org.opencontainers.image.version=$TAG")
args+=(--label "org.opencontainers.image.created=$(date -u +%Y-%m-%dT%H:%M:%SZ)")
[[ -n "$NO_CACHE" ]] && args+=("$NO_CACHE")
# Angular memory knobs — only the frontend images declare these ARGs, and
# passing an unused --build-arg is a warning, so scope them to the frontends.
if [[ "$key" != "backend" ]]; then
[[ -n "$NODE_MAX_OLD_SPACE" ]] && args+=(--build-arg "NODE_MAX_OLD_SPACE=$NODE_MAX_OLD_SPACE")
[[ -n "$NG_BUILD_MAX_WORKERS" ]] && args+=(--build-arg "NG_BUILD_MAX_WORKERS=$NG_BUILD_MAX_WORKERS")
fi
if [[ "$push" == "true" ]]; then
args+=(--push)
else
args+=(--load)
fi
args+=("$REPO_ROOT/$context")
log "${C_BOLD}$key${C_RESET} -> $repo:$TAG"
info "context=$context dockerfile=$dockerfile platforms=$PLATFORMS"
run docker "${args[@]}"
ok "$key done"
}
do_build() {
local push="$1"
validate_selection
# Multi-platform results cannot land in the local image store, so catch the
# conflict before spending time on a builder or a build.
if [[ "$push" != "true" && "$PLATFORMS" == *,* ]]; then
die "multi-platform builds must be pushed; use 'push'/'all', or -p linux/amd64"
fi
[[ -n "$TAG" ]] || TAG="$(make_tag)"
ensure_builder
log "Namespace ${C_BOLD}$DOCKERHUB_NAMESPACE${C_RESET} tag ${C_BOLD}$TAG${C_RESET}$( [[ "$PUSH_LATEST" == "true" ]] && printf ' (+ latest)' )"
local key image context dockerfile _desc
local -a pushed=()
while IFS='|' read -r key image context dockerfile _desc; do
build_image "$key" "$image" "$context" "$dockerfile" "$push"
pushed+=("$DOCKERHUB_NAMESPACE/$image:$TAG")
done < <(selected_images)
echo
if [[ "$push" == "true" ]]; then
log "Pushed to Docker Hub:"
else
log "Built locally:"
fi
printf ' %s\n' "${pushed[@]}"
if [[ "$push" == "true" ]]; then
echo
info "Deploy with: cd docker-release && TAG=$TAG docker compose up -d"
fi
}
do_list() {
[[ -n "$TAG" ]] || TAG="$(make_tag)"
log "Namespace ${C_BOLD}$DOCKERHUB_NAMESPACE${C_RESET} tag ${C_BOLD}$TAG${C_RESET} platforms ${C_BOLD}$PLATFORMS${C_RESET}"
echo
printf ' %-10s %-40s %s\n' "KEY" "IMAGE" "DESCRIPTION"
local key image context dockerfile desc
while IFS='|' read -r key image context dockerfile desc; do
printf ' %-10s %-40s %s\n' "$key" "$DOCKERHUB_NAMESPACE/$image" "$desc"
done < <(selected_images)
}
# ------------------------------------------------------------ dispatch ----
command -v docker >/dev/null 2>&1 || die "docker is not installed or not on PATH"
case "$COMMAND" in
list) do_list ;;
login) docker_login ;;
build) do_build false ;;
push|all)
docker_login
do_build true ;;
-h|--help|help) usage ;;
*) die "unknown command: $COMMAND (try --help)" ;;
esac