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. The dry-run makes the plan visible before merge, the apply keeps a merged policy converging the org, and the scheduled run (apply, or dry-run if you prefer alerts over correction) catches out-of-band drift even when nobody edits the policy.

Exit codes make this easy to wire: 0 success, 1 guardrail block, 2 arg/config error, 3 runtime/apply failure (CLI.md). A guardrail block failing the job is the desired behavior: it means a human should look at the plan.

The token is a repository secret; warden never takes it as a file in the repo or as a flag, only from an env var via --token-env. Create a FORGEJO_WARDEN_TOKEN secret holding an API token with write access to the managed orgs (SETUP.md).

Forgejo Actions

Forgejo Actions is GitHub-Actions-compatible, so the workflow reads the same. Enable Actions on the policy repo and add the secret under repo Settings > Actions > Secrets. One workflow can cover all three triggers:

# .forgejo/workflows/governance.yml
name: governance

on:
  pull_request:
  push:
    branches: [main]
  schedule:
    - cron: "17 4 * * *"   # nightly drift run

jobs:
  reconcile:
    runs-on: docker
    container:
      image: node:22-bookworm
    steps:
      - uses: actions/checkout@v4

      - name: Dry-run (pull request)
        if: github.event_name == 'pull_request'
        env:
          FORGEJO_TOKEN: ${{ secrets.FORGEJO_WARDEN_TOKEN }}
        run: |
          npx @intentius/forgejo-warden reconcile \
            --config governance.yml \
            --base-url ${{ github.server_url }} \
            --token-env FORGEJO_TOKEN \
            --mode dry-run

      - name: Apply (main / schedule)
        if: github.event_name != 'pull_request'
        env:
          FORGEJO_TOKEN: ${{ secrets.FORGEJO_WARDEN_TOKEN }}
        run: |
          npx @intentius/forgejo-warden reconcile \
            --config governance.yml \
            --base-url ${{ github.server_url }} \
            --token-env FORGEJO_TOKEN \
            --mode apply

${{ github.server_url }} is the instance the workflow runs on, so the same workflow file works on any Forgejo. Hardcode --base-url instead if the policy repo lives on a different instance than the one it governs.

GitHub Actions

If the policy repo lives on GitHub (governing a Forgejo instance elsewhere), the same shape works with the instance URL pinned. You need two values in the repo: the FORGEJO_WARDEN_TOKEN secret, and the instance URL in a plain variable (it isn't sensitive).

# .github/workflows/governance.yml
name: governance

on:
  pull_request:
  push:
    branches: [main]
  schedule:
    - cron: "17 4 * * *"

permissions:
  contents: read

jobs:
  reconcile:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22

      - name: Dry-run (pull request)
        if: github.event_name == 'pull_request'
        env:
          FORGEJO_TOKEN: ${{ secrets.FORGEJO_WARDEN_TOKEN }}
        run: |
          npx @intentius/forgejo-warden reconcile \
            --config governance.yml \
            --base-url ${{ vars.FORGEJO_URL }} \
            --token-env FORGEJO_TOKEN \
            --mode dry-run

      - name: Apply (main / schedule)
        if: github.event_name != 'pull_request'
        env:
          FORGEJO_TOKEN: ${{ secrets.FORGEJO_WARDEN_TOKEN }}
        run: |
          npx @intentius/forgejo-warden reconcile \
            --config governance.yml \
            --base-url ${{ vars.FORGEJO_URL }} \
            --token-env FORGEJO_TOKEN \
            --mode apply

Notes

The secrets-variables cycle writes a secret's value from FORGEJO_SECRET_<NAME> in the warden process's environment. To have CI provision real values, map them in the apply step: env: { FORGEJO_SECRET_DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} }. Without that, a newly created secret gets an empty placeholder.

--cycles lets a job reconcile a subset, for instance a frequent schedule for branch-protection and a daily one for everything.

Dry-run plans on PRs go to the job log. If you want the plan on the PR itself, capture stdout and post it as a comment with your forge's API; the plan is plain text and stable-ordered, so diffs between runs are readable.

Pin the version with npx @intentius/forgejo-warden@0.1.2 ... so a new release can't change CI behavior unreviewed.