name: runner-disk-clean

# 让 runner 自己清自己的磁盘。无需 SSH key —— act_runner 以 ubuntu 用户跑（sudo 免密）。
#
# 触发：
#   - workflow_dispatch: 手动急救（disk 满了立刻跑）
#   - schedule: 每周一 02:13 UTC（10:13 +08）周期清理，防 cache 累积撑爆
#   - push: 仅本文件变更时跑（方便 PR 阶段试跑）
#
# 行为（runner 是 host 执行模式 / 没装 docker —— 见 docs/ops/02-gitea-config.md）：
#   1. 打印 before disk + 用量大头（含 ~/.cache —— Prisma engine 缓存的家）
#   2. 清 /var/lib/act_runner/workdir 下 30min 没动过的目录（在跑 job mtime < 30min 自动跳过）
#   3. 清 /var/lib/act_runner/cache 下 mtime > 1d 的 action tarball（实测占 7.6G，atime 在 noatime 下失效）
#   4. 清 ~/.npm/_cacache 中 mtime > 3d entry（npm 写一次就不动，mtime 比 atime 靠谱 —— noatime 挂载不会刷 atime）
#   5. 清 ~/.cache/prisma/master 下旧 engine binary（每个 schema hash ~200MB，几十次 CI 累积撑爆）
#      —— 失败签名: ENOSPC copyfile '/home/ubuntu/.cache/prisma/master/<sha>/...libquery-engine'
#   6. 如果系统有 docker（host mode 现在没；预留兼容），跑 docker system prune
#   7. 打印 after，freed bytes 写 step summary
#
# 安全：
#   - 不 systemctl stop act_runner（自己 stop 自己会让本 job 失败）
#   - 所有删除按 mtime/atime 过滤，正在跑的 sibling job 不受影响
#   - 单步失败 || true 不阻断后续
#
# 关联：
#   - 上次手工清盘: PR #416 backend-integration ENOSPC（2026-05-18）
#   - 一次性扩容方法论: .learnings/2026-05-17-runner-disk-grow-online.md

on:
  workflow_dispatch:
    inputs:
      dry-run:
        description: 'dry-run（只打印不真删）'
        required: false
        default: 'false'
        type: choice
        options: ['false', 'true']
  schedule:
    - cron: '13 2 * * 1'  # Mon 02:13 UTC = Mon 10:13 Asia/Shanghai
  push:
    paths:
      - '.gitea/workflows/runner-disk-clean.yml'

