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 — 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. The consumption contract was frozen at v0.2.0 (2026-06-13); the current release is v0.3.1 (2026-07-05). It is no longer experimental — inputs/outputs evolve additively only, so pinned workflows keep working across the v0.x line.

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). 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 (^0.9.0) 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 across the frozen v0.x contract.
  • 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
src/summary.ts      # GITHUB_STEP_SUMMARY renderer (extracted at v0.3.0, unit-tested independently)
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   # floating major; or pin @v0.3.1
        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.3.1
        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). Removing it would be a SemVer-major event with its own migration note.
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. The action's runtime decision-row Rekor push stays deferred; this input is ignored.
cosign-key no '' Reserved. Runtime decision-row signing is not implemented; 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 until runtime decision-row signing lands (deferred).

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

The release.yml pipeline (added at v0.2.0, hardened at v0.3.0) fires on tag push: build → dist-reproducibility check → GitHub Release → floating-major retag. Production cosign signing of the committed dist/index.js is workflow_dispatch-only, dry-run by default, and gated fail-closed by the iah-E06 DNSSEC/CAA pre-flight against evals.intentsolutions.io.

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 — release-pipeline provenance. 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. The release.yml sign job (enabled v0.2.0) performs production-Rekor cosign signing of the committed dist/index.js, against the sigstore public-good trust root (fulcio.sigstore.dev + rekor.sigstore.dev, GitHub Actions ambient OIDC). Signing is workflow_dispatch-only with a reversible dry-run default and is gated fail-closed by the iah-E06 DNSSEC/CAA pre-flight. At v0.3.0 the checkout was pinned to the dispatched tag (not main), closing a wrong-bytes attestation class — a signed-provenance one-way door.

What is deferred, stated plainly.

  • The action's runtime decision-row signing is deferred. The action does not emit, sign, or Rekor-anchor a rollout-decision/v1 row at runtime. This is distinct from the release-pipeline signing of the committed dist/index.js artifact (which v0.2.0 enabled). cosign-key warns and no-ops; rekor-url is ignored; signed-decision-row-path is always empty. When runtime signing lands, it delegates to audit-harness emit-evidence, inheriting the same DNSSEC/CAA pre-flight already wired here.
  • 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 across v0.x — inputs/outputs evolve additively only (Evidence Bundle SPEC R18); no breaking change ships without a new predicate URI (SPEC R17).
  • Test fixtures use synthetic gate IDs only (synth-gate-*); no real engagement gate IDs appear in fixtures.

Current state (v0.3.1, 2026-07-05)

Milestone Status
M4 — Substantive bootstrap DONE. Repo, design doc, no-op action stub (v0.0.1, 2026-05-26).
M5 — Implementation DONE. Runtime locked to TypeScript by DR-002; decision logic delegated to @intentsolutions/rollout-gate@2.0.0.
Consumption contract FROZEN at v0.2.0 (2026-06-13). No longer experimental; uses: interface is additive-only across v0.x.
Release-pipeline signing LIVE (v0.2.0+). Production-Rekor cosign signing of the committed dist/index.js, dispatch-only / dry-run-default / DNSSEC-CAA-gated. Provenance one-way-door fix landed v0.3.0.
Runtime decision-row signing Deferred. rollout-decision/v1 runtime signing + Rekor anchoring not yet implemented.
M6 — First adopter Not started. audit-harness self-adopts as the first downstream before any external repo wires this in.

Changelog

Changelog

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

Format follows Keep a Changelog; versioning follows SemVer 2.0.0.

Pending

  • Runtime decision-row signing (rollout-decision/v1) — emit + sign the action's own in-toto decision row (distinct from signing the committed dist/index.js artifact, which v0.2.0 enabled). It delegates to audit-harness emit-evidence, inheriting the same DNSSEC/CAA pre-flight already wired here. The signed-decision-row-path output stays empty until this lands.
  • tests/TESTING.md policy parsing — deferred per DR-002 § 5; the action continues to consume JSON policy documents only.
  • M6 first adopteraudit-harness self-adopts the gate end-to-end before any partner repo (DR-002 § 6 criterion 5).

