Playbook: Vercel production deploy blocked — commit author lacks contributing access

See also: git-identity-and-roles.md — the canonical identity × role × action matrix; §8 covers the Vercel commit-author seat requirement and the token-Action fix.

Trigger: A gh pr merge into a project’s main (or any Vercel-git-integration-tracked branch) merges successfully on GitHub — checks green, issue auto-close via Closes #N works, a GitHub Release publishes fine — but the actual Vercel deployment silently never ships. The site keeps serving the previous build. There is no GitHub-side signal that anything failed; gh pr view and gh pr checks show nothing wrong, because the failure happens entirely on Vercel’s side of the GitHub integration webhook.

Confirmed live: 2026-08-18, radieu/et-operational-platform v2.10.2 release (PR #1704). Also found one earlier occurrence in the same project’s deployment history (commit e129a8bc, the #1629 GitHub App token migration) that never shipped until a later, differently-authored commit carried the same code forward — so this is a recurring gap, not a one-off.

How to confirm it

GitHub’s deployment status API is the fastest signal — check it after any main/production merge, don’t just trust that the PR merged:

$REPO = "radieu/<project>"
$sha = git rev-parse origin/main
gh api "repos/$REPO/deployments?sha=$sha" --jq '.[] | "\(.id) \(.environment)"'
# Then, for the production-environment deployment id:
gh api "repos/$REPO/deployments/<id>/statuses" --jq '.[0] | "\(.state) \(.description)"'

If you see failure / Deployment was blocked, confirm the exact cause via the Vercel API (needs VERCEL_API_TOKEN from secrets/monitoring.env.sops):

VTOKEN=$(sops -d --input-type dotenv --output-type dotenv secrets/monitoring.env.sops | grep "^VERCEL_API_TOKEN=" | cut -d= -f2-)
curl -s "https://api.vercel.com/v6/deployments?projectId=<project>&limit=5&target=production" \
  -H "Authorization: Bearer $VTOKEN" | grep -o '"errorMessage":"[^"]*"'

The tell-tale error: "The Deployment was blocked because the commit author does not have contributing access to the project on Vercel." — paired with "seatBlock":{"blockCode": "TEAM_ACCESS_REQUIRED", ...} and a gitUser.login that isn’t a Vercel team member.

Root cause

Vercel’s GitHub integration gates auto-deploys on the pushing/merging commit’s GitHub identity having a seat on the Vercel team with contributing access to that project — not on whether the merge itself succeeded, and not on repo-level GitHub permissions. Any commit (including a merge commit from gh pr merge) authored by a GitHub identity without that Vercel-side seat gets silently blocked, with zero signal on the GitHub side.

This machine’s git identity for et-operational-platform work is ai-dev-windev-1 — a dedicated bot account (mirroring the AI-Dev-BMS4-1 / ai-dev-bms4-2 naming pattern used for the bms-4 workers).

Updated 2026-10-08 (#7592). Current state of the devp24coms-projects Vercel team (Pro plan): the only paid/contributing member is dev.pinbox24@gmail.com (OWNER). ai-dev-windev-1 was switched to VIEWER_FOR_PLUS (a free viewer, not a contributing seat), so its merge commits are still blocked by the git-integration author check — same as the bms-4 bot identities AI-Dev-BMS4-1 / ai-dev-bms4-2 and the App-2109526 PR-merge bot, none of which have a seat either. The radieu@gmail.com invite is unconfirmed and does not consume a seat (verified in the dashboard — so there is no seat human-action to chase). Vercel Pro has no custom roles, so “give each bot its own scoped role” is not an available fix — a contributing seat is all-or-nothing. That is why the durable fix below moves off the git-integration path entirely rather than chasing per-identity seats.

Immediate fix — redeploy via the Vercel API directly

A direct Vercel API deployment request is not gated by the commit-author check (that check is specific to the GitHub-webhook integration path) — the API token’s own permissions are what’s checked instead. This is the fast, safe workaround; it does not touch team membership/permissions.

export SOPS_AGE_KEY_FILE="C:\Users\konar\.age\p24-infra-keys.txt"
VTOKEN=$(sops -d --input-type dotenv --output-type dotenv secrets/monitoring.env.sops | grep "^VERCEL_API_TOKEN=" | cut -d= -f2-)
unset SOPS_AGE_KEY_FILE
 
MAINSHA=$(git rev-parse origin/main)
cat > ./_deploy_payload.json <<EOF
{
  "name": "<vercel-project-name>",
  "project": "<vercel-project-id>",
  "target": "production",
  "gitSource": {
    "type": "github",
    "org": "radieu",
    "repo": "<repo-name>",
    "ref": "main",
    "sha": "$MAINSHA"
  }
}
EOF
curl -s -X POST "https://api.vercel.com/v13/deployments" \
  -H "Authorization: Bearer $VTOKEN" -H "Content-Type: application/json" \
  --data-binary @./_deploy_payload.json
VTOKEN=""
rm -f ./_deploy_payload.json

Poll GET /v13/deployments/<id> for readyState until READY, then confirm the alias field includes the real production domain(s) — not just the internal *.vercel.app URL. Verify live with curl -s -o /dev/null -w "HTTP %{http_code}\n" https://<prod-domain>.

Durable fix — deploy from a GH Action, not the git integration (#7592, target flow)

Because Vercel Pro has no custom roles and new bot identities keep appearing, chasing per-identity team seats is a losing game. The target fix makes production deploys independent of the commit author by deploying from a GitHub Action that calls the Vercel CLI/API with a token — the token path is not subject to the commit-author seat check.

The reusable building block lives in this repo: .github/workflows/reusable-deploy-vercel.yml (workflow_call). It deploys via the token, polls readyState until READY, optionally verifies the production alias with HTTP 200, and on any failure fires the Error-Notification Standard (Discord embed + GH bug issue). Consumer repos call it in one line.

Per production project — roll out one at a time. In-scope repos (actively maintained, a live git link in Vercel, deploy activity ≤ 2 months as of 2026-10-08):

RepoVercel project(s)Notes
radieu/et-operational-platformet-operational-platform (main), et-operational-platform-7ktl (staging)start here
radieu/whatsup-android-chat-pullerwhatsup-android-chat-puller, p24-wa-atwo projects, same repo
radieu/radekkonarski-personal-brandradekkonarski-com

Out of scope: brandpilot, portal, wasabi-iam-rotator already deploy outside the git integration (no git link in Vercel), so they are not affected by this bug. Art-Agency (last deploy 2026-06-28) is a later decision. portal/ additionally lives in p24-infra and deploys via its own portal-deploy.yml (which also builds the MkDocs docs), so it is intentionally left on that workflow.

Each in-scope repo needs its own issue/PR — a p24-infra PR cannot add workflow files to another repo.

  1. Add a push-to-main Action in the project repo that calls the reusable workflow here:

    # .github/workflows/deploy-vercel.yml in the project repo
    on:
      push:
        branches: [main]
      workflow_dispatch:
    jobs:
      deploy:
        uses: radieu/p24-infra/.github/workflows/reusable-deploy-vercel.yml@main
        with:
          environment: production
          vercel_project_name: <project>
          production_url: https://<prod-domain>   # optional HTTP-200 gate
        secrets:
          VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
          VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
          VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
          DISCORD_ERRORS_WEBHOOK: ${{ secrets.P24_DISCORD_INFRA_SCRIPTS_ERRORS_WEBHOOK_URL }}

    VERCEL_ORG_ID / VERCEL_PROJECT_ID pin the deploy to the right team+project without a committed .vercel link (the reusable job exports them as env for the CLI). A repo with two Vercel projects (e.g. whatsup-android-chat-puller) calls the reusable workflow once per project with the matching VERCEL_PROJECT_ID.

  2. Only after that project’s first green Action deploy, turn off the git auto-deploy so the two paths don’t double-ship. Set git.deploymentEnabled.main = false on the project via the Vercel API (do NOT disable it before the Action is proven, or the project ships nothing):

    # VERCEL_API_TOKEN from secrets/monitoring.env.sops — secret-manager / operational step
    curl -s -X PATCH "https://api.vercel.com/v9/projects/<project-id>" \
      -H "Authorization: Bearer $VTOKEN" -H "Content-Type: application/json" \
      -d '{"git":{"deploymentEnabled":{"main":false}}}'

Token: the deploy token is only ever a GH Secret / SOPS value delivered through the existing secret-manager flow — never printed, never hardcoded. VERCEL_TOKEN already exists as a GH Secret in p24-infra (used by portal-deploy.yml), so the reusable workflow itself needs no new secret; a consumer repo that lacks VERCEL_TOKEN / VERCEL_ORG_ID / VERCEL_PROJECT_ID files a secret-manager request to add them as repo GH Secrets.

The CLI still inherits the checkout’s git author — strip .git first (#7706)

The token path removes the webhook commit-author check, but it is not enough on its own. The vercel deploy CLI reads git metadata from the local checkout — the HEAD commit author — and attaches it to the deployment as githubCommitAuthor*. Vercel then re-applies the same commit-author seat check to that metadata, so a bot-authored tip commit is still BLOCKED, and the CLI (which waits for the deployment to become READY) hangs on the blocked deployment instead of exiting.

Confirmed 2026-10-09 (#7706): radieu/radekkonarski-personal-brand run 37901972898 (deploy-vercel.yml → this reusable workflow), head commit e7903f2f authored by ai-dev-bms4-2: the CLI deployment for radekkonarski-com ended readyState=BLOCKED, readyStateReason="The deployment was blocked because the commit author doesn't have permission to create deployments for this project.", githubCommitAuthorName=ai-dev-bms4-2. The “Deploy” step then hung ~55 minutes until cancelled by hand. The same repo’s et-operational-platform deploys only succeeded because their tip commits were authored by radieu (who does hold a seat) — a bot-authored tip there would block identically.

Fix (in reusable-deploy-vercel.yml): rm -rf .git at the workspace root before vercel deploy, so the CLI has no commit author to report. This is genuine author-independence — no author, not a borrowed/forged one (no --reset-author, no --meta githubCommitAuthor* impersonation). VERCEL_ORG_ID / VERCEL_PROJECT_ID (exported as env) still pin the team+project without the .git link, so the deploy targets the right project.

Hard timeout: the deploy step carries timeout-minutes (input deploy_timeout_minutes, default 15) and wraps vercel deploy in timeout ~30s under that backstop, so a blocked/queued deployment fails fast with the Error-Notification Standard (Discord + GH bug issue) instead of hanging an hour. The failure notifier runs vercel inspect on the captured deployment URL and includes the deployment’s readyStateReason in the Discord/issue message when available.

Diagnose a hang/block: check readyStateReason on the deployment — if it names the commit author ("commit author doesn't have permission" / TEAM_ACCESS_REQUIRED), the checkout still carried a .git with a seatless author. Confirm via the Vercel API (see How to confirm it above) or vercel inspect <deployment-url> --token "$VTOKEN".

Affected callers (all use @main of this reusable workflow, so the fix applies as soon as it merges): radieu/et-operational-platform (vercel-deploy.yml), radieu/radekkonarski-personal-brand (deploy-vercel.yml), radieu/whatsup-android-chat-puller (PR #227). After merge, re-run radekkonarski-personal-brand’s deploy-vercel.yml (workflow_dispatch) to verify a bot-authored tip commit now deploys without blocking.

Prevention

After every rc → main (or equivalent production) merge, verify the deployment actually shipped — do not trust a green gh pr merge alone. Add the confirmation check above as a standard step in /rc-to-main-style promotion flows for any project using Vercel’s GitHub integration, until the durable fix lands.