@simplysf/simply-gitlab 0.4.0 → 0.5.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 +6 -0
- package/docs/getting-started.md +86 -0
- package/docs/guides/baselines.md +186 -0
- package/docs/guides/credentials.md +100 -0
- package/docs/guides/scripting.md +122 -0
- package/docs/guides/write-safety.md +121 -0
- package/docs/schema/baseline-v1.json +124 -0
- package/oclif.manifest.json +303 -303
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -13,6 +13,12 @@ human-readable by default and raw JSON with `--json`, and every command that cha
|
|
|
13
13
|
The same capabilities are available to AI agents through
|
|
14
14
|
[`@simplysf/simply-gitlab-mcp`](https://github.com/SimplySF/simply-gitlab/tree/main/packages/simply-gitlab-mcp).
|
|
15
15
|
|
|
16
|
+
## Documentation
|
|
17
|
+
|
|
18
|
+
Guides and the full command reference: **https://simplysf.github.io/simply-cli/gitlab/**
|
|
19
|
+
|
|
20
|
+
The guides also ship inside this package, under [`docs/`](docs).
|
|
21
|
+
|
|
16
22
|
## Install
|
|
17
23
|
|
|
18
24
|
`@simplysf/simply-gitlab` is a plugin of the `simply` CLI, not a command of its own. Install the host; this
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Get Started
|
|
3
|
+
description: Requirements, installation, and a first command for the Simply GitLab CLI.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Requirements
|
|
7
|
+
|
|
8
|
+
- Node.js 22 or later
|
|
9
|
+
- A GitLab account on gitlab.com or a self-managed instance, and an access token
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm install -g @simplysf/simply-cli
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Verify it worked
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
simply gitlab --help
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Get a token
|
|
24
|
+
|
|
25
|
+
Create a personal access token under **Preferences › Access tokens** in GitLab, or a project or
|
|
26
|
+
group access token from that project's or group's settings. Two scopes matter:
|
|
27
|
+
|
|
28
|
+
| Scope | What it allows |
|
|
29
|
+
| ---------- | -------------------------------------------------------- |
|
|
30
|
+
| `read_api` | Everything on this site that reads. Nothing that writes. |
|
|
31
|
+
| `api` | Reads and writes. |
|
|
32
|
+
|
|
33
|
+
Start with `read_api`. It is not just a smaller permission — it is the only thing that makes a
|
|
34
|
+
write structurally impossible, which matters as soon as a script or an agent is running these
|
|
35
|
+
commands. See [Write safety](/guides/write-safety/).
|
|
36
|
+
|
|
37
|
+
## Connect to your instance
|
|
38
|
+
|
|
39
|
+
Every command reads its connection settings from environment variables, from flags, or from a
|
|
40
|
+
`.env` file passed with `-e/--env-file`:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
GITLAB_TOKEN=glpat-...
|
|
44
|
+
GITLAB_URL=https://gitlab.example.com # omit entirely for gitlab.com
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`GITLAB_URL` defaults to `https://gitlab.com`, so a gitlab.com user needs only the token. See
|
|
48
|
+
[Credentials](/guides/credentials/) for precedence between flags, environment, and file, and for
|
|
49
|
+
trusting an internal certificate authority.
|
|
50
|
+
|
|
51
|
+
## First commands
|
|
52
|
+
|
|
53
|
+
Find a project you can reach, then look at its open merge requests:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
simply gitlab project list --membership
|
|
57
|
+
simply gitlab mr list --project group/project
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Add `--json` to either to get the raw API payload instead of the formatted table.
|
|
61
|
+
|
|
62
|
+
## Naming a project
|
|
63
|
+
|
|
64
|
+
Every project-scoped command takes `-p/--project`, which accepts a numeric id or the full path.
|
|
65
|
+
Both of these are the same project:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
simply gitlab mr list --project 1234
|
|
69
|
+
simply gitlab mr list --project group/subgroup/project
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The path is URL-encoded for you — do not encode it yourself. If you work in one project most of
|
|
73
|
+
the time, set `GITLAB_PROJECT` and drop the flag.
|
|
74
|
+
|
|
75
|
+
## Where to go next
|
|
76
|
+
|
|
77
|
+
- The [Command Reference](/reference/) lists every command by topic, with its flags and examples.
|
|
78
|
+
- [Credentials](/guides/credentials/) covers tokens, precedence, and certificates.
|
|
79
|
+
- [Write safety](/guides/write-safety/) explains `--dry-run`, `GITLAB_READ_ONLY`, and why a
|
|
80
|
+
`read_api` token is the only one of those that actually binds.
|
|
81
|
+
- [Scripts and agents](/guides/scripting/) covers the `--json` contract and exit codes.
|
|
82
|
+
- [MCP server](/guides/mcp-server/) gives an AI agent the same capabilities as tools, read-only by
|
|
83
|
+
default.
|
|
84
|
+
- Want to contribute? See
|
|
85
|
+
[CONTRIBUTING.md](https://github.com/SimplySF/simply-gitlab/blob/main/CONTRIBUTING.md) in the
|
|
86
|
+
repo.
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Configuration baselines
|
|
3
|
+
description: Hold many GitLab projects to one standard - export a baseline from a project you like, see what differs across the rest, and converge them.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Fifty projects drift. One was created before you settled on squash merges, someone unprotected
|
|
7
|
+
`main` to fix something and never put it back, and two never had an approval rule at all. There is
|
|
8
|
+
no answer to "are these the same?" short of opening fifty tabs.
|
|
9
|
+
|
|
10
|
+
A baseline is a JSON file saying what those projects should have in common, and three commands that
|
|
11
|
+
read it:
|
|
12
|
+
|
|
13
|
+
| Command | Writes? | What it does |
|
|
14
|
+
| ----------------------------- | ------- | ---------------------------------------------------- |
|
|
15
|
+
| `simply gitlab config export` | no | Emits a config from a project you already like |
|
|
16
|
+
| `simply gitlab config plan` | no | Reports, per project, what differs from the baseline |
|
|
17
|
+
| `simply gitlab config apply` | yes | Converges each project onto the baseline |
|
|
18
|
+
|
|
19
|
+
## Start by exporting
|
|
20
|
+
|
|
21
|
+
Do not write the file by hand. Export the project that already looks right and delete what you do
|
|
22
|
+
not care about:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
simply gitlab config export --project group/api > baseline.json
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The output is deliberately verbose — every supported key, not a curated subset — because deleting a
|
|
29
|
+
line is easier than discovering that a key exists. Variables come out as metadata only: keys,
|
|
30
|
+
scopes, and flags, never values.
|
|
31
|
+
|
|
32
|
+
## Point it at the rest
|
|
33
|
+
|
|
34
|
+
The exported file targets only the project it came from. Widen it on purpose:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"version": 1,
|
|
39
|
+
"targets": {
|
|
40
|
+
"groups": ["platform"],
|
|
41
|
+
"exclude": ["platform/sandbox", "platform/archived-*"]
|
|
42
|
+
},
|
|
43
|
+
"project": {
|
|
44
|
+
"settings": {
|
|
45
|
+
"merge_method": "ff",
|
|
46
|
+
"squash_option": "default_on",
|
|
47
|
+
"only_allow_merge_if_pipeline_succeeds": true
|
|
48
|
+
},
|
|
49
|
+
"protectedBranches": [{ "name": "main", "allow_force_push": false }],
|
|
50
|
+
"approvalRules": [{ "name": "Two maintainers", "approvals_required": 2, "usernames": ["alice", "bob"] }]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`targets.groups` expands through subgroups. `exclude` takes a trailing `*` to drop a whole subtree.
|
|
56
|
+
A project the token cannot see is a hard error, not a silent omission — a baseline that quietly
|
|
57
|
+
skipped six of fifty would leave those six wrong with nobody knowing to look.
|
|
58
|
+
|
|
59
|
+
## Read the plan
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
simply gitlab config plan --config ./baseline.json
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
Targets: 50 project(s), 1 group(s).
|
|
67
|
+
|
|
68
|
+
platform/api
|
|
69
|
+
settings
|
|
70
|
+
~ merge_method merge → ff
|
|
71
|
+
~ only_allow_merge_if_pipeline_succeeds false → true
|
|
72
|
+
protectedBranches
|
|
73
|
+
~ main.allow_force_push true → false
|
|
74
|
+
- legacy (not pruned; set "prune": true on this section to remove it)
|
|
75
|
+
|
|
76
|
+
platform/worker
|
|
77
|
+
✓ matches the baseline
|
|
78
|
+
|
|
79
|
+
50 target(s): 31 match, 19 differ.
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Three things in that output are worth understanding.
|
|
83
|
+
|
|
84
|
+
**Only what you declared is compared.** A project with forty settings the config never mentions
|
|
85
|
+
shows nothing for them. That is what makes a baseline safe to point at projects that already exist:
|
|
86
|
+
the blast radius is the set of keys you wrote down.
|
|
87
|
+
|
|
88
|
+
**Drift you are not fixing is still shown.** `- legacy` is a protected branch that exists but is not
|
|
89
|
+
in the config. By default nothing is deleted, and the line says so. Seeing what you are choosing not
|
|
90
|
+
to correct is most of the value of running this across fifty projects.
|
|
91
|
+
|
|
92
|
+
**A section can come back unsupported.** Approval rules, approval settings, and push rules are
|
|
93
|
+
Premium/Ultimate. On an instance without them the section reports that and the rest still converges.
|
|
94
|
+
|
|
95
|
+
## Apply it
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
simply gitlab config apply --config ./baseline.json --dry-run # prints the plan, sends nothing
|
|
99
|
+
simply gitlab config apply --config ./baseline.json
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Apply executes exactly what the plan showed — the two are computed in one pass, so a preview is a
|
|
103
|
+
guarantee rather than an approximation. A second run against a converged project sends no requests
|
|
104
|
+
at all.
|
|
105
|
+
|
|
106
|
+
Sections are applied in a fixed order: settings, protected branches, protected tags, approval
|
|
107
|
+
settings, approval rules, push rules. That order exists because an approval rule scoped to `main`
|
|
108
|
+
needs `main` protected before it can reference it.
|
|
109
|
+
|
|
110
|
+
One project failing does not abandon the other forty-nine. There is no transaction across GitLab
|
|
111
|
+
endpoints, so a project that fails part-way keeps the sections already applied; the summary says
|
|
112
|
+
where it stopped, and `plan` afterwards shows what is left.
|
|
113
|
+
|
|
114
|
+
## Per-project exceptions
|
|
115
|
+
|
|
116
|
+
`overrides` is keyed by project path:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
"overrides": {
|
|
120
|
+
"platform/legacy-api": {
|
|
121
|
+
"project": {
|
|
122
|
+
"settings": { "merge_method": "merge" },
|
|
123
|
+
"approvalRules": [{ "name": "Two maintainers", "approvals_required": 1 }]
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Lists merge by a natural key — `name` for branches, tags, and rules; `key` plus `environment_scope`
|
|
130
|
+
for variables — so an override adjusts one entry rather than restating the array. The rule above
|
|
131
|
+
changes one number and inherits the usernames from the baseline.
|
|
132
|
+
|
|
133
|
+
Set `"$replace": true` on a section to swap the list wholesale instead, for the project that needs
|
|
134
|
+
genuinely different rules rather than adjusted ones.
|
|
135
|
+
|
|
136
|
+
## Turning on pruning
|
|
137
|
+
|
|
138
|
+
By default the config adds and corrects; it never deletes. When you are ready for a section to be
|
|
139
|
+
authoritative, opt in:
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
"protectedBranches": {
|
|
143
|
+
"prune": true,
|
|
144
|
+
"entries": [{ "name": "main", "allow_force_push": false }]
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Now an undeclared protected branch is removed. Adopt this section by section, after `plan` has shown
|
|
149
|
+
you what it would take away — a first run against fifty existing projects with everything pruning
|
|
150
|
+
could remove protections that predate the config.
|
|
151
|
+
|
|
152
|
+
## Checking for drift in CI
|
|
153
|
+
|
|
154
|
+
```sh
|
|
155
|
+
simply gitlab config plan --config ./baseline.json --fail-on-drift
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Exits `1` when anything differs, so a scheduled job tells you something drifted without anyone
|
|
159
|
+
reading the output. `--json` returns the whole plan as structured data if you would rather track it
|
|
160
|
+
over time:
|
|
161
|
+
|
|
162
|
+
```sh
|
|
163
|
+
simply gitlab config plan --config ./baseline.json --json | jq '.summary'
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## CI/CD variables are reported, not written
|
|
167
|
+
|
|
168
|
+
The config can declare which variables must exist and what flags they carry. `plan` reports the ones
|
|
169
|
+
that are missing or wrong; `apply` does not touch them.
|
|
170
|
+
|
|
171
|
+
That is the API, not caution. Creating a variable requires a value, and so does updating one — even
|
|
172
|
+
to change `masked` from false to true. A variable created as `masked_and_hidden` will not return its
|
|
173
|
+
value at all. There is no honest way to reconcile secrets from a file that, by design, holds none.
|
|
174
|
+
|
|
175
|
+
Knowing that six projects are missing `DEPLOY_ENV` is still the question people actually ask; setting
|
|
176
|
+
it is a person's job.
|
|
177
|
+
|
|
178
|
+
## From an agent
|
|
179
|
+
|
|
180
|
+
The MCP server exposes `gitlab_config_plan` and `gitlab_config_export`, so you can ask an agent
|
|
181
|
+
"which of our projects are missing the two-approval rule?" and get an answer.
|
|
182
|
+
|
|
183
|
+
There is deliberately no tool that applies a baseline. Changing settings across fifty projects is a
|
|
184
|
+
deployment, not a tool call, and belongs to a person with the plan in front of them. An agent that
|
|
185
|
+
should apply one can run the CLI, where it is a shell command you can see and approve. See the
|
|
186
|
+
[MCP server guide](/guides/mcp-server/).
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Credentials
|
|
3
|
+
description: How the Simply GitLab CLI finds its connection settings, which token scope to use, and how flags, environment variables, and an env file take precedence.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Connection settings come from environment variables, from flags, or from a `.env` file named with
|
|
7
|
+
`-e/--env-file`. Only GitLab connection variables are read from that file; anything else in it is
|
|
8
|
+
ignored.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
GITLAB_TOKEN=glpat-...
|
|
12
|
+
GITLAB_URL=https://gitlab.example.com # omit entirely for gitlab.com
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
There are only two settings, and one of them has a default — `GITLAB_URL` is `https://gitlab.com`
|
|
16
|
+
unless you say otherwise. A gitlab.com user needs the token and nothing else.
|
|
17
|
+
|
|
18
|
+
## Tokens and scopes
|
|
19
|
+
|
|
20
|
+
The token is a personal access token (**Preferences › Access tokens**), a project access token, or
|
|
21
|
+
a group access token. All three work the same way; the CLI sends whichever you give it as GitLab's
|
|
22
|
+
`PRIVATE-TOKEN` header.
|
|
23
|
+
|
|
24
|
+
| Scope | Covers |
|
|
25
|
+
| ---------- | ------------------------------------------- |
|
|
26
|
+
| `read_api` | Every read command on this site. No writes. |
|
|
27
|
+
| `api` | Reads and writes. |
|
|
28
|
+
|
|
29
|
+
Prefer `read_api` and reach for `api` only when you mean to change something — see
|
|
30
|
+
[Write safety](/guides/write-safety/) for why that distinction carries more weight than it looks
|
|
31
|
+
like it should.
|
|
32
|
+
|
|
33
|
+
OAuth 2 tokens are not supported: GitLab accepts those only as `Authorization: Bearer`, and this
|
|
34
|
+
CLI runs no OAuth flow.
|
|
35
|
+
|
|
36
|
+
## Precedence
|
|
37
|
+
|
|
38
|
+
Explicit flags beat the environment, which beats the file's contents. That ordering is what makes
|
|
39
|
+
the two-file arrangement in [Write safety](/guides/write-safety/) work: a `read_api` file can be
|
|
40
|
+
the default, and a write-capable one is named explicitly for the one command that needs it.
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
simply gitlab mr list -e ~/gitlab.env --project group/project
|
|
44
|
+
simply gitlab mr update -e ~/gitlab-write.env --project group/project --mr 42 --state close
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
A variable exported as an empty string does not outrank the file. `export GITLAB_TOKEN=` in a
|
|
48
|
+
wrapper script would otherwise discard the real token and fail as though nothing were configured.
|
|
49
|
+
|
|
50
|
+
## Naming a project
|
|
51
|
+
|
|
52
|
+
Not a credential, but it belongs in the same shell profile. `GITLAB_PROJECT` sets the default for
|
|
53
|
+
`-p/--project`:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
export GITLAB_PROJECT=group/subgroup/project
|
|
57
|
+
simply gitlab mr list
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
A project is named by numeric id or full path, and the path is URL-encoded for you.
|
|
61
|
+
|
|
62
|
+
## Self-managed instances
|
|
63
|
+
|
|
64
|
+
Set `GITLAB_URL` to the instance origin. A subpath is kept, because self-managed instances are
|
|
65
|
+
routinely served from one:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
GITLAB_URL=https://example.com/gitlab
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
One thing to watch: pasting a **project** URL from gitlab.com — `https://gitlab.com/group/project`
|
|
72
|
+
— sets the instance, not the project. The CLI keeps only the origin in that case, since gitlab.com
|
|
73
|
+
has no subpath. On a self-managed host it cannot tell a subpath from a project path, so give it the
|
|
74
|
+
instance root and name the project with `--project`.
|
|
75
|
+
|
|
76
|
+
## Certificate verification
|
|
77
|
+
|
|
78
|
+
Certificate verification is always on. For an instance behind an internal or agency certificate
|
|
79
|
+
authority, trust that CA rather than disabling verification:
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
NODE_EXTRA_CA_CERTS=/path/to/ca.pem simply gitlab project list
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
A `GITLAB_SSL_VERIFY=false` carried over from other GitLab tooling is rejected with an error rather
|
|
86
|
+
than silently ignored, so nobody ends up believing verification is off when it is on, or the
|
|
87
|
+
reverse.
|
|
88
|
+
|
|
89
|
+
## Checking what you're connected as
|
|
90
|
+
|
|
91
|
+
There is no `whoami` command. The cheapest check is a list you expect to be non-empty:
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
simply gitlab project list --membership --limit 5
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
A wrong or expired token comes back as an authentication error with exit code `3`. A `403` on a
|
|
98
|
+
command that changes data says the token may be `read_api`-scoped, or that your role on the project
|
|
99
|
+
does not permit the change — so a refused write is distinguishable from a genuine permissions
|
|
100
|
+
problem.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Scripts and agents
|
|
3
|
+
description: The output contract that makes the Simply GitLab CLI safe to drive from a shell script or an AI agent - raw JSON, stable exit codes, honest truncation, no prompts.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
The CLI is designed for two callers at once: a person at a terminal, and a script or AI agent
|
|
7
|
+
running it as shell commands. The defaults serve the person; a small, stable contract serves the
|
|
8
|
+
script.
|
|
9
|
+
|
|
10
|
+
## `--json` returns the raw API payload
|
|
11
|
+
|
|
12
|
+
Every command accepts `--json`, without exception. The output is the unmodified response from the
|
|
13
|
+
GitLab API — the full object, not the curated subset the table shows — so anything the API exposes
|
|
14
|
+
is reachable with `jq`, and the payload shape is GitLab's contract rather than this CLI's.
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
simply gitlab mr view --project group/project --mr 42 --json | jq -r '.detailed_merge_status'
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Lists tell you when they were cut short
|
|
21
|
+
|
|
22
|
+
Every list command follows pages up to `--limit`, so there is no single response to hand back.
|
|
23
|
+
They return an envelope instead:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{ "items": [...], "total": 500, "pages": 3, "complete": false }
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The elements inside `items` are still raw API objects. **`complete: false` means `--limit` cut the
|
|
30
|
+
results short** and more exist, so a caller can detect truncation without counting rows. `total` is
|
|
31
|
+
present only when GitLab reports one — keyset-paginated endpoints and very large result sets omit
|
|
32
|
+
it, so treat a missing `total` as unknown rather than zero.
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
out=$(simply gitlab mr list --project group/project --state all --limit 100 --json)
|
|
36
|
+
if [ "$(jq -r .complete <<<"$out")" = "false" ]; then
|
|
37
|
+
echo "more than 100 merge requests; raise --limit" >&2
|
|
38
|
+
fi
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Exit codes and errors
|
|
42
|
+
|
|
43
|
+
Errors carry no stack trace and map to stable exit codes:
|
|
44
|
+
|
|
45
|
+
| Exit code | Meaning |
|
|
46
|
+
| --------- | ---------------------------------------------------------- |
|
|
47
|
+
| `0` | Success |
|
|
48
|
+
| `2` | Configuration or usage problem, e.g. a missing token |
|
|
49
|
+
| `3` | Authentication or authorization failure |
|
|
50
|
+
| `1` | Anything else, including an API error or a network failure |
|
|
51
|
+
|
|
52
|
+
Under `--json`, a failure is written as one JSON object on **stderr** and stdout stays empty, so a
|
|
53
|
+
caller that captures stdout never has to disambiguate an error from a payload. The object nests
|
|
54
|
+
under an `error` key:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{ "error": { "name": "HttpError", "message": "...", "exitCode": 1, "status": 404, "body": {} } }
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`body` carries the API's own response when there was one. Every string in the object — the message
|
|
61
|
+
and everything inside `body` — is scrubbed of any known credential value and of characters that
|
|
62
|
+
could rewrite the terminal or hide text, so a response that quotes your token back at you does not
|
|
63
|
+
put it on the stream.
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
if ! out=$(simply gitlab mr view --project group/project --mr 999 --json 2>err.json); then
|
|
67
|
+
case $? in
|
|
68
|
+
3) echo "credentials rejected";;
|
|
69
|
+
*) jq -r .error.message err.json;;
|
|
70
|
+
esac
|
|
71
|
+
fi
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Address merge requests by `iid`
|
|
75
|
+
|
|
76
|
+
A merge request has both an `id` and an `iid`. Both are bare integers, both appear in every
|
|
77
|
+
payload, and the endpoints take the **`iid`** — the number in the web URL. Passing the wrong one
|
|
78
|
+
does not fail: it addresses a different merge request in a different project that happens to share
|
|
79
|
+
the number.
|
|
80
|
+
|
|
81
|
+
The CLI validates the shape and refuses anything that is not a positive whole number, but it cannot
|
|
82
|
+
catch a valid-looking wrong number. When scripting, read `iid` from the list output, never `id`:
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
simply gitlab mr list --project group/project --json | jq -r '.items[].iid'
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Keep payloads small
|
|
89
|
+
|
|
90
|
+
`--simple` on `project list` returns a much smaller object. `--stat` on `commit diff` lists the
|
|
91
|
+
changed files without the hunks. `--tail` on `ci job log` defaults to the last 200 lines, because a
|
|
92
|
+
trace is regularly tens of megabytes and an agent pays for every token it reads:
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
simply gitlab ci job list --project group/project --scope failed --limit 5 --json \
|
|
96
|
+
| jq -r '.items[0].id' \
|
|
97
|
+
| xargs -I{} simply gitlab ci job log --project group/project --job {} --tail 50
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Retries are handled for you
|
|
101
|
+
|
|
102
|
+
429 and 5xx responses retry with exponential backoff, honouring a capped `Retry-After`. DNS
|
|
103
|
+
failures and certificate problems do not retry, because another attempt cannot fix them. A script
|
|
104
|
+
does not need its own retry loop around these commands.
|
|
105
|
+
|
|
106
|
+
## Never interactive
|
|
107
|
+
|
|
108
|
+
No command prompts or blocks on stdin, and there is no `--confirm` flag to hang on. Every write
|
|
109
|
+
takes `--dry-run` instead. See [Write safety](/guides/write-safety/) for the full set of layers and
|
|
110
|
+
the two-credential-file arrangement that keeps an agent's normal loop read-only.
|
|
111
|
+
|
|
112
|
+
## Help is discovery
|
|
113
|
+
|
|
114
|
+
`--help` output is generated from the command classes and written to be self-sufficient, so an
|
|
115
|
+
agent can discover flags and examples the same way a person does:
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
simply gitlab mr --help
|
|
119
|
+
simply gitlab mr create --help
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The same text, for every command, is on the [Command Reference](/reference/) pages.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Write safety
|
|
3
|
+
description: The layers between a Simply GitLab command and a change to your repository, which of them is a real boundary, and why CI variable values are hidden by default.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Six commands change data — `branch create`, `file create`, `file update`, `commit create`,
|
|
7
|
+
`mr create`, and `mr update`. Nothing in this CLI deletes anything. Those six sit behind several
|
|
8
|
+
layers, and only the last is a real boundary.
|
|
9
|
+
|
|
10
|
+
## No `--confirm`, deliberately
|
|
11
|
+
|
|
12
|
+
Every write here either creates something or edits a merge request, and both are recoverable: a
|
|
13
|
+
branch can be deleted, a file can be committed over, a closed merge request can be reopened.
|
|
14
|
+
Requiring a confirmation for actions like those would train you to pass it always, at which point
|
|
15
|
+
it protects nothing while still implying that it does.
|
|
16
|
+
|
|
17
|
+
So there is no `--confirm` flag anywhere in this CLI. The first command that irreversibly destroys
|
|
18
|
+
something is the one that brings it back.
|
|
19
|
+
|
|
20
|
+
## `--dry-run`
|
|
21
|
+
|
|
22
|
+
Accepted by all six. It prints the exact request body that would be sent and sends nothing, which
|
|
23
|
+
is the cheapest way to see what is about to happen:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
simply gitlab mr create --project group/project \
|
|
27
|
+
--source-branch feature/thing --target-branch main --title "feat: the thing" --dry-run
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
Dry run — not sent. Request body:
|
|
32
|
+
{
|
|
33
|
+
"source_branch": "feature/thing",
|
|
34
|
+
"target_branch": "main",
|
|
35
|
+
"title": "feat: the thing"
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## `--last-commit-id` on a file update
|
|
40
|
+
|
|
41
|
+
`file update` replaces the whole file; there is no partial edit. That makes a lost-update race real
|
|
42
|
+
whenever two people or two runs touch the same file. `file view` reports the file's last commit,
|
|
43
|
+
and passing it back makes GitLab reject the write if anything changed meanwhile:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
simply gitlab file view --project group/project --path README.md --ref main
|
|
47
|
+
# Last commit: 9a1b2c3
|
|
48
|
+
|
|
49
|
+
simply gitlab file update --project group/project --path README.md --branch main \
|
|
50
|
+
--content-file ./README.md --message "docs: refresh" --last-commit-id 9a1b2c3
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Without it, the last writer silently wins.
|
|
54
|
+
|
|
55
|
+
## `GITLAB_READ_ONLY`
|
|
56
|
+
|
|
57
|
+
Set it to `1`, `true`, `yes`, or `on` and every write command refuses before making any request.
|
|
58
|
+
Reads are unaffected. This guards against misconfiguration: the wrong credential file, the wrong
|
|
59
|
+
project, the wrong context. It is not a security boundary, because anything that can run commands
|
|
60
|
+
can also unset an environment variable.
|
|
61
|
+
|
|
62
|
+
## A `read_api` token
|
|
63
|
+
|
|
64
|
+
This is the only layer that actually binds. A `read_api`-scoped token cannot create, edit, or push
|
|
65
|
+
anything — GitLab refuses server-side, regardless of what this CLI sends or what any caller is
|
|
66
|
+
persuaded to attempt.
|
|
67
|
+
|
|
68
|
+
That matters most when an AI agent drives the CLI, because merge request descriptions, commit
|
|
69
|
+
messages, and file contents are written by anyone who can push, and an agent reading that text
|
|
70
|
+
cannot reliably tell instructions from content. The arrangement worth adopting is two credential
|
|
71
|
+
files:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
~/gitlab.env # read_api token — what the agent uses by default
|
|
75
|
+
~/gitlab-write.env # api token — passed explicitly, by a person
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
simply gitlab mr list -e ~/gitlab.env --project group/project
|
|
80
|
+
simply gitlab mr update -e ~/gitlab-write.env --project group/project --mr 42 --state close
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The agent's normal loop is then structurally incapable of changing anything, and a write becomes a
|
|
84
|
+
deliberate act.
|
|
85
|
+
|
|
86
|
+
## CI variable values are hidden
|
|
87
|
+
|
|
88
|
+
`simply gitlab ci variable list` replaces every value with `<hidden>`, in the table and under
|
|
89
|
+
`--json` alike:
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
simply gitlab ci variable list --project group/project
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
KEY SCOPE TYPE PROTECTED MASKED VALUE
|
|
97
|
+
────────── ───── ─────── ───────── ────── ────────
|
|
98
|
+
DEPLOY_KEY * env_var yes yes <hidden>
|
|
99
|
+
|
|
100
|
+
Showing 1 of 1 variable(s).
|
|
101
|
+
Values are hidden. Pass --reveal to print them.
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
This is worth explaining, because GitLab's own API does not do this. `GET /projects/:id/variables`
|
|
105
|
+
returns **every** value in clear text — including the ones marked "masked", since masking only
|
|
106
|
+
hides a value in job logs, not from the API. Those values are deploy keys, signing certificates,
|
|
107
|
+
and production credentials, and the usual reason to list them is to see _which_ exist, not to read
|
|
108
|
+
them.
|
|
109
|
+
|
|
110
|
+
`--reveal` prints them. It exists on the CLI and has no equivalent in the
|
|
111
|
+
[MCP server](/guides/mcp-server/), deliberately: a value revealed to a person is on their screen,
|
|
112
|
+
and a value revealed to an agent is in a context window, a transcript, and whatever that agent
|
|
113
|
+
writes next.
|
|
114
|
+
|
|
115
|
+
## Untrusted text is stripped
|
|
116
|
+
|
|
117
|
+
Merge request titles and descriptions, commit messages, repository file contents, and CI job traces
|
|
118
|
+
are all written by whoever can push to a project or open a merge request. Every one of them is
|
|
119
|
+
stripped of control characters before it is printed, so an escape sequence cannot make your
|
|
120
|
+
terminal show something other than what the API returned — and your token is redacted from every
|
|
121
|
+
error message and response body.
|