A GitHub Action that consumes signed Evidence Bundles and decides allow / block for your CI pipeline — fail closed, zero decision logic in the action itself.
intent-rollout-gate is the GitHub Action shell layer of the Intent Eval Platform. It reads an Evidence Bundle (in-toto Statement v1 rows under predicateType https://evals.intentsolutions.io/gate-result/v1), resolves a declared rollout policy, and emits an allow or block decision with full reasoning. Every line of decision logic is delegated to the published @intentsolutions/rollout-gate package (Apache-2.0, sigstore provenance) — the action only wires inputs, files, outputs, and exit codes around it. v0.1.0 (the M5 TypeScript MVP) released 2026-06-12. Status: v0.1.0-experimental — behavior is real and fail-closed, but the consumption contract is not yet frozen (see graduation criteria below).
Links: GitHub · @intentsolutions/rollout-gate on npm
CI pipelines ship on vibes. A green checkmark usually means "the steps that happened to run exited 0" — not "the gates this repo declared as required all passed, with verifiable evidence." Test results live in ephemeral logs, gate semantics are scattered across ad-hoc shell steps, and nothing forces the ship/no-ship decision to be derived from a declared policy against structured, signed evidence. When a release goes out, there is no machine-readable record of why it was allowed.
A gate Action that consumes Evidence Bundles and fails closed. Upstream tools (deterministic static gates, behavioral eval harnesses) emit gate-result/v1 rows into an Evidence Bundle. This action reads that bundle, checks it against a declared rollout policy (required_gates, forbid_decisions, and related knobs), and produces a single allow / block decision with every contributing reason listed. A block fails the job by default. Malformed bundle, garbage policy, ambiguous inputs, schema-invalid rows, or any unexpected wiring error all produce block — there is no silent pass.
| Question | Answer |
|---|---|
| What | A node24 GitHub Action: Evidence Bundle + rollout policy in → allow/block decision, reasons, and a step-summary table out. |
| Why | Make the ship/no-ship decision a policy evaluation over structured evidence instead of an implicit side effect of whichever steps exited 0. |
| Who | Repos in (or adopting) the Intent Eval Platform convergence — anything that emits or consumes gate-result/v1 Evidence Bundle rows. First downstream adopter (M6) will be audit-harness itself. |
| When | At the rollout decision point of a CI pipeline, after evidence-emitting jobs complete. |
| Where | GitHub Actions (uses: jeremylongshore/intent-rollout-gate@v0.1.0). Apache-2.0, open source. |
| Layer | Choice |
|---|---|
| Runtime | GitHub Actions node24 runtime; esbuild CJS bundle transpiled to a node20-compatible target (DR-002 "Node 20+" lock) |
| Language | TypeScript (locked by DR-002, recording the upstream ecosystem TS-primary decision for signing surfaces) |
| Decision logic | @intentsolutions/rollout-gate@2.0.0 — decide() / parsePolicy(), pinned exact, published with sigstore provenance |
| Schema validation | Kernel @intentsolutions/core gate-result/v1 statement schema, reused via the package — no schema is re-declared in this repo |
| Action wiring | @actions/core (inputs, outputs, step summary, exit codes) |
| Tests | vitest unit suite over the shell wiring + CI smoke job running the real action against synthetic fixtures |
| Quality gates | @intentsolutions/audit-harness (hash-pinned policy verification in CI), typecheck, dist-sync check |
| Tooling | pnpm (frozen lockfile), esbuild, TypeScript 5.x |
- Thin-shell architecture. Zero decision logic lives in the action. Gate semantics, policy interpretation, and predicate evaluation all live in the provenance-published
@intentsolutions/rollout-gatelibrary; the action contains only input validation, file I/O, summary rendering, and exit-code wiring. If decision behavior must change, it changes upstream and the dependency is bumped here. - Fail closed, everywhere. Missing/unreadable/invalid-JSON bundle, both-or-neither policy inputs, garbage policy, malformed or empty bundle, schema-invalid rows, missing or non-passing required gates, forbidden decisions, unexpected errors — every failure mode is
decision=blockwith reasons, and the job fails unlessfail-on-block: 'false'is set explicitly. - Additive-only input evolution. v0.0.x inputs are retained as deprecated aliases (
policy-file,dry-run) or honest reserved no-ops (predicate-uri,rekor-url,cosign-key) per Evidence Bundle SPEC R18 — existing workflow wiring does not break. - Composable partial attestation. A bundle covering a subset of gate categories can pass if the declared policy only requires that subset (Evidence Bundle SPEC R2).
The repo is deliberately small. The entire runtime surface is two source files:
action.yml # public contract: inputs, outputs, runs.using: node24, main: dist/index.js
src/main.ts # entrypoint — imports and invokes run()
src/run.ts # ALL shell wiring: input validation, file I/O, summary rendering, exit codes
dist/index.js # committed esbuild CJS bundle (GitHub Actions convention); CI enforces dist↔src sync
tests/ # vitest suite + synthetic-gate-ID fixtures (allow / fail-row / malformed bundles)
Decision flow at runtime:
- Read the Evidence Bundle at
bundle-path. Both wire forms are accepted: the v2 plain array of in-toto Statements (kernelEvidenceBundlePayload) and the v1 legacy container{"bundle_format":"json-array","rows":[...]}. - Resolve the rollout policy from exactly one of
policy-path(JSON file) orpolicy-json(inline JSON string). Both or neither → block. - Delegate the decision to
decide(bundle, policy)from@intentsolutions/rollout-gate@2.0.0. Row validation reuses the kernelgate-result/v1statement schema. - Report: set
decision/reasons/summaryoutputs, write a markdown step summary (required-gate table + blocking rows + flat reason list), and fail the job onblockunlessfail-on-block: 'false'.
The thin-shell rule is a binding architectural constraint, not a style preference: this repo must never re-implement gate semantics, policy interpretation, or predicate evaluation. PRs that re-implement decision logic locally are out of order by design.
Ecosystem position — fourth repo in the Intent Eval Platform convergence, coupled at the schema layer (the gate-result/v1 predicate), not via package consolidation:
| Sister repo | Role |
|---|---|
intent-eval-lab |
Methodology, Evidence Bundle SPEC, taxonomy, OTel RFC |
audit-harness |
Deterministic static gates — emits gate-result/v1 Evidence Bundle rows |
j-rig-skill-binary-eval |
Behavioral judgment harness; home of the @intentsolutions/rollout-gate decision library |
intent-rollout-gate (this repo) |
Thin GitHub Action shell — delegates the ship/no-ship decision to the library |
# .github/workflows/release.yml
name: release
on:
push:
branches: [main]
jobs:
static-gates:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- run: pnpm exec audit-harness verify
- run: pnpm exec audit-harness emit-evidence --out evidence/
rollout-decision:
needs: [static-gates]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: jeremylongshore/intent-rollout-gate@v0.1.0
id: gate
with:
bundle-path: evidence/bundle.json
policy-json: |
{
"required_gates": ["audit-harness:ci:*"],
"forbid_decisions": ["fail", "error"]
}
- run: echo "decision=${{ steps.gate.outputs.decision }}"Or keep the policy in a committed file (enforcement travels with the code):
- uses: jeremylongshore/intent-rollout-gate@v0.1.0
with:
bundle-path: evidence/bundle.json
policy-path: tests/rollout-policy.json
fail-on-block: "true" # default; 'false' = report-only modePolicy document shape (required_gates patterns match gate_id values; * is the only wildcard; everything except required_gates is optional with fail-closed defaults):
{
"required_gates": ["audit-harness:ci:*"],
"forbid_decisions": ["fail", "error"],
"advisory_blocks": false,
"allow_unknown_gates": true
}Inputs
| Input | Required | Default | Purpose |
|---|---|---|---|
bundle-path |
yes | — | Path to the Evidence Bundle JSON file (v2 plain array or v1 container). Missing/unreadable/invalid JSON → block. |
policy-path |
one of | '' |
Path to the rollout policy JSON document. Exactly one of policy-path / policy-json is required. |
policy-json |
one of | '' |
Inline rollout policy JSON string. |
fail-on-block |
no | 'true' |
'true': a block decision fails the job. 'false': report-only. Anything other than an explicit 'false' fails on block. |
policy-file |
no | '' |
Deprecated alias for policy-path (v0.0.x name). The old tests/TESTING.md default is gone. |
predicate-uri |
no | gate-result/v1 URI |
Reserved. Only the stable v1 URI is supported; any other value blocks. |
rekor-url |
no | https://rekor.sigstore.dev |
Reserved. Decision-row Rekor anchoring is not implemented at v0.1.0; ignored. |
cosign-key |
no | '' |
Reserved. Decision-row signing is not implemented at v0.1.0; setting it warns and performs no signing. |
dry-run |
no | 'false' |
Deprecated alias for fail-on-block: 'false'. |
Outputs
| Output | Purpose |
|---|---|
decision |
allow or block, verbatim from @intentsolutions/rollout-gate. |
reasons |
JSON array string of every blocking reason — empty array exactly when decision is allow. |
summary |
Markdown decision summary (required-gate table + blocking rows + reasons). Also written to the job step summary. |
signed-decision-row-path |
Reserved — always empty at v0.1.0; populated once decision-row signing lands. |
Development
pnpm install --frozen-lockfile
pnpm run check # typecheck + vitest unit tests
pnpm run build # esbuild bundle src/main.ts → dist/index.js (node20 target)
pnpm run dist:check # rebuild + git diff --exit-code dist/CI (.github/workflows/ci.yml, three jobs; branch protection on main is live):
| Job | What it does |
|---|---|
check |
pnpm frozen install → audit-harness verify (hash-pinned policy integrity, per the Testing SOP) → typecheck → vitest → dist-sync (rebuild + git diff --exit-code dist/; stale bundles fail). |
lint-action-yaml |
Validates action.yml structure — YAML well-formed, required keys, node runtime declares runs.main. |
smoke-action |
Runs the real action (uses: ./) against synthetic fixtures: allow bundle → decision=allow + job succeeds; fail-row bundle with fail-on-block: 'false' → decision=block + job still succeeds; asserts outputs. |
Fail-closed is the load-bearing property. Every wiring-failure path in src/run.ts concludes with decision=block and an explicit reason: unreadable or invalid-JSON bundle file, both-or-neither policy inputs, a policy parsePolicy() rejects (no default-policy fallback exists), a non-default predicate-uri, and a final catch-all for unexpected errors. The v0.0.x always-exit-0 stub contract is retired and must not be reintroduced.
Supply chain. The decision library is pinned exact (@intentsolutions/rollout-gate@2.0.0) and published to npm with sigstore provenance. CI installs with a frozen lockfile. dist/index.js is committed (GitHub Actions convention) and the dist-sync CI job fails any PR whose committed bundle does not match a clean rebuild of src/ — the published action code is reviewable against its source on every change.
Honest experimental framing — read this before depending on the contract. v0.1.0 is explicitly the v0.1.0-experimental step of the DR-002 § 6 transition (000-docs/004-AT-DECR-runtime-language-typescript-2026-06-10.md in the repo). Of the five graduation criteria for v0.2.0 (stable consumption contract):
- Landed at v0.1.0: kernel-pinned contract (criterion 1) and real policy-driven allow/block decisions (criterion 2). The Testing SOP gate (criterion 4) is installed in CI and must stay green.
- Still open: the signing preconditions (criterion 3 — the DNSSEC + CAA pre-condition that gates any Rekor push, carried from the upstream council record DR-004 § 6.1) and the M6 first-downstream-adopter milestone (criterion 5 —
audit-harnessself-adopts before any external repo).
Consequences of that, stated plainly:
- No decision-row signing exists yet. The action does not emit, sign, or Rekor-anchor a
rollout-decision/v1row at v0.1.0.cosign-keywarns and no-ops;rekor-urlis ignored;signed-decision-row-pathis always empty. No signed attestation will be pushed until the predicate-URI namespace is DNSSEC-enabled with CAA records pinned. - Predicate URI immutability is a hard constraint. Only
https://evals.intentsolutions.io/gate-result/v1is evaluated; any otherpredicate-urivalue blocks. Breaking predicate changes mint/v2— URI strings are permanent once any signed row references them. - The public
uses:interface is forward-compatible until graduation — inputs/outputs evolve additively only (Evidence Bundle SPEC R18). - Test fixtures use synthetic gate IDs only (
synth-gate-*); no real engagement gate IDs appear in fixtures.
| Milestone | Status |
|---|---|
| M4 — Substantive bootstrap | DONE. Repo, design doc, no-op action stub (v0.0.1, 2026-05-26). |
| M5 — Implementation | DONE (v0.1.0). Runtime locked to TypeScript by DR-002; decision logic delegated to @intentsolutions/rollout-gate@2.0.0. |
| M6 — First adopter | Pending. audit-harness self-adopts as the first downstream before any external repo wires this in. |
| Decision-row signing | Pending. rollout-decision/v1 signing + Rekor anchoring behind the DNSSEC + CAA pre-condition. |
| v0.2.0 graduation | Gated by DR-002 § 6 — criteria 1, 2, 4 hold; criteria 3 (signing preconditions) and 5 (M6 adoption) remain open. |
All notable changes to intent-rollout-gate are documented here.
Format follows Keep a Changelog; versioning follows SemVer 2.0.0.
- Decision-row signing — emit + sign the
rollout-decision/v1in-toto row, behind the DNSSEC + CAA pre-condition (DR-004 § 6.1, DR-002 § 6.3).signed-decision-row-pathoutput stays empty until then. tests/TESTING.mdpolicy parsing — deferred per DR-002 § 5; v0.1.0 consumes JSON policy documents only.- M6 first adopter —
audit-harnessself-adopts the gate (DR-002 § 6.5). - Phase 7.5 gist (deferred per release-sweep CTO call —
iep-gist-coveragefollow-up bead; each landing-page gist deserves bespoke/appaudittreatment).
0.1.0 - 2026-06-11
M5 TypeScript MVP. The action graduates from the v0.0.x composite no-op stub to a real Node-runtime action. Runtime language locked to TypeScript by DR-002 (recording the upstream DR-010 § 13.5 TS-primary lock). Thin shell by design (Blueprint A): every line of decision logic is delegated to the published @intentsolutions/rollout-gate@2.0.0 package (Apache-2.0, sigstore provenance) — decide() / parsePolicy(); row validation reuses the kernel @intentsolutions/core gate-result/v1 statement schema. Zero gate semantics live in this repo.
- Node runtime action —
runs.using: node24,main: dist/index.js(esbuild CJS bundle, node20-compatible transpile target per the DR-002 "Node 20+" lock).dist/is committed per GitHub Actions convention; CI enforces dist↔src sync (rebuild +git diff --exit-code dist/). - Inputs:
policy-path(JSON policy file),policy-json(inline policy),fail-on-block(default'true'). Exactly one ofpolicy-path/policy-jsonis required — both or neither blocks (fail closed). - Outputs:
reasons(JSON array string of every blocking reason;[]exactly when allowed).decisionnow emitsallow/blockverbatim from the package (allow≙ ship,block≙ no-ship; the stub-eranot-implementedvalue is retired). - Step summary — markdown table of evaluated required gates (pattern / status / matched gate IDs) + blocking rows + flat reason list; also exposed as the
summaryoutput. - Fail-closed wiring — missing/unreadable/invalid-JSON bundle file, ambiguous policy inputs, garbage policy (
parsePolicythrows; no default-policy fallback), non-defaultpredicate-uri, and any unexpected error all producedecision=block. The job fails on block unlessfail-on-block: 'false'(or legacydry-run: 'true'). - Unit tests — vitest suite over the shell wiring (input validation, policy resolution, summary rendering, exit behavior) against synthetic-gate-ID fixtures: an allow bundle, a fail-row bundle, a malformed bundle.
- CI —
checkjob (pnpm frozen install → typecheck → vitest → dist-sync), retainedlint-action-yamljob (extended for the node runtime), newsmoke-actionjob running the real action against the fixtures (allow path + non-failing block path).
- BREAKING (stub-era behavior): the action no longer unconditionally exits 0. A
blockdecision fails the job by default. The v0.0.x always-exit-0 contract was explicitly a bootstrap affordance ("substantive enforcement begins at v0.1.0"). policy-fileinput is now a deprecated alias forpolicy-pathand itstests/TESTING.mddefault is removed (TESTING.md parsing stays deferred per DR-002 § 5; a markdown policy would fail closed anyway).dry-runinput is now a deprecated alias forfail-on-block: 'false'.predicate-uri,rekor-url,cosign-keyinputs are retained additively (Evidence Bundle SPEC R18) as reserved: only the default v1 predicate URI is accepted (anything else blocks), no Rekor push ever happens at v0.1.0, andcosign-keywarns + no-ops.signed-decision-row-pathoutput stays empty until decision-row signing lands..gitignorerewritten for the locked TS runtime (Go/Python sections removed;dist/now tracked).
- DR-002 — runtime language TypeScript on Node 20+; § 6 acceptance criteria frame the v0.1.0-experimental → v0.2.0 transition (this release is the v0.1.0-experimental step: criteria 1 + 2 land, criterion 4 (Testing SOP gate) is installed here and must stay green, criteria 3 (signing preconditions) + 5 (M6 adoption) gate the future v0.2.0 graduation, tracked in Unreleased)
- DR-018 § 9.2 — decision-logic delegation to the j-rig-published rollout-gate package; this repo is the thin shell
0.0.1 - 2026-05-26
0.0.1 - 2026-05-26
Baseline release. Establishes the tag + CHANGELOG baseline for this repo. No npm-publish surface yet — this is a GitHub Action distributed via the action.yml manifest at the repo root. Tag enables GitHub Marketplace listing.
- Initial repo scaffold (commit
8abcfdc) — repository bootstrap - Beads issue tracking initialized (commit
fc40b3f) - M4 substantive bootstrap (commit
87de651) — repository,action.ymlAction manifest (Intent Rollout Gate — consume Evidence Bundle + policy → ship/no-ship decision per the consuming repo'stests/TESTING.md), initial design doc at000-docs/001-DR-DESIGN-rollout-gate-architecture-2026-05-12.md. Predicate URI is the stable v1 formhttps://evals.intentsolutions.io/gate-result/v1; consumers MUST NOT change unless consuming a different predicate type. - First IEP /appaudit baseline (PR #13) — operator-grade devops playbook filed at
000-docs/002-AA-AUDT-appaudit-devops-playbook.md+.pdf - Repo scaffolding for baseline release:
CHANGELOG.md(this file),CODE_OF_CONDUCT.md(Contributor Covenant 2.1) - Baseline release AAR at
000-docs/003-RL-REPT-baseline-release-v0.0.1-2026-05-26.md(firstRLfiling-code use in this repo — Release Report) version.txttracking baseline as0.0.1
- License relicensed from MIT to Apache 2.0 (PR #12, commit
295cbe4) — BREAKING. Mirrors the audit-harness (#32) andj-rig-skill-binary-eval(#73) relicenses. Per Blueprint A, the 5-repo IEP taxonomy standardizes on Apache 2.0 for downstream-friendly patent grant.
- Blueprint A — 12 binding principles, 5-repo taxonomy (this repo is the GitHub Action shell layer)
- Blueprint B § 7 —
gate-result/v1NORMATIVE predicate spec (the Action consumes this predicate from Evidence Bundles) - DR-018 § 9.2 —
@j-rig/rollout-gatedecision-logic delegation (M5 consumes; this repo is the thin shell)
- CI workflow (
.github/workflows/ci.yml) —yamllint action.ymlfor manifest validation. M5 substantive runtime adds the full TS gate chain. - Scaffolding files present:
AGENTS.md,CLAUDE.md,CONTRIBUTING.md,LICENSE(Apache 2.0),NOTICE,README.md,SECURITY.md,CODE_OF_CONDUCT.md(this release)