0.3.1 - 2026-07-05

Changed

  • Bump @intentsolutions/core ^0.7.0^0.9.0. The committed dist/index.js is rebuilt with the current kernel validators, so the action's advisory GateResultV1Schema check (countKernelInvalidPredicates) runs against the 0.9.0 predicate schema. No change to the action's public uses: interface (inputs/outputs byte-identical to v0.3.0); adopters upgrade the pin, no rewiring required. (#50)

0.3.0 - 2026-06-15

Release-pipeline hardening + provenance correctness. No change to the action's public uses: interface (inputs/outputs are byte-identical to v0.2.0) — this release hardens the release/signing pipeline itself and fixes a provenance-correctness bug in the dispatch re-release path. Adopters upgrade the pin; no workflow rewiring is required.

Added

  • release.sh helper + extracted GITHUB_STEP_SUMMARY renderer. Step-summary rendering moved out of src/main.ts into a dedicated src/summary.ts module (unit-tested independently); a release.sh script standardizes the local bump-tag-push flow. (#29)
  • ntfy CI-failure alert over the tailnet. A new alert-on-failure job joins the tailnet via Tailscale OIDC and pushes a high-priority alert to the prod-deploys ntfy topic when a tag-release build or release job fails. (#28)
  • Advisory actionlint CI lane — non-blocking workflow-manifest linting. (#31)
  • Advisory typos spell-check CI lane — non-blocking. (#30)

Changed

  • Production signing is dispatch-only with a reversible dry-run; the sigstage path is removed. The sign job runs ONLY on workflow_dispatch — a plain tag push does build + GitHub Release + floating-major retag and stops, never signing. Signing always targets the PRODUCTION sigstore public-good trust root (cosign defaults: fulcio.sigstore.dev + rekor.sigstore.dev, GitHub Actions ambient OIDC); dry-run (default true) controls only whether a permanent Rekor transparency-log entry is written. The non-representative sigstage path was dropped (its SCT verification failed against cosign's production-default TUF trust root). (#27)
  • The iah-E06 DNSSEC/CAA pre-flight always gates production signing (fail-closed). Because production is the only target, the read-only DNSSEC + CAA verification against evals.intentsolutions.io runs on EVERY dispatch (dry-run and real fire); either non-zero exit aborts before cosign runs. (#27)
  • Release bump to v0.3.0package.json + version.txt advanced; CHANGELOG section cut for the pipeline-hardening release. (#33)

Fixed

  • Checkout now pins to the dispatched tag, not main (provenance one-way-door fix). On workflow_dispatch the build, release, and sign jobs previously resolved GITHUB_REF (the default branch) — so a re-release / sign dispatch would rebuild and attest dist/index.js bytes from main, NOT the bytes consumers resolve via uses: jeremylongshore/intent-rollout-gate@<tag>. Every checkout now sets ref: ${{ inputs.tag }}; on a plain tag-push event inputs.tag is empty and checkout correctly falls back to the pushed tag. This closes a wrong-bytes attestation class — a signed-provenance one-way door (CISO reproducible-from-tag invariant). (#32)

0.2.0 - 2026-06-13

Stable consumption contract + production-Rekor signing enabled. Graduates the M5 TypeScript MVP from v0.1.0 (experimental — behavior present, contract not yet frozen) to a frozen consumption contract, and enables the previously-HELD sigstore PRODUCTION transparency-log signing of the committed dist/index.js behind the iah-E06 DNSSEC/CAA pre-flight (fail-closed). The action's public uses: interface stays forward-compatible: inputs/outputs are additive only (Evidence Bundle SPEC R18); no breaking change ships without a new predicate URI (SPEC R17). Adopters upgrade the pin; no workflow rewiring is required.

Added

  • Tag-triggered release pipeline + dist reproducibility. A release.yml pipeline fires on tag push: build → dist-reproducibility check → GitHub Release → floating-major retag, with sigstore wiring present. (#25)
  • Production-Rekor signing ENABLED (fail-closed). The release.yml sign job previously HARD-REFUSED production (HELD pending the CISO pre-flight). Both former preconditions are now met: @intentsolutions/audit-harness@1.1.8 ships the iah-E06 DNSSEC/CAA pre-flight scripts (read-only, fail-closed), and evals.intentsolutions.io has DNSSEC enabled + CAA pinned (verified live). A production pre-flight step runs both scripts against the predicate-URI host under set -euo pipefail; either non-zero exit fails the job → cosign NEVER anchors to production Rekor. Production is reachable only via explicit use-production-rekor=true workflow_dispatch. (#26)
  • Recorded public gist id for sweep/release tooling. (#22)

Changed

  • Consumed-row contract frozen — kernel predicate URI consumed. The action consumes @intentsolutions/core for gate-result/v1 directly; the kernel gate-result/v1 JSON Schema is pinned as the stable consumed-row contract. (#23, #24)
  • @intentsolutions/audit-harness dev-dependency bumped ^1.1.7^1.1.8 — 1.1.8 is the first release to ship the iah-E06 pre-flight scripts the production gate consumes. (#26)
  • Decision logic remains delegated to @intentsolutions/rollout-gate@2.0.0 (thin shell preserved); no gate semantics added to this repo.
  • The policy-file and dry-run deprecated aliases (introduced at v0.1.0) remain accepted — deprecated, not removed. Removing them would be a SemVer-major event with its own migration note.

0.1.0 - 2026-06-12

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. 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/). (#21)
  • 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). (#21)
  • Outputs: reasons (JSON array string of every blocking reason; [] exactly when allowed). decision emits allow / block verbatim from the package (the stub-era not-implemented value is retired). (#21)
  • Step summary — markdown table of evaluated required gates (pattern / status / matched gate IDs) + blocking rows + flat reason list; also exposed as the summary output. (#21)
  • 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'. (#21)
  • Unit tests — vitest suite over the shell wiring against synthetic-gate-ID fixtures: an allow bundle, a fail-row bundle, a malformed bundle. (#21)
  • CIcheck job (pnpm frozen install → typecheck → vitest → dist-sync), retained lint-action-yaml job, new smoke-action job running the real action against the fixtures. (#21)

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. (#21)
  • IEP convergence-debt fixes — license-drift fix, action.yml heredoc hardening, and the runtime-language lock recorded as DR-002. (#19)
  • policy-file becomes a deprecated alias for policy-path; its tests/TESTING.md default is removed. dry-run becomes a deprecated alias for fail-on-block: 'false'. (#21)
  • 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 runtime Rekor push happens, and cosign-key warns + no-ops. (#21)
  • .gitignore rewritten for the locked TS runtime (Go/Python sections removed; dist/ now tracked). (#21)

0.0.1 - 2026-05-26

Baseline release. Establishes the tag + CHANGELOG baseline for this repo. No npm-publish surface — 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, 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 it unless consuming a different predicate type.
  • First IEP /appaudit baseline (#13) — operator-grade devops playbook filed at 000-docs/002-AA-AUDT-appaudit-devops-playbook.md + .pdf.
  • Repo scaffolding for baseline release: CHANGELOG.md, CODE_OF_CONDUCT.md (Contributor Covenant 2.1), version.txt tracking baseline as 0.0.1. (#14)
  • Baseline release AAR at 000-docs/003-RL-REPT-baseline-release-v0.0.1-2026-05-26.md. (#14)

Changed

  • License relicensed from MIT to Apache 2.0 (#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 a downstream-friendly patent grant.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment