Ship verification — Coolify
Watches a commit from push → GitHub CI → Coolify deployment → live, then visually confirms the site renders. Report anything broken; stop once the deploy is live and you've eyeballed it.
This is the Coolify member of the ship-* family. The platform-agnostic orchestration (CI gate, poll loop, result contract) lives in scripts/ship/core.ts; this skill's probe is scripts/ship/coolify.ts.
Prerequisites
Everything is read from the environment — nothing about your instance is committed to this repo.
| Variable | Required | Purpose |
|---|---|---|
COOLIFY_BASE_URL |
yes | Instance root, no trailing /api/v1 (e.g. https://coolify.example.net) |
COOLIFY_ACCESS_TOKEN |
yes | API token — Coolify → Keys & Tokens → API tokens |
COOLIFY_APP_UUID_<ENV> |
one of | Per-environment app UUID, e.g. COOLIFY_APP_UUID_STAGE |
COOLIFY_APP_UUID |
one of | Fallback when no per-environment UUID is set |
Put them in .env (see .env.sample). It is gitignored and secretlint-ignored, and bun auto-loads it from the project root, so scripts/ship/coolify.ts picks them up with no shell setup at all. Values stay scoped to this repo, which matters because each project is a different Coolify app.
This works identically in Claude Code, Cursor, Codex, Copilot, and opencode — no per-agent MCP config and no incompatible substitution syntax.
For raw curl in a shell, .env is not loaded automatically — source it first:
set -a; source .env; set +a
Also needs gh authenticated (for the CI gate) and playwright-cli available — always open with --browser=chromium (see AGENTS.md).
Steps
Know the commit. The SHA under test is the one pushed (defaults to
git rev-parse HEAD).Run the poller — it blocks until CI + deployment are done, or fails fast:
bun scripts/ship/coolify.ts # verify current HEAD on stage bun scripts/ship/coolify.ts --sha=<sha> bun scripts/ship/coolify.ts --env=production --branch=main bun scripts/ship/coolify.ts --app=<uuid> # explicit app override bun scripts/ship/coolify.ts --no-ci # skip the GitHub CI gateIt gates on, in order: GitHub CI
conclusion==success→ thedeployworkflow run for that SHA concludingsuccess→ the Coolify deployment it triggered reachingstatus=="finished". Exit0with a final{"ok":true,...}line means it's deployed. Non-zero +{"ok":false,"error":...}means it broke — surface the error verbatim.Why three gates and not two. The app uses the
dockerimagebuild pack, so Coolify pulls a prebuilt image and never learns which commit it is running — every deployment record readscommit: "HEAD". Thedeployworkflow run is the only thing that ties a commit to a deployment, since it fires the webhook solely for its own SHA. Its success alone is not enough either: the webhook returns as soon as Coolify queues the build, so the run can go green while the deployment is still building or about to fail. The poller therefore uses the run's start time to pick out the deployment it triggered, then waits on that deployment's real status.Visual check (only after the poller succeeds) — use the environment's URL:
playwright-cli open --browser=chromium <url> playwright-cli screenshot --filename=.playwright-cli/ship-coolify.png playwright-cli close-allReadthe PNG. Confirm the page renders (nav, hero, no obvious breakage) and note the console error count from theopenoutput.Report. State: commit shipped, deployment status, deployment UUID, and the visual result. If this is a first live run or anything looks off, call it out explicitly. Do not open the PR into main — that's the user's call.
Stop condition
Stop when the poller returns ok:true and you've done the visual check. If the poller fails at any gate, stop and report the exact failure (which gate, which run/deployment id) instead of pushing on.
Coolify REST API reference
Base URL is $COOLIFY_BASE_URL/api/v1. Auth is a bearer token on every request. Source .env first so the variables resolve in the shell:
set -a; source .env; set +a
curl -s -H "Authorization: Bearer $COOLIFY_ACCESS_TOKEN" \
"$COOLIFY_BASE_URL/api/v1/applications"
Never echo the token, and never paste a resolved URL or UUID into a committed file.
Endpoints worth knowing
| Method | Path | Use |
|---|---|---|
GET |
/health |
Liveness probe |
GET |
/version |
Instance version |
GET |
/applications |
List apps — this is how you discover a UUID instead of hardcoding one |
GET |
/applications/{uuid} |
App detail |
GET |
/applications/{uuid}/logs |
Container logs |
POST |
/applications/{uuid}/restart |
Restart |
GET |
/deployments |
Currently running deployments only |
GET |
/deployments/applications/{uuid}?take=N |
Deployment history for one app |
GET |
/deployments/{uuid} |
One deployment by UUID |
POST |
/deployments/{uuid}/cancel |
Cancel a running deployment |
POST |
/deploy?uuid=<uuid>&force=<bool> |
Trigger a deploy (tag, pr also accepted) |
Deployment record
GET /deployments/{uuid} returns an ApplicationDeploymentQueue. The fields that matter for shipping:
commit— the git SHA, but only for git-based build packs. On adockerimageapp (this repo) it is always the literal string"HEAD", so it cannot be used to identify a push.created_at— the key this repo correlates against, bounded by thedeployworkflow run's start timestatus— see enum belowdeployment_uuid,application_name,commit_message,server_namelogs— build output, useful when a deploy failscreated_at/updated_at
Status enum
Verified against app/Enums/ApplicationDeploymentStatus.php:
| Value | Meaning |
|---|---|
queued |
Waiting to start — keep polling |
in_progress |
Building/deploying — keep polling |
finished |
Success |
failed |
Terminal failure |
cancelled-by-user |
Terminal, user-initiated |
finished is the only success value. Treat failed and cancelled-by-user as terminal and stop polling.
Finding an app UUID without hardcoding it
set -a; source .env; set +a
curl -s -H "Authorization: Bearer $COOLIFY_ACCESS_TOKEN" \
"$COOLIFY_BASE_URL/api/v1/applications" \
| jq -r '.[] | "\(.uuid) \(.name)"'
Take the UUID, add it to .env as COOLIFY_APP_UUID_<ENV>, and the poller resolves it automatically.
Notes
scripts/ship/coolify.tshardcodes no host, app name, or UUID — everything resolves from env or--app=, so this skill ships unchanged to any fork.- Coolify's published OpenAPI declares
GET /deployments/applications/{uuid}as returningApplication[]; the running API returns deployment records, sometimes wrapped. The provider normalizes bare arrays,{deployments:[…]}, and{data:[…]}. - SHA matching is prefix-tolerant in both directions — Coolify may store either a short or full commit hash.