#!/usr/bin/env bash set -euo pipefail # ============================================================================ # build-and-push.sh # # Purpose: # - Build & push Docker images for each service under ./containers/* # - Two modes: # t (test) = only push :dev # r (release) = push :, :dev, :latest # version is read from the top of changelog.md # # No git operations: committing and tagging is done manually. # # Usage: # ./build-and-push.sh [mode] # - mode = t -> test build, push :dev only # - mode = r -> release build, version taken from changelog.md # - omitted -> prompt (default: t) # # Requirements: # - docs/changelog.md (relative to repo root), with the most recent release # at the top as: # ## vX.Y.Z — YYYY-MM-DD # (the version is parsed from the first such line) # - One Dockerfile per service under ./containers//Dockerfile # ============================================================================ DOCKER_REGISTRY="gitea.oskamp.info" DOCKER_NAMESPACE="ivooskamp" CHANGELOG_FILE="docs/changelog.md" CONTAINERS_DIR="containers" # --- Input: prompt if missing ------------------------------------------------ MODE="${1:-}" if [[ -z "${MODE}" ]]; then echo "Select build type: [t] test build (push :dev only), [r] release build (default: t)" read -r MODE MODE="${MODE:-t}" fi case "$MODE" in t|test) MODE="t" ;; r|release) MODE="r" ;; *) echo "[ERROR] Unknown mode '$MODE' (use 't' for test or 'r' for release)." exit 1 ;; esac # --- Helpers ----------------------------------------------------------------- check_docker_ready() { if ! docker info >/dev/null 2>&1; then echo "[ERROR] Docker daemon not reachable. Is Docker running and do you have permission to use it?" exit 1 fi } ensure_registry_login() { local cfg="${HOME}/.docker/config.json" if [[ ! -f "$cfg" ]]; then echo "[ERROR] Docker config not found at $cfg. Please login: docker login ${DOCKER_REGISTRY}" exit 1 fi if ! grep -q "\"${DOCKER_REGISTRY}\"" "$cfg"; then echo "[ERROR] No registry auth found for ${DOCKER_REGISTRY}. Please run: docker login ${DOCKER_REGISTRY}" exit 1 fi } validate_repo_component() { local comp="$1" if [[ ! "$comp" =~ ^[a-z0-9]+([._-][a-z0-9]+)*$ ]]; then echo "[ERROR] Invalid repository component '$comp'." echo " Must match: ^[a-z0-9]+([._-][a-z0-9]+)*$ (lowercase, digits, ., _, - as separators)." return 1 fi } validate_tag() { local tag="$1" local len="${#tag}" if (( len < 1 || len > 128 )); then echo "[ERROR] Invalid tag length ($len). Must be between 1 and 128 characters." return 1 fi if [[ ! "$tag" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]*$ ]]; then echo "[ERROR] Invalid tag '$tag'. Allowed: [A-Za-z0-9_.-], must start with alphanumeric or underscore." return 1 fi } # Parse the first "## vX.Y.Z ..." heading from changelog.md. # Accepts: ## v1.0.3 — 2026-04-24 # ## v1.0.3 - 2026-04-24 # ## v1.0.3 read_version_from_changelog() { if [[ ! -f "$CHANGELOG_FILE" ]]; then echo "[ERROR] $CHANGELOG_FILE not found in $(pwd)." >&2 exit 1 fi local line # Match lines starting with "## v.." line="$(grep -m1 -E '^##[[:space:]]+v[0-9]+\.[0-9]+\.[0-9]+' "$CHANGELOG_FILE" || true)" if [[ -z "$line" ]]; then echo "[ERROR] No release heading found in $CHANGELOG_FILE (expected e.g. '## v1.0.3 — 2026-04-24' near the top)." >&2 exit 1 fi # Extract the vX.Y.Z token local version version="$(echo "$line" | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' | head -n1)" if [[ -z "$version" ]]; then echo "[ERROR] Could not parse version from line: $line" >&2 exit 1 fi echo "$version" } # --- Preflight --------------------------------------------------------------- if [[ ! -d "$CONTAINERS_DIR" ]]; then echo "[ERROR] '$CONTAINERS_DIR' directory missing. Expected ./${CONTAINERS_DIR}// with a Dockerfile." exit 1 fi check_docker_ready ensure_registry_login validate_repo_component "$DOCKER_NAMESPACE" # Informational: show branch and HEAD if this happens to be a git repo. BRANCH_INFO="" HEAD_INFO="" if [[ -d ".git" ]]; then BRANCH_INFO="$(git branch --show-current 2>/dev/null || echo unknown)" HEAD_INFO="$(git rev-parse --short HEAD 2>/dev/null || echo unknown)" echo "[INFO] Repo: $(pwd)" echo "[INFO] Current branch: $BRANCH_INFO" echo "[INFO] HEAD (sha): $HEAD_INFO" else echo "[INFO] Repo: $(pwd) (not a git checkout)" fi # --- Determine version (release only) ---------------------------------------- VERSION="" if [[ "$MODE" == "r" ]]; then VERSION="$(read_version_from_changelog)" echo "[INFO] Release version (from $CHANGELOG_FILE): $VERSION" validate_tag "$VERSION" validate_tag "latest" # Ask for confirmation so you never accidentally re-push an old version or a wrong one. read -r -p "Proceed building & pushing as ${VERSION}? [y/N] " CONFIRM CONFIRM="${CONFIRM:-N}" if [[ ! "$CONFIRM" =~ ^[Yy]$ ]]; then echo "[INFO] Aborted by user." exit 0 fi else echo "[INFO] Test build: only :dev will be pushed." fi validate_tag "dev" # --- Build & push per service ------------------------------------------------ shopt -s nullglob services=( "$CONTAINERS_DIR"/* ) if [[ ${#services[@]} -eq 0 ]]; then echo "[ERROR] No services found under $CONTAINERS_DIR" exit 1 fi BUILT_IMAGES=() for svc_path in "${services[@]}"; do [[ -d "$svc_path" ]] || continue svc="$(basename "$svc_path")" dockerfile="$svc_path/Dockerfile" validate_repo_component "$svc" if [[ ! -f "$dockerfile" ]]; then echo "[WARNING] Skipping '${svc}': Dockerfile not found in ${svc_path}" continue fi IMAGE_BASE="${DOCKER_REGISTRY}/${DOCKER_NAMESPACE}/${svc}" if [[ "$MODE" == "r" ]]; then echo "============================================================" echo "[INFO] Building ${svc} -> tags: ${VERSION}, dev, latest" echo "============================================================" docker build \ -t "${IMAGE_BASE}:${VERSION}" \ -t "${IMAGE_BASE}:dev" \ -t "${IMAGE_BASE}:latest" \ "$svc_path" docker push "${IMAGE_BASE}:${VERSION}" docker push "${IMAGE_BASE}:dev" docker push "${IMAGE_BASE}:latest" BUILT_IMAGES+=("${IMAGE_BASE}:${VERSION}" "${IMAGE_BASE}:dev" "${IMAGE_BASE}:latest") else echo "============================================================" echo "[INFO] Test build ${svc} -> tag: dev" echo "============================================================" docker build -t "${IMAGE_BASE}:dev" "$svc_path" docker push "${IMAGE_BASE}:dev" BUILT_IMAGES+=("${IMAGE_BASE}:dev") fi done # --- Summary ----------------------------------------------------------------- echo "" echo "============================================================" if [[ "$MODE" == "r" ]]; then echo "[SUMMARY] Release build & push complete: $VERSION" else echo "[SUMMARY] Test build & push complete (:dev only)" fi if [[ -n "$BRANCH_INFO" ]]; then echo "[INFO] Branch: $BRANCH_INFO HEAD: $HEAD_INFO" fi echo "[INFO] Images pushed:" for img in "${BUILT_IMAGES[@]}"; do echo " - $img" done echo "============================================================" echo "" echo "[REMINDER] No git operations were performed. If this was a release," echo " commit and tag manually, e.g.:" if [[ "$MODE" == "r" ]]; then echo " git add -A && git commit -m \"Release ${VERSION}\"" echo " git tag -a ${VERSION} -m \"Release ${VERSION}\"" echo " git push && git push --tags" fi