#!/usr/bin/env bash # Shared functions for retention-cleanup.sh # Can be sourced by tests for unit testing age_days() { local published="$1" epoch_pub now epoch_pub=$(date -d "$published" +%s 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%SZ" "$published" +%s 2>/dev/null || echo 0) [ "$epoch_pub" -eq 0 ] && echo 99999 && return now=$(date -u +%s) echo $(( (now - epoch_pub) / 86400 )) } parse_path() { local rel="$1" OWNER="${rel%%/*}" rest="${rel#*/}" REPO="${rest%%/*}" } read_rule() { local config="$1" branch="$2" key="$3" default="$4" v=$(jq -r --arg b "$branch" --arg k "$key" '.branches[$b][$k] // empty' "$config") [ -n "$v" ] && echo "$v" || echo "$default" } # --------------------------------------------------------------------------- # Gitea branch/repo checking via git ls-remote # Uses global: GITEA_API_URL, GITEA_TOKEN # Sets global: REPO_BRANCHES_CACHE, REPO_STATUS # --------------------------------------------------------------------------- # Fetch all branches for a repo (one git ls-remote call per repo). # Sets REPO_STATUS[owner/repo]. # Echos branch list on success. # Returns: 0=ok, 1=deleted, 2=cert_error, 3=error (fail-safe keep) repo_branches() { local owner="$1" repo="$2" key="${owner}/${repo}" local attempt output [ -z "$GITEA_API_URL" ] && return 0 [ -z "$GITEA_TOKEN" ] && return 0 # Check cached status first (avoids re-running git on every report) case "${REPO_STATUS[$key]:-}" in deleted) return 1 ;; cert_error) return 2 ;; error) echo "${REPO_BRANCHES_CACHE[$key]:-}"; return 3 ;; esac # Cache hit (success with branch list) [ -n "${REPO_BRANCHES_CACHE[$key]:-}" ] && { echo "${REPO_BRANCHES_CACHE[$key]}"; return 0; } local git_host git_host=$(echo "$GITEA_API_URL" | sed -E 's|^https?://||' | sed 's|/.*$||') local git_url="https://token:${GITEA_TOKEN}@${git_host}/${owner}/${repo}.git" for attempt in 1 2 3; do output=$(git ls-remote --heads "$git_url" 2>&1) && { local branches branches=$(echo "$output" | sed -n 's|.*refs/heads/||p') REPO_BRANCHES_CACHE[$key]="$branches" REPO_STATUS[$key]="ok" echo "$branches" return 0 } # Repo deleted → no retry if echo "$output" | grep -qiE "fatal:.*(not found|repository.*not|could not read)"; then REPO_BRANCHES_CACHE[$key]="__REPO_DELETED__" REPO_STATUS[$key]="deleted" echo " REPO DELETED: ${owner}/${repo}" >&2 return 1 fi [ "$attempt" -lt 3 ] && sleep 10 done # Certificate verification failure → configuration error, stop if echo "$output" | grep -qi "server certificate verification failed"; then REPO_STATUS[$key]="cert_error" echo "[ERROR] git-pages.retention: certificate verification failed for ${owner}/${repo}" >&2 echo "[ERROR] git-pages.retention: check CA certificates or set GIT_SSL_NO_VERIFY=1" >&2 echo "[ERROR] git-pages.retention: git output:" >&2 echo "$output" >&2 return 2 fi # Other network errors → fail-safe keep, continue REPO_BRANCHES_CACHE[$key]="__REPO_ERROR__" REPO_STATUS[$key]="error" echo "[WARN] git-pages.retention: cannot reach Gitea for ${owner}/${repo} — keeping all reports" >&2 echo "[WARN] git-pages.retention: git output:" >&2 echo "$output" >&2 return 3 } # Check if a specific branch exists in a repo. # Returns 0 (exists), 1 (not found/deleted). # Returns 2 (cert error), 3 (network error). branch_exists() { local owner="$1" repo="$2" branch="$3" local branches rc [ -z "$GITEA_API_URL" ] && return 0 [ -z "$GITEA_TOKEN" ] && return 0 branches=$(repo_branches "$owner" "$repo") rc=$? # Return codes from repo_branches propagate through $() subshell: # 0=ok, 1=deleted, 2=cert_error, 3=error case $rc in 2) echo "[FATAL] git-pages.retention: cannot reach Gitea (${owner}/${repo}) — check configuration" >&2 exit 1 ;; 3) return 0 ;; # network error → fail-safe keep 1) return 1 ;; # repo/branch gone esac echo "$branches" | grep -qxF "$branch" } # Phase 3: apply retention rules to KEEP array, populate TO_DELETE # Reads from global KEEP array # Populates global TO_DELETE array # Usage: apply_retention apply_retention() { local config="$1" local default_max_age default_keep_min local max_age keep_min key count seen_key commit_dir local entry dir owner repo branch days default_max_age=$(jq -r '.branches.default.maxAgeDays // 90' "$config") default_keep_min=$(jq -r '.branches.default.keepMin // 5' "$config") declare -A BRANCH_COUNTS declare -A SEEN_COMMITS declare -A DELETED_COMMITS if [ "${#KEEP[@]}" -eq 0 ]; then return fi IFS=$'\n' for entry in $(printf '%s\n' "${KEEP[@]}" | sort -t'|' -k4,4 -k5,5n); do IFS='|' read -r dir owner repo branch days <<< "$entry" max_age=$(read_rule "$config" "$branch" "maxAgeDays" "$default_max_age") keep_min=$(read_rule "$config" "$branch" "keepMin" "$default_keep_min") # Age check — per-report-type deletion if [ "$days" -gt "$max_age" ]; then echo " DELETE: ${dir} (age ${days}d > maxAge ${max_age}d, branch ${branch})" TO_DELETE+=("$dir") REASON_MAP["$dir"]="maxAgeDays exceed" MAXAGE_DELETED=$((MAXAGE_DELETED + 1)) continue fi # keepMin — per-commit counting commit_dir=$(dirname "$dir") key="$branch" seen_key="${key}|${commit_dir}" if [ -z "${SEEN_COMMITS[$seen_key]:-}" ]; then SEEN_COMMITS["$seen_key"]=1 count="${BRANCH_COUNTS[$key]:-0}" count=$((count + 1)) BRANCH_COUNTS["$key"]=$count else count="${BRANCH_COUNTS[$key]:-0}" fi if [ "$count" -gt "$keep_min" ]; then if [ -z "${DELETED_COMMITS[$commit_dir]:-}" ]; then DELETED_COMMITS[$commit_dir]=1 echo " DELETE: ${commit_dir} (kept ${keep_min}/${count} commits, exceeds keepMin, branch ${branch})" TO_DELETE+=("$commit_dir") REASON_MAP["$commit_dir"]="keepMin exceed" KEEPMIN_DELETED=$((KEEPMIN_DELETED + 1)) fi fi done unset IFS }