From aa17c4d70626b296884514d4e0f37db5c364ec55 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Wed, 19 Aug 2026 17:17:12 +0100 Subject: [PATCH] chore(ci): deploy the dashboard agent dormant, drop the reviewer gate, add a ref input (#4710) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem Every merge to main touching the agent queued a gated `staging`+`prod` deploy that sat `pending` on a reviewer approval nobody grants routinely. Because the gated runs never completed, they never drained the concurrency queue and cancelled each other, so the Actions tab filled with never-completing runs and the agent only ever actually deployed via a manual dispatch + approval. The reviewer gate bought nothing here: the agent deploys with `--skip-promotion`, so a deploy lands **dormant** and nothing goes live until the consuming webapp flips `DASHBOARD_AGENT_VERSION`. Promotion is already a deliberate act (the env-var flip); gating the dormant deploy on top of that just created the pile-up. ## Change - **Remove the reviewer gate** by dropping the required-reviewers rule on the `dashboard-agent-*` environments (repo-settings change, done). The `environment:` key **stays** so the per-environment scoped deploy token still resolves — no secret migration. - **`workflow_dispatch` `ref` input** — deploy a specific commit SHA, branch, or tag; defaults to the ref the run launches from. Checkout uses `github.event.inputs.ref || github.sha`. - **Require the ref to be an ancestor of `main`.** Constrains which commit gets deployed to merged code only. A push is always main's tip (passes trivially); a dispatched unmerged ref is rejected before the deploy step. Because an explicit `ref:` checkout doesn't create remote-tracking branches, `origin/main` is fetched explicitly before `git merge-base --is-ancestor`. - **`cancel-in-progress: false`** (kept). Cancelling the runner wouldn't stop the remote build (it finishes server-side), and a superseding concurrent deploy would race the same project's indexer. With the gate gone, deploys are short, so a brief queue can't pile up. - `max-parallel: 1` stays (parallel deploys of the same project race at the indexer). ## Owner actions (repo settings — not in the diff) 1. **Remove required-reviewers** on `dashboard-agent-staging` and `dashboard-agent-prod` — done. 2. **Add a deployment branch policy** on both environments restricting deployments to `main`. This is the authoritative token guard: `workflow_dispatch` runs the workflow file from the selected ref, so the in-file ancestor check alone can't protect `TRIGGER_ACCESS_TOKEN` (a branch could edit the check out). GitHub enforces the branch policy server-side against `GITHUB_REF` regardless of file contents. With it in place, the workflow only runs (and the token is only exposed) when dispatched from `main`, and the in-file check then constrains the independent `ref` input to merged commits. ## Pile-up root cause The stacking was caused by the **reviewer gate** (runs waited forever, so the queue never drained), not by `cancel-in-progress`. Removing the gate is what fixes it; `cancel-in-progress` stays `false`. --- .github/workflows/dashboard-agent-deploy.yml | 55 ++++++++++++++++++-- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dashboard-agent-deploy.yml b/.github/workflows/dashboard-agent-deploy.yml index e5ea85606..ef8c1a9cb 100644 --- a/.github/workflows/dashboard-agent-deploy.yml +++ b/.github/workflows/dashboard-agent-deploy.yml @@ -3,9 +3,19 @@ name: "🤖 Deploy dashboard agent" # Deploys the @internal/dashboard-agent chat.agent to its Trigger.dev project # with --skip-promotion, so a deploy never becomes "current" on its own. The # consuming app cuts over by pinning DASHBOARD_AGENT_VERSION to the new version. -# Runs a leg per environment (staging + prod), each gated by its own environment; -# a push to main that touches the agent or its store triggers both. Version -# numbers are per-environment, so pin each environment to its own leg's version. +# Runs a leg per environment (staging + prod); a push to main that touches the +# agent or its store deploys both. Version numbers are per-environment, so pin +# each environment to its own leg's version. +# +# The deploy lands dormant, so it doesn't need a reviewer gate: nothing goes live +# until DASHBOARD_AGENT_VERSION is flipped. The `environment:` below is kept only +# to scope the deploy token per environment; its required-reviewers rule is +# removed in repo settings so pushes deploy unattended. workflow_dispatch takes an +# optional ref (SHA, branch, or tag) to deploy a specific commit instead of head. +# +# The deployed ref must be an ancestor of main, so only reviewed, merged code ever +# runs with the deploy token (the checked-out build + trigger.config.ts execute +# with it). A push is always on main; a dispatched ref is checked before deploy. on: push: @@ -14,6 +24,11 @@ on: - "internal-packages/dashboard-agent/**" - "internal-packages/dashboard-agent-db/**" workflow_dispatch: + inputs: + ref: + description: "Commit SHA, branch, or tag to deploy. Defaults to the ref the workflow runs from." + required: false + type: string permissions: {} @@ -27,9 +42,15 @@ jobs: max-parallel: 1 matrix: environment: [staging, prod] - # Per-environment reviewer gate + source of the scoped deploy PAT. + # Kept to scope the deploy token per environment. The required-reviewers rule + # on these environments is removed in repo settings, so this no longer gates. environment: dashboard-agent-${{ matrix.environment }} concurrency: + # Queue a superseding deploy behind an in-flight one; do NOT cancel it. + # Cancelling the runner wouldn't stop the remote build (it finishes + # server-side), and a second concurrent deploy of the same project would + # race the indexer. Deploys are short now the gate is gone, so a brief queue + # is fine and can't pile up. group: dashboard-agent-deploy-${{ matrix.environment }} cancel-in-progress: false permissions: @@ -41,8 +62,34 @@ jobs: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: + # push: the pushed commit. workflow_dispatch: the input ref if given, + # otherwise the head of the ref the run was launched from. + ref: ${{ github.event.inputs.ref || github.sha }} + # Full history so the ancestor-of-main check below can find a merge base. + fetch-depth: 0 persist-credentials: false + - name: Require the ref to be an ancestor of main + # The deploy token runs the checked-out code, so refuse anything that + # hasn't landed on main. A push is main's tip (ancestor of itself); this + # only ever rejects a dispatched, unmerged ref. + # + # NOTE: this in-file check only constrains WHICH commit is deployed. It + # can't protect the token on its own, because workflow_dispatch runs the + # workflow file from the selected ref. The real guard is the deployment + # branch policy on the dashboard-agent-* environments (main only), set in + # repo settings, which GitHub enforces server-side against GITHUB_REF. + run: | + set -euo pipefail + # An explicit `ref:` checkout doesn't create remote-tracking branches, + # so fetch main before comparing against it. + git fetch --no-tags --quiet origin +refs/heads/main:refs/remotes/origin/main + if ! git merge-base --is-ancestor HEAD origin/main; then + echo "::error::Refusing to deploy $(git rev-parse HEAD): not an ancestor of origin/main. Only merged code can be deployed." + exit 1 + fi + echo "$(git rev-parse --short HEAD) is an ancestor of origin/main" + - name: Setup pnpm uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 with: