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.
- {runspec_gitlab-0.2.0 → runspec_gitlab-0.3.0}/CHANGELOG.md +33 -0
- runspec_gitlab-0.3.0/PKG-INFO +112 -0
- runspec_gitlab-0.3.0/README.md +94 -0
- {runspec_gitlab-0.2.0 → runspec_gitlab-0.3.0}/pyproject.toml +11 -3
- runspec_gitlab-0.3.0/runspec_gitlab/cli.py +330 -0
- runspec_gitlab-0.3.0/runspec_gitlab/runspec.toml +399 -0
- {runspec_gitlab-0.2.0 → runspec_gitlab-0.3.0}/tests/test_cli.py +89 -0
- {runspec_gitlab-0.2.0 → runspec_gitlab-0.3.0}/tests/test_spec.py +55 -0
- runspec_gitlab-0.2.0/PKG-INFO +0 -11
- runspec_gitlab-0.2.0/runspec_gitlab/cli.py +0 -169
- runspec_gitlab-0.2.0/runspec_gitlab/runspec.toml +0 -196
- {runspec_gitlab-0.2.0 → runspec_gitlab-0.3.0}/.gitignore +0 -0
- {runspec_gitlab-0.2.0 → runspec_gitlab-0.3.0}/runspec_gitlab/__init__.py +0 -0
- {runspec_gitlab-0.2.0 → runspec_gitlab-0.3.0}/tests/__init__.py +0 -0
|
@@ -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.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
requires-python = ">=3.10"
|
|
9
|
-
description = "GitLab
|
|
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.
|
|
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()
|