runspec-gitlab 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,54 @@
1
+ """runspec-gitlab — GitLab reads as a runspec runnable (the ``gitlab`` runnable).
2
+
3
+ Ported from the
4
+ `GitLab MCP server <https://docs.gitlab.com/user/model_context_protocol/mcp_server/>`_.
5
+ The pure REST logic lives in ``runspec-gitlab-core`` (importable with **no**
6
+ runspec dependency); this package wraps each helper as a verb of the ``gitlab``
7
+ runnable and is what ``runspec local`` / ``runspec serve`` discover.
8
+
9
+ The core helpers are re-exported here for convenience / back-compat, so a caller
10
+ that already depends on ``runspec-gitlab`` can ``from runspec_gitlab import
11
+ list_issues`` without also naming ``runspec-gitlab-core``.
12
+ """
13
+
14
+ from runspec_gitlab_core import (
15
+ GitlabConfig,
16
+ GitlabError,
17
+ config_from_env,
18
+ encode_project,
19
+ get_file,
20
+ get_issue,
21
+ get_job_log,
22
+ get_merge_request,
23
+ get_merge_request_changes,
24
+ get_pipeline_jobs,
25
+ get_project,
26
+ gitlab_request,
27
+ list_issues,
28
+ list_merge_requests,
29
+ list_pipelines,
30
+ list_tree,
31
+ search_projects,
32
+ whoami,
33
+ )
34
+
35
+ __all__ = [
36
+ "GitlabConfig",
37
+ "GitlabError",
38
+ "config_from_env",
39
+ "gitlab_request",
40
+ "encode_project",
41
+ "whoami",
42
+ "get_project",
43
+ "search_projects",
44
+ "list_issues",
45
+ "get_issue",
46
+ "list_merge_requests",
47
+ "get_merge_request",
48
+ "get_merge_request_changes",
49
+ "list_pipelines",
50
+ "get_pipeline_jobs",
51
+ "get_job_log",
52
+ "get_file",
53
+ "list_tree",
54
+ ]
runspec_gitlab/cli.py ADDED
@@ -0,0 +1,132 @@
1
+ """cli.py — the ``gitlab`` entry point.
2
+
3
+ One binary, a verb per GitLab MCP read tool. ``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 typing import Any
17
+
18
+ import runspec as rs
19
+ from runspec_gitlab_core import (
20
+ GitlabConfig,
21
+ GitlabError,
22
+ config_from_env,
23
+ get_file,
24
+ get_issue,
25
+ get_job_log,
26
+ get_merge_request,
27
+ get_merge_request_changes,
28
+ get_pipeline_jobs,
29
+ get_project,
30
+ list_issues,
31
+ list_merge_requests,
32
+ list_pipelines,
33
+ list_tree,
34
+ search_projects,
35
+ whoami,
36
+ )
37
+
38
+
39
+ def _emit(payload: Any) -> None:
40
+ print(json.dumps(payload, default=str))
41
+
42
+
43
+ def _dispatch(spec: rs.RunSpec, config: GitlabConfig) -> Any:
44
+ """Run the active verb against ``config``, unpacking ``spec.<arg>.value`` for
45
+ only the args the verb declares (globals + its own)."""
46
+ command = spec.runspec_command
47
+
48
+ if command == "whoami":
49
+ return whoami(config)
50
+ if command == "get-project":
51
+ return get_project(config, project=spec.project.value)
52
+ if command == "search-projects":
53
+ return search_projects(
54
+ config,
55
+ search=spec.search.value,
56
+ membership=spec.membership.value,
57
+ per_page=spec.per_page.value,
58
+ page=spec.page.value,
59
+ )
60
+ if command == "list-issues":
61
+ return list_issues(
62
+ config,
63
+ project=spec.project.value,
64
+ state=spec.state.value,
65
+ labels=spec.labels.value,
66
+ search=spec.search.value,
67
+ per_page=spec.per_page.value,
68
+ page=spec.page.value,
69
+ )
70
+ if command == "get-issue":
71
+ return get_issue(config, issue_iid=spec.issue_iid.value, project=spec.project.value)
72
+ if command == "list-merge-requests":
73
+ return list_merge_requests(
74
+ config,
75
+ project=spec.project.value,
76
+ state=spec.state.value,
77
+ per_page=spec.per_page.value,
78
+ page=spec.page.value,
79
+ )
80
+ if command == "get-merge-request":
81
+ return get_merge_request(config, mr_iid=spec.mr_iid.value, project=spec.project.value)
82
+ if command == "get-merge-request-changes":
83
+ return get_merge_request_changes(config, mr_iid=spec.mr_iid.value, project=spec.project.value)
84
+ if command == "list-pipelines":
85
+ return list_pipelines(
86
+ config,
87
+ project=spec.project.value,
88
+ ref=spec.ref.value,
89
+ status=spec.status.value,
90
+ per_page=spec.per_page.value,
91
+ page=spec.page.value,
92
+ )
93
+ if command == "get-pipeline-jobs":
94
+ return get_pipeline_jobs(
95
+ config,
96
+ pipeline_id=spec.pipeline_id.value,
97
+ project=spec.project.value,
98
+ per_page=spec.per_page.value,
99
+ page=spec.page.value,
100
+ )
101
+ if command == "get-job-log":
102
+ return get_job_log(config, job_id=spec.job_id.value, project=spec.project.value, tail=spec.tail.value)
103
+ if command == "get-file":
104
+ return get_file(config, file_path=spec.file_path.value, ref=spec.ref.value, project=spec.project.value)
105
+ if command == "list-tree":
106
+ return list_tree(
107
+ config,
108
+ path=spec.path.value,
109
+ ref=spec.ref.value,
110
+ recursive=spec.recursive.value,
111
+ project=spec.project.value,
112
+ per_page=spec.per_page.value,
113
+ page=spec.page.value,
114
+ )
115
+
116
+ # require-command = true makes a bare invocation unreachable.
117
+ raise SystemExit("no command — see `gitlab --help`") # pragma: no cover
118
+
119
+
120
+ def main() -> None:
121
+ spec = rs.parse("gitlab")
122
+ try:
123
+ config = config_from_env(env=spec.env.value)
124
+ result = _dispatch(spec, config)
125
+ except (GitlabError, ValueError) as exc:
126
+ _emit({"error": str(exc), "kind": type(exc).__name__})
127
+ sys.exit(1)
128
+ _emit(result)
129
+
130
+
131
+ if __name__ == "__main__":
132
+ main()
@@ -0,0 +1,156 @@
1
+ #:schema https://runspec.app/runspec.schema.json
2
+
3
+ # runspec-gitlab — GitLab reads as a runspec runnable, ported from the GitLab MCP
4
+ # server: https://docs.gitlab.com/user/model_context_protocol/mcp_server/
5
+ #
6
+ # One runnable, `gitlab`, with a verb per read tool. `runspec serve` re-expands
7
+ # each verb into its own leaf MCP tool (gitlab_get-issue, gitlab_list-pipelines,
8
+ # …), so a runspec-console agent calls them individually — over SSH on any fleet
9
+ # host.
10
+ #
11
+ # The tools are PORTED onto the GitLab REST API (v4), not proxied from GitLab's
12
+ # hosted MCP server, so no extra process is involved. Auth is env-var driven:
13
+ # GITLAB_URL (default https://gitlab.com) + GITLAB_TOKEN (a personal access
14
+ # token). Per-environment overrides use a GITLAB_<ENV>_ prefix (e.g.
15
+ # GITLAB_UAT_TOKEN), selected with the global --env (dev/uat/prod) arg.
16
+ #
17
+ # `project` accepts a numeric id or a URL-path (`group/subgroup/project`); it
18
+ # falls back to GITLAB_PROJECT when omitted.
19
+
20
+ [config]
21
+ autonomy-default = "confirm"
22
+
23
+ [config.logging]
24
+ store = "per-run"
25
+
26
+ [gitlab]
27
+ description = "Read GitLab issues, merge requests, pipelines, and repository files (over the REST API)"
28
+ output = "json"
29
+ require-command = true
30
+ examples = [
31
+ {cmd = "gitlab whoami", description = "Verify the token and show the authenticated user"},
32
+ {cmd = "gitlab list-issues --project group/app --state opened", description = "List a project's open issues"},
33
+ {cmd = "gitlab get-merge-request-changes --project 42 --mr-iid 17", description = "Show a merge request's diffs"},
34
+ {cmd = "gitlab get-job-log --project group/app --job-id 9001 --tail 100", description = "Tail the last 100 lines of a CI job log"},
35
+ {cmd = "gitlab get-file --project group/app --file-path src/main.py --ref main", description = "Read a repository file at a ref"},
36
+ ]
37
+
38
+ # ── global args (inherited by every verb) ──────────────────────────────────
39
+ [gitlab.args]
40
+ env = {type = "choice", options = ["dev", "uat", "prod"], short = "-e", required = false, description = "Target environment (selects GITLAB_<ENV>_* vars)"}
41
+
42
+ # ── identity / projects ────────────────────────────────────────────────────
43
+
44
+ [gitlab.commands.whoami]
45
+ description = "Return the authenticated user for the configured token (a quick identity/token check)"
46
+ autonomy = "autonomous"
47
+
48
+ [gitlab.commands.get-project]
49
+ description = "Fetch a single project's metadata by numeric id or namespace/project path"
50
+ autonomy = "autonomous"
51
+ [gitlab.commands.get-project.args]
52
+ project = {type = "str", short = "-p", required = false, description = "Project id or path (falls back to GITLAB_PROJECT)"}
53
+
54
+ [gitlab.commands.search-projects]
55
+ description = "Search visible projects by name/path"
56
+ autonomy = "autonomous"
57
+ [gitlab.commands.search-projects.args]
58
+ search = {type = "str", required = true, description = "Text to match against project name/path"}
59
+ membership = {type = "flag", default = false, description = "Only projects the token's user is a member of"}
60
+ per-page = {type = "int", default = 20, description = "Results per page (GitLab caps at 100)"}
61
+ page = {type = "int", default = 1, description = "Page number"}
62
+
63
+ # ── issues ─────────────────────────────────────────────────────────────────
64
+
65
+ [gitlab.commands.list-issues]
66
+ description = "List a project's issues, optionally filtered by state/labels/search"
67
+ autonomy = "autonomous"
68
+ [gitlab.commands.list-issues.args]
69
+ project = {type = "str", short = "-p", required = false, description = "Project id or path (falls back to GITLAB_PROJECT)"}
70
+ state = {type = "choice", options = ["opened", "closed", "all"], required = false, description = "Issue state filter"}
71
+ labels = {type = "str", required = false, description = "Comma-separated label list to filter by"}
72
+ search = {type = "str", required = false, description = "Match title/description text"}
73
+ per-page = {type = "int", default = 20, description = "Results per page (GitLab caps at 100)"}
74
+ page = {type = "int", default = 1, description = "Page number"}
75
+
76
+ [gitlab.commands.get-issue]
77
+ description = "Fetch a single issue by its project-scoped internal id (iid)"
78
+ autonomy = "autonomous"
79
+ [gitlab.commands.get-issue.args]
80
+ project = {type = "str", short = "-p", required = false, description = "Project id or path (falls back to GITLAB_PROJECT)"}
81
+ issue-iid = {type = "int", required = true, description = "Issue internal id (the #N shown in the UI)"}
82
+
83
+ # ── merge requests ─────────────────────────────────────────────────────────
84
+
85
+ [gitlab.commands.list-merge-requests]
86
+ description = "List a project's merge requests, optionally filtered by state"
87
+ autonomy = "autonomous"
88
+ [gitlab.commands.list-merge-requests.args]
89
+ project = {type = "str", short = "-p", required = false, description = "Project id or path (falls back to GITLAB_PROJECT)"}
90
+ state = {type = "choice", options = ["opened", "closed", "merged", "locked", "all"], required = false, description = "Merge request state filter"}
91
+ per-page = {type = "int", default = 20, description = "Results per page (GitLab caps at 100)"}
92
+ page = {type = "int", default = 1, description = "Page number"}
93
+
94
+ [gitlab.commands.get-merge-request]
95
+ description = "Fetch a single merge request by its project-scoped internal id (iid)"
96
+ autonomy = "autonomous"
97
+ [gitlab.commands.get-merge-request.args]
98
+ project = {type = "str", short = "-p", required = false, description = "Project id or path (falls back to GITLAB_PROJECT)"}
99
+ mr-iid = {type = "int", required = true, description = "Merge request internal id (the !N shown in the UI)"}
100
+
101
+ [gitlab.commands.get-merge-request-changes]
102
+ description = "Fetch a merge request together with its file changes / diffs"
103
+ autonomy = "autonomous"
104
+ [gitlab.commands.get-merge-request-changes.args]
105
+ project = {type = "str", short = "-p", required = false, description = "Project id or path (falls back to GITLAB_PROJECT)"}
106
+ mr-iid = {type = "int", required = true, description = "Merge request internal id (the !N shown in the UI)"}
107
+
108
+ # ── pipelines / jobs ───────────────────────────────────────────────────────
109
+
110
+ [gitlab.commands.list-pipelines]
111
+ description = "List a project's CI/CD pipelines, optionally filtered by ref/status"
112
+ autonomy = "autonomous"
113
+ [gitlab.commands.list-pipelines.args]
114
+ project = {type = "str", short = "-p", required = false, description = "Project id or path (falls back to GITLAB_PROJECT)"}
115
+ ref = {type = "str", required = false, description = "Branch or tag to filter by"}
116
+ status = {type = "choice", options = ["created", "waiting_for_resource", "preparing", "pending", "running", "success", "failed", "canceled", "skipped", "manual", "scheduled"], required = false, description = "Pipeline status filter"}
117
+ per-page = {type = "int", default = 20, description = "Results per page (GitLab caps at 100)"}
118
+ page = {type = "int", default = 1, description = "Page number"}
119
+
120
+ [gitlab.commands.get-pipeline-jobs]
121
+ description = "List the jobs of a pipeline (by pipeline id)"
122
+ autonomy = "autonomous"
123
+ [gitlab.commands.get-pipeline-jobs.args]
124
+ project = {type = "str", short = "-p", required = false, description = "Project id or path (falls back to GITLAB_PROJECT)"}
125
+ pipeline-id = {type = "int", required = true, description = "Pipeline id (not the iid)"}
126
+ per-page = {type = "int", default = 20, description = "Results per page (GitLab caps at 100)"}
127
+ page = {type = "int", default = 1, description = "Page number"}
128
+
129
+ [gitlab.commands.get-job-log]
130
+ description = "Fetch a CI job's log (trace) as text; pass --tail to return only the last N lines"
131
+ autonomy = "autonomous"
132
+ [gitlab.commands.get-job-log.args]
133
+ project = {type = "str", short = "-p", required = false, description = "Project id or path (falls back to GITLAB_PROJECT)"}
134
+ job-id = {type = "int", required = true, description = "Job id"}
135
+ tail = {type = "int", required = false, description = "Return only the last N lines (logs are large — usually pass this)"}
136
+
137
+ # ── repository ─────────────────────────────────────────────────────────────
138
+
139
+ [gitlab.commands.get-file]
140
+ description = "Read a single repository file at a ref (base64 is decoded; non-UTF-8 files are flagged binary)"
141
+ autonomy = "autonomous"
142
+ [gitlab.commands.get-file.args]
143
+ project = {type = "str", short = "-p", required = false, description = "Project id or path (falls back to GITLAB_PROJECT)"}
144
+ file-path = {type = "str", required = true, description = "Path within the repository (e.g. src/main.py)"}
145
+ ref = {type = "str", default = "HEAD", description = "Branch, tag, or commit (default HEAD = the default branch tip)"}
146
+
147
+ [gitlab.commands.list-tree]
148
+ description = "List a repository tree at a path/ref; --recursive walks subdirectories"
149
+ autonomy = "autonomous"
150
+ [gitlab.commands.list-tree.args]
151
+ project = {type = "str", short = "-p", required = false, description = "Project id or path (falls back to GITLAB_PROJECT)"}
152
+ path = {type = "str", required = false, description = "Subdirectory to list (default: repository root)"}
153
+ ref = {type = "str", required = false, description = "Branch, tag, or commit (default: the default branch)"}
154
+ recursive = {type = "flag", default = false, description = "Walk subdirectories recursively"}
155
+ per-page = {type = "int", default = 20, description = "Results per page (GitLab caps at 100)"}
156
+ page = {type = "int", default = 1, description = "Page number"}
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: runspec-gitlab
3
+ Version: 0.1.0
4
+ Summary: GitLab read runnable for runspec — issues, merge requests, pipelines, and repository reads, ported from the GitLab MCP server
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: runspec-gitlab-core>=0.1.0
7
+ Requires-Dist: runspec>=0.44.0
8
+ Provides-Extra: dev
9
+ Requires-Dist: mypy; extra == 'dev'
10
+ Requires-Dist: pytest>=8.0; extra == 'dev'
11
+ Requires-Dist: ruff==0.15.20; extra == 'dev'
@@ -0,0 +1,7 @@
1
+ runspec_gitlab/__init__.py,sha256=-92O9N4RKyJ-dbbhc0cnGMbwonhqjOPdxj5c4I9S5DE,1416
2
+ runspec_gitlab/cli.py,sha256=tcZSoz_WZ-4hfKq6sVdaQwAFOlrvjK7Qnm1-Xb9M3DE,4292
3
+ runspec_gitlab/runspec.toml,sha256=Lq4akFegqi6rARlQZ3Rb2OwWOPGUqiQd-PJzgJV7laA,9838
4
+ runspec_gitlab-0.1.0.dist-info/METADATA,sha256=5nU5HQUTJ-_mia33XR46MXqReL6SYIR6ylZ5oZX5-Bs,436
5
+ runspec_gitlab-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
6
+ runspec_gitlab-0.1.0.dist-info/entry_points.txt,sha256=hiQySJfu_gM3uoJYUqRGyQ948-SpFPkHT8B5mLrF-nw,51
7
+ runspec_gitlab-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ gitlab = runspec_gitlab.cli:main