Running warden in CI
The steady state for governance is a pipeline: dry-run the plan on every merge request, apply on the default branch, and a scheduled run to correct drift that happens between pushes (the push-rules cycle exists for exactly that). The exit codes make the wiring trivial: 0 clean, 1 guardrail block, 2 config error, 3 runtime/apply failure; anything non-zero fails the job.
Token setup (GitLab CI)
- Create a token with
apiscope and sufficient role on the declared nodes (see SETUP.md). A group access token on the top governed group is a good fit: it isn't tied to a person. - In the policy repo, open Settings > CI/CD > Variables and add a masked
and protected variable named
WARDEN_TOKEN(protected means MR pipelines from forks and unprotected branches never see it; that is also why the dry-run job below works for same-repo MRs but not fork MRs). - The predefined
CI_JOB_TOKENis no substitute; its permissions are far too narrow for governance writes.
$CI_SERVER_URL is predefined in every GitLab pipeline, so the same
.gitlab-ci.yml works on gitlab.com and self-managed without edits.
Sample .gitlab-ci.yml
stages: [governance]
.warden:
stage: governance
image: node:22
variables:
GIT_DEPTH: "1"
# MR pipeline: plan only, post the diff in the job log.
governance:plan:
extends: .warden
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
script:
- npx @intentius/gitlab-warden reconcile
--config governance.yaml
--mode dry-run
--base-url-env CI_SERVER_URL
--token-env WARDEN_TOKEN
# Default branch: converge what was merged.
governance:apply:
extends: .warden
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "push"
script:
- npx @intentius/gitlab-warden reconcile
--config governance.yaml
--mode apply
--base-url-env CI_SERVER_URL
--token-env WARDEN_TOKEN
# Scheduled drift correction (Build → Pipeline schedules, e.g. hourly):
# re-asserts declared state even when nobody pushed — this is what keeps
# push rules from silently drifting apart across projects.
governance:drift:
extends: .warden
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
script:
- npx @intentius/gitlab-warden reconcile
--config governance.yaml
--mode apply
--base-url-env CI_SERVER_URL
--token-env WARDEN_TOKEN
Notes:
- A guardrail block exits 1 and fails the apply job, which is the intended
behavior. Review the plan; if the deletions are intended, re-run with
--allow-guardrail-override(ideally as a manual job, not a default). - Pin a version (
npx @intentius/gitlab-warden@0.2.2 …) if you want reproducible pipelines. - Narrow a job with
--cycles(for example a fast hourly--cycles push-rulesschedule plus a nightly full run) to stay well inside the 1000-request budget on large policies.
Running migrate in a pipeline
The migrate subcommand also runs fine inside CI. A common shape while a team moves over: keep the GitHub workflows as the source of truth for a transition window, and let a job re-translate them on every change, publishing the stitched pipeline and the SARIF findings as artifacts for review.
migrate:translate:
stage: governance
image: node:22
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
changes: [".github/workflows/*"]
script:
- npx @intentius/gitlab-warden migrate .github/workflows/
-o migrated/
--report migrate-findings.sarif
artifacts:
paths: [migrated/, migrate-findings.sarif]
expire_in: 1 week
Add --strict to fail the job while error-severity findings remain, which
turns "everything lossy has been reviewed" into a merge gate. The findings
land on stderr in the job log; the SARIF file feeds any viewer that speaks
it.
From GitHub Actions
If the policy repo lives on GitHub, the same commands work there, since warden talks to GitLab over its API regardless of where it runs:
name: governance
on:
pull_request:
push:
branches: [main]
schedule:
- cron: "17 * * * *"
jobs:
reconcile:
runs-on: ubuntu-latest
env:
GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- run: >
npx @intentius/gitlab-warden reconcile
--config governance.yaml
--mode ${{ github.event_name == 'pull_request' && 'dry-run' || 'apply' }}
--base-url https://gitlab.example.com
--token-env GITLAB_TOKEN
Store the token as a repository secret; swap --base-url for your instance
(omit it for gitlab.com).