CI/CD conformance: adapter, security and parity tests
How execution adapters, policy, failure recovery and migrations are tested, what a migration must attach before cutover, and what was really exercised.
Last updated
Orgabot's CI/CD runtime places each pipeline target on an execution backend: local Docker, a native macOS/Linux/Windows worker, local GitHub Actions compatibility (act), or GitHub Actions. The conformance kit holds every backend, the policy resolver and every migration to the same rules, so a green result means the same thing wherever the work ran.
The kit lives in framework/src/cicd/conformance/. Run it with:
orgabot ci-conformance runIt exits 0 only when every suite passes and every adapter report is complete.
The three rules
Every case in the kit enforces these:
- Failures stay failures. A non-zero exit, a timeout, a cancellation or a refused capability is never reported as success, and a late observation never changes a finished run. A test failure is never retried, and never sent to another provider in the hope that it passes there.
- Uncertain stays uncertain. A run or side effect whose outcome could not be observed is
unknown. It holds the pipeline until a provider lookup answers, and it is never retried blind. - No manufactured green. A success needs observed exit evidence for a pinned commit. A required check counts only from the expected reporter, for the exact commit, from a trusted workload.
Adapter conformance
Each adapter binds to one small port (ConformanceSubject in port.ts), and the same cases run against all of them:
| Area | What is checked |
|---|---|
| Source pinning | A branch or tag name is refused before anything launches; results and artifacts name the pinned commit |
| Commands and services | Exit codes are kept; later steps stop after a failure; a missing command fails; a service is reachable or the job is refused up front |
| Outputs, logs, artifacts | Outputs are returned; logs are ordered and never contain a secret or its encodings; an artifact's digest matches its bytes |
| Timeouts and cancellation | Each is its own terminal state, leaves no containers or processes, and a late cancel does not rewrite a finished run |
| Retries | Infrastructure faults are retryable; test failures are final |
| Capability mismatch | Another OS, an unsupported architecture or a missing feature is refused with the backend's own reason, and every reason is listed |
| Normalized failures | An unknown run is unknown, never success; every observed result passes the honesty rules |
Running it on real processes
orgabot ci-conformance run --binding native--binding native runs the same adapter cases against this machine's real processes instead of a simulated backend. Each job is checked out from a real git repository at its pinned commit, each step runs in its own /bin/sh process group, timeouts and cancellation send real signals, and the infrastructure-fault case kills a real step with SIGKILL from outside Orgabot. Jobs get a clean environment, so nothing from your shell (including credentials) reaches them. This binding declares no emulation and no service containers, because it cannot verify either. It is the baseline for the native family. The native worker adapter (placement, worker leases, signing) binds to the same port when it lands.
The suite is proven to discriminate: each of the fourteen built-in mutants (an adapter that reports success on a non-zero exit, ignores cancellation, skips redaction, accepts a branch name, and so on) must fail it. A report also records complete: a binding that cannot inject faults gets not_exercised for those cases, which is never a pass.
Policy resolution
The policy suite checks organization, project and target resolution: inheritance is visible field by field, a narrower override can never weaken an organization restriction, a budget ceiling or an approval, an unreadable policy blocks, a project that still runs its own workflows resolves to them unchanged, and the CLI, dashboard, dashboard terminal, agent skills and mission verification all get the identical decision.
Adversarial cases
| Attack | Defense |
|---|---|
| Fork or PR privilege escalation | Trust follows the code, not the event. Fork code gets no secrets, no side effects, no check authority, and never runs automatically on a persistent personal worker |
| Forged checks | A commit status, another app's check, a check for another commit or from an untrusted run is refused |
| Hostile workflow inputs | Inputs must match a declared type and reach jobs only as environment variables; a step that splices an event or input expression into shell is refused |
| Poisoned cache or artifact | An untrusted cache never restores into a trusted job; bytes must match the recorded digest at every hand-off |
| Stale attestation | Refused when the artifact, commit, pipeline definition, toolchain or builder changed, or when it is too old or dated in the future |
| Secret redaction | Raw, base64, URL-encoded, hex and JSON-escaped forms are masked; a secret too short to mask is reported |
| Wrong-architecture output | The architecture is read from the ELF, Mach-O or PE header, never from a label |
Failure recovery and scenarios
Faults injected and checked: worker restart, controller restart, durable runtime restart, Docker loss, webhook loss, duplicate webhooks, provider token expiry, rate limits, disk exhaustion, memory pressure, a remote dispatch accepted with its response lost, and unknown signing or deployment outcomes.
Scenarios checked: a clean PR, a failing PR, a fork PR, a merge queue (evidence for the PR head never counts for the merge commit), a tag release, a scheduled run with the Mac offline (blocked, not skipped), and release recovery after a crash around publication.
The hybrid release follows the Premail reference flow: local verification and macOS/Linux builds, a GitHub-hosted Windows installer build with its existing signing identity, and one release join. The join needs an approval bound to the exact commit and artifact digests, verifies every artifact's bytes and architecture, and publishes once. Cancellation stops the hosted run and publishes nothing; rollback reverts exactly once, across restarts. The hosted runner is dispatched only after local verification passes, so it is never billed while waiting on local work.
Mixed mission and CI load runs through the same admission rule missions use, and must stay inside the host's memory reserve, CPU and concurrency ceiling while every item still completes.
Migration parity and cutover
Before a target moves from its existing pipeline to Orgabot, the migration attaches parity evidence:
orgabot ci-conformance run --family docker --report-out report.json
orgabot ci-conformance parity --migration mig-site --project site --target site \
--old-coverage old.json --new-coverage new.json \
--old-artifacts old/ --new-artifacts new/ \
--rollback rollback.json --conformance report.json --out evidence/parity.json
orgabot ci-conformance verify-evidence evidence/parity.jsonCutover is blocked unless all of these hold:
- Every required check still runs on every OS and architecture it ran on, still required. A renamed check needs an explicit entry in
--renames. - Artifacts are equivalent. Plain files compare by content. Archives compare entry by entry, ignoring timestamps. Signed or notarized binaries compare by code with the signature excluded, and both signatures must verify with the same identity and keep notarization. Re-signing alone is recorded, not treated as a regression.
- The target's adapter has a complete conformance report attached.
- Parity ran twice with identical results.
- Rollback restores the old triggers, was rehearsed, saw the old checks run again, and performed no production side effect.
Signatures are checked with codesign and xcrun stapler on macOS and osslsigncode for Windows binaries. If a tool is missing, that artifact is unverified, which blocks cutover. The evidence file is digest-stamped, so verify-evidence refuses a copy edited after it was written.
Test cost
| Tier | Runs when | Cost |
|---|---|---|
| Deterministic | Always | Simulated backends and virtual time: no containers, runners, network or spend |
| Local live | ORGABOT_CI_LOCAL_LIVE=1 | Real local backends on this machine |
| Hosted smoke | ORGABOT_CI_HOSTED_SMOKE=1 and ORGABOT_CI_HOSTED_SMOKE_APPROVAL=apr_... | One Linux and one Windows cell, at most 8 billed minutes, no signing or publishing |
No tier may publish, deploy, submit to a store, notify, or sign with a production identity.
What was exercised versus simulated
Only the adapter suite's native-process binding runs on real hardware today. Everything else in the kit runs on simulated backends:
| Area | Exercised | Why not real yet |
|---|---|---|
| Adapter suite: native-process binding | Real hardware, every routine test run | Exercised |
| Adapter suite: Docker, native worker, act, GitHub | Simulated | The real adapters are not merged yet, so there is nothing to bind the suite to |
| PR, merge queue, tag, schedule, release recovery | Simulated | They run on the kit's reference reconciler until the production pipeline controller and check reporting land |
| Injected faults | Simulated | Real Docker loss and real worker or runtime restarts need the adapters and controller first |
| Hybrid release | Simulated | A live run needs the GitHub-hosted Windows dispatch adapter, Premail's signing identity and an approved hosted smoke run |
| Signed binary comparison | Fixture headers | Real notarized apps and Authenticode-signed installers have not been compared yet |
| Mixed stress | Modeled host | Uses the real admission rule, not real processes |
EXERCISE_LEDGER in costBounds.ts is the source of this table, and a test fails if a new scenario is added without an entry. Update it only after a real run, and record where that run's evidence lives.