@simplysf/simply-gitlab 0.4.0 → 0.6.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 +110 -25
- 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 +122 -0
- package/docs/schema/baseline-v1.json +124 -0
- package/lib/commands/gitlab/project/create.d.ts +25 -0
- package/lib/commands/gitlab/project/create.js +111 -0
- package/lib/commands/gitlab/project/create.js.map +1 -0
- package/oclif.manifest.json +187 -1
- package/package.json +4 -3
|
@@ -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,122 @@
|
|
|
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
|
+
Seven commands change data — `project create`, `branch create`, `file create`, `file update`,
|
|
7
|
+
`commit create`, `mr create`, and `mr update`. Nothing in this CLI deletes anything. Those seven sit
|
|
8
|
+
behind several 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 seven. 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. The one nuance is `project create`: a namespace
|
|
24
|
+
or template given as a path is still looked up, so the printed ids are the ones that would be sent.
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
simply gitlab mr create --project group/project \
|
|
28
|
+
--source-branch feature/thing --target-branch main --title "feat: the thing" --dry-run
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
Dry run — not sent. Request body:
|
|
33
|
+
{
|
|
34
|
+
"source_branch": "feature/thing",
|
|
35
|
+
"target_branch": "main",
|
|
36
|
+
"title": "feat: the thing"
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## `--last-commit-id` on a file update
|
|
41
|
+
|
|
42
|
+
`file update` replaces the whole file; there is no partial edit. That makes a lost-update race real
|
|
43
|
+
whenever two people or two runs touch the same file. `file view` reports the file's last commit,
|
|
44
|
+
and passing it back makes GitLab reject the write if anything changed meanwhile:
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
simply gitlab file view --project group/project --path README.md --ref main
|
|
48
|
+
# Last commit: 9a1b2c3
|
|
49
|
+
|
|
50
|
+
simply gitlab file update --project group/project --path README.md --branch main \
|
|
51
|
+
--content-file ./README.md --message "docs: refresh" --last-commit-id 9a1b2c3
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Without it, the last writer silently wins.
|
|
55
|
+
|
|
56
|
+
## `GITLAB_READ_ONLY`
|
|
57
|
+
|
|
58
|
+
Set it to `1`, `true`, `yes`, or `on` and every write command refuses before making any request.
|
|
59
|
+
Reads are unaffected. This guards against misconfiguration: the wrong credential file, the wrong
|
|
60
|
+
project, the wrong context. It is not a security boundary, because anything that can run commands
|
|
61
|
+
can also unset an environment variable.
|
|
62
|
+
|
|
63
|
+
## A `read_api` token
|
|
64
|
+
|
|
65
|
+
This is the only layer that actually binds. A `read_api`-scoped token cannot create, edit, or push
|
|
66
|
+
anything — GitLab refuses server-side, regardless of what this CLI sends or what any caller is
|
|
67
|
+
persuaded to attempt.
|
|
68
|
+
|
|
69
|
+
That matters most when an AI agent drives the CLI, because merge request descriptions, commit
|
|
70
|
+
messages, and file contents are written by anyone who can push, and an agent reading that text
|
|
71
|
+
cannot reliably tell instructions from content. The arrangement worth adopting is two credential
|
|
72
|
+
files:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
~/gitlab.env # read_api token — what the agent uses by default
|
|
76
|
+
~/gitlab-write.env # api token — passed explicitly, by a person
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
simply gitlab mr list -e ~/gitlab.env --project group/project
|
|
81
|
+
simply gitlab mr update -e ~/gitlab-write.env --project group/project --mr 42 --state close
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The agent's normal loop is then structurally incapable of changing anything, and a write becomes a
|
|
85
|
+
deliberate act.
|
|
86
|
+
|
|
87
|
+
## CI variable values are hidden
|
|
88
|
+
|
|
89
|
+
`simply gitlab ci variable list` replaces every value with `<hidden>`, in the table and under
|
|
90
|
+
`--json` alike:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
simply gitlab ci variable list --project group/project
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
KEY SCOPE TYPE PROTECTED MASKED VALUE
|
|
98
|
+
────────── ───── ─────── ───────── ────── ────────
|
|
99
|
+
DEPLOY_KEY * env_var yes yes <hidden>
|
|
100
|
+
|
|
101
|
+
Showing 1 of 1 variable(s).
|
|
102
|
+
Values are hidden. Pass --reveal to print them.
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
This is worth explaining, because GitLab's own API does not do this. `GET /projects/:id/variables`
|
|
106
|
+
returns **every** value in clear text — including the ones marked "masked", since masking only
|
|
107
|
+
hides a value in job logs, not from the API. Those values are deploy keys, signing certificates,
|
|
108
|
+
and production credentials, and the usual reason to list them is to see _which_ exist, not to read
|
|
109
|
+
them.
|
|
110
|
+
|
|
111
|
+
`--reveal` prints them. It exists on the CLI and has no equivalent in the
|
|
112
|
+
[MCP server](/guides/mcp-server/), deliberately: a value revealed to a person is on their screen,
|
|
113
|
+
and a value revealed to an agent is in a context window, a transcript, and whatever that agent
|
|
114
|
+
writes next.
|
|
115
|
+
|
|
116
|
+
## Untrusted text is stripped
|
|
117
|
+
|
|
118
|
+
Merge request titles and descriptions, commit messages, repository file contents, and CI job traces
|
|
119
|
+
are all written by whoever can push to a project or open a merge request. Every one of them is
|
|
120
|
+
stripped of control characters before it is printed, so an escape sequence cannot make your
|
|
121
|
+
terminal show something other than what the API returned — and your token is redacted from every
|
|
122
|
+
error message and response body.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://simplysf.github.io/simply-cli/schema/baseline-v1.json",
|
|
4
|
+
"title": "Simply GitLab configuration baseline",
|
|
5
|
+
"description": "What a set of GitLab projects should have in common. Read by `simply gitlab config plan` and `simply gitlab config apply`. Advisory: the CLI's own validator is what runs, and its errors name the exact path that is wrong.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["version"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"$schema": { "type": "string" },
|
|
11
|
+
"version": {
|
|
12
|
+
"const": 1,
|
|
13
|
+
"description": "Config format version. Only 1 exists."
|
|
14
|
+
},
|
|
15
|
+
"targets": { "$ref": "#/$defs/targets" },
|
|
16
|
+
"group": { "$ref": "#/$defs/groupDesired" },
|
|
17
|
+
"project": { "$ref": "#/$defs/projectDesired" },
|
|
18
|
+
"overrides": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"description": "Per-target exceptions, keyed by full project or group path.",
|
|
21
|
+
"additionalProperties": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"additionalProperties": false,
|
|
24
|
+
"properties": {
|
|
25
|
+
"project": { "$ref": "#/$defs/projectDesired" },
|
|
26
|
+
"group": { "$ref": "#/$defs/groupDesired" }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"$defs": {
|
|
32
|
+
"targets": {
|
|
33
|
+
"type": "object",
|
|
34
|
+
"description": "What the run acts on. Groups expand through their subgroups.",
|
|
35
|
+
"additionalProperties": false,
|
|
36
|
+
"anyOf": [{ "required": ["groups"] }, { "required": ["projects"] }],
|
|
37
|
+
"properties": {
|
|
38
|
+
"groups": {
|
|
39
|
+
"type": "array",
|
|
40
|
+
"description": "Group paths or ids. Each expands to every project in the group and its subgroups.",
|
|
41
|
+
"items": { "type": "string", "minLength": 1 }
|
|
42
|
+
},
|
|
43
|
+
"projects": {
|
|
44
|
+
"type": "array",
|
|
45
|
+
"description": "Individual project paths or ids, added to whatever the groups expanded to.",
|
|
46
|
+
"items": { "type": "string", "minLength": 1 }
|
|
47
|
+
},
|
|
48
|
+
"exclude": {
|
|
49
|
+
"type": "array",
|
|
50
|
+
"description": "Removed from the union. A trailing * drops a whole subtree.",
|
|
51
|
+
"items": { "type": "string", "minLength": 1 }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"groupDesired": {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"additionalProperties": false,
|
|
58
|
+
"properties": {
|
|
59
|
+
"settings": {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"description": "Attributes for PUT /groups/:id. Names are GitLab's own."
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"projectDesired": {
|
|
66
|
+
"type": "object",
|
|
67
|
+
"additionalProperties": false,
|
|
68
|
+
"properties": {
|
|
69
|
+
"settings": {
|
|
70
|
+
"type": "object",
|
|
71
|
+
"description": "Attributes for PUT /projects/:id — merge method, CI config path, build timeout, and the rest. Names are GitLab's own; only what you declare is compared."
|
|
72
|
+
},
|
|
73
|
+
"protectedBranches": {
|
|
74
|
+
"$ref": "#/$defs/section",
|
|
75
|
+
"description": "Branch protections, keyed by name. Corrected in place with PATCH."
|
|
76
|
+
},
|
|
77
|
+
"protectedTags": {
|
|
78
|
+
"$ref": "#/$defs/section",
|
|
79
|
+
"description": "Tag protections, keyed by name. GitLab has no update endpoint, so a change is a delete and a create."
|
|
80
|
+
},
|
|
81
|
+
"approvals": {
|
|
82
|
+
"type": "object",
|
|
83
|
+
"description": "Project approval settings (Premium/Ultimate)."
|
|
84
|
+
},
|
|
85
|
+
"approvalRules": {
|
|
86
|
+
"$ref": "#/$defs/section",
|
|
87
|
+
"description": "Approval rules, keyed by name (Premium/Ultimate). Accepts `usernames`, group paths under `groups`, and branch names under `protectedBranches`; the ids are resolved for you."
|
|
88
|
+
},
|
|
89
|
+
"pushRules": {
|
|
90
|
+
"type": "object",
|
|
91
|
+
"description": "Push rules (Premium/Ultimate)."
|
|
92
|
+
},
|
|
93
|
+
"variables": {
|
|
94
|
+
"$ref": "#/$defs/section",
|
|
95
|
+
"description": "CI/CD variable metadata, keyed by key + environment_scope. Reported only, never written: creating or updating a variable requires its value, and this file holds none."
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"section": {
|
|
100
|
+
"description": "A list section: a bare array, or an object that can also prune or replace.",
|
|
101
|
+
"oneOf": [
|
|
102
|
+
{ "type": "array", "items": { "type": "object" } },
|
|
103
|
+
{
|
|
104
|
+
"type": "object",
|
|
105
|
+
"required": ["entries"],
|
|
106
|
+
"additionalProperties": false,
|
|
107
|
+
"properties": {
|
|
108
|
+
"prune": {
|
|
109
|
+
"type": "boolean",
|
|
110
|
+
"description": "Delete entries that exist on the instance but are not declared here. Off by default, so a first run cannot remove a protection that predates the config.",
|
|
111
|
+
"default": false
|
|
112
|
+
},
|
|
113
|
+
"$replace": {
|
|
114
|
+
"type": "boolean",
|
|
115
|
+
"description": "In an override, replace the baseline's entries entirely instead of merging into them by key.",
|
|
116
|
+
"default": false
|
|
117
|
+
},
|
|
118
|
+
"entries": { "type": "array", "items": { "type": "object" } }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { GitLabCommand } from '../../../shared/base-command.js';
|
|
2
|
+
export default class GitlabProjectCreate extends GitLabCommand<typeof GitlabProjectCreate> {
|
|
3
|
+
static isWrite: boolean;
|
|
4
|
+
static readonly summary = "Create a project, blank or from a template.";
|
|
5
|
+
static readonly description: string;
|
|
6
|
+
static readonly examples: string[];
|
|
7
|
+
static readonly flags: {
|
|
8
|
+
name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
path: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
namespace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
visibility: import("@oclif/core/interfaces").OptionFlag<"private" | "internal" | "public" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
'default-branch': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
'initialize-with-readme': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
topics: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
template: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
'custom-template': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
|
+
'template-group': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
19
|
+
'template-project': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
20
|
+
body: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
21
|
+
'body-file': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
22
|
+
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
23
|
+
};
|
|
24
|
+
run(): Promise<unknown>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026, SimplySF.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import { Flags } from '@oclif/core';
|
|
17
|
+
import { formatKeyValue, parseBodyInput, prepareProjectCreate } from '@simplysf/simply-gitlab-core';
|
|
18
|
+
import { GitLabCommand, parseList, writeFlags } from '../../../shared/base-command.js';
|
|
19
|
+
export default class GitlabProjectCreate extends GitLabCommand {
|
|
20
|
+
static isWrite = true;
|
|
21
|
+
static summary = 'Create a project, blank or from a template.';
|
|
22
|
+
static description = 'Creates the project in --namespace, or in the namespace of the token owner when none is ' +
|
|
23
|
+
'given. Give it --name or --path; GitLab derives whichever one is missing.\n\n' +
|
|
24
|
+
'--template names one of the built-in templates GitLab ships, such as express or rails. To ' +
|
|
25
|
+
'create from a custom template instead, add --custom-template for one the instance ' +
|
|
26
|
+
'administrator registered, --template-group for one in a group, or use --template-project ' +
|
|
27
|
+
'to name the template project itself by id or path — which GitLab prefers, because a name ' +
|
|
28
|
+
'can be ambiguous. Custom templates need a Premium or Ultimate instance.\n\n' +
|
|
29
|
+
'A template is applied asynchronously: this command returns as soon as the project record ' +
|
|
30
|
+
'exists, with an import status of "scheduled", and the files arrive a few seconds later. ' +
|
|
31
|
+
'Check "project view" until its import_status reads "finished" before writing to the ' +
|
|
32
|
+
'repository. --dry-run prints the exact request body; a namespace or template given as a ' +
|
|
33
|
+
'path is still looked up, so the printed ids are the ones that would be sent.';
|
|
34
|
+
static examples = [
|
|
35
|
+
'<%= config.bin %> <%= command.id %> --name new-service --namespace platform/apps',
|
|
36
|
+
'<%= config.bin %> <%= command.id %> --name new-service --namespace platform/apps --template express --visibility internal',
|
|
37
|
+
'<%= config.bin %> <%= command.id %> --name new-service --namespace platform/apps --template service-skeleton --template-group platform/templates',
|
|
38
|
+
'<%= config.bin %> <%= command.id %> --path new-service --template-project platform/templates/service-skeleton --dry-run',
|
|
39
|
+
];
|
|
40
|
+
static flags = {
|
|
41
|
+
...writeFlags,
|
|
42
|
+
name: Flags.string({ summary: 'Project name. The path is derived from it when --path is not given.' }),
|
|
43
|
+
path: Flags.string({
|
|
44
|
+
summary: 'Repository path, the URL slug. The name is derived from it when --name is not given.',
|
|
45
|
+
}),
|
|
46
|
+
namespace: Flags.string({
|
|
47
|
+
summary: 'Group or user namespace to create the project in, by id or full path.',
|
|
48
|
+
description: 'Defaults to the namespace of the token owner.',
|
|
49
|
+
}),
|
|
50
|
+
description: Flags.string({ summary: 'Project description.' }),
|
|
51
|
+
visibility: Flags.option({
|
|
52
|
+
summary: 'Who can see the project.',
|
|
53
|
+
options: ['private', 'internal', 'public'],
|
|
54
|
+
})(),
|
|
55
|
+
'default-branch': Flags.string({ summary: 'Name of the default branch.' }),
|
|
56
|
+
'initialize-with-readme': Flags.boolean({
|
|
57
|
+
summary: 'Create a first commit holding a README, so the repository is not empty.',
|
|
58
|
+
default: false,
|
|
59
|
+
}),
|
|
60
|
+
topics: Flags.string({ summary: 'Comma-separated topics to label the project with.' }),
|
|
61
|
+
template: Flags.string({
|
|
62
|
+
summary: 'Template to create from: a built-in name, or with --custom-template or --template-group a custom one.',
|
|
63
|
+
}),
|
|
64
|
+
'custom-template': Flags.boolean({
|
|
65
|
+
summary: "Look --template up among the instance's custom project templates rather than the built-in ones.",
|
|
66
|
+
default: false,
|
|
67
|
+
}),
|
|
68
|
+
'template-group': Flags.string({
|
|
69
|
+
summary: 'Group whose custom project templates --template names, by id or full path.',
|
|
70
|
+
}),
|
|
71
|
+
'template-project': Flags.string({
|
|
72
|
+
summary: 'Custom template project to create from, by id or full path. Cannot be combined with --template.',
|
|
73
|
+
}),
|
|
74
|
+
body: Flags.string({ summary: 'Raw JSON request body.', exclusive: ['body-file'] }),
|
|
75
|
+
'body-file': Flags.string({ summary: 'Path to a file holding the raw JSON request body.' }),
|
|
76
|
+
};
|
|
77
|
+
async run() {
|
|
78
|
+
const client = this.gitlab();
|
|
79
|
+
const request = await prepareProjectCreate(client, {
|
|
80
|
+
name: this.flags.name,
|
|
81
|
+
path: this.flags.path,
|
|
82
|
+
namespace: this.flags.namespace,
|
|
83
|
+
description: this.flags.description,
|
|
84
|
+
visibility: this.flags.visibility,
|
|
85
|
+
defaultBranch: this.flags['default-branch'],
|
|
86
|
+
initializeWithReadme: this.flags['initialize-with-readme'] || undefined,
|
|
87
|
+
topics: parseList(this.flags.topics),
|
|
88
|
+
template: this.flags.template,
|
|
89
|
+
customTemplate: this.flags['custom-template'] || undefined,
|
|
90
|
+
templateGroup: this.flags['template-group'],
|
|
91
|
+
templateProject: this.flags['template-project'],
|
|
92
|
+
body: parseBodyInput(this.flags.body, this.flags['body-file']),
|
|
93
|
+
});
|
|
94
|
+
if (this.flags['dry-run']) {
|
|
95
|
+
this.log('Dry run — not sent. Request body:');
|
|
96
|
+
this.logSafe(JSON.stringify(request, null, 2));
|
|
97
|
+
return request;
|
|
98
|
+
}
|
|
99
|
+
const created = (await client.createProject(request));
|
|
100
|
+
this.logSafe(formatKeyValue([
|
|
101
|
+
['Created', created.path_with_namespace ?? this.flags.name ?? this.flags.path],
|
|
102
|
+
['ID', created.id],
|
|
103
|
+
['Visibility', created.visibility],
|
|
104
|
+
['Default branch', created.default_branch],
|
|
105
|
+
['Import status', created.import_status],
|
|
106
|
+
['URL', created.web_url],
|
|
107
|
+
]));
|
|
108
|
+
return created;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=create.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create.js","sourceRoot":"","sources":["../../../../src/commands/gitlab/project/create.ts"],"names":[],"mappings":"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"}
|