runspec-gitlab 0.2.0__tar.gz → 0.3.0__tar.gz

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.
@@ -2,6 +2,39 @@
2
2
 
3
3
  All notable changes to `runspec-gitlab` are documented here.
4
4
 
5
+ ## [0.3.0] — 2026-10-10
6
+
7
+ ### Added
8
+
9
+ - **15 write verbs**, all `autonomy = "confirm"` (the 17 read verbs stay
10
+ `autonomous`):
11
+ - issues: `create-issue`, `update-issue` (title/description, add/remove
12
+ labels, assignees, milestone, due date, close/reopen), `add-issue-note`;
13
+ - merge requests: `create-merge-request` (`--draft`, reviewers, squash,
14
+ remove source branch), `update-merge-request`, `add-merge-request-note`,
15
+ `merge-merge-request` (`--sha` guard, `--when-pipeline-succeeds`);
16
+ - repository: `create-branch`, `write-file` (creates or replaces a text file
17
+ in one commit, from `--content` or `--content-file`, optionally on a new
18
+ branch via `--start-branch`);
19
+ - pipelines/jobs: `create-pipeline` (`--variable KEY=VALUE`),
20
+ `retry-pipeline`, `cancel-pipeline`, `retry-job`, `cancel-job`, `play-job`.
21
+ - **4 delete verbs**, all `autonomy = "manual"` so an agent can never run
22
+ them (a person can, from the CLI or the console's Forms tab):
23
+ `delete-issue`, `delete-branch`, `delete-file`, `delete-pipeline`.
24
+ - Assignees and reviewers are taken as usernames. Writes and deletes need a
25
+ token with the `api` scope.
26
+ - Requires `runspec-gitlab-core >= 0.3.0`.
27
+
28
+ ## [0.2.1] — 2026-09-20
29
+
30
+ ### Changed
31
+
32
+ - **Packaging metadata for the runspec-console Marketplace.** Add a README
33
+ (rendered in the Marketplace card's Details view) and publish `keywords`
34
+ and Documentation / Source / Changelog / Issues project URLs, so the
35
+ package's Marketplace card shows a description, tags and links. No
36
+ runnable, argument, or behaviour changes.
37
+
5
38
  ## [0.2.0]
6
39
 
7
40
  Reconciled the verb set against GitLab's canonical MCP tool registry
@@ -0,0 +1,112 @@
1
+ Metadata-Version: 2.5
2
+ Name: runspec-gitlab
3
+ Version: 0.3.0
4
+ Summary: GitLab runnable for runspec — read and update issues, merge requests, pipelines, and repository files, ported from the GitLab MCP server
5
+ Project-URL: Documentation, https://runspec.app/
6
+ Project-URL: Source, https://github.com/jasonfinestone/runspec/tree/main/packages/python/runspec-gitlab
7
+ Project-URL: Changelog, https://github.com/jasonfinestone/runspec/blob/main/packages/python/runspec-gitlab/CHANGELOG.md
8
+ Project-URL: Issues, https://github.com/jasonfinestone/runspec/issues
9
+ Keywords: gitlab,mcp,runnable,runspec
10
+ Requires-Python: >=3.10
11
+ Requires-Dist: runspec-gitlab-core>=0.3.0
12
+ Requires-Dist: runspec>=0.44.0
13
+ Provides-Extra: dev
14
+ Requires-Dist: mypy; extra == 'dev'
15
+ Requires-Dist: pytest>=8.0; extra == 'dev'
16
+ Requires-Dist: ruff==0.15.20; extra == 'dev'
17
+ Description-Content-Type: text/markdown
18
+
19
+ # runspec-gitlab
20
+
21
+ A GitLab runnable for [runspec](https://pypi.org/project/runspec/), ported onto
22
+ the GitLab REST API (v4) — **not** proxying GitLab's hosted MCP server. One
23
+ runnable — **`gitlab`** — with 17 read verbs (`autonomy = "autonomous"`), 15
24
+ write verbs (`autonomy = "confirm"`, so an agent asks before each one) and 4
25
+ delete verbs (`autonomy = "manual"`, so only a person can run them). `pip install` it into a venv and it's discoverable by `runspec
26
+ local`, `runspec serve` (MCP), and runspec-console.
27
+
28
+ `runspec serve` flattens each verb into a leaf MCP tool (`gitlab_get-issue`, …).
29
+ Verb names are reconciled against GitLab's canonical MCP tool registry; the
30
+ extras (`whoami`, `get-project`, `search-projects`, `list-issues`, `get-file`,
31
+ `list-tree`) are useful REST reads GitLab has no MCP tool for.
32
+
33
+ ## Install
34
+
35
+ ```
36
+ pip install runspec-gitlab
37
+ ```
38
+
39
+ ## Verbs
40
+
41
+ | Area | Verbs |
42
+ |---|---|
43
+ | Account / project | `whoami`, `get-project`, `search-projects`, `get-file`, `list-tree` |
44
+ | Issues | `list-issues`, `get-issue` |
45
+ | Merge requests | `list-merge-requests`, `get-merge-request`, `get-merge-request-diffs`, `get-merge-request-commits`, `get-merge-request-notes`, `get-merge-request-pipelines`, `get-merge-request-conflicts` |
46
+ | Pipelines | `list-pipelines`, `get-pipeline-jobs`, `get-job-log` |
47
+
48
+ Writes (all `confirm`):
49
+
50
+ | Area | Verbs |
51
+ |---|---|
52
+ | Issues | `create-issue`, `update-issue` (edit, labels, assignees, close/reopen), `add-issue-note` |
53
+ | Merge requests | `create-merge-request`, `update-merge-request` (edit, labels, assignees, reviewers, close/reopen), `add-merge-request-note`, `merge-merge-request` |
54
+ | Repository | `create-branch`, `write-file` (create or replace a text file in one commit) |
55
+ | Pipelines / jobs | `create-pipeline`, `retry-pipeline`, `cancel-pipeline`, `retry-job`, `cancel-job`, `play-job` |
56
+
57
+ Assignees and reviewers are given as usernames. Labels, assignees, reviewers and
58
+ pipeline variables (`KEY=VALUE`) are repeatable args. Writes need a token with
59
+ the `api` scope; reads work with `read_api`.
60
+
61
+ Deletes (all `manual` — refused to an agent, run from the command line or the
62
+ console's Forms tab): `delete-issue`, `delete-branch`, `delete-file`,
63
+ `delete-pipeline`. GitLab itself limits issue and pipeline deletes to project
64
+ owners/admins and refuses protected or default branches.
65
+
66
+ `get-merge-request-conflicts` is best-effort — GitLab has no public `/api/v4`
67
+ conflicts endpoint, so it relays the web-UI endpoint and returns its JSON
68
+ verbatim.
69
+
70
+ ## Auth — env vars only
71
+
72
+ ```
73
+ export GITLAB_URL=https://gitlab.example.com # default https://gitlab.com
74
+ export GITLAB_TOKEN=<personal-or-project-access-token>
75
+ export GITLAB_PROJECT=group/subgroup/project # optional default
76
+ ```
77
+
78
+ Every verb takes `--env`, selecting `GITLAB_<ENV>_*` prefixed vars. `--project`
79
+ accepts a numeric id or a `group/project` path and falls back to
80
+ `GITLAB_PROJECT`.
81
+
82
+ ```
83
+ gitlab whoami
84
+ gitlab list-merge-requests --project group/app --state opened
85
+ gitlab get-job-log --project group/app --job-id 998877
86
+ gitlab create-issue --project group/app --title "Login fails" --label bug --assignee alice
87
+ gitlab write-file --project group/app --branch fix --start-branch main \
88
+ --file-path docs/notes.md --content-file notes.md --commit-message "Add notes"
89
+ ```
90
+
91
+ ## Public Python API
92
+
93
+ The pure logic is in **[`runspec-gitlab-core`](../runspec-gitlab-core)** —
94
+ `httpx`-only, no `runspec` dependency, no entry points. Every op takes a
95
+ `GitlabConfig` first arg and raises `GitlabError`, so a private wrapper can
96
+ inject credentials and bake in a default `project` / `env`:
97
+
98
+ ```python
99
+ from runspec_gitlab_core import config_from_env, get_issue, list_merge_requests
100
+
101
+ cfg = config_from_env(env="prod")
102
+ mrs = list_merge_requests(cfg, state="opened")
103
+ ```
104
+
105
+ ## Development
106
+
107
+ ```
108
+ python -m venv .venv && . .venv/bin/activate
109
+ pip install -e ".[dev]"
110
+ ruff check . && ruff format --check .
111
+ pytest
112
+ ```
@@ -0,0 +1,94 @@
1
+ # runspec-gitlab
2
+
3
+ A GitLab runnable for [runspec](https://pypi.org/project/runspec/), ported onto
4
+ the GitLab REST API (v4) — **not** proxying GitLab's hosted MCP server. One
5
+ runnable — **`gitlab`** — with 17 read verbs (`autonomy = "autonomous"`), 15
6
+ write verbs (`autonomy = "confirm"`, so an agent asks before each one) and 4
7
+ delete verbs (`autonomy = "manual"`, so only a person can run them). `pip install` it into a venv and it's discoverable by `runspec
8
+ local`, `runspec serve` (MCP), and runspec-console.
9
+
10
+ `runspec serve` flattens each verb into a leaf MCP tool (`gitlab_get-issue`, …).
11
+ Verb names are reconciled against GitLab's canonical MCP tool registry; the
12
+ extras (`whoami`, `get-project`, `search-projects`, `list-issues`, `get-file`,
13
+ `list-tree`) are useful REST reads GitLab has no MCP tool for.
14
+
15
+ ## Install
16
+
17
+ ```
18
+ pip install runspec-gitlab
19
+ ```
20
+
21
+ ## Verbs
22
+
23
+ | Area | Verbs |
24
+ |---|---|
25
+ | Account / project | `whoami`, `get-project`, `search-projects`, `get-file`, `list-tree` |
26
+ | Issues | `list-issues`, `get-issue` |
27
+ | Merge requests | `list-merge-requests`, `get-merge-request`, `get-merge-request-diffs`, `get-merge-request-commits`, `get-merge-request-notes`, `get-merge-request-pipelines`, `get-merge-request-conflicts` |
28
+ | Pipelines | `list-pipelines`, `get-pipeline-jobs`, `get-job-log` |
29
+
30
+ Writes (all `confirm`):
31
+
32
+ | Area | Verbs |
33
+ |---|---|
34
+ | Issues | `create-issue`, `update-issue` (edit, labels, assignees, close/reopen), `add-issue-note` |
35
+ | Merge requests | `create-merge-request`, `update-merge-request` (edit, labels, assignees, reviewers, close/reopen), `add-merge-request-note`, `merge-merge-request` |
36
+ | Repository | `create-branch`, `write-file` (create or replace a text file in one commit) |
37
+ | Pipelines / jobs | `create-pipeline`, `retry-pipeline`, `cancel-pipeline`, `retry-job`, `cancel-job`, `play-job` |
38
+
39
+ Assignees and reviewers are given as usernames. Labels, assignees, reviewers and
40
+ pipeline variables (`KEY=VALUE`) are repeatable args. Writes need a token with
41
+ the `api` scope; reads work with `read_api`.
42
+
43
+ Deletes (all `manual` — refused to an agent, run from the command line or the
44
+ console's Forms tab): `delete-issue`, `delete-branch`, `delete-file`,
45
+ `delete-pipeline`. GitLab itself limits issue and pipeline deletes to project
46
+ owners/admins and refuses protected or default branches.
47
+
48
+ `get-merge-request-conflicts` is best-effort — GitLab has no public `/api/v4`
49
+ conflicts endpoint, so it relays the web-UI endpoint and returns its JSON
50
+ verbatim.
51
+
52
+ ## Auth — env vars only
53
+
54
+ ```
55
+ export GITLAB_URL=https://gitlab.example.com # default https://gitlab.com
56
+ export GITLAB_TOKEN=<personal-or-project-access-token>
57
+ export GITLAB_PROJECT=group/subgroup/project # optional default
58
+ ```
59
+
60
+ Every verb takes `--env`, selecting `GITLAB_<ENV>_*` prefixed vars. `--project`
61
+ accepts a numeric id or a `group/project` path and falls back to
62
+ `GITLAB_PROJECT`.
63
+
64
+ ```
65
+ gitlab whoami
66
+ gitlab list-merge-requests --project group/app --state opened
67
+ gitlab get-job-log --project group/app --job-id 998877
68
+ gitlab create-issue --project group/app --title "Login fails" --label bug --assignee alice
69
+ gitlab write-file --project group/app --branch fix --start-branch main \
70
+ --file-path docs/notes.md --content-file notes.md --commit-message "Add notes"
71
+ ```
72
+
73
+ ## Public Python API
74
+
75
+ The pure logic is in **[`runspec-gitlab-core`](../runspec-gitlab-core)** —
76
+ `httpx`-only, no `runspec` dependency, no entry points. Every op takes a
77
+ `GitlabConfig` first arg and raises `GitlabError`, so a private wrapper can
78
+ inject credentials and bake in a default `project` / `env`:
79
+
80
+ ```python
81
+ from runspec_gitlab_core import config_from_env, get_issue, list_merge_requests
82
+
83
+ cfg = config_from_env(env="prod")
84
+ mrs = list_merge_requests(cfg, state="opened")
85
+ ```
86
+
87
+ ## Development
88
+
89
+ ```
90
+ python -m venv .venv && . .venv/bin/activate
91
+ pip install -e ".[dev]"
92
+ ruff check . && ruff format --check .
93
+ pytest
94
+ ```
@@ -4,16 +4,24 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "runspec-gitlab"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  requires-python = ">=3.10"
9
- description = "GitLab read runnable for runspec — issues, merge requests, pipelines, and repository reads, ported from the GitLab MCP server"
9
+ description = "GitLab runnable for runspec — read and update issues, merge requests, pipelines, and repository files, ported from the GitLab MCP server"
10
+ readme = "README.md"
11
+ keywords = ["runspec", "runnable", "gitlab", "mcp"]
10
12
  dependencies = [
11
13
  # 0.44.0 surfaces inherited args (the global --env) on each leaf subcommand's
12
14
  # MCP tool via `runspec serve`.
13
15
  "runspec>=0.44.0",
14
- "runspec-gitlab-core>=0.2.0",
16
+ "runspec-gitlab-core>=0.3.0",
15
17
  ]
16
18
 
19
+ [project.urls]
20
+ Documentation = "https://runspec.app/"
21
+ Source = "https://github.com/jasonfinestone/runspec/tree/main/packages/python/runspec-gitlab"
22
+ Changelog = "https://github.com/jasonfinestone/runspec/blob/main/packages/python/runspec-gitlab/CHANGELOG.md"
23
+ Issues = "https://github.com/jasonfinestone/runspec/issues"
24
+
17
25
  [project.scripts]
18
26
  # Entry point name MUST match the runnable name in runspec.toml (`gitlab`).
19
27
  gitlab = "runspec_gitlab.cli:main"
@@ -0,0 +1,330 @@
1
+ """cli.py — the ``gitlab`` entry point.
2
+
3
+ One binary, a verb per GitLab tool (reads + writes). ``rs.parse("gitlab")`` resolves the
4
+ subcommand and its args; we build a :class:`GitlabConfig` from the environment
5
+ (scoped by the global ``--env``), unpack each ``.value`` at the boundary, and call
6
+ the matching :mod:`runspec_gitlab_core` helper.
7
+
8
+ Failures (the ``runspec_gitlab_core`` :class:`GitlabError` and arg-shape
9
+ ``ValueError``s) become a JSON ``{"error": ...}`` payload + a non-zero exit.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import sys
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ import runspec as rs
20
+ from runspec_gitlab_core import (
21
+ GitlabConfig,
22
+ GitlabError,
23
+ add_issue_note,
24
+ add_merge_request_note,
25
+ cancel_job,
26
+ cancel_pipeline,
27
+ config_from_env,
28
+ create_branch,
29
+ create_issue,
30
+ create_merge_request,
31
+ create_pipeline,
32
+ delete_branch,
33
+ delete_file,
34
+ delete_issue,
35
+ delete_pipeline,
36
+ get_file,
37
+ get_issue,
38
+ get_job_log,
39
+ get_merge_request,
40
+ get_merge_request_commits,
41
+ get_merge_request_conflicts,
42
+ get_merge_request_diffs,
43
+ get_merge_request_notes,
44
+ get_merge_request_pipelines,
45
+ get_pipeline_jobs,
46
+ get_project,
47
+ list_issues,
48
+ list_merge_requests,
49
+ list_pipelines,
50
+ list_tree,
51
+ merge_merge_request,
52
+ play_job,
53
+ retry_job,
54
+ retry_pipeline,
55
+ search_projects,
56
+ update_issue,
57
+ update_merge_request,
58
+ whoami,
59
+ write_file,
60
+ )
61
+
62
+
63
+ def _emit(payload: Any) -> None:
64
+ print(json.dumps(payload, default=str))
65
+
66
+
67
+ def _list(arg: rs.Arg) -> list[str] | None:
68
+ """A repeatable arg's values, or ``None`` when it wasn't given (so the core
69
+ leaves that field unchanged rather than clearing it)."""
70
+ return list(arg.value) if arg.value else None
71
+
72
+
73
+ def _content(spec: rs.RunSpec) -> str:
74
+ """``write-file`` content: ``--content`` verbatim, or the text of
75
+ ``--content-file`` (the group guarantees exactly one is set)."""
76
+ if spec.content_file.value is not None:
77
+ return Path(spec.content_file.value).read_text(encoding="utf-8")
78
+ return spec.content.value
79
+
80
+
81
+ def _dispatch(spec: rs.RunSpec, config: GitlabConfig) -> Any:
82
+ """Run the active verb against ``config``, unpacking ``spec.<arg>.value`` for
83
+ only the args the verb declares (globals + its own)."""
84
+ command = spec.runspec_command
85
+
86
+ if command == "whoami":
87
+ return whoami(config)
88
+ if command == "get-project":
89
+ return get_project(config, project=spec.project.value)
90
+ if command == "search-projects":
91
+ return search_projects(
92
+ config,
93
+ search=spec.search.value,
94
+ membership=spec.membership.value,
95
+ per_page=spec.per_page.value,
96
+ page=spec.page.value,
97
+ )
98
+ if command == "list-issues":
99
+ return list_issues(
100
+ config,
101
+ project=spec.project.value,
102
+ state=spec.state.value,
103
+ labels=spec.labels.value,
104
+ search=spec.search.value,
105
+ per_page=spec.per_page.value,
106
+ page=spec.page.value,
107
+ )
108
+ if command == "get-issue":
109
+ return get_issue(config, issue_iid=spec.issue_iid.value, project=spec.project.value)
110
+ if command == "list-merge-requests":
111
+ return list_merge_requests(
112
+ config,
113
+ project=spec.project.value,
114
+ state=spec.state.value,
115
+ per_page=spec.per_page.value,
116
+ page=spec.page.value,
117
+ )
118
+ if command == "get-merge-request":
119
+ return get_merge_request(config, mr_iid=spec.mr_iid.value, project=spec.project.value)
120
+ if command == "get-merge-request-diffs":
121
+ return get_merge_request_diffs(
122
+ config,
123
+ mr_iid=spec.mr_iid.value,
124
+ project=spec.project.value,
125
+ per_page=spec.per_page.value,
126
+ page=spec.page.value,
127
+ )
128
+ if command == "get-merge-request-commits":
129
+ return get_merge_request_commits(
130
+ config,
131
+ mr_iid=spec.mr_iid.value,
132
+ project=spec.project.value,
133
+ per_page=spec.per_page.value,
134
+ page=spec.page.value,
135
+ )
136
+ if command == "get-merge-request-notes":
137
+ return get_merge_request_notes(
138
+ config,
139
+ mr_iid=spec.mr_iid.value,
140
+ project=spec.project.value,
141
+ sort=spec.sort.value,
142
+ per_page=spec.per_page.value,
143
+ page=spec.page.value,
144
+ )
145
+ if command == "get-merge-request-pipelines":
146
+ return get_merge_request_pipelines(
147
+ config,
148
+ mr_iid=spec.mr_iid.value,
149
+ project=spec.project.value,
150
+ per_page=spec.per_page.value,
151
+ page=spec.page.value,
152
+ )
153
+ if command == "get-merge-request-conflicts":
154
+ return get_merge_request_conflicts(config, mr_iid=spec.mr_iid.value, project=spec.project.value)
155
+ if command == "list-pipelines":
156
+ return list_pipelines(
157
+ config,
158
+ project=spec.project.value,
159
+ ref=spec.ref.value,
160
+ status=spec.status.value,
161
+ per_page=spec.per_page.value,
162
+ page=spec.page.value,
163
+ )
164
+ if command == "get-pipeline-jobs":
165
+ return get_pipeline_jobs(
166
+ config,
167
+ pipeline_id=spec.pipeline_id.value,
168
+ project=spec.project.value,
169
+ per_page=spec.per_page.value,
170
+ page=spec.page.value,
171
+ )
172
+ if command == "get-job-log":
173
+ return get_job_log(config, job_id=spec.job_id.value, project=spec.project.value, tail=spec.tail.value)
174
+ if command == "get-file":
175
+ return get_file(config, file_path=spec.file_path.value, ref=spec.ref.value, project=spec.project.value)
176
+ if command == "list-tree":
177
+ return list_tree(
178
+ config,
179
+ path=spec.path.value,
180
+ ref=spec.ref.value,
181
+ recursive=spec.recursive.value,
182
+ project=spec.project.value,
183
+ per_page=spec.per_page.value,
184
+ page=spec.page.value,
185
+ )
186
+
187
+ # ── writes (all autonomy = "confirm") ──
188
+ if command == "create-issue":
189
+ return create_issue(
190
+ config,
191
+ title=spec.title.value,
192
+ description=spec.description.value,
193
+ labels=_list(spec.label),
194
+ assignees=_list(spec.assignee),
195
+ milestone_id=spec.milestone_id.value,
196
+ due_date=spec.due_date.value,
197
+ confidential=spec.confidential.value,
198
+ project=spec.project.value,
199
+ )
200
+ if command == "update-issue":
201
+ return update_issue(
202
+ config,
203
+ issue_iid=spec.issue_iid.value,
204
+ title=spec.title.value,
205
+ description=spec.description.value,
206
+ add_labels=_list(spec.add_label),
207
+ remove_labels=_list(spec.remove_label),
208
+ assignees=_list(spec.assignee),
209
+ milestone_id=spec.milestone_id.value,
210
+ due_date=spec.due_date.value,
211
+ state_event=spec.state.value,
212
+ project=spec.project.value,
213
+ )
214
+ if command == "add-issue-note":
215
+ return add_issue_note(
216
+ config,
217
+ issue_iid=spec.issue_iid.value,
218
+ body=spec.body.value,
219
+ internal=spec.internal.value,
220
+ project=spec.project.value,
221
+ )
222
+ if command == "create-merge-request":
223
+ return create_merge_request(
224
+ config,
225
+ source_branch=spec.source_branch.value,
226
+ target_branch=spec.target_branch.value,
227
+ title=spec.title.value,
228
+ description=spec.description.value,
229
+ draft=spec.draft.value,
230
+ labels=_list(spec.label),
231
+ assignees=_list(spec.assignee),
232
+ reviewers=_list(spec.reviewer),
233
+ remove_source_branch=spec.remove_source_branch.value,
234
+ squash=spec.squash.value,
235
+ project=spec.project.value,
236
+ )
237
+ if command == "update-merge-request":
238
+ return update_merge_request(
239
+ config,
240
+ mr_iid=spec.mr_iid.value,
241
+ title=spec.title.value,
242
+ description=spec.description.value,
243
+ target_branch=spec.target_branch.value,
244
+ add_labels=_list(spec.add_label),
245
+ remove_labels=_list(spec.remove_label),
246
+ assignees=_list(spec.assignee),
247
+ reviewers=_list(spec.reviewer),
248
+ state_event=spec.state.value,
249
+ project=spec.project.value,
250
+ )
251
+ if command == "add-merge-request-note":
252
+ return add_merge_request_note(
253
+ config,
254
+ mr_iid=spec.mr_iid.value,
255
+ body=spec.body.value,
256
+ internal=spec.internal.value,
257
+ project=spec.project.value,
258
+ )
259
+ if command == "merge-merge-request":
260
+ return merge_merge_request(
261
+ config,
262
+ mr_iid=spec.mr_iid.value,
263
+ sha=spec.sha.value,
264
+ # An unset flag keeps the merge request's own setting.
265
+ squash=spec.squash.value or None,
266
+ remove_source_branch=spec.remove_source_branch.value or None,
267
+ when_pipeline_succeeds=spec.when_pipeline_succeeds.value,
268
+ merge_commit_message=spec.merge_commit_message.value,
269
+ project=spec.project.value,
270
+ )
271
+ if command == "create-branch":
272
+ return create_branch(config, branch=spec.branch.value, ref=spec.ref.value, project=spec.project.value)
273
+ if command == "write-file":
274
+ return write_file(
275
+ config,
276
+ file_path=spec.file_path.value,
277
+ content=_content(spec),
278
+ branch=spec.branch.value,
279
+ commit_message=spec.commit_message.value,
280
+ start_branch=spec.start_branch.value,
281
+ last_commit_id=spec.last_commit_id.value,
282
+ project=spec.project.value,
283
+ )
284
+ if command == "create-pipeline":
285
+ return create_pipeline(config, ref=spec.ref.value, variables=_list(spec.variable), project=spec.project.value)
286
+ if command == "retry-pipeline":
287
+ return retry_pipeline(config, pipeline_id=spec.pipeline_id.value, project=spec.project.value)
288
+ if command == "cancel-pipeline":
289
+ return cancel_pipeline(config, pipeline_id=spec.pipeline_id.value, project=spec.project.value)
290
+ if command == "retry-job":
291
+ return retry_job(config, job_id=spec.job_id.value, project=spec.project.value)
292
+ if command == "cancel-job":
293
+ return cancel_job(config, job_id=spec.job_id.value, project=spec.project.value)
294
+ if command == "play-job":
295
+ return play_job(config, job_id=spec.job_id.value, variables=_list(spec.variable), project=spec.project.value)
296
+
297
+ # ── deletes (autonomy = "manual": human-only) ──
298
+ if command == "delete-issue":
299
+ return delete_issue(config, issue_iid=spec.issue_iid.value, project=spec.project.value)
300
+ if command == "delete-branch":
301
+ return delete_branch(config, branch=spec.branch.value, project=spec.project.value)
302
+ if command == "delete-file":
303
+ return delete_file(
304
+ config,
305
+ file_path=spec.file_path.value,
306
+ branch=spec.branch.value,
307
+ commit_message=spec.commit_message.value,
308
+ last_commit_id=spec.last_commit_id.value,
309
+ project=spec.project.value,
310
+ )
311
+ if command == "delete-pipeline":
312
+ return delete_pipeline(config, pipeline_id=spec.pipeline_id.value, project=spec.project.value)
313
+
314
+ # require-command = true makes a bare invocation unreachable.
315
+ raise SystemExit("no command — see `gitlab --help`") # pragma: no cover
316
+
317
+
318
+ def main() -> None:
319
+ spec = rs.parse("gitlab")
320
+ try:
321
+ config = config_from_env(env=spec.env.value)
322
+ result = _dispatch(spec, config)
323
+ except (GitlabError, ValueError, OSError) as exc:
324
+ _emit({"error": str(exc), "kind": type(exc).__name__})
325
+ sys.exit(1)
326
+ _emit(result)
327
+
328
+
329
+ if __name__ == "__main__":
330
+ main()