Connect Your CI / Any Other Runner
CircleCI, Buildkite, Drone, Woodpecker, a cron job on a build box — if it can run curl, it can report status to UnityFreak. This is the same API the GitHub Actions, GitLab CI, and Jenkins recipes wrap.
POST /api/v1/repos/{owner}/{slug}/statuses/{sha} with header X-API-Key: <repo:write token> and body {"state": "pending" | "success" | "failure" | "error"}.context you chose to Required Status Checks in the repository's UnityFreak settings.One endpoint, authenticated with a repo:write-scoped personal access token (generate one under Settings → Personal Access Tokens) sent as the X-API-Key header — or, if your CI system handles secrets more naturally as a bearer credential, Authorization: Bearer <token> (also accepts GitHub's classic Authorization: token <token> scheme). Both carry the same token; if you send both headers on one request, X-API-Key wins.
state — one of pending, success, failure, error (required).context — a name for this check, e.g. ci/tests, ci/lint — post as many distinct contexts as you have build stages worth gating on separately. Defaults to default if omitted.targetUrl — link back to the build log (shown in the PR checks panel).description — free-text detail (e.g. "12 passed, 0 failed").Only state is required. Repeat posts for the same (sha, context) are append-only — the latest one wins.
Before the build:
curl -X POST "https://api.unityfreak.com/api/v1/repos/OWNER/REPO/statuses/SHA" \
-H "X-API-Key: $UF_API_KEY" \
-H "Content-Type: application/json" \
-d '{"state":"pending","context":"ci/tests","targetUrl":"https://ci.example.com/build/123"}'After it finishes:
curl -X POST "https://api.unityfreak.com/api/v1/repos/OWNER/REPO/statuses/SHA" \
-H "X-API-Key: $UF_API_KEY" \
-H "Content-Type: application/json" \
-d '{"state":"success","context":"ci/tests","description":"12 passed, 0 failed"}'Wraps both calls around any command — drop this into whatever runs your build, with UF_API_KEY, UF_OWNER, UF_REPO, and SHA exported however your runner exposes environment variables and the commit SHA:
#!/usr/bin/env bash
set -uo pipefail
report() {
curl -sf -X POST "https://api.unityfreak.com/api/v1/repos/$UF_OWNER/$UF_REPO/statuses/$SHA" \
-H "X-API-Key: $UF_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"state\":\"$1\",\"context\":\"ci/tests\"}"
}
report pending
npm ci && npm test
STATUS=$?
if [ $STATUS -eq 0 ]; then report success; else report failure; fi
exit $STATUSSTATUS=$? captures the test command's exit code before anything else can overwrite it, and the final exit $STATUS makes the script itself fail when the tests did — without it, the last thing that runs is a successful curl, so the runner's own job would report green even on a failing build (UnityFreak would still show the correct failure status either way).
In the repository's branch protection settings on UnityFreak, add your context to Required Status Checks. It only appears in that picker after being reported at least once.
GET /api/v1/repos/{owner}/{slug}/statuses/{sha} returns the combined state plus the latest status per context — no auth required for a public repository. Useful for a badge, a Slack notifier, or a dashboard built on top of your own CI.
Full request/response schemas are in the API documentation.
Every status you POST above — from this CI integration or from UnityFreak's own native checks — also fires a status event to any of the repository's webhooks subscribed to it (repo settings → Webhooks → Events → Commit status created). Useful for mirroring build results into another system without polling GET .../statuses/{sha} yourself:
{
"event": "status",
"sha": "a1b2c3d4e5f6...",
"context": "ci/tests",
"state": "success",
"description": "12 passed, 0 failed",
"targetUrl": "https://ci.example.com/build/123",
"creatorId": "9f1c...-a PAT/API-key user id, or null for a native check",
"deliveryId": "..."
}Signed with HMAC-SHA256 in the X-Signature header, same as every other UnityFreak webhook delivery.
POST /api/v1/repos/{owner}/{slug}/check-runs is the same authenticated act as the plain status POST above — one CI system, one token, one trust decision — but lets you attach a full output log and per-line annotations that show up inline in the PR's diff, not just a one-line description:
curl -X POST "https://api.unityfreak.com/api/v1/repos/OWNER/REPO/check-runs" \
-H "X-API-Key: $UF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "ci/tests",
"sha": "SHA",
"status": "completed",
"conclusion": "failure",
"output": {
"summary": "12 passed, 3 failed",
"text": "FAIL src/foo.test.ts\n ✕ handles empty input\n...",
"annotations": [
{ "path": "src/foo.ts", "line": 42, "level": "failure", "message": "off-by-one on the last page" }
]
}
}'Every check-run POST also writes a plain status under the same context (name above becomes context on the paired row) — required checks, badges, auto-merge, and the status webhook above all keep working from this one call. A repeat POST for the same (sha, name) replaces the check run (unlike the append-only status log).
For a long-running build, PATCH the run as it progresses instead of waiting for the final POST — annotations you send in a PATCH append to what's already stored, capped at 50 total per run (with an explicit truncation flag past that, never a silent drop); output text is capped at 64 KB:
curl -X PATCH "https://api.unityfreak.com/api/v1/repos/OWNER/REPO/check-runs/RUN_ID" \
-H "X-API-Key: $UF_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status":"in_progress"}'Same reserved-prefix rule as context above: a name starting with unityfreak/ is rejected (422) — that prefix is reserved for UnityFreak's own native checks (secret scanning, large-file detection, policy lints).
Read this before wiring CI to pull_request events.
If your CI triggers on pull_request events, check fromFork before doing anything with the PR's code. A fork-sourced PR contains code written by someone who does not have write access to your repository. If your CI checks out that code and runs it (tests, builds, lint) in a job that has access to your repository's secrets, deploy keys, or a writable token, those secrets are effectively handed to the PR author — build scripts, test files, and even dependency manifests execute during CI. Run fork-sourced PRs with no secrets and read-only access, or require maintainer approval before CI runs. This is the vulnerability class known as a "pwn request"; it is the single most common CI compromise on every code-hosting platform.
Every pull_request webhook delivery now carries fromFork: boolean — always present, so you can gate on it without an existence check. On the opened and synchronize actions specifically, a fork-sourced delivery also carries a sourceRepo: {id, owner, slug, visibility} object identifying the fork:
{
"event": "pull_request",
"action": "opened",
"number": 42,
"title": "Fix the off-by-one in pagination",
"sourceBranch": "fix-pagination",
"targetBranch": "main",
"authorId": "9f1c2a3b-...-a-user-id",
"fromFork": true,
"sourceRepo": {
"id": "3ad0f1e2-...-a-repo-id",
"owner": "some-contributor",
"slug": "app",
"visibility": "public"
},
"deliveryId": "..."
}UnityFreak deliberately does not include the fork's clone URL or the internal ref your CI would need to check out its code — if you want to build fork-PR CI, you have to consciously construct that fetch yourself, which is exactly the point where you should stop and apply the rule above.
The action field also gains a synchronize value: it fires every time the contributor pushes a new commit to the fork while the PR stays open, re-delivering exactly the same fields as above (this event carries no commit sha — read the PR back via the API if you need the new head). Treat it exactly like opened for trust purposes — it is still fork-authored code. Both deliveries land on the target repository's own webhooks (repo settings → Webhooks → Events → Pull request opened or labeled) — the fork's own webhooks never see them. That same event also delivers labeled/unlabeled actions when a label changes on the PR — those still carry fromFork, but not sourceBranch, targetBranch, authorId, or sourceRepo — a much leaner payload than opened/synchronize.
If you've configured GitHub Actions before, this is the same concept as its pull_request vs. pull_request_target distinction — the former runs against the fork's code with no access to secrets, the latter runs with the base repository's secrets against a ref you choose (and is exactly what a pwn request exploits when that ref is chosen carelessly). fromFork is UnityFreak's equivalent signal — the decision of what to trust is still yours to make in your own CI config.