name: Auto-promote staging → main

# Fast-forwards `main` to `staging` when staging is strictly ahead (main
# is an ancestor). Eliminates the manual sync-PR round for non-critical
# repos.
#
# Gate handling:
# - If the repo has required_status_checks configured AND the API
#   returns them, all must be SUCCESS on the staging HEAD commit.
# - If no gates are configured (or the API 403s on a private free-tier
#   repo), `--ff-only` is the sole safety. It refuses if main has
#   independent commits staging doesn't contain.
#
# Excluded by policy: molecule-core + molecule-controlplane. Those two
# stay manual per CEO directive 2026-04-24.
#
# Safety:
# - Only fires on push to staging (PRs into staging don't promote)
# - `--ff-only` refuses if main has diverged (hotfix landed directly)
# - Promote commit goes through GITHUB_TOKEN; shows up in git log as
#   a deliberate act

on:
  push:
    branches: [staging]
  workflow_dispatch:

permissions:
  contents: write
  statuses: read

jobs:
  promote:
    # self-hosted Mac mini — org's GitHub-hosted minute budget is exhausted
    # for private repos; same runner used by ci.yml
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
          token: ${{ secrets.GITHUB_TOKEN }}

      - name: Check required gates (if configured) on staging HEAD
        id: gates
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          REPO: ${{ github.repository }}
          HEAD_SHA: ${{ github.sha }}
        shell: bash
        run: |
          set -euo pipefail

          # Try to read required gates from branch protection. Free-tier
          # private repos may 403; handle that gracefully.
          GATES_JSON=$(gh api "repos/${REPO}/branches/staging/protection/required_status_checks" 2>/dev/null || echo '{}')
          GATES=$(echo "${GATES_JSON}" | jq -r '.contexts[]?' 2>/dev/null || true)

          if [ -z "$GATES" ]; then
            echo "No required gates configured (or API inaccessible). Relying on --ff-only safety."
            echo "ok=true" >> "$GITHUB_OUTPUT"
            exit 0
          fi

          echo "Required gates on staging:"
          echo "${GATES}" | sed 's/^/  - /'

          ALL_GREEN=true
          while IFS= read -r gate; do
            [ -z "$gate" ] && continue

            conclusion=$(gh api "repos/${REPO}/commits/${HEAD_SHA}/check-runs" \
                          --jq "[.check_runs[] | select(.name == \"${gate}\")] | sort_by(.completed_at) | last.conclusion" \
                          2>/dev/null || echo "")

            if [ -z "$conclusion" ] || [ "$conclusion" = "null" ]; then
              conclusion=$(gh api "repos/${REPO}/commits/${HEAD_SHA}/status" \
                            --jq "[.statuses[] | select(.context == \"${gate}\")] | sort_by(.updated_at) | last.state" \
                            2>/dev/null || echo "")
            fi

            if [ "$conclusion" != "success" ] && [ "$conclusion" != "SUCCESS" ]; then
              echo "::warning::Gate '${gate}' is '${conclusion:-missing}' on ${HEAD_SHA} — skipping promote."
              ALL_GREEN=false
            else
              echo "  ✓ ${gate}: success"
            fi
          done <<< "$GATES"

          echo "ok=${ALL_GREEN}" >> "$GITHUB_OUTPUT"

      - name: Fast-forward main to staging
        if: steps.gates.outputs.ok == 'true'
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        shell: bash
        run: |
          set -euo pipefail

          git config user.email "actions@github.com"
          git config user.name "github-actions[bot]"

          # staging is the checked-out branch (workflow fires on push to
          # staging). Can't fetch into it. Fetch main into a local main.
          git fetch origin main
          git checkout -B main origin/main

          # Check if main is already at or ahead of origin/staging.
          if git merge-base --is-ancestor origin/staging main 2>/dev/null; then
            echo "main already contains staging; nothing to promote."
            exit 0
          fi

          # --ff-only refuses if main has independent commits not on
          # staging (divergence — hotfix direct to main). Human resolves.
          if ! git merge --ff-only origin/staging 2>&1; then
            echo "::warning::main has diverged from staging — refusing fast-forward. Resolve manually (likely a direct-to-main commit exists that staging doesn't have)."
            exit 0
          fi

          git push origin main
          echo "::notice::Promoted: main is now at $(git rev-parse --short HEAD)"
