# Gated dev -> main promotion (the PROD deploy gate under managed LangGraph Cloud). # # PROD now runs on managed LangGraph Cloud (git-connected to `main`) + Vercel (UI). # The platform AUTO-DEPLOYS prod on every push to `main`, so a fast-forward of `main` # to `dev` IS the prod deploy trigger -- there is no separate AWS deploy step anymore # (the bespoke S3/SSM/packer CD is retired). This workflow therefore carries the whole # prod-promotion gate: # 1. manual dispatch only (no scheduled auto-promote -- prod ships when a human asks), # 2. the `prod` GitHub Environment approval (required reviewer: amoussa1229), # 3. the dev-HEAD-fully-green precondition (check-dev-green.sh), # 4. a fast-forward-only push of `main` -> `dev` via the seahaven-promotion App, the # sole non-admin bypass actor on the `main` ruleset (id 18238334). name: Promote to main (prod) permissions: contents: write on: workflow_dispatch: concurrency: group: promote-dev-to-main cancel-in-progress: false jobs: promote: runs-on: ubuntu-latest # The manual-approval gate. The `prod` Environment's required reviewer # (amoussa1229) must approve before this job runs -- and because the FF push # below auto-deploys managed prod, that approval IS the prod-deploy approval. environment: prod permissions: contents: write checks: read steps: # Defense-in-depth: the checkout below pins `ref: dev`, so this workflow only # ever promotes dev's HEAD. But workflow_dispatch runs the workflow DEFINITION # from whichever ref it was launched on, so a branch that edited this file could # otherwise reach the privileged steps. Refuse any dispatch not from `dev`, # before the App token is minted. (The `prod` Environment approval still gates # everything after this regardless.) - name: Guard — only promote from dev env: DISPATCH_REF: ${{ github.ref_name }} run: | if [ "${DISPATCH_REF}" != "dev" ]; then echo "::error::promote-to-main must be dispatched from 'dev' (got '${DISPATCH_REF}')." exit 1 fi echo "Dispatch ref OK: ${DISPATCH_REF}" # Mint a GitHub App installation token for the protected-branch push below. # A plain ref push by github-actions[bot] is REJECTED by the `main` ruleset # (PRs required + a required status check; the default token is not a bypass # actor). The App behind these secrets MUST be added to the `main` ruleset's # bypass actors; the push is then accepted and attributed to the App (not a # human PAT). The promoted commit already passed every check on dev (gated # below), so re-gating it via a PR on main would be redundant. - name: Mint app token for the protected-branch push id: app-token uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: app-id: ${{ secrets.PROMOTE_APP_ID }} private-key: ${{ secrets.PROMOTE_APP_PRIVATE_KEY }} - uses: actions/checkout@v7 with: ref: dev fetch-depth: 0 # Persist the App token as the git credential so the fast-forward push uses # the bypass-actor identity (not the default github-actions[bot]). token: ${{ steps.app-token.outputs.token }} - name: Require dev HEAD fully green # Hard precondition: every check-run on the dev HEAD commit must be # completed + passing before we let it become prod. A red OR still-pending # check blocks the promotion. The promotion workflow's OWN check-runs are # excluded by the gate (this run by id, plus any STALE prior promote runs by # name + Actions URL) so the gate can't deadlock on itself. env: GH_TOKEN: ${{ github.token }} # exclude THIS run's own check-run by its run id (not by name). EXCLUDE_RUN_ID: ${{ github.run_id }} run: | SHA="$(git rev-parse HEAD)" echo "dev HEAD = ${SHA}" gh api --paginate "repos/${GITHUB_REPOSITORY}/commits/${SHA}/check-runs" \ -q '.check_runs[] | [.name, .status, (.conclusion // ""), (.details_url // "")] | join("\u001f")' \ | bash .github/scripts/check-dev-green.sh - name: Fast-forward main (PROD) to dev # main is the production branch: managed LangGraph Cloud is git-connected to it # and auto-deploys prod on every push, so THIS push is the prod deploy trigger. # A direct ref push is normally rejected by the `main` ruleset (PRs required), # so it succeeds only because the App minted above is a bypass actor. The push # is fast-forward-only, so a diverged main fails loudly rather than force-updating. # # SECURITY: this is the LAST step on purpose. The App token persists as the # git credential after checkout — do not add steps after this push that run # untrusted code or could echo the credential. run: | git push origin HEAD:refs/heads/main