#!/usr/bin/env bash set -euo pipefail # ============================================================================ # build-and-push.sh # Location: repo root (e.g. /docker/develop/backup-monitoring) # # Purpose: # - Automatic version bump: # 1 = patch, 2 = minor, 3 = major, t = test # - Test builds: only update :dev (no commit/tag) # - Release builds: update version.txt, commit, tag, push (to the current branch) # - Build & push Docker images for each service under ./compose/* # - Preflight checks: Docker daemon up, logged in to registry, valid names/tags # - Summary: show all images + tags built and pushed # - Branch visibility: # - Shows currently checked out branch (authoritative) # - Reads .last-branch for info (if present) when BRANCH is not set # - Writes the current branch back to .last-branch at the end # # Usage: # BRANCH= ./build-and-push.sh [bump] # BRANCH is optional; informative only # ./build-and-push.sh [bump] # If [bump] is omitted, you will be prompted (default = t). # # Tagging rules: # - Release build (1/2/3): push :, :dev, :latest # - Test build (t): push only :dev (no :latest, no version tag) # ============================================================================ DOCKER_REGISTRY="gitea.oskamp.info" DOCKER_NAMESPACE="ivooskamp" VERSION_FILE="version.txt" START_VERSION="v0.1.0" COMPOSE_DIR="containers" LAST_BRANCH_FILE=".last-branch" # stored in repo root # --- Input: prompt if missing ------------------------------------------------ BUMP="${1:-}" if [[ -z "${BUMP}" ]]; then echo "Select bump type: [1] patch, [2] minor, [3] major, [t] test (default: t)" read -r BUMP BUMP="${BUMP:-t}" fi if [[ "$BUMP" != "1" && "$BUMP" != "2" && "$BUMP" != "3" && "$BUMP" != "t" ]]; then echo "[ERROR] Unknown bump type '$BUMP' (use 1, 2, 3, or t)." exit 1 fi # --- Helpers ----------------------------------------------------------------- read_version() { if [[ -f "$VERSION_FILE" ]]; then tr -d ' \t\n\r' < "$VERSION_FILE" else echo "$START_VERSION" fi } write_version() { echo "$1" > "$VERSION_FILE" } bump_version() { local cur="$1" local kind="$2" local core="${cur#v}" IFS='.' read -r MA MI PA <<< "$core" case "$kind" in 1) PA=$((PA + 1));; 2) MI=$((MI + 1)); PA=0;; 3) MA=$((MA + 1)); MI=0; PA=0;; *) echo "[ERROR] Unknown bump kind"; exit 1;; esac echo "v${MA}.${MI}.${PA}" } 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 } # --- Preflight --------------------------------------------------------------- if [[ ! -d ".git" ]]; then echo "[ERROR] Not a git repository (.git missing)." exit 1 fi if [[ ! -d "$COMPOSE_DIR" ]]; then echo "[ERROR] '$COMPOSE_DIR' directory missing. Expected ./compose// with a Dockerfile." exit 1 fi check_docker_ready ensure_registry_login validate_repo_component "$DOCKER_NAMESPACE" # Detect currently checked out branch (authoritative for this script) DETECTED_BRANCH="$(git branch --show-current 2>/dev/null || true)" if [[ -z "$DETECTED_BRANCH" ]]; then DETECTED_BRANCH="$(git symbolic-ref --quiet --short HEAD 2>/dev/null || true)" fi if [[ -z "$DETECTED_BRANCH" ]]; then # Try to derive from upstream UPSTREAM_REF_DERIVED="$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null || true)" if [[ -n "$UPSTREAM_REF_DERIVED" ]]; then DETECTED_BRANCH="${UPSTREAM_REF_DERIVED#origin/}" fi fi if [[ -z "$DETECTED_BRANCH" ]]; then DETECTED_BRANCH="main" fi # Optional signals: BRANCH env and .last-branch (informational only) ENV_BRANCH="${BRANCH:-}" LAST_BRANCH_FILE_PATH="$(pwd)/$LAST_BRANCH_FILE" LAST_BRANCH_VALUE="" if [[ -z "$ENV_BRANCH" && -f "$LAST_BRANCH_FILE_PATH" ]]; then LAST_BRANCH_VALUE="$(tr -d ' \t\n\r' < "$LAST_BRANCH_FILE_PATH")" fi UPSTREAM_REF="$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null || echo "origin/$DETECTED_BRANCH")" HEAD_SHA="$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")" echo "[INFO] Repo: $(pwd)" echo "[INFO] Current branch: $DETECTED_BRANCH" echo "[INFO] Upstream: $UPSTREAM_REF" echo "[INFO] HEAD (sha): $HEAD_SHA" if [[ -n "$ENV_BRANCH" && "$ENV_BRANCH" != "$DETECTED_BRANCH" ]]; then echo "[WARNING] BRANCH='$ENV_BRANCH' differs from checked out branch '$DETECTED_BRANCH'." echo "[WARNING] This script does not switch branches; continuing on '$DETECTED_BRANCH'." fi if [[ -n "$LAST_BRANCH_VALUE" && "$LAST_BRANCH_VALUE" != "$DETECTED_BRANCH" && -z "$ENV_BRANCH" ]]; then echo "[INFO] .last-branch suggests '$LAST_BRANCH_VALUE', but current checkout is '$DETECTED_BRANCH'." echo "[INFO] If you intended to build '$LAST_BRANCH_VALUE', switch branches first (use update-and-build.sh)." fi # --- Versioning -------------------------------------------------------------- CURRENT_VERSION="$(read_version)" NEW_VERSION="$CURRENT_VERSION" DO_TAG_AND_BUMP=true if [[ "$BUMP" == "t" ]]; then echo "[INFO] Test build: keeping version $CURRENT_VERSION; will only update :dev." DO_TAG_AND_BUMP=false else NEW_VERSION="$(bump_version "$CURRENT_VERSION" "$BUMP")" echo "[INFO] New version: $NEW_VERSION" fi if $DO_TAG_AND_BUMP; then validate_tag "$NEW_VERSION" validate_tag "latest" fi validate_tag "dev" # --- Version update + VCS ops (release builds only) -------------------------- if $DO_TAG_AND_BUMP; then echo "[INFO] Writing $NEW_VERSION to $VERSION_FILE" write_version "$NEW_VERSION" echo "[INFO] Git add + commit (branch: $DETECTED_BRANCH)" git add "$VERSION_FILE" git commit -m "Release $NEW_VERSION on branch $DETECTED_BRANCH (bump type $BUMP)" echo "[INFO] Git tag $NEW_VERSION" git tag -a "$NEW_VERSION" -m "Release $NEW_VERSION" echo "[INFO] Git push + tags" git push origin "$DETECTED_BRANCH" git push --tags else echo "[INFO] Skipping commit/tagging (test build)." fi # --- Build & push per service ------------------------------------------------ shopt -s nullglob services=( "$COMPOSE_DIR"/* ) if [[ ${#services[@]} -eq 0 ]]; then echo "[ERROR] No services found under $COMPOSE_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 $DO_TAG_AND_BUMP; then echo "============================================================" echo "[INFO] Building ${svc} -> tags: ${NEW_VERSION}, dev, latest" echo "============================================================" docker build \ -t "${IMAGE_BASE}:${NEW_VERSION}" \ -t "${IMAGE_BASE}:dev" \ -t "${IMAGE_BASE}:latest" \ "$svc_path" docker push "${IMAGE_BASE}:${NEW_VERSION}" docker push "${IMAGE_BASE}:dev" docker push "${IMAGE_BASE}:latest" BUILT_IMAGES+=("${IMAGE_BASE}:${NEW_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 # --- Persist current branch to .last-branch ---------------------------------- # (This helps script 1 to preselect next time, and is informative if you run script 2 standalone) echo "$DETECTED_BRANCH" > "$LAST_BRANCH_FILE_PATH" # --- Summary ----------------------------------------------------------------- echo "" echo "============================================================" echo "[SUMMARY] Build & push complete (branch: $DETECTED_BRANCH)" if $DO_TAG_AND_BUMP; then echo "[INFO] Release version: $NEW_VERSION" else echo "[INFO] Test build (no version bump)" fi echo "[INFO] Images pushed:" for img in "${BUILT_IMAGES[@]}"; do echo " - $img" done echo "============================================================"