Skip to content

Setup

From zero to a reviewed governance plan. Nothing here mutates GitLab. The default mode is dry-run, and this page stops right before --mode apply.

Install

No install needed with npx:

npx @intentius/gitlab-warden

Or install the gitlab-warden binary globally:

npm install -g @intentius/gitlab-warden

Node 22+ (the CLI is a single bundled ESM file; yaml is its only runtime dependency beside the shared reconcile core).

Create a token

warden authenticates with a plain API token sent as PRIVATE-TOKEN. Either:

  • Personal access token: avatar > Edit profile > Access tokens; add a token with the api scope.
  • Group access token (better for CI): group > Settings > Access tokens; scope api, role Owner (or Maintainer if you only govern projects). On GitLab.com this needs a paid namespace; on self-managed it works on Free.

Role requirements on the nodes you declare:

Node kind Needed role
group Owner (member management, group settings, tokens)
project Maintainer or above
instance instance admin (self-managed only)

Export it; never put it in the config file or on the command line:

export GITLAB_TOKEN=glpat-…

--token-env names a different variable if you prefer.

Point at your instance

gitlab.com is the default. For self-managed, pass the instance URL:

--base-url https://gitlab.example.com        # literal
--base-url-env CI_SERVER_URL                 # or from an env var (nice in CI)

The client talks to <base-url>/api/v4 (REST) and <base-url>/api/graphql (the two Ultimate GraphQL cycles).

First dry-run

Write a minimal policy that declares one group and one project with one slice each (the full schema is in the policy reference):

# governance.yaml
nodes:
  acme/platform:
    kind: group
    pushRules:
      preventSecrets: true
  acme/platform/api:
    kind: project
    protectedBranches:
      - name: main
        pushAccessLevel: 40
        mergeAccessLevel: 30

Run the reconcile against it:

npx @intentius/gitlab-warden reconcile --config governance.yaml

Dry-run is the default mode: it reads live state and prints one plan section per cycle and node (=== push-rules @ group:acme/platform ===), changing nothing. Slices you didn't declare produce no entries (selective-by-omission). On a Free/CE instance the push-rules read 403s and is tolerated rather than treated as an error; because the slice is declared here anyway, the plan shows it as a create, and an apply would surface the 403 in that cycle's failed[] output. Live things you didn't declare are left alone, and a policy without owned (like this one) never plans a delete.

Narrow the run while iterating (--cycles push-rules,protected-branches), widen the policy node by node, and only reach for --mode apply once the printed plan says exactly what you meant. The CLI reference has the full flag and exit-code list; the CI guide shows the pipeline wiring.

A disposable sandbox: the e2e stack

The repo ships a fully hermetic e2e environment (GitLab CE in Docker Compose, with no external account and no secrets) that doubles as a safe place to try --mode apply for real:

git clone https://github.com/INTENTIUS/gitlab-warden && cd gitlab-warden
npm ci
eval "$(npm run --silent e2e:up)"   # compose up + mint a root token

Be aware that GitLab CE is heavy (multi-GB image) and first boot runs gitlab-ctl reconfigure, which takes a few minutes locally and up to ~15 on small machines. The bootstrap script waits and reports progress. When it finishes it exports:

  • GITLAB_E2E_URL (http://localhost:8929)
  • GITLAB_E2E_TOKEN, a root api-scope token (24h expiry)

Point warden at it and do whatever you like, because this stack is all yours:

npx @intentius/gitlab-warden reconcile \
  --config governance.yaml \
  --mode apply \
  --base-url-env GITLAB_E2E_URL \
  --token-env GITLAB_E2E_TOKEN

(Create a top-level group first: log in as root at http://localhost:8929 with the password from e2e/docker-compose.yml; once a group exists, baselines entries can provision everything under it.) The e2e suite itself (npm run test:e2e:run) provisions its own group/project and exercises every cycle's read path against this stack (plus one opt-in apply). Tear it all down, state included:

npm run e2e:down