Reusing trusted CI results without weakening coverage
Reuse a signed CI result only when the work is provably equivalent, run only affected jobs, coalesce identical ones, and measure the time saved.
Last updated
Orgabot's CI runtime does not run the same verification twice when it can prove the result would be the same. A mission's local run, the pull request head, and the commit that lands on main after a squash merge often build exactly the same files. When they do, the job runs once and the other runs reuse its signed result.
Reuse only ever happens when equivalence is proven. Any doubt means the job runs. A job that runs when it did not need to costs a few minutes; a job that is skipped when it should have run is a gate reporting a pass for code nobody tested.
What must be equal
Every job that can be reused gets a reuse key. The key is a hash of everything that could change its result:
- The source. The git tree of the commit under test, so two commits with identical contents are the same work. A job that declares
pathshashes only the files it maps, plus shared scripts, configuration and lockfiles, plus any script its commands run, plus a fingerprint of every file no job maps. Scripts are followed transitively: a package manager script such asnpm run setupis resolved through thepackage.jsonit reads, and every script that runs is read for the files it calls and the modules it imports in turn. A shell glob such asbash checks/*.shis expanded against the tree and every matching script is followed. A Python module run by name, such aspython3 -m checks.gate, is resolved to its file in the tree (and a package's__main__.py) and followed the same way. An interpreter or package manager named by a path or version, such as/usr/bin/python3,python3.12,.venv/bin/pythonor/usr/local/bin/npm, is read as that program, so/usr/bin/python3 -m checks.gateis followed exactly likepython3 -m checks.gate. A command run through a package manager or runner, such asnpm exec -- node tools/gate.js,pnpm exec ...,yarn node ...ornpx node ..., is followed as a command of its own, including everything after--. When Orgabot cannot establish what a command runs (an unreadable script, an unparseablepackage.json, a flag such as--workspacesthat selects an unknown set of packages, acdinto a directory it cannot resolve, a script named by a variable such asbash $SETUP, a glob that runs a script but matches nothing, a bracket or brace expansion such aschecks/{a,b}.sh, a named script that is not in the tree, a tool such asmakethat picks its own files, an interpreter option that runs code Orgabot does not see, such aspython3 -c,node -e,node --require,node --testorbash -c(also when wrapped, as innpm exec -- node -e ...), a runner option such asnpx -pthat names what runs, a-mmodule whose package is in the tree but whose module is not, a path to an interpreter-like program Orgabot cannot name, such as/usr/bin/python3-dbg, a JavaScript or Python script that imports or runs code by a computed name such asimport(dir + file),execSync(cmd)ormodule["requ" + "ire"](path), a Python script that loads a module by name or runs source text even with a literal argument, such asimportlib.import_module("other.gate")orexec("import x"), a Pythonimportthat Orgabot cannot read as a statement (an import inside a one-line block such asif ok: import xortry: import xis read and followed, and importinga.balso follows the package filesa/__init__.pyanda/b/__init__.py), an import of a local relative module that is not in the tree or of a workspace package by name, or a script in a language other than shell, JavaScript, TypeScript or Python), the job is keyed to the whole tree instead. - The context. A plain commit, or a synthetic merge onto one exact base. A moved base is new work.
- The definition and policy. The job, every job it needs, and the target's settings.
- What actually runs. The registered project's setup and verification commands and its working directory. A job with no
commandsruns the project's verification, so adding a check there means earlier results no longer count. If that configuration cannot be read, the job always runs. - The toolchain. The controller's runtime version and every pinned image, action and tool version. An image or action that is not pinned to a digest or commit (for example
node:22oractions/checkout@v4) makes the job always run. - The platform. Operating system and CPU architecture, unless the job is declared
portable. - Trust. A fork pull request never shares results with a trusted push or pull request, so an untrusted pull request cannot plant a result a trusted run later stands on.
- Who ran it. The controller that signed the result.
- Non-secret configuration. The values of the environment variables a job lists in
configEnv. A name that looks like a secret is refused, and the job always runs. - External state. The vulnerability database version a security job checked against, or the resolved version of a mutable dependency set.
The key is signed into the result. A reused job carries a second signed statement naming the run it reused and the key they share, and the check that GitHub sees is computed from both. An edited run record cannot turn into a cached pass.
What is never reused
- Signing, notarization, publishing and deployment. Jobs with
kindset tosign,notarize,publishordeployalways run. Reusing artifacts and reconciling side effects are separate contracts. A job with nokindwhose id or commands look like one of these (for examplenpm publishorcodesign) is treated the same way; setkind: "verify"if it only checks something. - Always-run controls. A job with
reuse: "never", or one that listsnetworkinexternal, runs every time. - Security audits without fresh advisory data. A job with
kind: "security"is reused only when the advisory database version is known and was refreshed within the last 24 hours, and only while that version is unchanged. New advisory data means the audit runs again. - Scheduled, manual and forced runs. They exist to run. They only reuse their own earlier attempt when you retry the same request, and they never wait on another run that is executing the same job at the same moment.
- Failures. A failed result is never reused as a pass. If the newest run with the same key failed, an older pass with that key is not reused either, so a flaky or newly failing test cannot hide behind it.
- Old results. A pass is reused for up to 7 days by default (
maxEvidenceAgeHours), or 24 hours for a security job.
Affected jobs
By default a job's source input is the whole tree, so any change runs it. To run only the jobs a change affects, give each job the paths it depends on:
{
"id": "ci",
"sharedPaths": ["config/**"],
"ignorePaths": ["docs/**"],
"jobs": [
{ "id": "api", "required": true, "placement": "local", "commands": ["npm test -w api"], "paths": ["packages/api/**"] },
{ "id": "web", "required": true, "placement": "local", "commands": ["npm test -w web"], "paths": ["packages/web/**"], "needs": ["api"] },
{ "id": "macos", "required": true, "placement": "local", "kind": "platform", "commands": ["swift test"] },
{ "id": "audit", "required": true, "placement": "local", "kind": "security", "commands": ["npm audit"] }
]
}The selection is conservative:
- A job also depends on the paths of every job it
needs. - Lockfiles, package manifests, toolchain pins, build configuration,
.github/,.orgabot/,scripts/,bin/,ci/andtools/are inputs of every job, along with anything insharedPaths. Changing one runs every job. - A change to a file no job maps is an unknown change, and every path-scoped job runs.
ignorePathsmarks files whose change affects nothing, such as documentation. Platform and security jobs ignore bothpathsandignorePathsand depend on the whole tree, so a change that looks cosmetic never skips them.
A job that is not affected is not skipped. It is satisfied only by a signed pass of the same job with the same key. If no such pass exists, it runs.
Identical jobs at the same time
When two runs reach the same job with the same key at the same time (a mission and the pull request check, for example), only one executes. The other waits for it and attaches its result, pass or fail, to its own run. If the run holding the job stops, the waiting run takes over and executes the job itself.
A portable job's result can stand in on another architecture. A platform job always runs on each architecture it is required on.
Forcing a run
orgabot cicd verify <project-id> --forceruns every job again for that commit.orgabot cicd run <owner/repo> <target> <sha>is always a fresh run.
Seeing why
orgabot cicd reuse <run-id> lists, for each job, whether its result was reused and why:
- For a reused result: the run it came from, that the reuse keys match, how old the result is, and the time saved.
- For a job that ran: what changed since the last result. For example, "base/merge context", "external state (advisory database, mutable dependencies)", "toolchain/image/action versions" or "platform/architecture", with the old and new values.
- For a job that can never be reused: the rule that applies.
orgabot cicd inspect <run-id> shows the same decision on each job line. Add --json to either for structured output.
Measured savings
orgabot cicd savings [<owner/repo>] totals reuse hits, coalesced jobs, jobs that ran, and the time saved. The time saved is the measured duration of each job whose result was reused. If that job did not record a duration, the reuse is counted but its saving is left out, never estimated. Each reused job is also recorded as a CI optimization signal (cached with saved_ms), and each run's summary carries the total.