Declarative governance for GitLab groups & projects — the whole surface, in one lightweight tool you run in CI.
# Dry-run — reads only, prints a plan, changes nothing.
npx @intentius/gitlab-warden reconcile --config governance.yaml --token-env GITLAB_TOKEN --mode dry-runInstalls the gitlab-warden CLI. Add --base-url for self-managed (defaults to
gitlab.com). Full config + flags in Usage below.
The third warden, built on the shared
provider-agnostic reconcile primitive in
@intentius/chant/reconcile — the same core
behind github-warden and
forgejo-warden. gitlab-warden
supplies the GitLab layer: a REST + GraphQL client (configurable host for
self-managed + GitLab.com), config + live-state types, a GitLab diff(), and the
reconcile cycles.
You declare desired state in YAML (selective-by-omission — an absent field is never
touched); warden diffs it against the live GitLab API and, in apply mode,
converges it — guarded so a typo can't mass-delete.
It's a single binary + a YAML file in CI: no state file, no HCL, no provider toolchain to stand up. That's the whole point — governance-as-code without the weight, covering the full GitLab governance surface in one place:
- Stateless — diff against live, reconcile. Nothing to drift, import, or lock.
- Continuous drift correction — a reconcile loop, not a one-shot apply.
- Selective-by-omission + ownership-gated deletes — manage a slice of a large instance without claiming the rest.
- Guardrails + dry-run default — removal cap, lockout protection.
- Tier-graceful — Premium/Ultimate-gated endpoints that 403 are reported and skipped, never fatal.
GitLab push rules aren't version-controlled and their inheritance is broken — copied at project creation, never propagated; change a group rule and existing projects don't get it (each fixed by hand). A reconcile loop that re-asserts declared push rules across a whole group tree fixes that, continuously — one clear example of the model, which warden applies across the entire governance surface.
| Scope | Cycles |
|---|---|
| Group | settings · members · subgroup provisioning · variables · webhooks · push rules · access tokens · protected environments · integrations · MR approval settings · compliance frameworks · security policies · member roles |
| Project | settings · members · protected branches · protected tags · protected environments · push rules · MR approvals · variables · webhooks · integrations · deploy keys/tokens · access tokens · advanced protections (job-token scope, registry/package protection) · compliance assignment · security policy attachment |
| Instance (self-managed) | application settings · instance CI/CD variables · system hooks · custom member roles |
REST for most of it; GraphQL for the few surfaces that require it (compliance frameworks, security-policy attachment). The Ultimate-only GraphQL cycles are best-effort (unvalidated against a live Ultimate instance — the e2e runs CE).
npx @intentius/gitlab-warden reconcile \
--config governance.yaml \
--mode dry-run \
--token-env GITLAB_TOKEN \
--base-url https://gitlab.example.com # omit for gitlab.com# governance.yaml — declared nodes, keyed by full path
nodes:
acme/platform:
kind: group
groupSettings: { description: "Platform team", visibility: private }
members:
- { user: alice, accessLevel: owner }
pushRules: { preventSecrets: true }
acme/platform/api:
kind: project
projectSettings: { mergeMethod: ff, topics: [go, service] }
protectedBranches:
- { name: main, pushAccessLevel: 40, mergeAccessLevel: 30 }--mode dry-run (default) prints the plan; --mode apply converges it. Guardrails
block a mass-delete and tier-gated (Premium/Ultimate) endpoints that 403 are
reported and skipped, never fatal.
npm test runs the unit suite (mock-client, fully offline). The e2e suite
is fully hermetic — it stands up GitLab CE via Docker Compose, mints a token,
provisions its own group/project, exercises every cycle, and tears down (no
external account or secrets):
eval "$(npm run --silent e2e:up)" # compose up + mint token (GitLab CE is slow)
npm run test:e2e:run
npm run e2e:downGitLab is a tree (nested groups, inherited membership), not a flat org. Membership
is diffed against direct members (/members) while effective members
(/members/all) are read for context only — an inherited member is never treated
as deletable drift (the DELETE would fail, since the grant lives at an ancestor). The
scope and inheritance model documents the rules the membership cycle
follows.
| github-warden | forgejo-warden | gitlab-warden | |
|---|---|---|---|
| Hierarchy | flat org → repo | flat org → repo | nested groups → projects |
| Membership | direct | team-driven | direct + inherited |
| Auth | GitHub App | token, self-hosted | token, self-managed + SaaS |
| API | REST | REST | REST + GraphQL |
| Reconcile core | @intentius/chant/reconcile |
(same) | (same) |