#!/usr/bin/env bash # ============================================================= # update-check.sh # Reads Watchtower's recent logs to find outdated images, # maps them to containers, and fetches GitHub release notes. # # Usage: # bash /opt/stacks/scripts/update-check.sh # ============================================================= set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/image-sources.conf" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' GRAY='\033[0;90m' BOLD='\033[1m' NC='\033[0m' SEP="${GRAY}$(printf '─%.0s' $(seq 1 60))${NC}" # ── Check Watchtower is running ─────────────────────────────── if ! docker inspect watchtower &>/dev/null; then echo -e "${RED}✗ Watchtower is not running.${NC}" echo -e " Start it: cd /opt/stacks/watchtower && docker compose up -d" exit 1 fi # ── Parse Watchtower logs for outdated images (last 36h) ───── echo -e "${BOLD}Checking Watchtower logs for updates...${NC}" echo "" OUTDATED_IMAGES=() while IFS= read -r line; do # Extract image name from: "Found new some/image:tag image (sha256:...)" img=$(echo "$line" | grep -oP '(?<=Found new )\S+(?= image)') [ -n "$img" ] && OUTDATED_IMAGES+=("$img") done < <(docker logs watchtower --since 36h 2>&1 | grep "Found new") if [ ${#OUTDATED_IMAGES[@]} -eq 0 ]; then echo -e "${GREEN}✓ All images are up to date${NC} ${GRAY}(per last Watchtower scan)${NC}" echo "" LAST=$(docker logs watchtower --since 72h 2>&1 | grep "Session done" | tail -1 | grep -oP '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}') [ -n "$LAST" ] && echo -e "${GRAY} Last scan: ${LAST}${NC}" exit 0 fi echo -e "${YELLOW}${BOLD}${#OUTDATED_IMAGES[@]} image update(s) available:${NC}" echo "" # ── For each outdated image: find container + fetch notes ───── gh_release_notes() { local repo=$1 local result result=$(curl -sf "https://api.github.com/repos/${repo}/releases/latest" 2>/dev/null) || true if [ -n "$result" ]; then TAG=$(echo "$result" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('tag_name','?'))" 2>/dev/null) BODY=$(echo "$result" | python3 -c " import sys,json,re d=json.load(sys.stdin) body=d.get('body','No release notes available.') # Strip markdown links/badges, collapse whitespace body=re.sub(r'!\[.*?\]\(.*?\)','',body) body=re.sub(r'\[([^\]]+)\]\([^\)]+\)',r'\1',body) body=re.sub(r'','',body,flags=re.DOTALL) lines=[l.strip() for l in body.split('\n') if l.strip()] print('\n'.join(lines[:6])) " 2>/dev/null || echo "No release notes available.") echo "$TAG|$BODY" fi } for image in "${OUTDATED_IMAGES[@]}"; do # Find container(s) using this image CONTAINERS=$(docker ps -a --format '{{.Names}}|{{.Image}}' | awk -F'|' -v img="$image" '$2==img {print $1}' | tr '\n' ' ') [ -z "$CONTAINERS" ] && CONTAINERS="${GRAY}(no running containers)${NC}" echo -e "$SEP" echo -e " ${CYAN}${BOLD}${image}${NC}" echo -e " ${GRAY}containers:${NC} ${CONTAINERS}" # Strip tag for image name lookup base_image=$(echo "$image" | sed 's/:.*//') if [ -n "${IMAGE_REPOS[$base_image]+_}" ]; then REPO="${IMAGE_REPOS[$base_image]}" RELEASE=$(gh_release_notes "$REPO") if [ -n "$RELEASE" ]; then TAG=$(echo "$RELEASE" | cut -d'|' -f1) NOTES=$(echo "$RELEASE" | cut -d'|' -f2-) echo -e " ${GRAY}latest tag:${NC} ${GREEN}${TAG}${NC}" echo -e " ${GRAY}notes:${NC}" while IFS= read -r line; do echo -e " ${GRAY}${line}${NC}" done <<< "$NOTES" echo -e " ${GRAY}full notes:${NC} https://github.com/${REPO}/releases/latest" else echo -e " ${GRAY}→ https://hub.docker.com/r/${base_image}/tags${NC}" fi else echo -e " ${GRAY}no GitHub mapping — add to scripts/image-sources.conf${NC}" echo -e " ${GRAY}→ https://hub.docker.com/r/${base_image}/tags${NC}" fi echo "" done echo -e "$SEP" echo "" echo -e " Ready to update? Run: ${CYAN}bash /opt/stacks/scripts/do-update.sh${NC}" echo ""