Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save jeremylongshore/b8a3e7b9c28e21a2155bc92c98cefcf4 to your computer and use it in GitHub Desktop.

Select an option

Save jeremylongshore/b8a3e7b9c28e21a2155bc92c98cefcf4 to your computer and use it in GitHub Desktop.
intent-rollout-gate — one-pager + operator audit + changelog

intent-rollout-gate

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).

License Release

Links: GitHub · @intentsolutions/rollout-gate on npm


One-Pager

Problem

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.

Solution

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.

W5

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.

Stack

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.0decide() / 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

Differentiators

  • 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-gate library; 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=block with reasons, and the job fails unless fail-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).

Operator-Grade System Analysis

Architecture

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:

  1. Read the Evidence Bundle at bundle-path. Both wire forms are accepted: the v2 plain array of in-toto Statements (kernel EvidenceBundlePayload) and the v1 legacy container {"bundle_format":"json-array","rows":[...]}.
  2. Resolve the rollout policy from exactly one of policy-path (JSON file) or policy-json (inline JSON string). Both or neither → block.
  3. Delegate the decision to decide(bundle, policy) from @intentsolutions/rollout-gate@2.0.0. Row validation reuses the kernel gate-result/v1 statement schema.
  4. Report: set decision / reasons / summary outputs, write a markdown step summary (required-gate table + blocking rows + flat reason list), and fail the job on block unless fail-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

Usage

# .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 mode

Policy 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
}

Operational reference

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.

Security posture

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-harness self-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/v1 row at v0.1.0. cosign-key warns and no-ops; rekor-url is ignored; signed-decision-row-path is 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/v1 is evaluated; any other predicate-uri value 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.

Current state (v0.1.0, 2026-06-12)

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.

Changelog

Changelog

All notable changes to intent-rollout-gate are documented here.

Format follows Keep a Changelog; versioning follows SemVer 2.0.0.

Pending

  • Decision-row signing — emit + sign the rollout-decision/v1 in-toto row, behind the DNSSEC + CAA pre-condition (DR-004 § 6.1, DR-002 § 6.3). signed-decision-row-path output stays empty until then.
  • tests/TESTING.md policy parsing — deferred per DR-002 § 5; v0.1.0 consumes JSON policy documents only.
  • M6 first adopteraudit-harness self-adopts the gate (DR-002 § 6.5).
  • Phase 7.5 gist (deferred per release-sweep CTO call — iep-gist-coverage follow-up bead; each landing-page gist deserves bespoke /appaudit treatment).

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.

Added

  • Node runtime actionruns.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 of policy-path / policy-json is required — both or neither blocks (fail closed).
  • Outputs: reasons (JSON array string of every blocking reason; [] exactly when allowed). decision now emits allow / block verbatim from the package (allow ≙ ship, block ≙ no-ship; the stub-era not-implemented value is retired).
  • Step summary — markdown table of evaluated required gates (pattern / status / matched gate IDs) + blocking rows + flat reason list; also exposed as the summary output.
  • Fail-closed wiring — missing/unreadable/invalid-JSON bundle file, ambiguous policy inputs, garbage policy (parsePolicy throws; no default-policy fallback), non-default predicate-uri, and any unexpected error all produce decision=block. The job fails on block unless fail-on-block: 'false' (or legacy dry-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.
  • CIcheck job (pnpm frozen install → typecheck → vitest → dist-sync), retained lint-action-yaml job (extended for the node runtime), new smoke-action job running the real action against the fixtures (allow path + non-failing block path).

Changed

  • BREAKING (stub-era behavior): the action no longer unconditionally exits 0. A block decision 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-file input is now a deprecated alias for policy-path and its tests/TESTING.md default is removed (TESTING.md parsing stays deferred per DR-002 § 5; a markdown policy would fail closed anyway).
  • dry-run input is now a deprecated alias for fail-on-block: 'false'.
  • predicate-uri, rekor-url, cosign-key inputs 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, and cosign-key warns + no-ops. signed-decision-row-path output stays empty until decision-row signing lands.
  • .gitignore rewritten for the locked TS runtime (Go/Python sections removed; dist/ now tracked).

Architectural bindings

  • 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.

Added

  • Initial repo scaffold (commit 8abcfdc) — repository bootstrap
  • Beads issue tracking initialized (commit fc40b3f)
  • M4 substantive bootstrap (commit 87de651) — repository, action.yml Action manifest (Intent Rollout Gate — consume Evidence Bundle + policy → ship/no-ship decision per the consuming repo's tests/TESTING.md), initial design doc at 000-docs/001-DR-DESIGN-rollout-gate-architecture-2026-05-12.md. Predicate URI is the stable v1 form https://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 (first RL filing-code use in this repo — Release Report)
  • version.txt tracking baseline as 0.0.1

Changed

  • License relicensed from MIT to Apache 2.0 (PR #12, commit 295cbe4) — BREAKING. Mirrors the audit-harness (#32) and j-rig-skill-binary-eval (#73) relicenses. Per Blueprint A, the 5-repo IEP taxonomy standardizes on Apache 2.0 for downstream-friendly patent grant.

Architectural bindings

  • Blueprint A — 12 binding principles, 5-repo taxonomy (this repo is the GitHub Action shell layer)
  • Blueprint B § 7gate-result/v1 NORMATIVE predicate spec (the Action consumes this predicate from Evidence Bundles)
  • DR-018 § 9.2@j-rig/rollout-gate decision-logic delegation (M5 consumes; this repo is the thin shell)

Quality posture

  • CI workflow (.github/workflows/ci.yml) — yamllint action.yml for 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment