#!/bin/bash
# Nightly Forgejo updater for the PACKAGED forgejo (forgejo LXD container on ollivanders; Ubuntu .deb or Gentoo binpkg).
# The binary is the `forgejo` package from the ctg apt repo (official release, sha256-pinned and
# OpenPGP-verified when it was packaged); this script never touches the binary itself. It only:
#  - applies patch/minor updates within the installed major (a major bump is logged and left for a human),
#  - waits for running CI tasks to drain (a restart mid-job loses the job's final report; Rustinion run, 2026-10-16),
#  - installs the new package, restarts once, health-checks, and rolls back to the previous package on failure.
set -u
STATE=/var/lib/ctg-forgejo-update
mkdir -p "$STATE"
log() { logger -t ctg-forgejo-update "$*"; echo "$*"; }
# ---------------------------------------------------------------------------------------------------------------------------
# Gentoo (forgejo-gentoo): www-apps/forgejo from canutethegreat-overlay, built ONLY on the binhost CI (never compiled here).
# Same rules as the apt path below: patch/minor inside the installed major, wait for CI to drain, one restart, health-check,
# roll back on failure. A new version is only applied when its BINPKG is already published (--usepkgonly): an overlay bump
# whose lane has not finished is deferred to the next run. Rollback = the quickpkg of the previous version taken before the update.
# CTG_FJ_FORCE_VERSION=<pv> reinstalls/targets a specific version (rehearsal); CTG_FJ_TEST_FAIL=1 fakes a failed health check.
# ---------------------------------------------------------------------------------------------------------------------------
if [[ -x /usr/bin/emerge && ! -x /usr/bin/apt-get ]]; then
    ATOM=www-apps/forgejo
    RB=$STATE/rollback; mkdir -p "$RB"
    pv() { sed -E 's/^[^ ]*\/[a-z0-9_-]+-([0-9][^ ]*)$/\1/'; }
    emaint sync -r canutethegreat-overlay >/dev/null 2>&1 || { log "overlay sync failed, skipping"; exit 0; }
    cur=$(portageq best_version / $ATOM 2>/dev/null | pv); [[ -n "$cur" ]] || { log "$ATOM not installed"; exit 0; }
    cand=${CTG_FJ_FORCE_VERSION:-$(portageq best_visible / $ATOM 2>/dev/null | pv)}
    [[ -n "$cand" ]] || { log "no candidate, skipping"; exit 0; }
    if [[ -z "${CTG_FJ_FORCE_VERSION:-}" ]] && [[ $(python3 -c 'import sys;from portage.versions import vercmp;print(vercmp(sys.argv[1],sys.argv[2]))' "$cand" "$cur") -le 0 ]]; then echo "No update available! ($cur)"; exit 0; fi
    cur_ver=${cur%%-r*}; new_ver=${cand%%-r*}
    if [[ "${new_ver%%.*}" != "${cur_ver%%.*}" ]]; then
        log "MAJOR upgrade $cur_ver -> $new_ver available; not auto-applying (needs manual upgrade + runner compatibility check)"
        echo "$new_ver" > "$STATE/pending-major"; exit 0
    fi
    # binpkg must exist already (no compile on this host)
    if ! emerge --pretend --oneshot --usepkgonly --getbinpkg --binpkg-respect-use=n --nodeps "=$ATOM-$cand" >/dev/null 2>&1; then
        log "binpkg for $ATOM-$cand not published on the binhost yet, deferring to the next run"; exit 0
    fi
    DRAIN_MAX_MIN=${DRAIN_MAX_MIN:-180}
    running_tasks() {
        local a=/etc/forgejo/app.ini h pw
        g() { awk -F' *= *' '/^\[database\]/{d=1;next} /^\[/{d=0} d&&$1=="'"$1"'"{print $2}' $a; }
        h=$(g HOST); pw=$(g PASSWD)
        PGPASSWORD="$pw" psql -h "${h%%:*}" -p "${h##*:}" -U "$(g USER)" -d "$(g NAME)" -tAc "select count(*) from action_task where status = 6" 2>/dev/null
    }
    waited=0
    while :; do
        n=$(running_tasks | tr -d '[:space:]')
        if [[ ! "$n" =~ ^[0-9]+$ ]]; then log "cannot count running CI tasks, deferring $cur -> $cand to the next run"; exit 0; fi
        (( n == 0 )) && break
        if (( waited >= DRAIN_MAX_MIN )); then log "$n CI task(s) still running after ${waited} min, deferring $cur -> $cand to the next run"; exit 0; fi
        (( waited == 0 )) && log "$n CI task(s) running, waiting up to ${DRAIN_MAX_MIN} min before updating"
        sleep 60; waited=$((waited + 1))
    done
    log "installing forgejo $cand (was $cur), then restarting"
    rm -f "$RB"/*.gpkg.tar "$RB"/*.tbz2 2>/dev/null
    PKGDIR=$RB quickpkg --include-config=y "=$ATOM-$cur" >/dev/null 2>&1 || { log "quickpkg of $cur failed, keeping $cur"; exit 0; }
    cp -a /etc/forgejo/app.ini "$STATE/app.ini.pre-$cand"
    if ! emerge --oneshot --usepkgonly --getbinpkg --binpkg-respect-use=n --nodeps "=$ATOM-$cand" >/dev/null 2>&1; then
        log "emerge of forgejo $cand failed, keeping $cur"; exit 0
    fi
    rc-service forgejo restart >/dev/null 2>&1
    ok=0
    for i in $(seq 1 30); do
        sleep 2
        if rc-service forgejo status >/dev/null 2>&1 && curl -fsS --max-time 5 http://127.0.0.1:3000/api/healthz >/dev/null 2>&1; then ok=1; break; fi
    done
    [[ -n "${CTG_FJ_TEST_FAIL:-}" ]] && ok=0
    if (( ok )); then log "forgejo $cand is up"; exit 0; fi
    log "forgejo $cand did not come up, rolling back to $cur"
    PKGDIR=$RB emerge --oneshot --usepkgonly --getbinpkg=n --binpkg-respect-use=n --nodeps "=$ATOM-$cur" >/dev/null 2>&1
    cp -a "$STATE/app.ini.pre-$cand" /etc/forgejo/app.ini
    rc-service forgejo restart >/dev/null 2>&1
    exit 1
fi

apt-get update -qq >/dev/null 2>&1 || { log "apt-get update failed, skipping"; exit 0; }
cur=$(dpkg-query -W -f='${Version}' forgejo 2>/dev/null) || { log "forgejo package not installed"; exit 0; }
cand=$(apt-cache policy forgejo | awk '/Candidate:/{print $2}')
[[ -n "$cand" && "$cand" != "(none)" ]] || { log "no candidate, skipping"; exit 0; }
cur_ver=${cur%%-*}; new_ver=${cand%%-*}
if dpkg --compare-versions "$cand" le "$cur"; then echo "No update available! ($cur)"; exit 0; fi
if [[ "${new_ver%%.*}" != "${cur_ver%%.*}" ]]; then
    log "MAJOR upgrade $cur_ver -> $new_ver available; not auto-applying (needs manual upgrade + runner compatibility check)"
    echo "$new_ver" > "$STATE/pending-major"
    exit 0
fi

# Don't restart under running CI jobs. A restart mid-job loses the job's
# final report: the runner logs "Job succeeded" but Forgejo marks the task
# failed ~15 min later ("Cannot transfer logs of task N") and skips every
# downstream job, including deploys (Rustinion ci, 2026-10-16 01:00). Wait
# for running tasks (action_task.status 6) to drain; if CI is still busy
# after DRAIN_MAX_MIN, leave the update for the next night.
DRAIN_MAX_MIN=${DRAIN_MAX_MIN:-180}
running_tasks() {
    local pw
    pw=$(awk -F' *= *' '/^\[database\]/{d=1;next} /^\[/{d=0} d&&$1=="PASSWD"{print $2}' /etc/forgejo/app.ini)
    local host name user
    host=$(awk -F' *= *' '/^\[database\]/{d=1;next} /^\[/{d=0} d&&$1=="HOST"{print $2}' /etc/forgejo/app.ini)
    name=$(awk -F' *= *' '/^\[database\]/{d=1;next} /^\[/{d=0} d&&$1=="NAME"{print $2}' /etc/forgejo/app.ini)
    user=$(awk -F' *= *' '/^\[database\]/{d=1;next} /^\[/{d=0} d&&$1=="USER"{print $2}' /etc/forgejo/app.ini)
    PGPASSWORD="$pw" psql -h "${host%%:*}" -p "${host##*:}" -U "$user" -d "$name" -tAc \
        "select count(*) from action_task where status = 6" 2>/dev/null
}
waited=0
while :; do
    n=$(running_tasks)
    if [[ ! "$n" =~ ^[0-9]+$ ]]; then
        log "cannot count running CI tasks, deferring $cur_ver -> $new_ver to the next run"; exit 0
    fi
    (( n == 0 )) && break
    if (( waited >= DRAIN_MAX_MIN )); then
        log "$n CI task(s) still running after ${waited} min, deferring $cur_ver -> $new_ver to the next run"; exit 0
    fi
    (( waited == 0 )) && log "$n CI task(s) running, waiting up to ${DRAIN_MAX_MIN} min before updating"
    sleep 60; waited=$((waited + 1))
done


log "installing forgejo $cand (was $cur), then restarting"
if ! DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --only-upgrade forgejo >/dev/null 2>&1; then
    log "apt install of forgejo $cand failed, keeping $cur"; exit 0
fi
systemctl restart forgejo
ok=0
for i in $(seq 1 30); do
    sleep 2
    if systemctl is-active --quiet forgejo && curl -fsS --max-time 5 http://127.0.0.1:3000/api/healthz >/dev/null 2>&1; then ok=1; break; fi
done
if (( ok )); then log "forgejo $cand is up"; exit 0; fi
log "forgejo $cand did not come up, rolling back to $cur"
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --allow-downgrades "forgejo=$cur" >/dev/null 2>&1
systemctl restart forgejo
exit 1
