qodev-gitlab-cli 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 @@
1
+ """GitLab CLI — agent-friendly CLI for the GitLab API."""
@@ -0,0 +1,5 @@
1
+ """Allow running as `python -m qodev_gitlab_cli`."""
2
+
3
+ from qodev_gitlab_cli.app import main
4
+
5
+ main()
@@ -0,0 +1,93 @@
1
+ """Root App definition, global options, and error handling."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from typing import Annotated
7
+
8
+ from cyclopts import App, Group, Parameter
9
+ from qodev_gitlab_api import APIError, AuthenticationError, ConfigurationError, NotFoundError
10
+
11
+ import qodev_gitlab_cli.context as _ctx
12
+
13
+ app = App(
14
+ name="qodev-gitlab",
15
+ help="Agent-friendly CLI for the GitLab API.",
16
+ version_flags=[],
17
+ )
18
+
19
+ app.meta.group_parameters = Group("Global Options", sort_key=0)
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Import and register command groups
23
+ # ---------------------------------------------------------------------------
24
+ from qodev_gitlab_cli.commands.issues import issues_app # noqa: E402
25
+ from qodev_gitlab_cli.commands.jobs import jobs_app # noqa: E402
26
+ from qodev_gitlab_cli.commands.mrs import mrs_app # noqa: E402
27
+ from qodev_gitlab_cli.commands.pipelines import pipelines_app # noqa: E402
28
+ from qodev_gitlab_cli.commands.projects import projects_app # noqa: E402
29
+ from qodev_gitlab_cli.commands.releases import releases_app # noqa: E402
30
+ from qodev_gitlab_cli.commands.variables import variables_app # noqa: E402
31
+
32
+ app.command(projects_app)
33
+ app.command(mrs_app)
34
+ app.command(pipelines_app)
35
+ app.command(jobs_app)
36
+ app.command(issues_app)
37
+ app.command(releases_app)
38
+ app.command(variables_app)
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # Exit codes
42
+ # ---------------------------------------------------------------------------
43
+ EXIT_AUTH = 80
44
+ EXIT_NOT_FOUND = 81
45
+ EXIT_API = 82
46
+ EXIT_VALIDATION = 83
47
+ EXIT_CONFIG = 84
48
+
49
+
50
+ # ---------------------------------------------------------------------------
51
+ # Meta launcher — global options & error handling
52
+ # ---------------------------------------------------------------------------
53
+ @app.meta.default
54
+ def launcher(
55
+ *tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
56
+ json: Annotated[bool, Parameter(name="--json", help="Output as JSON", negative="")] = False,
57
+ token: Annotated[
58
+ str | None, Parameter(name="--token", help="GitLab token (overrides GITLAB_TOKEN)", show=False)
59
+ ] = None,
60
+ url: Annotated[str | None, Parameter(name="--url", help="GitLab URL (overrides GITLAB_URL)", show=False)] = None,
61
+ project: Annotated[str | None, Parameter(name=["--project", "-p"], help="Project ID or path")] = None,
62
+ limit: Annotated[int, Parameter(name="--limit", help="Results per page")] = 25,
63
+ page: Annotated[int, Parameter(name="--page", help="Page number")] = 1,
64
+ ) -> None:
65
+ """GitLab CLI — manage projects, merge requests, pipelines, and more."""
66
+ _ctx.ctx.configure(json_mode=json, token=token, base_url=url, project=project, limit=limit, page=page)
67
+
68
+ try:
69
+ app(tokens)
70
+ except AuthenticationError as exc:
71
+ _handle_error(str(exc), code="authentication", exit_code=EXIT_AUTH)
72
+ except NotFoundError as exc:
73
+ _handle_error(str(exc), code="not_found", exit_code=EXIT_NOT_FOUND)
74
+ except APIError as exc:
75
+ _handle_error(str(exc), code="api_error", exit_code=EXIT_API)
76
+ except ConfigurationError as exc:
77
+ _handle_error(str(exc), code="configuration", exit_code=EXIT_CONFIG)
78
+ except SystemExit:
79
+ raise
80
+ except KeyboardInterrupt:
81
+ sys.exit(130)
82
+ except Exception as exc:
83
+ _handle_error(f"Unexpected error: {exc}", code="unknown", exit_code=1)
84
+
85
+
86
+ def _handle_error(message: str, *, code: str, exit_code: int) -> None:
87
+ from qodev_gitlab_cli.output import error
88
+
89
+ error(message, ctx=_ctx.ctx, code=code, exit_code=exit_code)
90
+
91
+
92
+ def main() -> None:
93
+ app.meta()
@@ -0,0 +1 @@
1
+ """Command groups for the GitLab CLI."""
@@ -0,0 +1,102 @@
1
+ """Issue commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ from cyclopts import App, Parameter
8
+
9
+ from qodev_gitlab_cli.context import ctx
10
+ from qodev_gitlab_cli.formatters.issues import format_issue_detail, format_issue_list, format_note_list
11
+ from qodev_gitlab_cli.output import output, output_list
12
+
13
+ issues_app = App(name="issues", help="Manage issues.")
14
+
15
+
16
+ @issues_app.command
17
+ def list(
18
+ *,
19
+ state: Annotated[str, Parameter(name="--state", help="Filter: opened, closed, all")] = "opened",
20
+ labels: Annotated[str | None, Parameter(name="--labels", help="Filter by labels")] = None,
21
+ milestone: Annotated[str | None, Parameter(name="--milestone", help="Filter by milestone")] = None,
22
+ ) -> None:
23
+ """List issues."""
24
+ client = ctx.client()
25
+ project = ctx.resolve_project()
26
+ items = client.get_issues(project, state=state, labels=labels, milestone=milestone)
27
+ output_list(items=items, ctx=ctx, format_fn=format_issue_list)
28
+
29
+
30
+ @issues_app.command
31
+ def get(
32
+ iid: Annotated[int, Parameter(help="Issue IID")],
33
+ ) -> None:
34
+ """Get issue details."""
35
+ client = ctx.client()
36
+ project = ctx.resolve_project()
37
+ result = client.get_issue(project, iid)
38
+ output(result, ctx=ctx, format_fn=format_issue_detail)
39
+
40
+
41
+ @issues_app.command
42
+ def create(
43
+ *,
44
+ title: Annotated[str, Parameter(name="--title", help="Issue title")],
45
+ description: Annotated[str | None, Parameter(name="--description", help="Issue description")] = None,
46
+ labels: Annotated[str | None, Parameter(name="--labels", help="Comma-separated labels")] = None,
47
+ ) -> None:
48
+ """Create a new issue."""
49
+ client = ctx.client()
50
+ project = ctx.resolve_project()
51
+ result = client.create_issue(project, title=title, description=description, labels=labels)
52
+ output(result, ctx=ctx, format_fn=format_issue_detail)
53
+
54
+
55
+ @issues_app.command
56
+ def update(
57
+ iid: Annotated[int, Parameter(help="Issue IID")],
58
+ *,
59
+ title: Annotated[str | None, Parameter(name="--title", help="New title")] = None,
60
+ description: Annotated[str | None, Parameter(name="--description", help="New description")] = None,
61
+ labels: Annotated[str | None, Parameter(name="--labels", help="New labels")] = None,
62
+ ) -> None:
63
+ """Update an issue."""
64
+ client = ctx.client()
65
+ project = ctx.resolve_project()
66
+ result = client.update_issue(project, iid, title=title, description=description, labels=labels)
67
+ output(result, ctx=ctx, format_fn=format_issue_detail)
68
+
69
+
70
+ @issues_app.command
71
+ def close(
72
+ iid: Annotated[int, Parameter(help="Issue IID")],
73
+ ) -> None:
74
+ """Close an issue."""
75
+ client = ctx.client()
76
+ project = ctx.resolve_project()
77
+ result = client.close_issue(project, iid)
78
+ output(result, ctx=ctx, format_fn=format_issue_detail)
79
+
80
+
81
+ @issues_app.command
82
+ def comment(
83
+ iid: Annotated[int, Parameter(help="Issue IID")],
84
+ *,
85
+ body: Annotated[str, Parameter(name="--body", help="Comment text")],
86
+ ) -> None:
87
+ """Comment on an issue."""
88
+ client = ctx.client()
89
+ project = ctx.resolve_project()
90
+ result = client.create_issue_note(project, iid, body)
91
+ output(result, ctx=ctx)
92
+
93
+
94
+ @issues_app.command
95
+ def notes(
96
+ iid: Annotated[int, Parameter(help="Issue IID")],
97
+ ) -> None:
98
+ """List comments/notes on an issue."""
99
+ client = ctx.client()
100
+ project = ctx.resolve_project()
101
+ items = client.get_issue_notes(project, iid)
102
+ output_list(items=items, ctx=ctx, format_fn=format_note_list)
@@ -0,0 +1,52 @@
1
+ """Job commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ from cyclopts import App, Parameter
8
+
9
+ from qodev_gitlab_cli.context import ctx
10
+ from qodev_gitlab_cli.formatters.jobs import format_job_detail
11
+ from qodev_gitlab_cli.output import output, output_markdown
12
+
13
+ jobs_app = App(name="jobs", help="Manage jobs.")
14
+
15
+
16
+ @jobs_app.command
17
+ def get(
18
+ id: Annotated[int, Parameter(help="Job ID")],
19
+ ) -> None:
20
+ """Get job details."""
21
+ client = ctx.client()
22
+ project = ctx.resolve_project()
23
+ result = client.get_job(project, id)
24
+ output(result, ctx=ctx, format_fn=format_job_detail)
25
+
26
+
27
+ @jobs_app.command
28
+ def log(
29
+ id: Annotated[int, Parameter(help="Job ID")],
30
+ ) -> None:
31
+ """Get job log output."""
32
+ client = ctx.client()
33
+ project = ctx.resolve_project()
34
+ log_text = client.get_job_log(project, id)
35
+
36
+ if ctx.json_mode:
37
+ from qodev_gitlab_cli.output import output_json
38
+
39
+ output_json({"job_id": id, "log": log_text})
40
+ else:
41
+ output_markdown(f"# Job #{id} Log\n\n```\n{log_text}\n```")
42
+
43
+
44
+ @jobs_app.command
45
+ def retry(
46
+ id: Annotated[int, Parameter(help="Job ID")],
47
+ ) -> None:
48
+ """Retry a failed job."""
49
+ client = ctx.client()
50
+ project = ctx.resolve_project()
51
+ result = client.retry_job(project, id)
52
+ output(result, ctx=ctx, format_fn=format_job_detail)
@@ -0,0 +1,202 @@
1
+ """Merge request commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ from cyclopts import App, Parameter
8
+
9
+ from qodev_gitlab_cli.context import ctx
10
+ from qodev_gitlab_cli.formatters.mrs import (
11
+ format_approval_detail,
12
+ format_commit_list,
13
+ format_discussion_list,
14
+ format_mr_detail,
15
+ format_mr_list,
16
+ )
17
+ from qodev_gitlab_cli.output import output, output_list, output_markdown
18
+
19
+ mrs_app = App(name="mrs", help="Manage merge requests.")
20
+
21
+
22
+ @mrs_app.command
23
+ def list(
24
+ *,
25
+ state: Annotated[str, Parameter(name="--state", help="Filter by state: opened, closed, merged, all")] = "opened",
26
+ ) -> None:
27
+ """List merge requests."""
28
+ client = ctx.client()
29
+ project = ctx.resolve_project()
30
+ items = client.get_merge_requests(project, state=state)
31
+ output_list(items=items, ctx=ctx, format_fn=format_mr_list)
32
+
33
+
34
+ @mrs_app.command
35
+ def get(
36
+ iid: Annotated[int, Parameter(help="Merge request IID")],
37
+ ) -> None:
38
+ """Get merge request details."""
39
+ client = ctx.client()
40
+ project = ctx.resolve_project()
41
+ result = client.get_merge_request(project, iid)
42
+ output(result, ctx=ctx, format_fn=format_mr_detail)
43
+
44
+
45
+ @mrs_app.command
46
+ def create(
47
+ *,
48
+ title: Annotated[str, Parameter(name="--title", help="MR title")],
49
+ source: Annotated[str | None, Parameter(name="--source", help="Source branch (default: current)")] = None,
50
+ target: Annotated[str, Parameter(name="--target", help="Target branch")] = "main",
51
+ description: Annotated[str | None, Parameter(name="--description", help="MR description")] = None,
52
+ labels: Annotated[str | None, Parameter(name="--labels", help="Comma-separated labels")] = None,
53
+ squash: Annotated[bool | None, Parameter(name="--squash", help="Squash commits on merge")] = None,
54
+ ) -> None:
55
+ """Create a new merge request."""
56
+ client = ctx.client()
57
+ project = ctx.resolve_project()
58
+
59
+ if source is None:
60
+ from qodev_gitlab_cli.project import get_current_branch
61
+
62
+ source = get_current_branch()
63
+ if not source:
64
+ from qodev_gitlab_cli.output import error
65
+
66
+ error("Could not detect current branch. Use --source.", ctx=ctx)
67
+
68
+ result = client.create_merge_request(
69
+ project,
70
+ source_branch=source, # type: ignore[arg-type]
71
+ target_branch=target,
72
+ title=title,
73
+ description=description,
74
+ labels=labels,
75
+ squash=squash,
76
+ )
77
+ output(result, ctx=ctx, format_fn=format_mr_detail)
78
+
79
+
80
+ @mrs_app.command
81
+ def update(
82
+ iid: Annotated[int, Parameter(help="Merge request IID")],
83
+ *,
84
+ title: Annotated[str | None, Parameter(name="--title", help="New title")] = None,
85
+ description: Annotated[str | None, Parameter(name="--description", help="New description")] = None,
86
+ labels: Annotated[str | None, Parameter(name="--labels", help="New labels")] = None,
87
+ target: Annotated[str | None, Parameter(name="--target", help="New target branch")] = None,
88
+ ) -> None:
89
+ """Update a merge request."""
90
+ client = ctx.client()
91
+ project = ctx.resolve_project()
92
+ result = client.update_mr(project, iid, title=title, description=description, labels=labels, target_branch=target)
93
+ output(result, ctx=ctx, format_fn=format_mr_detail)
94
+
95
+
96
+ @mrs_app.command
97
+ def merge(
98
+ iid: Annotated[int, Parameter(help="Merge request IID")],
99
+ *,
100
+ squash: Annotated[bool | None, Parameter(name="--squash", help="Squash commits")] = None,
101
+ when_pipeline_succeeds: Annotated[
102
+ bool, Parameter(name="--when-pipeline-succeeds", help="Merge when pipeline succeeds", negative="")
103
+ ] = False,
104
+ ) -> None:
105
+ """Merge a merge request."""
106
+ client = ctx.client()
107
+ project = ctx.resolve_project()
108
+ result = client.merge_mr(project, iid, squash=squash, merge_when_pipeline_succeeds=when_pipeline_succeeds)
109
+ output(result, ctx=ctx, format_fn=format_mr_detail)
110
+
111
+
112
+ @mrs_app.command
113
+ def close(
114
+ iid: Annotated[int, Parameter(help="Merge request IID")],
115
+ ) -> None:
116
+ """Close a merge request."""
117
+ client = ctx.client()
118
+ project = ctx.resolve_project()
119
+ result = client.close_mr(project, iid)
120
+ output(result, ctx=ctx, format_fn=format_mr_detail)
121
+
122
+
123
+ @mrs_app.command
124
+ def discussions(
125
+ iid: Annotated[int, Parameter(help="Merge request IID")],
126
+ ) -> None:
127
+ """List discussions on a merge request."""
128
+ client = ctx.client()
129
+ project = ctx.resolve_project()
130
+ items = client.get_mr_discussions(project, iid)
131
+ output_list(items=items, ctx=ctx, format_fn=format_discussion_list)
132
+
133
+
134
+ @mrs_app.command
135
+ def changes(
136
+ iid: Annotated[int, Parameter(help="Merge request IID")],
137
+ ) -> None:
138
+ """Show changes/diff for a merge request."""
139
+ client = ctx.client()
140
+ project = ctx.resolve_project()
141
+ result = client.get_mr_changes(project, iid)
142
+
143
+ if ctx.json_mode:
144
+ from qodev_gitlab_cli.output import output_json
145
+
146
+ output_json(result)
147
+ else:
148
+ diffs = result.get("changes", [])
149
+ lines = [f"# Changes for !{iid}", ""]
150
+ for diff in diffs:
151
+ lines.append(f"## {diff.get('new_path', '?')}")
152
+ lines.append(f"```diff\n{diff.get('diff', '')}\n```")
153
+ lines.append("")
154
+ output_markdown("\n".join(lines))
155
+
156
+
157
+ @mrs_app.command
158
+ def commits(
159
+ iid: Annotated[int, Parameter(help="Merge request IID")],
160
+ ) -> None:
161
+ """List commits in a merge request."""
162
+ client = ctx.client()
163
+ project = ctx.resolve_project()
164
+ items = client.get_mr_commits(project, iid)
165
+ output_list(items=items, ctx=ctx, format_fn=format_commit_list)
166
+
167
+
168
+ @mrs_app.command
169
+ def approvals(
170
+ iid: Annotated[int, Parameter(help="Merge request IID")],
171
+ ) -> None:
172
+ """Show approval status for a merge request."""
173
+ client = ctx.client()
174
+ project = ctx.resolve_project()
175
+ result = client.get_mr_approvals(project, iid)
176
+ output(result, ctx=ctx, format_fn=format_approval_detail)
177
+
178
+
179
+ @mrs_app.command
180
+ def comment(
181
+ iid: Annotated[int, Parameter(help="Merge request IID")],
182
+ *,
183
+ body: Annotated[str, Parameter(name="--body", help="Comment text")],
184
+ ) -> None:
185
+ """Comment on a merge request."""
186
+ client = ctx.client()
187
+ project = ctx.resolve_project()
188
+ result = client.create_mr_note(project, iid, body)
189
+ output(result, ctx=ctx)
190
+
191
+
192
+ @mrs_app.command
193
+ def pipelines(
194
+ iid: Annotated[int, Parameter(help="Merge request IID")],
195
+ ) -> None:
196
+ """List pipelines for a merge request."""
197
+ client = ctx.client()
198
+ project = ctx.resolve_project()
199
+ items = client.get_mr_pipelines(project, iid)
200
+ from qodev_gitlab_cli.formatters.pipelines import format_pipeline_list
201
+
202
+ output_list(items=items, ctx=ctx, format_fn=format_pipeline_list)
@@ -0,0 +1,63 @@
1
+ """Pipeline commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ from cyclopts import App, Parameter
8
+
9
+ from qodev_gitlab_cli.context import ctx
10
+ from qodev_gitlab_cli.formatters.jobs import format_job_list
11
+ from qodev_gitlab_cli.formatters.pipelines import format_pipeline_detail, format_pipeline_list, format_wait_result
12
+ from qodev_gitlab_cli.output import output, output_list
13
+
14
+ pipelines_app = App(name="pipelines", help="Manage pipelines.")
15
+
16
+
17
+ @pipelines_app.command
18
+ def list(
19
+ *,
20
+ ref: Annotated[str | None, Parameter(name="--ref", help="Filter by branch/tag")] = None,
21
+ limit: Annotated[int, Parameter(name="--limit", help="Max pipelines to return")] = 20,
22
+ ) -> None:
23
+ """List pipelines."""
24
+ client = ctx.client()
25
+ project = ctx.resolve_project()
26
+ items = client.get_pipelines(project, ref=ref, per_page=limit, max_pages=1)
27
+ output_list(items=items, ctx=ctx, format_fn=format_pipeline_list)
28
+
29
+
30
+ @pipelines_app.command
31
+ def get(
32
+ id: Annotated[int, Parameter(help="Pipeline ID")],
33
+ ) -> None:
34
+ """Get pipeline details."""
35
+ client = ctx.client()
36
+ project = ctx.resolve_project()
37
+ result = client.get_pipeline(project, id)
38
+ output(result, ctx=ctx, format_fn=format_pipeline_detail)
39
+
40
+
41
+ @pipelines_app.command
42
+ def jobs(
43
+ id: Annotated[int, Parameter(help="Pipeline ID")],
44
+ ) -> None:
45
+ """List jobs for a pipeline."""
46
+ client = ctx.client()
47
+ project = ctx.resolve_project()
48
+ items = client.get_pipeline_jobs(project, id)
49
+ output_list(items=items, ctx=ctx, format_fn=format_job_list)
50
+
51
+
52
+ @pipelines_app.command
53
+ def wait(
54
+ id: Annotated[int, Parameter(help="Pipeline ID")],
55
+ *,
56
+ timeout: Annotated[int, Parameter(name="--timeout", help="Timeout in seconds")] = 3600,
57
+ interval: Annotated[int, Parameter(name="--interval", help="Check interval in seconds")] = 10,
58
+ ) -> None:
59
+ """Wait for a pipeline to complete."""
60
+ client = ctx.client()
61
+ project = ctx.resolve_project()
62
+ result = client.wait_for_pipeline(project, id, timeout_seconds=timeout, check_interval=interval)
63
+ output(result, ctx=ctx, format_fn=format_wait_result)
@@ -0,0 +1,35 @@
1
+ """Project commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ from cyclopts import App, Parameter
8
+
9
+ from qodev_gitlab_cli.context import ctx
10
+ from qodev_gitlab_cli.formatters.projects import format_project_detail, format_project_list
11
+ from qodev_gitlab_cli.output import output, output_list
12
+
13
+ projects_app = App(name="projects", help="Manage projects.")
14
+
15
+
16
+ @projects_app.command
17
+ def list(
18
+ *,
19
+ owned: Annotated[bool, Parameter(name="--owned", help="Only owned projects", negative="")] = False,
20
+ ) -> None:
21
+ """List projects."""
22
+ client = ctx.client()
23
+ items = client.get_projects(owned=owned)
24
+ output_list(items=items, ctx=ctx, format_fn=format_project_list)
25
+
26
+
27
+ @projects_app.command
28
+ def get(
29
+ id: Annotated[str | None, Parameter(help="Project ID or path (defaults to auto-detected)")] = None,
30
+ ) -> None:
31
+ """Get project details."""
32
+ client = ctx.client()
33
+ project_id = id or ctx.resolve_project()
34
+ result = client.get_project(project_id)
35
+ output(result, ctx=ctx, format_fn=format_project_detail)
@@ -0,0 +1,48 @@
1
+ """Release commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ from cyclopts import App, Parameter
8
+
9
+ from qodev_gitlab_cli.context import ctx
10
+ from qodev_gitlab_cli.formatters.releases import format_release_detail, format_release_list
11
+ from qodev_gitlab_cli.output import output, output_list
12
+
13
+ releases_app = App(name="releases", help="Manage releases.")
14
+
15
+
16
+ @releases_app.command
17
+ def list() -> None:
18
+ """List releases."""
19
+ client = ctx.client()
20
+ project = ctx.resolve_project()
21
+ items = client.get_releases(project)
22
+ output_list(items=items, ctx=ctx, format_fn=format_release_list)
23
+
24
+
25
+ @releases_app.command
26
+ def get(
27
+ tag: Annotated[str, Parameter(help="Tag name")],
28
+ ) -> None:
29
+ """Get release details."""
30
+ client = ctx.client()
31
+ project = ctx.resolve_project()
32
+ result = client.get_release(project, tag)
33
+ output(result, ctx=ctx, format_fn=format_release_detail)
34
+
35
+
36
+ @releases_app.command
37
+ def create(
38
+ *,
39
+ tag: Annotated[str, Parameter(name="--tag", help="Tag name for the release")],
40
+ name: Annotated[str | None, Parameter(name="--name", help="Release title")] = None,
41
+ description: Annotated[str | None, Parameter(name="--description", help="Release notes")] = None,
42
+ ref: Annotated[str | None, Parameter(name="--ref", help="Commit SHA, branch, or tag")] = None,
43
+ ) -> None:
44
+ """Create a new release."""
45
+ client = ctx.client()
46
+ project = ctx.resolve_project()
47
+ result = client.create_release(project, tag_name=tag, name=name, description=description, ref=ref)
48
+ output(result, ctx=ctx, format_fn=format_release_detail)
@@ -0,0 +1,61 @@
1
+ """CI/CD variable commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ from cyclopts import App, Parameter
8
+
9
+ from qodev_gitlab_cli.context import ctx
10
+ from qodev_gitlab_cli.formatters.variables import format_variable_detail, format_variable_list
11
+ from qodev_gitlab_cli.output import output, output_list
12
+
13
+ variables_app = App(name="variables", help="Manage CI/CD variables.")
14
+
15
+
16
+ @variables_app.command
17
+ def list() -> None:
18
+ """List CI/CD variables (values hidden)."""
19
+ client = ctx.client()
20
+ project = ctx.resolve_project()
21
+ items = client.list_project_variables(project)
22
+ output_list(items=items, ctx=ctx, format_fn=format_variable_list)
23
+
24
+
25
+ @variables_app.command
26
+ def get(
27
+ key: Annotated[str, Parameter(help="Variable key")],
28
+ ) -> None:
29
+ """Get a CI/CD variable."""
30
+ client = ctx.client()
31
+ project = ctx.resolve_project()
32
+ result = client.get_project_variable(project, key)
33
+ if result is None:
34
+ from qodev_gitlab_cli.output import error
35
+
36
+ error(f"Variable '{key}' not found.", ctx=ctx, code="not_found", exit_code=81)
37
+ else:
38
+ output(result, ctx=ctx, format_fn=format_variable_detail)
39
+
40
+
41
+ @variables_app.command
42
+ def set(
43
+ key: Annotated[str, Parameter(help="Variable key")],
44
+ value: Annotated[str, Parameter(help="Variable value")],
45
+ *,
46
+ protected: Annotated[bool, Parameter(name="--protected", help="Only in protected branches", negative="")] = False,
47
+ masked: Annotated[bool, Parameter(name="--masked", help="Hidden in job logs", negative="")] = False,
48
+ ) -> None:
49
+ """Set a CI/CD variable (create or update)."""
50
+ client = ctx.client()
51
+ project = ctx.resolve_project()
52
+ var, action = client.set_project_variable(project, key, value, protected=protected, masked=masked)
53
+
54
+ if ctx.json_mode:
55
+ from qodev_gitlab_cli.output import output_json
56
+
57
+ output_json({"variable": var, "action": action})
58
+ else:
59
+ from qodev_gitlab_cli.output import output_markdown
60
+
61
+ output_markdown(f"Variable `{key}` **{action}** successfully.")