#!/bin/bash
# Prunes stale CI scratch. Read-only dirs are made writable first, otherwise
# rm cannot unlink their children even though we own them.
set -uo pipefail
prune() {
  local root="$1"; shift
  [ -d "$root" ] || return 0
  /usr/bin/find "$root" -maxdepth 1 -mindepth 1 "$@" -print0 2>/dev/null \
    | /usr/bin/xargs -0 -r -P4 -n100 /bin/chmod -R u+rwX -- 2>/dev/null
  /usr/bin/find "$root" -maxdepth 1 -mindepth 1 "$@" -print0 2>/dev/null \
    | /usr/bin/xargs -0 -r -P4 -n100 /bin/rm -rf --
}
prune "${CI_TMP_ROOT:-/tmp}" -user "$(/usr/bin/id -un)" -mmin +1440
# ~/builds has its own bounded cache policy. It must not be age-pruned here:
# an otherwise healthy cache below its cap is valuable incremental-build state.

# Entry dir mtime alone misses activity deep in the tree, so an entry is stale
# only when NOTHING under it is newer than the window.
prune_idle_entries() {
  local root="$1" days="$2" entry
  [ -d "$root" ] || return 0
  while IFS= read -r -d '' entry; do
    [ -n "$(/usr/bin/find "$entry" -newermt "-${days} days" -print -quit 2>/dev/null)" ] && continue
    /bin/chmod -R u+rwX -- "$entry" 2>/dev/null
    /bin/rm -rf -- "$entry"
  done < <(/usr/bin/find -H "$root" -maxdepth 1 -mindepth 1 -type d -not -name '.*' -print0 2>/dev/null)
}
prune_idle_entries "${SANDBOX_WORKSPACES_ROOT:-$HOME/sandbox/workspaces}" "${SANDBOX_WORKSPACE_MAX_IDLE_DAYS:-7}"
prune_idle_entries "${BUILDBOX_RUNS_ROOT:-/var/lib/buildbox/runs}" "${BUILDBOX_RUNS_MAX_IDLE_DAYS:-3}"
# Agent offload checkouts. Follows the ~/cdx-offload symlink so the entries are
# pruned wherever the tree was relocated to.
prune_idle_entries "${CDX_OFFLOAD_ROOT:-$HOME/cdx-offload}" "${CDX_OFFLOAD_MAX_IDLE_DAYS:-7}"

# Fleet ssh-transport staging orphans: the writer cleans up on every error path,
# but a SIGKILL between upload and promote leaves `.<name>.staging-<pid>-<hex>`.
/usr/bin/find "$HOME/.local/bin" "$HOME/.config/systemd/user" -maxdepth 1 \
  -name '.*.staging-*' -mmin +1440 -delete 2>/dev/null

# --- disk floor alarm -------------------------------------------------------
# Runs after the prune, so it reports only what the prune could not reclaim.
# /tmp is a 16G tmpfs capped at 1,048,576 inodes and it is inodes, not bytes,
# that hit 100% on debian1 on 2026-07-26 — all three floors are load-bearing.
COOLDOWN_SECS=$((6 * 60 * 60))
STAMP="$HOME/.cache/ci-disk-alarm.stamp"
ROOT_FLOOR_G=15
TMP_FLOOR_G=3
TMP_FLOOR_INODES=150000
SCRATCH_MNT=/var/lib/buildbox
SCRATCH_FLOOR_G=15

disk_floor_alarm() {
  local breaches="" root_b tmp_b tmp_i scratch_b now last

  root_b=$(/bin/df -B1 --output=avail / 2>/dev/null | /usr/bin/tail -1 | /usr/bin/tr -dc '0-9')
  tmp_b=$(/bin/df -B1 --output=avail /tmp 2>/dev/null | /usr/bin/tail -1 | /usr/bin/tr -dc '0-9')
  tmp_i=$(/bin/df --output=iavail /tmp 2>/dev/null | /usr/bin/tail -1 | /usr/bin/tr -dc '0-9')
  # only when the scratch disk is actually mounted there — otherwise the path is on /
  # and this would double-report the root breach under a second name
  if /usr/bin/findmnt -no TARGET --mountpoint "$SCRATCH_MNT" >/dev/null 2>&1; then
    scratch_b=$(/bin/df -B1 --output=avail "$SCRATCH_MNT" 2>/dev/null | /usr/bin/tail -1 | /usr/bin/tr -dc '0-9')
  fi

  [ -n "$root_b" ] && [ "$root_b" -lt $((ROOT_FLOOR_G * 1024 * 1024 * 1024)) ] \
    && breaches="${breaches}/ has $((root_b / 1024 / 1024 / 1024))G free, floor ${ROOT_FLOOR_G}G. "
  [ -n "${scratch_b:-}" ] && [ "$scratch_b" -lt $((SCRATCH_FLOOR_G * 1024 * 1024 * 1024)) ] \
    && breaches="${breaches}${SCRATCH_MNT} has $((scratch_b / 1024 / 1024 / 1024))G free, floor ${SCRATCH_FLOOR_G}G. "
  [ -n "$tmp_b" ] && [ "$tmp_b" -lt $((TMP_FLOOR_G * 1024 * 1024 * 1024)) ] \
    && breaches="${breaches}/tmp has $((tmp_b / 1024 / 1024 / 1024))G free, floor ${TMP_FLOOR_G}G. "
  [ -n "$tmp_i" ] && [ "$tmp_i" -lt "$TMP_FLOOR_INODES" ] \
    && breaches="${breaches}/tmp has ${tmp_i} inodes free, floor ${TMP_FLOOR_INODES} — this is what ENOSPCs CI. "

  [ -n "$breaches" ] || return 0

  now=$(/bin/date +%s)
  last=$(/bin/cat "$STAMP" 2>/dev/null | /usr/bin/tr -dc '0-9')
  [ -n "$last" ] && [ $((now - last)) -lt "$COOLDOWN_SECS" ] && return 0

  # syslog, not ntfy: the systemd journal already retains and rotates it, and it needs no
  # server, no topic and no phone.
  /usr/bin/logger -t ci-disk-alarm -p daemon.err \
    "below a CI disk floor after the hourly prune: ${breaches}"
  /bin/mkdir -p "$(/usr/bin/dirname "$STAMP")"
  echo "$now" > "$STAMP"
}

disk_floor_alarm || true
