Skip to content
Central verification

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 security and store for forge, whose private siblings it must clone);
  • re-derives the profile from the repo itself. The payload’s kind is not trusted;
  • refuses anything that is public, or that is neither forge nor tagged forged;
  • sets core/ci to “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.

See Verification profiles.

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.yml runs 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 a core-verify run 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.yml also 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.yml times out at 45 minutes and waits behind at most one running verification. A pending status 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.

FieldMeaning
contextcore/ci, on the verified commit
pendingqueued (by the sweep) or running (in core-verify.yml)
successevery check of the profile passed
failurea check failed, including “Nothing to verify yet” for an empty pipeline
errorthe infrastructure could not run the checks: token, checkout, a missing toolchain, or a cancelled or timed-out run
target_urlthe core-verify.yml run that decided it
descriptionat most 140 characters, and says why

Success descriptions per profile:

Profilecore/ci success description
forgego vet/build/test -race and flutter analyze/test/build passed
webgo 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 as core/ci. A lane with nothing to check is failure with a description starting Nothing to verify yet. Every lane context goes pending (“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:
ConditionStateDescription
any lane errorerrorN/M components could not be verified — <id>: <why>
any real lane failurefailureN/M components failed — <id>: <why>
every lane has nothing to verifyfailureNothing to verify yet: no component has code yet
otherwisesuccessP/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/forge in CORE’s console, which derives building / live / failed / idle from the core/ci status 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.