GitLab CI

The merge gate: stateful code cannot merge unverified.

polygate wires the Polygraph engines into a GitLab merge pipeline. The design rests on one fact: only spec generation calls a paid model — replay, model checking, and the compat gates run locally, free. So the pipeline splits into three jobs with three trust levels, and the org API key touches exactly one of them.

1 · Three jobs, three trust levels

polygraph-check

every MR · keyless · blocks merge

Runs on every merge request and on main: a staleness gate (content hash — did the machine change without re-verification?), the exhaustive model check against the invariants, and trace replay. All deterministic, no API key anywhere near it. Any failure blocks the merge.

regenerate-specs

manual · protected branch · the one key-bearing job

The single job that can spend money or leak a key. Manual trigger, main only, fed by a masked + protected group variable — so the key simply does not exist in MR pipelines from forks or feature branches. It re-runs generation and re-stamps the verification manifest under governance.

push-artifacts

main · keyless

Publishes the whitelisted bundle — hashes, verdicts, provenance — to a control plane. Nothing secret in it, nothing executable from it.

The pipeline skeleton

# .gitlab-ci.yml — the shape (full version in the polygate repo)
stages: [verify, generate, publish]

variables:
  POLYGRAPH_REPO: https://github.com/cognitive-fab/polygraph.git
  POLYGRAPH_SHA: <pinned commit>   # bump deliberately, never track main

polygraph-check:          # every MR + main — FREE, keyless
  stage: verify
  script:
    - node scripts/check-stale.mjs                # hash gate
    - node /tmp/polygraph/scripts/check.mjs …     # exhaustive model check
    - node /tmp/polygraph/scripts/verify.mjs …    # trace replay
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == "main"

regenerate-specs:         # manual, main only — the ONE key-bearing job
  stage: generate
  when: manual
  variables:
    ANTHROPIC_API_KEY: $POLYGRAPH_ORG_KEY   # masked + protected — absent on MRs

push-artifacts:           # main — keyless provenance
  stage: publish
  needs: [polygraph-check]

The engines run from a pinned public clone — the gate's own code is content-addressed too, so "the gate passed" always means "this exact gate passed."

2 · The two proof points

Both demonstrated end-to-end on a self-hosted GitLab CE mock, with the real engines and a real polygen-authored subscription-billing machine (12 states exhaustive, 0 violations, 65-window trace corpus).

Gates block unverified change. An MR edits the state machine without re-running verification. The staleness gate compares content hashes, fails the pipeline, and the merge is blocked — before any human review happens.

Gates survive a gamed hash. The adversarial case: an MR seeds a real bug and re-stamps the manifest so the hash gate passes. The exhaustive model check still fails it — with a shortest-path counterexample as the ready-made repro.

hash gate: PASS (gamed) → model check: FAIL + counterexample → merge blocked
Why this matters: the free path is what runs on every MR, forever. A gate you can afford to run unconditionally is a gate nobody is tempted to skip.

3 · Try it

The polygate repo ships a complete self-hosted GitLab CE mock — Docker Compose, scripted configuration, both demo projects — so you can watch the gates fire without touching a production GitLab.

git clone https://github.com/cognitive-fab/polygate
cd polygate
docker compose -f ci-mock/docker-compose.yml up -d
# wait for GitLab CE to boot (see §2 of docs/CI-MOCK-SETUP.md), then:
pwsh ci-mock/setup-gitlab.ps1

On your own GitLab, the recipe is the skeleton above: copy the three-job split, pin the Polygraph clone, put the org key in a masked + protected group variable, and make polygraph-check a required check on merge requests.

Disclosure. polygate — like Polygraph itself — is experimental, not peer-reviewed technology. The gate is a consistency check, not a proof, over the finite domains the contract declares; a green pipeline is a strong signal, not a certificate.