Releasing · page 3 of 4
The deploy-github step and its options, and the Supabase migration steps.
Four optional ship steps take a story past the merge: deploy-github waits for the host’s production deployment, and prepare-migrations, supabase-credentials and migrate-supabase get a branch’s database migrations onto a hosted Supabase project before the code lands. This page is for a project on Vercel, Netlify or another host that reports its deployments to GitHub, with or without Supabase behind it. The order of the steps, the exit codes and the ship lock are on How a story ships; what happens when a deploy fails is on Release safety.
deploy-github
Hosts such as Vercel report each deployment to GitHub. The deploy-github step takes the merge commit’s sha (the sha fact the merge step recorded) and polls GitHub’s deployments for that sha in the configured environment every 15 seconds, reading the newest status of the newest deployment, until the status is final or the time runs out. success passes. Anything else exits 5 and writes HALT: the base branch already has the code, so production must be repaired by hand. The reasons are “<environment> deployment of <sha> ended as “failure”” (or error or inactive), “did not finish within N minutes (last seen “pending”)“, and “No merge commit is known, so the <environment> deployment cannot be found” when merge could not record a sha.
Its options live under ship["deploy-github"]:
| Name | What it does | Default |
|---|---|---|
environment |
The GitHub deployment environment to watch. | Production |
timeoutMinutes |
How long to wait for the deployment to finish. Also a dashboard setting, applied to the next story. | 15 |
statusContext |
The name of the commit status the host posts, such as Vercel. A host can refuse a deployment outright, for instance when a plan’s daily cap is reached; then no deployment is created and waiting would only time out. With statusContext set, while no deployment exists the step reads the commit’s statuses, and a failure or error under that name stops the release at once with the host’s own description: “Vercel refused the deployment: <description>. Production still runs the previous version.” |
none |
rollbackHint |
Your own words on how to roll back, such as “run vercel rollback –yes in the project folder”. The halt email adds it to a deploy, smoke or watch halt when production was not rolled back. The step never puts it in its own reason, so the email says it once. | none |
The step is marked as a deploy, so once it passes the new build is live, and a later failure (the smoke check, the watch) may roll production back when ship.rollback.command is set. Its health check, run by foreman health before a halt clears, asks whether the base branch’s newest commit has a successful deployment in the environment, any of them, since a redeploy makes a new one. A story merged but never deployed is not live, so it is not fixed.
The GitHub token the release runs with needs to read deployments: a classic token with the repo scope has that; a fine-grained token needs Deployments read as well as the permissions on Run in the cloud.
prepare-migrations
Migrations apply in name order, and a Supabase migration file starts with a 14-digit UTC stamp, YYYYMMDDHHMMSS. When another story landed a migration on the base while this branch was open, the branch’s own migrations may now sort before the base’s newest. prepare-migrations runs before anything is pushed and puts that right.
It lists the migration files the branch adds against the base in ship["prepare-migrations"].dir (supabase/migrations by default) and the base’s own files there. When any branch file’s stamp is at or before the base’s newest, every branch file is renamed in order, starting at the later of now and the base’s newest plus one second, one second apart, with git mv, and the move is committed as “<id> Renumber migration after <base> moved”. It then records the migrations fact, every migration file the branch changes against the base, for supabase-credentials and migrate-supabase to read; when it renamed anything, renumbered lists each { from, to }. Any git command failing exits 1.
supabase-credentials
A branch with migrations cannot ship without the hosted database, so missing credentials stop the release early rather than after CI has spent its minutes. supabase-credentials reads the migrations fact (or, run on its own, lists what the branch changes in ship["migrate-supabase"].dir). With no migrations it passes. Otherwise it checks that SUPABASE_ACCESS_TOKEN and SUPABASE_DB_PASSWORD are set in the release’s environment, and exits 3 when either is missing: “Missing in <envFile>: <names>”. Names only; no value is ever printed. The story session then blocks the story and adds an owner task naming the credentials.
Put it after resources and before push: a branch that needs a resource another slot holds stops with 7 first, and foreman ship --preflight warns when migrate-supabase is in the list without supabase-credentials before push.
migrate-supabase
migrate-supabase applies the branch’s migrations to the hosted Supabase project after CI and before the merge. Database before code: the new code is written for the new schema, and migrations only ever add to what the running code needs. With no migrations it does nothing. Otherwise it runs supabase link --project-ref <projectRef> and supabase db push --linked --yes, with the Supabase CLI, in an environment without the slot’s own SUPABASE_PROJECT_ID and SUPABASE_*_PORT variables, since the CLI would read those as the live project.
| Name | What it does | Default |
|---|---|---|
projectRef |
The hosted project’s reference. Required; with migrations to apply and no reference, the step exits 1 and the preflight warns. | none |
dir |
The migrations folder. | supabase/migrations |
A failed push exits 4 and writes HALT; nothing was merged, and Foreman does not retry. A successful push passes the point of no return: the live database has changed, so a failure in any later step exits 5 and halts, and a release cut off here while the run died is finished at the next start rather than redone. Its health check asks the Supabase Management API, with SUPABASE_ACCESS_TOKEN in the request header and a 15 second limit, whether the hosted project has exactly the base branch’s migrations, and names any missing or extra.
Put mergeable-recheck right before it, so a base branch that moved while CI ran is brought in again before the live database changes; the preflight warns when it is not there.
The Supabase slot environment (env.adapter: "supabase") is a different thing: one local Supabase stack per slot, with every port moved up so slots do not collide, for building and testing. It shares only the rule above about keeping slot variables out of the hosted commands. See Slots and environments.
A ship block for a Vercel project with a hosted Supabase database:
"ship": {
"steps": [
"merge-main", "prepare-migrations", "resources", "supabase-credentials", "push", "pr", "ci",
"mergeable-recheck", "migrate-supabase", "merge", "deploy-github", "smoke", "watch"
],
"ci": { "required": ["test"], "timeoutMinutes": 30 },
"prepare-migrations": { "dir": "supabase/migrations" },
"migrate-supabase": { "projectRef": "acmeqaproject12345", "dir": "supabase/migrations" },
"deploy-github": {
"environment": "Production",
"timeoutMinutes": 15,
"statusContext": "Vercel",
"rollbackHint": "run `vercel rollback --yes` in the acme folder"
},
"smoke": {
"urls": ["https://acme.example", "https://acme.example/api/health"],
"expect": { "https://acme.example/api/health": 200 }
},
"watch": { "command": "./scripts/errors-since-deploy.sh", "minutes": 10, "everySeconds": 60 },
"rollback": { "command": "vercel rollback --yes" }
}
SUPABASE_ACCESS_TOKEN and SUPABASE_DB_PASSWORD go in the project’s environment file (.env.local by default), which foreman ship loads itself; foreman ship --preflight lists any that is missing, by name.
Was this page useful? Something missing or wrong?Email us and say which section.