Create a GitHub App with these repository permissions:
- Checks: read and write
- Contents: read
- Actions: read and write
- Pull requests: read
- Metadata: read
Installation tokens are narrowed to the configured pilot repository and inherit the permissions accepted for that installation. Keep the App itself limited to the list above; CIHash does not request a broader token permission set.
Subscribe the App webhook to:
- Pull request
- Workflow run
Use a randomly generated webhook secret with at least 32 bytes. Install the App only on pilot repositories during shadow validation.
The App private key, webhook secret, and receipt-signing key are separate credentials. The submitted workload receives none of them.
cihash serve reads a non-secret JSON configuration:
{
"listen": "127.0.0.1:8080",
"webhookPath": "/webhooks/github",
"repository": "owner/project",
"checkName": "cihash/verify",
"policyFile": "./policy.json",
"receiptPublicKeyFile": "./receipt-signing.pub.pem",
"receiptStore": "./var/receipts",
"stateDirectory": "./var/state",
"mode": "shadow",
"shadowWorkflow": "CI",
"shadowJob": "tooling",
"buildMode": "production",
"fallbackWorkflow": "cihash-fallback.yml",
"detailsUrl": "https://cihash.example/checks",
"githubApiBaseUrl": "https://api.github.com"
}Relative paths resolve from the configuration file's directory. Keep secrets out of this file. Provision stateDirectory before starting the service; run-grant durability treats that directory as an existing administrator-owned boundary.
Supply credentials through the service environment:
CIHASH_GITHUB_CLIENT_ID
CIHASH_GITHUB_PRIVATE_KEY_PATH
CIHASH_GITHUB_WEBHOOK_SECRET
CIHASH_PRODUCER_TOKEN
GitHub recommends the App client ID as the JWT iss claim. CIHash signs RS256 JWTs with iat 60 seconds in the past and exp nine minutes in the future, then exchanges them for one-hour installation tokens scoped to the configured repository and the checks:write and actions:write permissions.
Validate configuration and credential parsing without opening a listener:
cihash serve --config ./hosted.json --check-configRun the service:
cihash serve --config ./hosted.jsonExpose only the configured webhook path through the deployment proxy. GitHub sends X-Hub-Signature-256; CIHash verifies the HMAC before parsing or recording the delivery.
The v0.1 control-plane protocol exposes authenticated endpoints for one server-authorized run:
POST /api/v1/runsaccepts an installation ID and pull-request number, resolves the current same-repository head, base, and GitHub merge-tree identity, persists a grant, and returns it;POST /api/v1/runs/{run-id}/receiptverifies the signed receipt and uploaded log against that exact grant before binding and storing the evidence.
The grant includes the approved command and structured immutable execution environment, so the producer does not read verification authority from the submitted tree. A distinct server nonce binds the resulting receipt to the grant. The run ID and nonce remain proof bindings rather than credentials; both endpoints require the separately configured producer bearer token.
Grant state moves only from issued to submitted to consumed. Initial
submission and consumption must both occur before expiry. Expired grants,
replacement receipt digests, and out-of-order transitions fail closed.
Repeating the same verified receipt submission or consumption is idempotent only
while the grant remains valid.
For opened, reopened, synchronize, ready_for_review, and edited events:
- read the current PR through GitHub's
2022-11-28REST representation, which still exposes the documentedmerge_commit_shaomitted by2026-03-10, instead of trusting revision fields in the webhook body; fetch that test-merge commit and require its two parents to equal the current base and head before accepting its tree; - reject fork PRs, which are outside the v0.1 trust boundary;
- derive the expected proof identity from the exact current head and base SHAs plus the approved server-side policy;
- load only evidence bound to an immutable server-issued run and matching submitted receipt digest;
- verify the receipt against the grant's nonce, timing, architecture, policy, and the current GitHub merge tree;
- atomically consume the valid run before publishing an App-owned
cihash/verifysuccess check; - publish
neutralin shadow mode when proof reuse is rejected; - create a queued check and dispatch the trusted fallback workflow in enforcement mode.
Webhook delivery IDs are persisted. Completed or concurrent duplicate deliveries do not create another check.
Evaluation happens at supported pull-request events and, for verified
successful receipts, immediately at submission. POST /api/v1/runs/{run-id}/receipt verifies and stores the evidence, then
re-evaluates the granted pull request: the service re-resolves the current
head, base, and merge tree through GitHub and acts only when they still equal
the granted revisions. Anything else is logged and left to the event-driven
path, which stays authoritative.
- In shadow mode, an accepted late proof publishes a new completed App check and records a shadow decision.
- In enforcement mode, an accepted late proof performs three separate ordered
operations: it consumes the run through the store's locked monotonic
lifecycle, updates this pull request's own pending queued check to
success, and only after a successful update marks the dispatched fallbacksupersededso its laterworkflow_runcompletion is ignored. If the check update fails, the fallback state is untouched and the fallback remains authoritative. If the state transition fails after a successful update, the fallback later overwrites the check with its real conclusion, which errs closed. If the fallback already completed the check, the late proof changes nothing. - Rejected and diagnostic receipts never publish a check and never dispatch fallback from the submission path.
Fallback state is indexed by repository, installation, pull request, and the App check's external identity, so pull requests that share a head commit cannot supersede each other's checks.
Because grant issuance resolves an existing pull request, the first evaluation
of a freshly pushed head still records proof_missing and, in enforcement
mode, dispatches fallback. The receipt-triggered path exists so evidence that
completes minutes later still concludes that check without waiting for the
fallback workflow. Superseding removes only the fallback's authority, not its
compute: the dispatched run finishes harmlessly.
In shadow mode, shadowWorkflow and shadowJob select the ordinary Actions
job used for parity evidence. CIHash accepts only first-attempt pull_request
runs whose webhook identifies exactly one pull request with the recorded head
and base. It then fetches the selected job through the installation-scoped
GitHub API and binds that immutable run to every CIHash evaluation of the same
pull request, head, and base. A push, manual dispatch, rerun, different base, or
second conflicting run cannot replace the evidence. CIHash records the proof
decision, rejection code, ordinary job conclusion, proof verification latency,
App decision latency, ordinary job duration, service source revision, service
binary digest, build mode, policy timeout, and observation timestamps in the
administrator-owned state directory. Aggregate workflow conclusions are not
used as a substitute for the selected job.
Observations are immutable audit facts: a recorded proof decision and its
correlated workflow evidence never change parity once written. Because
real repositories accumulate pending observations whose matching workflow
evidence never arrives (lost workflow_run deliveries, force-pushed heads,
merged or closed pull requests), an all-time readiness gate would stay
permanently blocked. CIHash instead derives enforcementReady over a
fixed eligibility window (shadow.ReadinessWindow, 24 hours). The window
is a package constant, not a per-invocation parameter: a report caller
cannot choose a cutoff that includes a recent match while excluding nearby
pending or mismatch evidence. Every parity class ages out of the window
together; no class is selectively expired, and no observation is relabeled
or removed. The all-time counts (total, matches, mismatches,
pending, comparable, notComparable) remain in the report for audit,
and windowSeconds records the fixed window governing the readiness
sample.
enforcementReady is true only when the windowed sample contains at least
one comparable observation, zero pending observations, zero mismatches, and
complete production build evidence. A single historical match cannot
authorize readiness after every other observation ages out, because the
window requires a fresh comparable observation. Manual state repair cannot
manufacture readiness: only a clean comparable sample inside the fixed
window can.
The fallback workflow must live on the protected base branch and declare these workflow_dispatch inputs:
cihash_fallback_idcihash_head_shacihash_base_shacihash_external_idcihash_policy_digest
The committed .github/workflows/cihash-fallback.yml is the CIHash repository's reference implementation. It checks out the exact base SHA, fetches the exact head SHA, prepares their merge tree, and runs the same formatting, vet, and test gates as the repository's ordinary CI workflow. actions/checkout and actions/setup-go are pinned to release commits.
CIHash records the workflow run ID returned by GitHub's dispatch endpoint. A signed workflow_run: completed webhook can finish the queued check only when:
- its run ID matches the recorded dispatch;
- repository and App installation match;
- event is
workflow_dispatch; - workflow head branch equals the recorded base branch;
- workflow head SHA equals the recorded base SHA;
- the fallback authorization has not expired.
Every mismatch finishes fail-closed as stale or failure. The fallback workload never receives authority to update the App-owned check.
- Install in a sandbox repository with
mode: shadow. - Confirm accepted proofs and ordinary CI agree.
- Confirm every mismatch produces a neutral explanation without affecting merges.
- Switch to
mode: enforcewhile leaving existing required CI intact. - Configure the ruleset to require branches to be up to date before merging. This prevents a success check attached to a head SHA from surviving a later base-branch change.
- Exercise missing, expired, stale-base, failed, cancelled, and timed-out fallback cases.
- Require
cihash/verifyfrom the CIHash App only after the shadow evidence has zero unexplained mismatches.