7 · Integrate into CI/CD
Connect your repo on the platform for zero-yaml, platform-authoritative CI, wire the GitHub Action and the Dystopic App, and land a branded Dystopic check on every PR.
You've proven the suite runs green locally with odyssey ci review. The last step is to make that run happen automatically on every pull request, and to surface its verdict as a branded Dystopic check right on the PR — the same base-vs-head diff, the same exit codes, now gating your merges.
You wire this up by connecting your repo on the platform. The platform maps it to an umbrella agent that carries the gate and the suite binding (both authoritative, RBAC'd, audited), so CI runs with nothing committed but the workflow file — no config, no scenarios in the repo. The platform resolves the execution recipe and sources the bound suite's frozen scenarios server-side.
Connect your repo on the platform
When your repo is connected, the platform maps it to an umbrella agent that carries the CI execution recipe (run command, timeout, tools/ledger, credential refs), the gate, and the suite binding (ci_suite_id). The platform resolves all of that server-side — so a connected repo runs zero-yaml CI with nothing committed but the workflow file.
Find the install URL. Hit GET /ci/app-info (or follow the "Connect a repository" button in platform Settings) to get the GitHub App install deep link. In dev, unconfigured OAuth returns {install_url: null} and the UI explains the App isn't wired up yet.
Install the Dystopic GitHub App on the repository via that link. GitHub redirects back to GET /ci/connect, which verifies you via GitHub user-OAuth and lists only the installations you control, then lets you pick a repo. To add a second repo to an org that already owns an installation, GET /ci/connect/available renders the picker on-platform without another GitHub round-trip.
Claim the repo and link the umbrella agent. Picking a repo writes the trust mapping via the registration endpoint with a claim_grant ownership proof, binding the repo to your org (org_id comes from your authenticated key, never the payload) and to the umbrella agent_id. The App now mints an installation token for private-repo clones.
Prefer the terminal? Once your org has installed the GitHub App at least once, the claim step is scriptable — no browser bounce:
dystopic repo connect owner/name [--agent-id 42] # claim the repo (auto-provisions an umbrella agent if unlinked)
dystopic repo status owner/name # connected? exit 0 / 1
dystopic repo list # everything connected to your orgOnly the first-time App install is inherently browser-bound (GitHub has no headless install API) — when the repo isn't claimable yet, connect prints the install URL and stops.
Once connected, configure the gate and suite on the umbrella agent — from the dashboard at https://platform.pipelines.tech (the gate card sets gate_config, the suite selector sets ci_suite_id) or from the CLI (dystopic agents gate set, dystopic agents ci-suite set — see 4 · Create a suite). The binding points CI at a frozen platform Suite. That's it: no per-repo config to maintain.
The old per-repo config editor (the in-app config-editing surface) was retired. Config now lives on the umbrella agent — the gate card, the suite binding, and the tools table — not in an editable committed file.
Run zero-yaml CI
A connected repo runs the check with no committed config. Point the CLI at the platform suite:
odyssey ci review --platform-suiteUnder --platform-suite, each side submits no agent config and no seeds. The platform resolves the execution recipe from the connected repo's umbrella agent and sources the bound suite's frozen scenarios (byte-identical to an in-app check of that suite), stamping each side's source_git to that side's commit with a platform-minted App clone token.
Zero-yaml runs fail closed, never open. An unconnected repo, an umbrella agent with no run command, or a suite that doesn't resolve to a registered platform Suite is a 422 naming exactly what to configure — not a silent skip.
The workflow file
.github/workflows/pipelines-regression.yml is the only file you commit to the repo. Scaffold it with dystopic ci init (offline; writes exactly this file plus the turn-it-on checklist) or copy it from the reference:
name: Dystopic regression
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
# push:
# branches: [main]
permissions:
contents: read
checks: write
pull-requests: write
concurrency:
group: pipelines-regression-${{ github.ref }}
cancel-in-progress: true
jobs:
regression:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: BuildPipelines/pipelines-regression-action@v1
with:
api-key: ${{ secrets.DYSTOPIC_API_KEY }}
platform-suite: true
# env:
# ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}Four lines in that template are load-bearing — don't strip them:
-
platform-suite: trueis what makes the action run zero-yaml: it passes--platform-suite, so the side submits no local config and the platform sources the bound CI suite's frozen scenarios. It requires an action release that includes the input (re-release@v1, or pin a revision that has it, if your copy predates SDK 0.5.0). The optionalsuite:input names a specific platform suite; the agent's bound CI suite wins server-side, else "default". -
fetch-depth: 0onactions/checkoutis required. On a pull request the action derives the base as the merge-base of the base branch and your PR head, and it materializes a worktree for each side — both commits must be present locally. A shallow checkout can't derive the merge-base and the check goes red. -
ready_for_reviewintypes:closes the draft-PR hole. It is not a defaultpull_requestevent type. Without it, a check that skipped while the PR was a draft would hang "expected" forever once the PR flips to ready. -
concurrency+cancel-in-progress: truecancels superseded runs when you push again to the same PR, so rapid pushes don't burn runs on stale commits.
Turn it on
The workflow is inert until you complete the checklist. Three things, in order:
Create an org and an API key. On the dashboard at https://platform.pipelines.tech, create a dystopic org and mint an API key under Settings → API keys. Without this the action has nothing to authenticate with.
Add the key as a repo secret. In GitHub → Settings → Secrets and variables → Actions, add a secret named DYSTOPIC_API_KEY with the key value. The workflow wires it in via api-key: ${{ secrets.DYSTOPIC_API_KEY }}.
If the key is missing, the action green-skips with a "no API key" notice — it never turns the job red for a missing key. This is by design: it's the same fail-closed path fork PRs take, where secrets are withheld.
Install the Dystopic GitHub App and connect the repo (dystopic repo connect owner/name, or the browser flow above). Without the App, runs still execute and the Actions job still reflects the verdict — but no branded Dystopic check is posted (results are log-only in the action output), no App-authored teaser fronts the PR comment, and private repos can't be cloned (there's no App-minted clone token). If you connected the repo on the platform, this is already done.
Once the key, the secret, and the App are in place, commit the workflow file and open a PR.
Open a PR and watch it land
With the key, the secret, and the App in place, open a pull request. Here's what happens — and the key design decision behind it.
The GitHub Action job is decoupled from the verdict. It submits the base and head runs, polls on a bounded budget, and then exits — it does not wait for the platform to finish grading. The platform completes the runs asynchronously, assembles the pairwise review, and posts (and updates) the branded Dystopic check-run via the GitHub App. The check typically appears within a few minutes.
The action posts a terminal check itself in exactly one situation: a client-side preflight failure, where no review row exists yet so the platform can't post it (a source that couldn't be resolved, or a merge-base that couldn't be derived). Every other outcome — pass, blocked, still-assembling — belongs to the platform's App-posted check.
Alongside the check, the App and action post a sticky PR comment carrying the per-scenario regression report — the same diff and findings you saw locally, with trace deep links into the platform review page.
What each trigger mode does
The action supports three GitHub event contexts, and the mode it runs in follows from the event:
pull_request— base-vs-head. Base is the derived merge-base (stable per PR, which is what makes the base run cache-hittable). This is the default mode.merge_group(merge queues) — base-vs-head keyed on the event's own SHAs; the PR number is parsed from the queue ref.push— head-only. No base side; the pushed commit is graded against the suite's absolute thresholds (min_pass_rateonly, no diff tiers). Uncomment thepush:block in the workflow to gate merges tomainthis way.
Which check to require
When you set up a required check in branch protection, require the Actions job (the regression job), not only the App-posted Dystopic check. The job is guaranteed to reach a terminal state on every path — green skip, green submit, or a red gate exit — even if a platform outage means the App-posted check never lands. A required check that only watches the App-posted Dystopic slot could sit "expected" forever during an outage.
Outcomes that are not failures
A few outcomes exit green on purpose. Knowing them keeps you from chasing phantom failures:
- Fork PRs skip green. GitHub withholds secrets from fork PRs and hands them a read-only token, so no run can be submitted and no branded check can be posted. The action detects the fork and log-skips with a notice — never red. (Maintainers push the branch into the base repo and open an internal PR to run it.)
- Draft PRs skip green until marked ready — which is exactly why
ready_for_reviewis in the triggertypes. - Poll-budget timeout exits 0. If the review is still assembling when the poll budget runs out, the job exits green with an in-progress notice; the platform finishes and posts the check asynchronously.
- Quota deferral (HTTP 429) exits 0. If the platform is at capacity it defers the submission neutrally — nothing was submitted, so there's no verdict. The action posts a neutral check and prints "platform busy … Re-run this job to retry." Capacity is not a verdict; re-run the job.
Where the gate comes from
A PR cannot weaken or remove its own gate. The gate is platform-authoritative, full stop.
The gate lives on the umbrella agent, set from the dashboard gate card (or dystopic agents gate set / PUT /api/agents/{id}/gate), and is authoritative (RBAC + audit). It cannot be weakened or removed by a PR — there is no base-vs-head weakening concept and no "gate weakened" banner. A platform-resolved base fails closed: dropping config on head turns the check red, it does not disarm it. Gate changes happen on the platform, not in a PR.
That's the flow
You've gone from a ported agent to a branded regression gate on every pull request: port → register → declare the world → author a suite → run checks → review findings → and now a Dystopic check that catches behavior regressions before they merge.
CI/CD reference
Every action input, all three trigger modes, the connect/claim mechanics, the preflight error catalog, and the platform gate rule in full.
Execution modes
Simulated, executed, and live tool responses — how each side's world resolves under a check.
Review the findings
Reread how to interpret the assembled review the Dystopic check surfaces.
6 · Review the findings
Read the assembled review — gate status, per-case diff, reviewer findings, and trace deep links — and decide what changed and why.
The dispatch contract
The contract every ported agent honors — the run context you receive, the response you return, the proxy tool-call format, and the tool-schema shape.