Skip to content

Running warden in CI

The steady state for governance is a pipeline: dry-run the plan on every pull request, apply on the default branch, and a scheduled run to correct drift that happens between pushes. This page covers the GitHub Action, a hand-written workflow, and the emit pipeline that generates one for you.

The GitHub Action

The repo ships a native Node 20 action (action.yml, entry action/index.mjs). It exposes the reconcile and audit subcommands; report is CLI-only. Auth is GitHub App only (the action mints the installation token internally from the three App inputs).

Input Required Default Meaning
command no reconcile reconcile or audit.
config yes — Path to the governance config (YAML/JSON).
mode no dry-run dry-run or apply (reconcile only).
cycles no all Comma-separated cycle names (reconcile only).
app-id yes — GitHub App ID (numeric).
installation-id yes — App installation ID (numeric).
private-key yes — App private key PEM. Pass as a secret, never a var.
fail-on no none none, merge-worthy, or any (audit only).
allow-guardrail-override no false 'true' applies even when guardrails trip (reconcile only).

Dry-run on PR, apply on main

A workflow you can paste. Store the App ID and installation ID as repository variables and the PEM as a secret:

name: Governance

on:
  pull_request:
    paths: [.github/governance.yml]
  push:
    branches: [main]
    paths: [.github/governance.yml]
  schedule:
    - cron: "0 2 * * *"     # daily reconcile catches out-of-band drift
  workflow_dispatch:

permissions:
  contents: read

jobs:
  plan:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4
      - uses: intentius/github-warden@v1
        with:
          command: reconcile
          config: .github/governance.yml
          mode: dry-run
          app-id: ${{ vars.GOVERNANCE_APP_ID }}
          installation-id: ${{ vars.GOVERNANCE_INSTALLATION_ID }}
          private-key: ${{ secrets.GOVERNANCE_APP_PRIVATE_KEY }}

  apply:
    if: github.event_name != 'pull_request'
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4
      - uses: intentius/github-warden@v1
        with:
          command: reconcile
          config: .github/governance.yml
          mode: apply
          app-id: ${{ vars.GOVERNANCE_APP_ID }}
          installation-id: ${{ vars.GOVERNANCE_INSTALLATION_ID }}
          private-key: ${{ secrets.GOVERNANCE_APP_PRIVATE_KEY }}

Exit-code behavior maps cleanly onto CI: a guardrail block fails the apply job with exit 1 (review the plan; overriding needs a deliberate allow-guardrail-override: 'true'), config mistakes fail fast with exit 2, and a failed apply entry surfaces as exit 3.

An audit gate is one more step:

      - uses: intentius/github-warden@v1
        with:
          command: audit
          config: .github/governance.yml
          fail-on: merge-worthy
          app-id: ${{ vars.GOVERNANCE_APP_ID }}
          installation-id: ${{ vars.GOVERNANCE_INSTALLATION_ID }}
          private-key: ${{ secrets.GOVERNANCE_APP_PRIVATE_KEY }}

If you prefer a token over App inputs (e.g. for repo-level cycles only), skip the action and run the CLI directly with --token-env, minting the token with actions/create-github-app-token first.

Generating the workflow: the emit pipeline

governancePipeline (src/emit/pipeline.ts, exported from the package root) generates .github/workflows/governance.yml through the chant GitHub lexicon builders, so the emitted workflow passes the same audit checks chant applies to hand-written ones: actions pinned to commit SHAs, least-privilege permissions (read-only at the workflow level with write scopes only on the job that needs them), timeout-minutes on every job, plus the private key passed via with: rather than interpolated into a script.

The emitted workflow runs the dry-run job on PRs touching the config (and posts a PR comment pointing at the plan), and the apply job on schedule and workflow_dispatch.

import { governancePipeline } from "@intentius/github-warden";
import { githubSerializer } from "@intentius/chant-lexicon-github";

const { workflow } = governancePipeline({ cron: "0 6 * * 1" });
const yaml = githubSerializer.serialize(new Map([["governance", workflow]]));
// write yaml to .github/workflows/governance.yml

Options (all optional, with defaults):

Option Default Meaning
cron "0 2 * * *" Schedule for the recurring apply.
configPath ".github/governance.yml" Where the policy lives in the repo.
cycles all Cycle names forwarded as --cycles.
appIdVar "GOVERNANCE_APP_ID" Actions variable holding the App ID.
installationIdVar "GOVERNANCE_INSTALLATION_ID" Actions variable holding the installation ID.
privateKeySecret "GOVERNANCE_APP_PRIVATE_KEY" Actions secret holding the PEM (must be a secret).
runsOn "ubuntu-latest" Runner label.
timeoutMinutes 30 Per-job timeout.

Scheduling notes

Two of the cycles only make sense on a schedule: token-governance (nothing fires an event when a token ages past the policy) and repo-baseline (the periodic backstop that declared repos exist). A daily cron covers both, and also re-applies any drift introduced outside the pipeline.