@intentius/gitlab-warden 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -0
- package/bin/gitlab-warden.js +14 -0
- package/dist/cli.js +31 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# gitlab-warden
|
|
2
|
+
|
|
3
|
+
Declarative governance for GitLab **groups & projects** — the whole surface, in one lightweight tool you run in CI.
|
|
4
|
+
|
|
5
|
+
The third [warden](https://github.com/INTENTIUS/github-warden), built on the shared
|
|
6
|
+
provider-agnostic reconcile primitive in
|
|
7
|
+
[`@intentius/chant/reconcile`](https://github.com/INTENTIUS/chant) — the same core
|
|
8
|
+
behind [github-warden](https://github.com/INTENTIUS/github-warden) and
|
|
9
|
+
[forgejo-warden](https://github.com/INTENTIUS/forgejo-warden). gitlab-warden
|
|
10
|
+
supplies the GitLab layer: a REST + GraphQL client (configurable host for
|
|
11
|
+
self-managed + GitLab.com), config + live-state types, a GitLab `diff()`, and the
|
|
12
|
+
reconcile cycles.
|
|
13
|
+
|
|
14
|
+
> 🚧 **Preview published; engine in progress.** `@intentius/gitlab-warden@0.1.0` is a
|
|
15
|
+
> placeholder that establishes the package + release pipeline. The reconcile engine
|
|
16
|
+
> is being built per the [roadmap epic](https://github.com/INTENTIUS/gitlab-warden/issues/21).
|
|
17
|
+
|
|
18
|
+
## What it is
|
|
19
|
+
|
|
20
|
+
You declare desired state in YAML (selective-by-omission — an absent field is never
|
|
21
|
+
touched); warden diffs it against the live GitLab API and, in `apply` mode,
|
|
22
|
+
converges it — guarded so a typo can't mass-delete.
|
|
23
|
+
|
|
24
|
+
It's a **single binary + a YAML file in CI**: no state file, no HCL, no provider
|
|
25
|
+
toolchain to stand up. That's the whole point — governance-as-code without the
|
|
26
|
+
weight, covering the **full** GitLab governance surface in one place:
|
|
27
|
+
|
|
28
|
+
- **Stateless** — diff against live, reconcile. Nothing to drift, import, or lock.
|
|
29
|
+
- **Continuous drift correction** — a reconcile loop, not a one-shot apply.
|
|
30
|
+
- **Selective-by-omission + ownership-gated deletes** — manage a slice of a large instance without claiming the rest.
|
|
31
|
+
- **Guardrails + dry-run default** — removal cap, lockout protection.
|
|
32
|
+
- **Tier-graceful** — Premium/Ultimate-gated endpoints that 403 are reported and skipped, never fatal.
|
|
33
|
+
|
|
34
|
+
### Flagship: push-rule drift
|
|
35
|
+
|
|
36
|
+
GitLab push rules aren't version-controlled and their inheritance is **broken** —
|
|
37
|
+
copied at project creation, never propagated; change a group rule and existing
|
|
38
|
+
projects don't get it (each fixed by hand). A reconcile loop that re-asserts
|
|
39
|
+
declared push rules across a whole group tree fixes that, continuously. It's the
|
|
40
|
+
sharpest single example of the model, but warden goes after the *entire* surface.
|
|
41
|
+
|
|
42
|
+
## Coverage (the full surface)
|
|
43
|
+
|
|
44
|
+
| Scope | Cycles |
|
|
45
|
+
|-------|--------|
|
|
46
|
+
| **Group** | settings · members · subgroup provisioning · variables · webhooks · push rules · access tokens · protected environments · integrations · MR approval settings · compliance frameworks · security policies · member roles |
|
|
47
|
+
| **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 |
|
|
48
|
+
| **Instance** (self-managed) | application settings · instance CI/CD variables · system hooks · custom member roles |
|
|
49
|
+
|
|
50
|
+
REST for most of it; **GraphQL** for the few surfaces that require it (compliance
|
|
51
|
+
frameworks, security-policy attachment, custom roles).
|
|
52
|
+
|
|
53
|
+
## The one genuinely hard part
|
|
54
|
+
|
|
55
|
+
GitLab is a *tree* (nested groups, inherited membership), not a flat org. Membership
|
|
56
|
+
must diff against **direct** members (`/members`) while *reading* effective members
|
|
57
|
+
(`/members/all`), and must **never** treat an inherited member as deletable drift
|
|
58
|
+
(the DELETE fails — the grant lives at an ancestor). This is the credibility gate;
|
|
59
|
+
it gets a dedicated design issue (#3) that the members cycle implements against.
|
|
60
|
+
Everything else is a straightforward cycle on the shared harness.
|
|
61
|
+
|
|
62
|
+
## How it relates to the sibling wardens
|
|
63
|
+
|
|
64
|
+
| | github-warden | forgejo-warden | gitlab-warden |
|
|
65
|
+
|---|---|---|---|
|
|
66
|
+
| Hierarchy | flat org → repo | flat org → repo | **nested groups → projects** |
|
|
67
|
+
| Membership | direct | team-driven | **direct + inherited** |
|
|
68
|
+
| Auth | GitHub App | token, self-hosted | token, self-managed + SaaS |
|
|
69
|
+
| API | REST | REST | **REST + GraphQL** |
|
|
70
|
+
| Reconcile core | `@intentius/chant/reconcile` | (same) | (same) |
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* gitlab-warden bin launcher.
|
|
4
|
+
*
|
|
5
|
+
* Committed so it exists at npm pack-validation time (before `prepublishOnly`/
|
|
6
|
+
* `build` runs). It loads the built dist/cli.js and calls the exported `run()`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import(new URL("../dist/cli.js", import.meta.url).href)
|
|
10
|
+
.then((mod) => mod.run(process.argv.slice(2)))
|
|
11
|
+
.catch((err) => {
|
|
12
|
+
process.stderr.write(`gitlab-warden: fatal: ${err?.message ?? err}\n`);
|
|
13
|
+
process.exit(3);
|
|
14
|
+
});
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import{createRequire as __cr}from'module';const require=__cr(import.meta.url);
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
var VERSION = "0.1.0";
|
|
5
|
+
var USAGE = `gitlab-warden ${VERSION} \u2014 declarative governance for GitLab groups & projects
|
|
6
|
+
|
|
7
|
+
\u26A0\uFE0F Early preview. The reconcile engine is under active development.
|
|
8
|
+
Roadmap: https://github.com/INTENTIUS/gitlab-warden/issues/21
|
|
9
|
+
|
|
10
|
+
Planned usage:
|
|
11
|
+
gitlab-warden reconcile --config <path> [--mode dry-run|apply] [--cycles ...]
|
|
12
|
+
|
|
13
|
+
Flags:
|
|
14
|
+
-h, --help Show this help
|
|
15
|
+
-v, --version Print the version
|
|
16
|
+
|
|
17
|
+
Today this command prints help only; functional reconcile arrives with the
|
|
18
|
+
cycles on the roadmap.
|
|
19
|
+
`;
|
|
20
|
+
async function run(argv = []) {
|
|
21
|
+
const arg = argv[0];
|
|
22
|
+
if (arg === "-v" || arg === "--version") {
|
|
23
|
+
process.stdout.write(`${VERSION}
|
|
24
|
+
`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
process.stdout.write(USAGE);
|
|
28
|
+
}
|
|
29
|
+
export {
|
|
30
|
+
run
|
|
31
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@intentius/gitlab-warden",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Declarative governance for GitLab groups & projects — stateless, drift-correcting reconcile in CI (no Terraform state, no Ultimate)",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/INTENTIUS/gitlab-warden.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/INTENTIUS/gitlab-warden#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/INTENTIUS/gitlab-warden/issues"
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"bin": {
|
|
19
|
+
"gitlab-warden": "bin/gitlab-warden.js"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"bin",
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"tsc": "tsc --noEmit",
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"build": "esbuild src/cli.ts --bundle --platform=node --format=esm --banner:js=\"import{createRequire as __cr}from'module';const require=__cr(import.meta.url);\" --outfile=dist/cli.js && chmod +x dist/cli.js",
|
|
29
|
+
"prepublishOnly": "npm run build"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^22.0.0",
|
|
33
|
+
"esbuild": "^0.28.0",
|
|
34
|
+
"typescript": "^5.9.3",
|
|
35
|
+
"vitest": "^4.1.9"
|
|
36
|
+
}
|
|
37
|
+
}
|