Central verification (core/ci)
FORGE and every repository it creates are verified centrally, by CORE, and the verdict is reported as one commit status, core/ci. This is not a deploy pipeline. Nothing deploys a forged app yet. A green core/ci means CORE’s verification passed, and nothing more.
The owner’s decision
“We are not configuring at this level. The github marketplace core will handle all forge functionalities (baked in core).” (2026-09-25)
So org-runink/forge and every repository FORGE creates (topic forged) carry no per-repo CI and no per-repo App credentials: no APP_ID or APP_PRIVATE_KEY copies, ever. On GitHub Free, org secrets do not reach private repositories, and each copied key could mint installation tokens for the whole org. Do not “fix” a forged repository’s missing CI by adding a workflow or a key to it. A repository FORGE creates tomorrow is covered the moment it is tagged forged.
The flow
The sweep finds unverified commits
core-verify-sweep.yml runs on */10 * * * * (and workflow_dispatch). It lists the org’s repositories with /orgs/{org}/repos, not search: the search index lags, and a repository forged a minute ago is exactly the one someone is waiting on. It keeps forge plus every repository tagged forged, and skips archived and disabled ones.
For each target it collects the default-branch head first, then each open PR head. It picks the first SHA that has no core/ci status, or whose core/ci status has been pending for more than STALE_MINUTES (90).
It claims, then dispatches
Before dispatching, it sets core/ci = pending with the description “queued for CORE verification”. The claim comes first, so the next sweep skips the commit even if this run dies. It then sends repository_dispatch event type core-verify to org-runink/core with {repo, sha, ref, kind}.
The dispatch happens this way because the runink App has statuses: write and contents: write but only checks: read and actions: read. It cannot create check-runs in another repo, and workflow_dispatch returns 403.
core-verify.yml verifies in CORE
core-verify.yml runs in org-runink/core. It:
- validates the target (
org/<name>and a 40-hex SHA); - mints an App token scoped to the target only (plus
securityandstoreforforge, whose private siblings it must clone); - re-derives the profile from the repo itself. The payload’s
kindis not trusted; - refuses anything that is public, or that is neither
forgenor taggedforged; - sets
core/cito “CORE verification running (<kind>)”; - checks out the SHA with
persist-credentials: false, and runs the profile’s checks. No step that runs the target’s code has a token in its environment.
It closes core/ci
A final step under always() mints a fresh token and posts the verdict. A cancelled or timed-out run therefore still closes its status, as error.
Throughput and safety rules
- One dispatch per sweep (
MAX_DISPATCH: "1").core-verify.ymlruns in a single concurrency group,core-verify. GitHub keeps one pending run per group and cancels the older one, so the sweep does not dispatch at all while acore-verifyrun is already waiting. That gives at most six verifications an hour, one at a time. - Default-branch heads first. They are what FORGE shows. A PR head waits behind every unverified default branch.
- Public repositories and fork PRs are never dispatched. Verification builds and runs the target’s code on a self-hosted runner.
core-verify.ymlalso refuses a public repository dispatched by hand: "… is public — its code is never built on a self-hosted runner". - Stale pending recovers itself.
core-verify.ymltimes out at 45 minutes and waits behind at most one running verification. Apendingstatus older than 90 minutes therefore has no run behind it and is re-dispatched. - It never merges, pushes or comments. It only checks, and posts statuses.
The status contract
FORGE reads this. Do not deviate.
| Field | Meaning |
|---|---|
| context | core/ci, on the verified commit |
pending | queued (by the sweep) or running (in core-verify.yml) |
success | every check of the profile passed |
failure | a check failed, including “Nothing to verify yet” for an empty pipeline |
error | the infrastructure could not run the checks: token, checkout, a missing toolchain, or a cancelled or timed-out run |
target_url | the core-verify.yml run that decided it |
| description | at most 140 characters, and says why |
Success descriptions per profile:
| Profile | core/ci success description |
|---|---|
forge | go vet/build/test -race and flutter analyze/test/build passed |
web | go build/vet/test and npm ci/build passed |
pipeline | <n> pipeline file(s) parse as TOML; go build/vet/test passed over <p> package(s), or …; no Go package |
Projects with forge.yaml: one status per lane
When a forged repository carries forge.yaml, the topic profile is not run. forgecheck manifest, then forgecheck lanes, then forgecheck aggregate (under always()) run instead, and post:
core/ci/<lane id>per lane, with the same states and meanings ascore/ci. A lane with nothing to check isfailurewith a description startingNothing to verify yet. Every lane context goespending(“CORE verification running”) as soon as the manifest is read.core/ci, the aggregate, set to “CORE verification running (N components, forge.yaml)” while running, and then:
| Condition | State | Description |
|---|---|---|
any lane error | error | N/M components could not be verified — <id>: <why> |
| any real lane failure | failure | N/M components failed — <id>: <why> |
| every lane has nothing to verify | failure | Nothing to verify yet: no component has code yet |
| otherwise | success | P/M components verified[, E with nothing to verify yet] |
A lane with no recorded result (a cancel or timeout) closes as error: “verification did not complete — cancelled or timed out”. An invalid manifest posts core/ci = failure with forge.yaml is invalid: <first reason> and no per-lane status. Repositories without forge.yaml are verified exactly as before the manifest existed.
Who reads core/ci
- FORGE, for its “CI build” stage. This is FORGE’s own reading of the same fact.
GET /api/forgein CORE’s console, which derivesbuilding/live/failed/idlefrom thecore/cistatus on the default-branch head. It never reads the repo’s Actions runs. See API Reference.
For every workflow in the platform, see CI workflows.