#!/bin/bash # Unattended update to the latest RELEASED state of this template. # # Design, in one paragraph: releases are the update channel. A tag is cut # only after CI has built the pinned images, booted the full stack, and # passed the smoke tests — so "update to the # newest tag" means "update to a combination a machine has already run", # which is the guarantee a floating `latest` can never give. The script # refuses to cross a MAJOR template version on its own: majors are # breaking by definition (an application platform jump, a changed variable # contract) and their release notes exist to be read by a person. # # Run it from cron or a systemd timer for unattended minor/patch updates: # 17 5 * * * /opt/sftp-traefik-letsencrypt-docker-compose/update.sh >> /var/log/sftp-update.log 2>&1 # # Flags: # --dry-run show what would happen, change nothing # --allow-major permit crossing a major version (you read the notes) set -euo pipefail cd "$(dirname "$0")" COMPOSE_FILE="sftp-traefik-letsencrypt-docker-compose.yml" PROJECT="${COMPOSE_PROJECT_NAME:-sftp}" DRY_RUN=false ALLOW_MAJOR=false for arg in "$@"; do case "$arg" in --dry-run) DRY_RUN=true ;; --allow-major) ALLOW_MAJOR=true ;; *) echo "unknown flag: $arg" >&2; exit 2 ;; esac done if ! git diff --quiet || ! git diff --cached --quiet; then echo "local changes present — commit or stash them first, nothing updated" >&2 exit 1 fi # docker compose reads .env last, after the checkout. A .env this user # cannot read (a copy restored without -p comes back root:root 0600) would # stop it there, with the tree already on the new tag, and would make every # required variable below read as "not set". Name the file and stop first. if [ -e .env ] && [ ! -r .env ]; then echo ".env is not readable by $(id -un) ($(stat -c '%A %U:%G' .env 2>/dev/null || stat -f '%Sp %Su:%Sg' .env)) — fix its ownership, nothing updated" >&2 exit 1 fi git fetch --tags --quiet origin latest="$(git tag -l 'v*' --sort=-v:refname | head -1)" if [ -z "$latest" ]; then echo "no release tags found — nothing to update to" >&2 exit 1 fi current="$(git describe --tags --abbrev=0 2>/dev/null || echo 'v0.0.0')" if [ "$(git rev-parse HEAD)" = "$(git rev-parse "$latest^{commit}")" ]; then echo "already on $latest" exit 0 fi cur_major="${current#v}"; cur_major="${cur_major%%.*}" new_major="${latest#v}"; new_major="${new_major%%.*}" if [ "$new_major" != "$cur_major" ] && [ "$ALLOW_MAJOR" != "true" ]; then echo "refusing to cross a major version unattended: $current -> $latest" >&2 echo "read the release notes, then re-run with --allow-major:" >&2 echo " https://github.com/heyvaldemar/sftp-traefik-letsencrypt-docker-compose/releases/tag/$latest" >&2 exit 3 fi # NEW VARIABLES SINCE YOUR VERSION. An update can add a required variable, # and `docker compose up` would then stop with a message naming it - after # the checkout, with the tree already on the new tag. Better to say so here, # before anything has moved. Names only, never values. if [ -n "${COMPOSE_FILES+x}" ]; then _files=("${COMPOSE_FILES[@]}"); else _files=("$COMPOSE_FILE"); fi _new="$(comm -13 <(git show "HEAD:.env.example" 2>/dev/null | grep -oE '^[A-Z0-9_]+=' | tr -d '=' | sort -u) \ <(git show "$latest:.env.example" 2>/dev/null | grep -oE '^[A-Z0-9_]+=' | tr -d '=' | sort -u))" if [ -n "$_new" ]; then echo "new variables in .env.example since $current:" while IFS= read -r _k; do echo " $_k"; done <<<"$_new" _required="" for _f in "${_files[@]}"; do _required="$_required $(git show "$latest:$_f" 2>/dev/null | grep -oE '\$\{[A-Z0-9_]+:\?' | sed -E 's/^\$\{//; s/:\?$//' | tr '\n' ' ' || true)" done _missing="" for _k in $_new; do case " $_required " in *" $_k "*) grep -qE "^${_k}=." .env 2>/dev/null || _missing="$_missing $_k" ;; esac done if [ -n "$_missing" ]; then echo "required in $latest and not set in .env:$_missing" >&2 echo "see .env.example at $latest for what each one is; nothing was changed" >&2 [ "$DRY_RUN" = "true" ] || exit 4 fi fi echo "updating $current -> $latest" if [ "$DRY_RUN" = "true" ]; then git log --oneline "HEAD..$latest^{commit}" | sed 's/^/ would apply: /' echo "dry run — nothing changed" exit 0 fi git checkout -q "$latest" docker compose -f "$COMPOSE_FILE" -p "$PROJECT" up -d --remove-orphans echo "now on $latest"