docker release
This commit is contained in:
280
build-and-push.sh
Executable file
280
build-and-push.sh
Executable file
@@ -0,0 +1,280 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user