diff --git a/github-profile-stats-deploy/SKILL.ja.md b/github-profile-stats-deploy/SKILL.ja.md new file mode 100644 index 0000000..a3b9d2c --- /dev/null +++ b/github-profile-stats-deploy/SKILL.ja.md @@ -0,0 +1,62 @@ +--- +name: github-profile-stats-deploy +description: > + Vercelにデプロイした自前のGitHubプロフィール統計カード(github-readme-stats、 + github-readme-streak-stats、あるいはそれらに類するPHP/Node製フォーク)を + セットアップする手順と、壊れた時に直す手順。ユーザーが自分のGitHub統計/ + ストリークカードを自前デプロイしたい時、プロフィールREADMEのカードが + 「Failed to retrieve contributions」「.env was not found」「Missing token + in config」を表示している時、対象フォークのVercelデプロイが「No more than + 12 Serverless Functions」で失敗する時、対象フォークのGitHub Actionsデプロイ + が落ちた時、統計カードプロジェクト用のVERCEL_TOKEN/VERCEL_ORG_ID/ + VERCEL_PROJECT_IDの設定について聞かれた時、プロフィールの統計/ストリーク + カードが壊れている・死んでいると言われた時に使う。当てずっぽうで直そうと + せず、以下の該当する手順を順番通りに進めること — ここで起きる不具合は + だいたい既知の原因を持っていて、エラーメッセージの見た目だけでは分からない + ことが多い。 +--- + +# GitHubプロフィール統計カード — セットアップと障害対応 + +このスキルは2つの手順をまとめたものだ:何もない状態から統計カードを立ち上げる手順と、既にデプロイ済みのカードが壊れた時に直す手順。各ステップには、そのステップに関係する注記だけを添えてある。詳しい説明は`references/`配下に逃がしてあり、該当ステップの注記から個別にリンクしているので、その注記が指す時だけ読みに行けばいい。手順を通しで読む段階では先回りして全部読む必要はない。 + +## 手順A: ゼロから統計カードをセットアップする + +1. **使いたい統計カード生成元を自分のアカウントにフォークする。** 例: contributions/言語比率カードなら`anuraghazra/github-readme-stats`、ストリークカードなら`DenverCoder1/github-readme-streak-stats`。両方要らなければ片方だけでいい。互いに依存関係はない。 + +2. **GitHub Personal Access Tokenを作る。** スコープは`repo` + `read:user`。作成場所は`https://github.com/settings/tokens/new`。 + > 有効期限は無期限か長めにしておくこと。期限切れがこの手のカードが壊れる原因のダントツ1位で、しかも壊れ方が分かりにくい。詳細は`references/secrets-and-tokens.md#L6-22`。 + +3. **各フォークをVercelに別々のプロジェクトとしてインポートする**(Vercelダッシュボード → Add New → Project → 対象リポジトリを選択)。確認方法: それぞれ独自の`*.vercel.app`ドメインが割り当たり、初回デプロイが走ること(この時点ではまだトークンを登録していないのでエラー表示になるのが正常)。 + +4. **そのプロジェクトの環境変数にPATを登録する。** + > 変数名はフォーク元のソースコードが決めているもので、**フォークによってバラバラ**。決め打ちせず実際の変数名を確認してから登録すること。詳細は`references/secrets-and-tokens.md#L24-50`。 + + 保存後は再デプロイし、実際に動いているか確認する: + ```bash + curl -s "<自分のプロジェクト>.vercel.app/<カードのパス>?username=<自分のユーザー名>" | grep -o "Total Contributions\|Failed to retrieve" + ``` + +5. **ビルド自体が失敗する場合**、特に「No more than 12 Serverless Functions can be added to a Deployment on the Hobby plan」のようなエラーが出た場合: + > これはトークンやコードとは無関係な、既知のVercelプラットフォーム側のバグ。`vercel.json`の直し方と、必要ならGitHub Actions経由のデプロイへの切り替え方が`references/vercel-deploy-bug.md`にある(`#L20-41`と`#L43-86`)。 + +6. **READMEからカードをどう参照するか決める**: Vercelの実URLを直接``で埋め込むか、定期実行でサーバー側からfetchして静的SVGとしてコミットするか。 + > ライブURLの方がセットアップは簡単だが、実体のデプロイを直した後も、間に挟まる複数のキャッシュ層のせいで何時間も古いエラーが表示され続けることがある。なぜそうなるかと、それを避けたい場合の定期fetch+コミット構成の最小形は`references/cache-layers.md`を見ること。 + + 静的SVG方式を選ぶ場合、最小構成は: 各プロジェクトのライブSVG出力をキャッシュバスター付きクエリでfetchしてファイルに書き込むスクリプトと、それを定期実行(数時間おき+オンデマンド実行用の`workflow_dispatch`)してコミットするワークフローの2つ。 + +7. **`README.md`にカードを埋め込む** — 手順6で選んだ方式に応じて、ライブURLか静的ファイルの相対パスのどちらか。確認方法: github.com上でプロフィールページの表示を実際に見る(rawファイルだけでなく。コミット直後はGitHub自体のページキャッシュで少し遅れて反映されることもある)。 + +## 手順B: 既にデプロイ済みのカードが壊れた時に直す + +1. **今実際に何が配信されているかを確認する。** 静的SVG方式で運用しているなら、READMEが実際に表示しているのは*コミット済みのファイル*なので、そちらを見る: + ```bash + gh api repos///contents/ --jq '.content' | base64 -d | grep -oE "]*>[^<]*" + ``` + ライブURL方式で運用しているなら、デプロイURLを直接curlする。 + +2. **エラー文言を読んで、既知の3パターンのどれに当てはまるか切り分ける**(変数が存在しない・変数が空・変数はあるが上流API呼び出しが失敗、の3つで、それぞれ直し方が違う。トークンの値そのものが悪いのは3パターン目だけ)。マッチさせるべき文言の具体例、決めつける前の確認方法、それぞれの直し方は`references/diagnosing-errors.md`にまとめてある。 + +3. **デプロイ自体がビルドの時点で失敗している場合**(エラーを表示するところまでも行っていない場合)は`references/vercel-deploy-bug.md`を見ること。これはプラットフォーム側の問題であって、トークンの問題として直そうとすると時間を無駄にする。 + +4. **環境変数やSecretsを変更したら、必ず再デプロイした上で、実際に配信されている中身を確認すること — ビルドやActionが緑になっただけで安心しない。** ビルド成功はコードがコンパイルできた証拠でしかなく、カードが正しく表示される証拠にはならない。確認コマンドは`references/diagnosing-errors.md#L63-76`。静的SVG方式で運用している場合は、次のスケジュール実行を待たずに生成ワークフローを一度手動実行し、修正を即座にREADMEへ反映させること。 diff --git a/github-profile-stats-deploy/SKILL.md b/github-profile-stats-deploy/SKILL.md new file mode 100644 index 0000000..6dd5964 --- /dev/null +++ b/github-profile-stats-deploy/SKILL.md @@ -0,0 +1,115 @@ +--- +name: github-profile-stats-deploy +description: > + How to set up self-hosted GitHub profile stat cards (github-readme-stats, + github-readme-streak-stats, or similar PHP/Node forks) deployed on Vercel + and embedded in a GitHub profile README, and how to diagnose and fix them + when they break. Use this skill whenever the user wants to deploy their + own instance of a GitHub stats/streak card, when a profile README's stat + card shows an error like "Failed to retrieve contributions", ".env was not + found", or "Missing token in config", when a Vercel deployment for one of + these forks fails with "No more than 12 Serverless Functions", when a + GitHub Actions deploy for one of these forks fails, when the user asks + about setting up VERCEL_TOKEN/VERCEL_ORG_ID/VERCEL_PROJECT_ID for a stat + card project, or when the user says a profile stats/streak card is broken + or dead. Work through the relevant procedure below in order rather than + guessing at a fix — most failures here have a specific, previously-known + cause that isn't obvious from the error message alone. +--- + +# GitHub profile stats cards — setup & troubleshooting + +This covers two related procedures: setting up a self-hosted stat card from +nothing, and fixing one that's already deployed but broken. Each step below +carries only the note relevant to that step; deeper explanations live in +`references/` and are linked from the step that needs them — read the +reference file only when that step's note tells you to, not up front. + +## Procedure A: set up a stat card from scratch + +1. **Fork the stat-card generator(s) you want** into your own GitHub account + — e.g. `anuraghazra/github-readme-stats` for a contributions/languages + card, `DenverCoder1/github-readme-streak-stats` for a streak card. You + can deploy one or both independently; they don't depend on each other. + +2. **Create a GitHub Personal Access Token.** Scopes: `repo` + `read:user`. + Create it at `https://github.com/settings/tokens/new`. + > Prefer no expiration or a long one — an expired token is the most + > common cause of these cards breaking later, and it fails in a + > non-obvious way. See `references/secrets-and-tokens.md#L6-22`. + +3. **Import each fork into Vercel as its own project** (Vercel dashboard → + Add New → Project → pick the repo). Confirm it worked: each project gets + its own `*.vercel.app` domain and runs an initial deployment (it's + expected to render an error at this point, since no token is registered + yet). + +4. **Register the PAT as that project's environment variable.** + > The variable name is decided by the fork's own source code and is + > **not** consistent across forks — confirm the actual name before + > guessing. See `references/secrets-and-tokens.md#L24-50`. + + Redeploy after saving, then confirm the card actually works: + ```bash + curl -s ".vercel.app/?username=" | grep -o "Total Contributions\|Failed to retrieve" + ``` + +5. **If the deployment fails to build at all**, especially with an error + like "No more than 12 Serverless Functions can be added to a Deployment + on the Hobby plan": + > This is a known Vercel platform bug unrelated to your token or code. + > See `references/vercel-deploy-bug.md` for the `vercel.json` fix and, + > if needed, a GitHub-Actions-based deploy workaround + > (`references/vercel-deploy-bug.md#L20-41` and `#L43-86`). + +6. **Decide how the README will reference the card**: a live ``, or a static SVG fetched + server-side on a schedule and committed to the repo. + > A live URL is simpler to set up but can show a stale error for hours + > after you've already fixed the underlying deployment, because of + > caching layers between the origin and the viewer. See + > `references/cache-layers.md` for why, and for the shape of a + > scheduled fetch-and-commit setup if you'd rather avoid that class of + > bug entirely. + + If you go with the static-SVG approach, the minimum viable version is: + a script that fetches each project's live SVG output (with a + cache-busting query param) and writes it to a file, plus a scheduled + workflow (e.g. every few hours, and `workflow_dispatch` for on-demand + runs) that runs the script and commits the result if it changed. + +7. **Embed the card in `README.md`** — either the live URL or the static + file's relative path, per your choice in step 6. Confirm by viewing the + rendered profile page on github.com (not just the raw file — GitHub's + own page cache can also lag briefly after a fresh commit). + +## Procedure B: fix a card that's already deployed but broken + +1. **Check what's actually being served right now.** If you're running a + static-SVG setup, check the *committed file's* content — that's what the + README actually displays, not the live Vercel URL: + ```bash + gh api repos///contents/ --jq '.content' | base64 -d | grep -oE "]*>[^<]*" + ``` + If you're running a live-URL setup, curl the deployment URL directly + instead. + +2. **Read the error text and match it to one of three known causes** + (missing variable, empty variable, or a failed upstream API call — each + needs a different fix and only one of them is actually about the token + value being wrong). See `references/diagnosing-errors.md` for the exact + wording to match on, how to verify each cause before acting on it, and + the fix for each. + +3. **If the deployment itself won't build** (rather than building but + rendering an error), see `references/vercel-deploy-bug.md` — this is a + platform-level issue, not a token problem, and trying to fix it as if it + were a token problem wastes time. + +4. **After changing any environment variable or secret, redeploy and verify + the actual served content — don't trust a green build/Action alone.** + A successful build only proves the code compiled, not that the card + renders correctly. See `references/diagnosing-errors.md#L63-76` for the + verification command, and if you're running the static-SVG setup, also + trigger that generation workflow once so the fix reaches the README + immediately instead of waiting for its next scheduled run. diff --git a/github-profile-stats-deploy/references/cache-layers.md b/github-profile-stats-deploy/references/cache-layers.md new file mode 100644 index 0000000..55734a5 --- /dev/null +++ b/github-profile-stats-deploy/references/cache-layers.md @@ -0,0 +1,59 @@ +# Why static SVGs beat a live `` + +Referenced from SKILL.md's setup step 6. + +Embedding the Vercel deployment URL directly as a README `` works, but +it fails in a way that's unusually hard to diagnose: the fix can be +completely correct and the card can *still* show the old error for hours +afterward, because the error got cached somewhere between the origin and +the viewer's eyes. + +## The three independent cache layers + +Many stat-card APIs (including the common `github-readme-stats` / +`github-readme-streak-stats` forks) set a long `Cache-Control` header on +their responses — e.g. `public, max-age=86400` (24h) — so repeat visitors +don't hammer the GitHub API on every page load. That's reasonable for a +*correct* response, but it applies just as much to an *error* response. +Once a broken response (say, from an expired token) gets cached, three +layers can each independently hold on to it: + +1. **Vercel's own edge cache**, which honors the response's Cache-Control + and can keep serving the cached error for up to the stated max-age even + after you've fixed the underlying deployment. +2. **GitHub's `camo` image proxy** — every external image embedded in a + README is served through `camo.githubusercontent.com`, not the origin + directly. `camo` also respects the origin's Cache-Control, and in + practice can hold entries even longer than the stated max-age. +3. **The visiting browser's own HTTP cache.** If a viewer already loaded + the broken image once, revisiting the same URL can be served straight + from local disk cache without even a revalidation request to the + network, showing the old error indefinitely from that browser's point + of view. + +Fixing the token only fixes what layer 1 serves to *new* cache entries. +Layers 2 and 3 keep whatever they already cached until their own TTL +expires — independently of each other and of the origin. Actively purging +`camo`'s cache (`curl -X PURGE https://camo.githubusercontent.com/…`) only +clears layer 2; layers 1 and 3 are untouched, so the same error can +resurface. + +## The fix: don't let the README point at a cacheable live endpoint at all + +Fetch the SVG **server-side**, on a schedule (e.g. a GitHub Actions workflow +running every few hours), and commit the result as a plain file in the repo +that hosts the README. Point the README's `` at that committed file's +relative path instead of the live Vercel URL. + +This sidesteps all three layers at once: a file committed to a repo isn't +served through `camo` (it's already same-origin as far as GitHub is +concerned) and carries none of the origin API's long-lived Cache-Control. +The only "staleness" left is bounded and expected — the gap between +schedule runs — not an indefinite, cache-layer-dependent one. + +One more detail worth building into the fetch step: add a cache-busting +query parameter (e.g. a timestamp) to the URL your scheduled job fetches +from, so layer 1 (Vercel's own edge cache) can't hand your fetcher a stale +cached response either — you want the freshest possible data at generation +time, since whatever you commit now will sit there until the next +scheduled run regardless. diff --git a/github-profile-stats-deploy/references/diagnosing-errors.md b/github-profile-stats-deploy/references/diagnosing-errors.md new file mode 100644 index 0000000..088ed1e --- /dev/null +++ b/github-profile-stats-deploy/references/diagnosing-errors.md @@ -0,0 +1,76 @@ +# Diagnosing the error text embedded in the SVG + +Referenced from SKILL.md's fix step 2. + +Most of these stat-card forks render their own error state as an SVG with +the message as visible text, rather than returning an HTTP error status +alone. Extract it with: + +```bash +curl -sS "" | grep -oE "]*>[^<]*" +``` + +(If you're running the static-SVG setup, check the *committed* file's +content instead — that's what the README actually displays. See SKILL.md's +fix step 1.) + +The exact wording differs slightly by fork, but the three underlying causes +are consistent: + +## ".env was not found" / similar "no config found" message + +The environment variable the fork's code checks for doesn't exist at all in +the deployment's environment variables — not just empty, genuinely absent. + +**Fix**: register it. See `secrets-and-tokens.md` for how to find the exact +variable name the fork's code expects (it varies by fork) and where to set +it. + +## "Missing token in config" / similar "empty value" message + +The environment variable exists but its value is an empty string. + +**Fix**: same as above — set an actual value, then redeploy (env var +changes don't apply retroactively; see `secrets-and-tokens.md`). + +## "Failed to retrieve contributions" / "GitHub API issue" / similar upstream-call-failed message + +This is the trickiest one: the variable exists and has *some* value, but +the fork's server-side call to the GitHub API itself failed. This almost +always means **the token value itself is wrong** — expired, revoked, or +corrupted in transit (a common cause: selecting the token text by +click-drag instead of triple-click leaves a trailing space or newline that +looks identical to the eye but fails auth). + +**Verify before touching anything**: test the same kind of call with a +token you're confident is good, independent of the deployment: + +```bash +gh api graphql -f query='query($login:String!){ user(login:$login){ createdAt } }' -f login="" +``` + +If that succeeds, the GitHub account and API are fine, and the problem is +100% the value sitting in the deployment's environment variable — not +something about your account being rate-limited or restricted. + +**Fix**: on GitHub, open the PAT and use **Regenerate token** (keeps the +same scopes and note, issues a fresh value) rather than creating a brand +new one from scratch — this way you only need to update the value in one +place per registered variable. Copy the new value via **triple-click** +(selects the whole line cleanly, unlike drag-select) → paste into each +place it's registered, overwriting the old value completely → redeploy. + +## After any fix in this file + +An environment variable change never applies to an already-running +deployment. Trigger a new deploy, then re-check the *actual served +content* — not just that the deploy job reported success, which only +proves the build compiled, not that the card renders correctly: + +```bash +curl -sS "" | grep -o "Failed to retrieve\|Missing token\|was not found\|Total Contributions" +``` + +If you're running the static-SVG setup, also trigger that generation +workflow once (rather than waiting for its next scheduled run) so the fix +reaches the README immediately instead of sitting behind the schedule gap. diff --git a/github-profile-stats-deploy/references/secrets-and-tokens.md b/github-profile-stats-deploy/references/secrets-and-tokens.md new file mode 100644 index 0000000..3388108 --- /dev/null +++ b/github-profile-stats-deploy/references/secrets-and-tokens.md @@ -0,0 +1,94 @@ +# Secrets & tokens reference + +Everything this setup depends on, where to create it, and where it goes. +Referenced from SKILL.md's setup steps 2 and 4. + +## GitHub Personal Access Token (the root credential) + +- **Create at**: GitHub → Settings → Developer settings → Personal access + tokens → Tokens (classic) → Generate new token + (`https://github.com/settings/tokens/new`) +- **Scopes**: `repo` + `read:user` +- **Expiration**: prefer no expiration or a long one. An expired token is + the single most common cause of these cards breaking, and the failure + mode is confusing (see `diagnosing-errors.md`) because nothing about the + deployment itself changes — only the token silently stops working. +- **Used by**: both forks' PHP code, to call the GitHub GraphQL API for + contribution/streak data. +- The **same PAT value can be reused for every fork/project** you deploy — + just paste it under each project's expected env var name. The PAT's + GitHub-side "Note" field is a cosmetic label only and has no bearing on + which Vercel variable name it needs to go into. Don't assume the Note + matches the variable name. + +## Vercel project environment variables (where the PAT is registered) + +Register at: Vercel → your project → Settings → Environment Variables. + +The **variable name is decided by the fork's own source code, not by +convention** — different forks of stat-card generators use different names +(`PAT_1`, `TOKEN`, `GITHUB_TOKEN`, etc.). Before registering, confirm the +actual name the code checks for: + +```bash +gh api repos///contents/api/index.php --jq '.content' | base64 -d | grep -i "_ENV\[" +``` + +or, if the fork isn't PHP: + +```bash +gh search code "process.env" --repo / +``` + +Leave the "Environment" setting at its default (All Environments / covers +Production) — no need to hand-pick. + +**Any time the PAT is rotated, every project's variable needs to be updated +and the project redeployed for the change to take effect.** Environment +variable changes never apply retroactively to an already-running +deployment — this is the single most common reason a "fixed" token still +shows the old error for a while after you thought you fixed it. + +## GitHub Actions secrets (only needed if you deploy via Actions instead of Vercel's Git integration) + +If a fork's Vercel deployment needs to go through GitHub Actions instead of +Vercel's native Git integration (see `vercel-deploy-bug.md` for when that's +necessary), register these at: that fork's repo → Settings → Secrets and +variables → Actions. + +| Secret | Value | How to get it | +|---|---|---| +| `VERCEL_TOKEN` | a Vercel API token | Vercel → Account Settings → Tokens → Create Token (`https://vercel.com/account/tokens`). **Must be Full Account/Team scope, not project-scoped** — a project-scoped token can call plain REST endpoints fine but `vercel pull` (which the CLI needs for org/project context) fails with "Could not retrieve Project Settings" on a project-scoped token. | +| `VERCEL_ORG_ID` | a `team_...`-prefixed id | Vercel → Team Settings → General → Team ID. Even a personal Hobby account (not a real multi-person team) exposes this. **Not** the "User ID" under Account Settings — that has no `team_` prefix and is the wrong id for this purpose despite looking like the obvious "personal account id". | +| `VERCEL_PROJECT_ID` | a `prj_...`-prefixed id | Target project → Settings → General → Project ID. | + +`VERCEL_TOKEN` is the one genuine secret of the three — paste it directly in +the GitHub UI rather than through a shell history or into a chat with an AI +assistant. `VERCEL_ORG_ID` and `VERCEL_PROJECT_ID` are plain identifiers +(not credentials), safe to retrieve and set via +`gh secret set NAME --repo owner/repo --body "value"` to avoid UI +copy-paste mistakes. + +If you need to derive `VERCEL_ORG_ID`/`VERCEL_PROJECT_ID` from a token you +already have: + +```bash +curl -s -H "Authorization: Bearer $VERCEL_TOKEN" \ + "https://api.vercel.com/v9/projects/" \ + | python3 -c "import json,sys; d=json.load(sys.stdin); print('accountId:', d['accountId']); print('id:', d['id'])" +``` + +`accountId` → `VERCEL_ORG_ID`, `id` → `VERCEL_PROJECT_ID` (the JSON has +other nested `id` fields, e.g. under `latestDeployments` — parse the +top-level key specifically, don't grep the first `"id"` match in the raw +response). Run this in your own terminal; the response fields themselves +are safe to share, but never paste `VERCEL_TOKEN` itself anywhere outside +your own shell. + +## The repo that hosts the README + +If your static-SVG generation workflow lives in the same repo as the +profile README, it needs no extra secrets — a workflow's auto-issued +`GITHUB_TOKEN` (via `permissions: contents: write`) is enough to commit the +regenerated SVGs. It only fetches from public Vercel URLs, with no auth +needed on that side. diff --git a/github-profile-stats-deploy/references/vercel-deploy-bug.md b/github-profile-stats-deploy/references/vercel-deploy-bug.md new file mode 100644 index 0000000..66c164e --- /dev/null +++ b/github-profile-stats-deploy/references/vercel-deploy-bug.md @@ -0,0 +1,86 @@ +# Vercel function-count deploy bug + +Referenced from SKILL.md's setup step 5 and fix step 3. + +Some PHP-based stat-card forks (notably ones using the community +`vercel-php` runtime) can hit a **known unresolved Vercel platform bug**: +excluded files (via `.vercelignore` or `excludeFiles`) are not respected +during a GitHub-triggered auto-deploy's serverless-function auto-detection, +even though a manual `vercel --prod` CLI deploy from your own machine works +fine. Reported by the `vercel-php` maintainer themselves: +https://github.com/vercel-community/php/issues/331 — unresolved as of this +writing. + +**Symptom**: `Error: No more than 12 Serverless Functions can be added to a +Deployment on the Hobby plan`, and the function count doesn't match what +`.vercelignore` should have excluded (e.g. test files or scripts under +`tests/` or `scripts/` get counted as functions even though they're +excluded). + +## Fix: bypass zero-config detection with the legacy `builds` array + +`vercel.json`'s newer `functions` property *coexists* with Vercel's +zero-config auto-detection, which scans every matching file in the whole +repo as a potential function/route regardless of what `functions` itself +specifies. The older `builds` array fully replaces zero-config detection +instead, so it's the one that actually respects an explicit scope: + +```json +{ + "builds": [ + { "src": "api/**/*.php", "use": "vercel-php@0.9.0" } + ], + "routes": [ + { "handle": "filesystem" }, + { "src": "/", "dest": "/api/index.php" } + ] +} +``` + +Adjust the `src` globs and routes to match your fork's actual API entry +points. + +## Fix: deploy via GitHub Actions instead of Vercel's Git integration + +Since the bug is specific to Vercel's *Git-integration-triggered* build, +switching to a GitHub Actions workflow that runs the Vercel CLI directly +sidesteps it entirely — this is the same workaround the `vercel-php` +maintainer uses in their own projects. + +**Important gotcha**: don't use the `vercel pull` → `vercel build --prod` → +`vercel deploy --prebuilt` three-step flow. `--prebuilt` is for the newer +Build Output API and is incompatible with the legacy `builds` config above +— it ignores the local build output and causes something like +`ENOENT: vendor/autoload.php` on the remote side. Use a single-step remote +deploy instead: + +```yaml +name: Deploy to Vercel + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + steps: + - uses: actions/checkout@v4 + - name: Install Vercel CLI + run: npm install --global vercel@latest + - name: Deploy to Vercel + run: vercel deploy --prod --yes --token=${{ secrets.VERCEL_TOKEN }} +``` + +See `secrets-and-tokens.md` for how to create the three secrets this +workflow needs. + +Once you switch to Actions-based deploys, **disconnect Vercel's Git +integration** for that project (Vercel → Project → Settings → Git → +Disconnect). Otherwise every push triggers two competing deployments: the +broken auto-deploy that still hits this bug, and the working one from your +workflow — and it becomes unclear which one is actually live.