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.