name: deploy-staging

# Canonical staging-deploy caller template — DISPATCHER half (Story #175,
# reworked for Story #272).
#
# > **Why two files (Story #272).** A reusable workflow's `environment:`-gated
# > jobs (`deploy-cloudflare.yml`'s `check-env` / `migration` / `deploy` /
# > `boot-smoke`) are **silently skipped on a `workflow_run` event** — a
# > documented GitHub limitation. The previous single-file template called
# > `deploy-cloudflare.yml` DIRECTLY from `on: workflow_run`, so those jobs
# > skipped and **0 workers deployed while the run reported green** (all-skipped,
# > none failed). One consumer hid 40+ consecutive non-deploys this way.
# >
# > The fix: this file is now a thin **dispatcher** that fires on CI-green and
# > re-launches the deploy via **`workflow_dispatch`** — the event on which the
# > `environment:` jobs DO run — against its sibling `deploy-staging-run.yml`.
# > `workflow_dispatch` and `repository_dispatch` are the two events that
# > "always create workflow runs" even when triggered with the built-in
# > `GITHUB_TOKEN`, so **no PAT is required** for this same-repo dispatch — just
# > `permissions: actions: write` below. (Cross-repo dispatch, like
# > `smoke-dispatch.yml`, still needs a PAT; same repo does not.)
#
# > **Thin local caller.** The defence-in-depth deploy core (secret-isolation
# > audit -> CF env gate -> migration (snapshot + apply) -> deploy ->
# > boot-smoke + auto-rollback) lives in the shared
# > `dsj1984/mandrel-platform` `deploy-cloudflare.yml` reusable workflow — see
# > https://github.com/dsj1984/mandrel-platform/blob/main/docs/reusable-workflows.md#deploy-cloudflareyml.
# > The <PROJECT_NAME>-specific values (worker names, build step, secret
# > mapping) live in the sibling `deploy-staging-run.yml`. When the deploy
# > PROCESS changes, that change lands upstream in mandrel-platform — not here.
#
# Adopt BOTH files together: this `deploy-staging.yml` (dispatcher) and
# `deploy-staging-run.yml` (the actual deploy). `platform-sync` materializes
# both. Replace every <PLACEHOLDER>:
#   <CI_WORKFLOW_NAME>   the `name:` of the workflow this deploy gates on (e.g.
#                        "quality", "CI"). Must match EXACTLY — GitHub matches
#                        `workflow_run.workflows` by workflow name, not path.
#
# See the full input/secret contract:
# https://github.com/dsj1984/mandrel-platform/blob/main/docs/reusable-workflows.md#deploy-cloudflareyml

on:
  # CI-green gate: fires when <CI_WORKFLOW_NAME> completes on main. Unlike the
  # old shape, this does NOT call the deploy directly — a `workflow_run` deploy
  # would skip every `environment:` job. It only DISPATCHES the deploy (below)
  # on success, so the actual deploy runs on `workflow_dispatch` where those
  # jobs execute. A red upstream run simply does not dispatch — there is no
  # green-but-didn't-deploy run at all.
  workflow_run:
    workflows: [<CI_WORKFLOW_NAME>]
    branches: [main]
    types: [completed]

# actions:write lets the built-in GITHUB_TOKEN dispatch deploy-staging-run.yml
# via the workflow_dispatch API. No PAT needed for a same-repo dispatch —
# workflow_dispatch always creates a run even from GITHUB_TOKEN.
permissions:
  contents: read
  actions: write

# Only the freshest tip of main should reach staging: cancel an in-flight
# DISPATCH when a newer CI run completes. (The deploy itself is serialized
# separately in deploy-staging-run.yml and per-environment inside the shared
# deploy-cloudflare.yml.)
concurrency:
  group: deploy-staging-dispatch
  cancel-in-progress: true

jobs:
  dispatch:
    name: Dispatch staging deploy on CI-green
    runs-on: ubuntu-latest
    timeout-minutes: 5
    # CI-green gate — three load-bearing conditions, all required (Story #284):
    #   1. `conclusion == 'success'` — `workflow_run` fires on both success and
    #      failure; without this a red main would still deploy.
    #   2. `event == 'push'` — the upstream CI run must itself have been a push
    #      to the repo, not a `pull_request` run. `workflow_run.branches:
    #      [main]` filters on the *head branch NAME*, so a fork PR whose head
    #      branch is literally named `main` with green CI otherwise satisfies
    #      the branch filter and the success guard (audit M3).
    #   3. `head_repository.full_name == github.repository` — the CI run must
    #      have originated from THIS repo, not a fork. Belt-and-suspenders with
    #      condition 2: even a same-name fork branch cannot spoof same-repo
    #      provenance, so an external contributor can no longer attacker-time a
    #      staging deploy off their fork's green CI.
    if: >-
      ${{ github.event.workflow_run.conclusion == 'success'
        && github.event.workflow_run.event == 'push'
        && github.event.workflow_run.head_repository.full_name == github.repository }}
    steps:
      - name: Dispatch deploy-staging-run.yml (workflow_dispatch)
        env:
          GH_TOKEN: ${{ github.token }}
          REPO: ${{ github.repository }}
          SHA: ${{ github.event.workflow_run.head_sha }}
        shell: bash
        run: |
          set -euo pipefail

          # L3: the dispatch is otherwise fire-and-forget — a transient GitHub
          # API 5xx fails `gh workflow run`, and the only signal is a red run in
          # a low-visibility dispatcher while that green commit never reaches
          # staging. Retry with backoff, then VERIFY a run was actually created
          # (a 2xx from the dispatch API is not proof a run materialized).
          dispatched=false
          for attempt in 1 2 3; do
            if gh workflow run deploy-staging-run.yml \
              --repo "${REPO}" \
              --ref main \
              -f sha="${SHA}"; then
              dispatched=true
              break
            fi
            echo "::warning::gh workflow run attempt ${attempt} failed; retrying after backoff."
            sleep $((attempt * 5))
          done

          if [ "${dispatched}" != "true" ]; then
            echo "::error::Failed to dispatch deploy-staging-run.yml after 3 attempts for ${SHA}." >&2
            exit 1
          fi

          # Verify a runner workflow run was actually created. The dispatch API
          # returns before the run row is queryable, so poll `gh run list` for a
          # recent workflow_dispatch run of the runner workflow. Fail loudly if
          # none appears — a silent non-creation is exactly the failure mode
          # this check exists to surface.
          created=false
          for attempt in 1 2 3 4 5; do
            count="$(gh run list \
              --repo "${REPO}" \
              --workflow deploy-staging-run.yml \
              --event workflow_dispatch \
              --limit 5 \
              --json databaseId --jq 'length' 2>/dev/null || echo 0)"
            if [ "${count:-0}" -gt 0 ]; then
              created=true
              break
            fi
            echo "Run not visible yet (attempt ${attempt}); waiting for the run row to appear."
            sleep $((attempt * 3))
          done

          if [ "${created}" != "true" ]; then
            echo "::error::Dispatched deploy-staging-run.yml for ${SHA} but no runner workflow run appeared via 'gh run list'." >&2
            exit 1
          fi

          echo "Dispatched and verified staging deploy for ${SHA} (deploy runs on workflow_dispatch so environment: jobs execute)."