jobs:
  clean:
    # 多 runner 部署：本仓库有 dedicated-runner-1（label: ubuntu-latest）和 uat-runner（label: uat-with-docker, ubuntu-latest）
    # 只跑 ubuntu-latest 会被调度到任一台，漏清另一台。fan-out 一遍每台都清。
    # backend-integration 因为依赖 docker(PG/Redis) 只能跑 uat-with-docker，那台才是真正会撑爆的。
    strategy:
      fail-fast: false
      matrix:
        runner: [ubuntu-latest, uat-with-docker]
    runs-on: ${{ matrix.runner }}
    timeout-minutes: 10
    steps:
      - name: Before — disk + top consumers
        run: |
          set -eu
          echo "## Before" >> "$GITHUB_STEP_SUMMARY"
          df -h / /var/lib/act_runner 2>/dev/null | tee -a "$GITHUB_STEP_SUMMARY"
          echo "" >> "$GITHUB_STEP_SUMMARY"
          echo "### Top consumers under /var/lib/act_runner" >> "$GITHUB_STEP_SUMMARY"
          sudo du -sh /var/lib/act_runner/* 2>/dev/null | sort -h | tail -10 | tee -a "$GITHUB_STEP_SUMMARY" || true
          echo "" >> "$GITHUB_STEP_SUMMARY"
          echo "### Top consumers under ~/.cache (Prisma engine cache lives here)" >> "$GITHUB_STEP_SUMMARY"
          du -sh "$HOME/.cache"/* 2>/dev/null | sort -h | tail -10 | tee -a "$GITHUB_STEP_SUMMARY" || true
          echo "" >> "$GITHUB_STEP_SUMMARY"
          if [ -d "$HOME/.cache/prisma/master" ]; then
            echo "### Prisma engine hashes" >> "$GITHUB_STEP_SUMMARY"
            du -sh "$HOME/.cache/prisma/master"/* 2>/dev/null | sort -h | tee -a "$GITHUB_STEP_SUMMARY" || true
          fi
          BEFORE_AVAIL=$(df -B1 /var/lib/act_runner | awk 'NR==2 {print $4}')
          echo "BEFORE_AVAIL=$BEFORE_AVAIL" >> "$GITHUB_ENV"

      - name: Clean stale act_runner workdirs (> 30min idle)
        if: ${{ inputs.dry-run != 'true' }}
        run: |
          set -eu
          # 当前 job 目录 mtime < 30min，自动被 find -mmin +30 跳过
          sudo find /var/lib/act_runner/workdir -mindepth 1 -maxdepth 1 -type d -mmin +30 -print -exec rm -rf {} + 2>/dev/null || true

      - name: Clean stale act_runner cache (mtime > 1d)
        if: ${{ inputs.dry-run != 'true' }}
        run: |
          set -eu
          # action tarball / tool cache —— 长期不用就清，常用的会自动重下
          # 实测这里 7.6GB 是 disk 最大占用；-atime 在 noatime 挂载下失效，必须用 mtime
          # 阈值收到 1 天：cache 是按需重下的，频繁 CI 不影响
          sudo find /var/lib/act_runner/cache -type f -mtime +1 -delete 2>/dev/null || true
          sudo find /var/lib/act_runner/cache -type d -empty -delete 2>/dev/null || true

      - name: Clean npm cache (mtime > 3d)
        if: ${{ inputs.dry-run != 'true' }}
        run: |
          set -eu
          # 用 mtime 不用 atime：noatime 挂载下 atime 不刷新，find -atime 会保留所有文件
          # npm cache 是 content-addressable，删了 npm install 会按需重下
          find "$HOME/.npm/_cacache" -type f -mtime +3 -delete 2>/dev/null || true
          find "$HOME/.npm/_cacache" -type d -empty -delete 2>/dev/null || true

      - name: Clean stale Prisma engine cache (keep newest 2 hashes)
        if: ${{ inputs.dry-run != 'true' }}
        run: |
          set -eu
          # 每个 Prisma schema 版本 = 一个 hash 目录 ~200MB，几十次升级累积 GB 级
          # 保留最新 2 个：当前 schema + 上一次（万一 rollback 不用重下）
          CACHE_DIR="$HOME/.cache/prisma/master"
          [ -d "$CACHE_DIR" ] || exit 0
          # 按 mtime 倒序，跳过最新 2 个，剩下的删
          # shellcheck disable=SC2012
          ls -1dt "$CACHE_DIR"/*/ 2>/dev/null | tail -n +3 | xargs -r rm -rf
          echo "Prisma cache after clean:"
          du -sh "$CACHE_DIR"/* 2>/dev/null || echo "  (empty)"

      - name: Prune docker (兼容预留 —— host mode 现在没装 docker)
        if: ${{ inputs.dry-run != 'true' }}
        run: |
          set -eu
          if command -v docker >/dev/null 2>&1; then
            docker system prune -af --volumes --filter "until=24h" || true
          else
            echo "skip: docker not installed (host execution mode)"
          fi

      - name: Dry-run preview (what WOULD be deleted)
        if: ${{ inputs.dry-run == 'true' }}
        run: |
          set -eu
          echo "### stale workdirs (> 30min)" >> "$GITHUB_STEP_SUMMARY"
          sudo find /var/lib/act_runner/workdir -mindepth 1 -maxdepth 1 -type d -mmin +30 2>/dev/null | tee -a "$GITHUB_STEP_SUMMARY" || true
          echo "" >> "$GITHUB_STEP_SUMMARY"
          echo "### stale cache files (mtime > 1d)" >> "$GITHUB_STEP_SUMMARY"
          sudo find /var/lib/act_runner/cache -type f -mtime +1 2>/dev/null | wc -l | sed 's/^/files: /' | tee -a "$GITHUB_STEP_SUMMARY" || true
          echo "### stale npm cache entries (mtime > 3d)" >> "$GITHUB_STEP_SUMMARY"
          find "$HOME/.npm/_cacache" -type f -mtime +3 2>/dev/null | wc -l | sed 's/^/files: /' | tee -a "$GITHUB_STEP_SUMMARY" || true
          echo "" >> "$GITHUB_STEP_SUMMARY"
          echo "### Prisma engine hashes to delete (keep newest 2)" >> "$GITHUB_STEP_SUMMARY"
          if [ -d "$HOME/.cache/prisma/master" ]; then
            ls -1dt "$HOME/.cache/prisma/master"/*/ 2>/dev/null | tail -n +3 | tee -a "$GITHUB_STEP_SUMMARY" || true
          fi

      - name: After — disk + freed bytes
        run: |
          set -eu
          echo "" >> "$GITHUB_STEP_SUMMARY"
          echo "## After" >> "$GITHUB_STEP_SUMMARY"
          df -h / /var/lib/act_runner 2>/dev/null | tee -a "$GITHUB_STEP_SUMMARY"
          AFTER_AVAIL=$(df -B1 /var/lib/act_runner | awk 'NR==2 {print $4}')
          FREED=$((AFTER_AVAIL - BEFORE_AVAIL))
          FREED_HUMAN=$(numfmt --to=iec --suffix=B "$FREED" 2>/dev/null || echo "${FREED} bytes")
          echo "" >> "$GITHUB_STEP_SUMMARY"
          echo "**Freed: $FREED_HUMAN**" >> "$GITHUB_STEP_SUMMARY"

      - name: Open issue if disk > 85% after clean
        if: ${{ inputs.dry-run != 'true' }}
        env:
          GITEA_API_TOKEN: ${{ secrets.GITEA_API_TOKEN }}
        run: |
          set -eu
          USE=$(df /var/lib/act_runner | awk 'NR==2 {print $5}' | tr -d %)
          if [ "$USE" -gt 85 ]; then
            echo "⚠️ disk still ${USE}% after clean — opening issue"
            if [ -n "${GITEA_API_TOKEN:-}" ]; then
              curl -sf -X POST \
                -H "Authorization: token $GITEA_API_TOKEN" \
                -H "Content-Type: application/json" \
                "http://43.130.59.228/api/v1/repos/FFAIWorkspace/workspace/issues" \
                -d "$(printf '{"title":"runner disk %d%% — 清盘后仍紧张，建议再扩容","body":"runner-disk-clean workflow 跑完 /var/lib/act_runner 仍 %d%%。\\n\\n参考 .learnings/2026-05-17-runner-disk-grow-online.md 在线扩容。","labels":["infra","runner"]}' "$USE" "$USE")" || true
            fi
            exit 1
          fi
