gitlab-mr-commenter 2.0.1__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.
@@ -0,0 +1,79 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ build:
13
+ name: Build distribution
14
+ runs-on: ubuntu-latest
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - uses: astral-sh/setup-uv@v5
20
+
21
+ - name: Build sdist and wheel
22
+ run: uv build
23
+
24
+ - uses: actions/upload-artifact@v4
25
+ with:
26
+ name: dist
27
+ path: dist/
28
+
29
+ # Verify the package installs and the CLI entry point is present.
30
+ smoke-test:
31
+ name: Smoke test
32
+ needs: build
33
+ runs-on: ubuntu-latest
34
+
35
+ steps:
36
+ - uses: actions/download-artifact@v4
37
+ with:
38
+ name: dist
39
+ path: dist/
40
+
41
+ - uses: astral-sh/setup-uv@v5
42
+
43
+ - name: Install from wheel
44
+ run: |
45
+ uv venv
46
+ uv pip install dist/*.whl
47
+
48
+ - name: Verify CLI entry point
49
+ run: uv run gitlab-mr-commenter --help
50
+
51
+ - name: Verify library import
52
+ run: uv run python -c "from gitlab_mr_commenter import MRCommenter, post_comment; print('OK')"
53
+
54
+ publish:
55
+ name: Publish to PyPI
56
+ needs: [build, smoke-test]
57
+ runs-on: ubuntu-latest
58
+
59
+ if: startsWith(github.ref, 'refs/tags/v')
60
+
61
+ environment:
62
+ name: pypi
63
+ url: https://pypi.org/p/gitlab-mr-commenter
64
+
65
+ # Trusted Publishing — no API token stored in GitHub secrets.
66
+ # Set this up once at https://pypi.org/manage/project/gitlab-mr-commenter/settings/publishing/
67
+ permissions:
68
+ id-token: write
69
+
70
+ steps:
71
+ - uses: actions/download-artifact@v4
72
+ with:
73
+ name: dist
74
+ path: dist/
75
+
76
+ - uses: astral-sh/setup-uv@v5
77
+
78
+ - name: Publish to PyPI
79
+ run: uv publish --trusted-publishing always
@@ -0,0 +1,33 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ permissions:
9
+ contents: write
10
+ id-token: write
11
+
12
+ jobs:
13
+ release:
14
+ name: Semantic release
15
+ runs-on: ubuntu-latest
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ with:
20
+ fetch-depth: 0
21
+ # Use a token that can push to protected branches and trigger
22
+ # downstream workflows (the default GITHUB_TOKEN cannot).
23
+ token: ${{ secrets.RELEASE_TOKEN }}
24
+
25
+ - uses: astral-sh/setup-uv@v5
26
+
27
+ - name: Install python-semantic-release
28
+ run: uv tool install python-semantic-release
29
+
30
+ - name: Run semantic-release
31
+ run: semantic-release version --push
32
+ env:
33
+ GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
@@ -0,0 +1,28 @@
1
+ # CHANGELOG
2
+
3
+ <!-- version list -->
4
+
5
+ ## v2.0.1 (2026-02-19)
6
+
7
+ ### Bug Fixes
8
+
9
+ - **ci**: Use uv venv instead of --system for smoke-test
10
+ ([`c0a8317`](https://github.com/gianklug/gitlab-mr-commenter/commit/c0a831799170348966dc6985e055f3093940399a))
11
+
12
+
13
+ ## v2.0.0 (2026-02-19)
14
+
15
+ ### Continuous Integration
16
+
17
+ - Add semantic release and fix smoke-test wheel install
18
+ ([`3b96b0a`](https://github.com/gianklug/gitlab-mr-commenter/commit/3b96b0a139f6acd36d3ce571f8d8cab188cba41b))
19
+
20
+ ### Features
21
+
22
+ - Switch to semrel
23
+ ([`d866c23`](https://github.com/gianklug/gitlab-mr-commenter/commit/d866c23dcef0e94d59ed2f831d5948ff53a41f6e))
24
+
25
+
26
+ ## v1.0.0 (2026-02-19)
27
+
28
+ - Initial Release
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: gitlab-mr-commenter
3
+ Version: 2.0.1
4
+ Summary: Post and idempotently update comments on GitLab merge requests
5
+ Author-email: Gian Klug <gk@gk.wtf>
6
+ Requires-Python: >=3.9
7
+ Requires-Dist: python-gitlab>=8.0
@@ -0,0 +1,183 @@
1
+ # gitlab-mr-commenter
2
+
3
+ Post and **idempotently update** comments on GitLab merge requests.
4
+
5
+ Re-running with the same `comment_id` updates the existing note in place rather
6
+ than creating a duplicate — useful for plan/apply pipelines where you want a
7
+ single, always-current comment per environment.
8
+
9
+ Inspired by the `mr-commenter` sub-command in
10
+ [`gitlab-tofu-ctl`](https://gitlab.com/components/opentofu), reimplemented as a
11
+ standalone Python package with both a library API and a CLI.
12
+
13
+ ---
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ # Add to a project
19
+ uv add gitlab-mr-commenter
20
+
21
+ # Run without installing (ephemeral)
22
+ uvx gitlab-mr-commenter --help
23
+
24
+ # pip also works
25
+ pip install gitlab-mr-commenter
26
+ ```
27
+
28
+ Python 3.9+ is required. The only dependency is
29
+ [`python-gitlab`](https://python-gitlab.readthedocs.io/) ≥ 8.0.
30
+
31
+ ---
32
+
33
+ ## Quick start
34
+
35
+ ### CLI
36
+
37
+ ```bash
38
+ # Content is read from stdin; IDs and token are read from CI environment variables.
39
+ echo "## Plan: no changes" | gitlab-mr-commenter plan-production
40
+
41
+ # Explicit flags override env vars.
42
+ echo "## Plan: no changes" | gitlab-mr-commenter \
43
+ --project-id 123 \
44
+ --mr-iid 45 \
45
+ --token "$GITLAB_TOKEN" \
46
+ --api-url "https://gitlab.com/api/v4" \
47
+ plan-production
48
+ ```
49
+
50
+ ### Python
51
+
52
+ ```python
53
+ from gitlab_mr_commenter import MRCommenter
54
+
55
+ commenter = MRCommenter() # reads token + URL from env
56
+ commenter.post(
57
+ project_id=123,
58
+ mr_iid=45,
59
+ comment_id="plan-production",
60
+ content="## Plan\n\nNo changes.",
61
+ )
62
+ ```
63
+
64
+ One-shot convenience function:
65
+
66
+ ```python
67
+ from gitlab_mr_commenter import post_comment
68
+
69
+ post_comment(123, 45, "plan-production", "## Plan\n\nNo changes.")
70
+ ```
71
+
72
+ ---
73
+
74
+ ## Configuration
75
+
76
+ All configuration can be supplied either as keyword arguments (library) / CLI
77
+ flags, or via environment variables. Environment variables are the natural
78
+ choice in CI pipelines.
79
+
80
+ | Environment variable | CLI flag | Description |
81
+ |---|---|---|
82
+ | `GITLAB_MR_PLAN_TOKEN` | `--token` | GitLab API token (preferred) |
83
+ | `GITLAB_TOKEN` | `--token` | GitLab API token (fallback) |
84
+ | `CI_API_V4_URL` | `--api-url` | GitLab API base URL, e.g. `https://gitlab.com/api/v4` |
85
+ | `CI_PROJECT_ID` | `--project-id` | Numeric project ID |
86
+ | `CI_MERGE_REQUEST_IID` | `--mr-iid` | MR internal (project-scoped) ID |
87
+
88
+ > **Token scopes** — the token needs at least the `api` scope (or
89
+ > `write_repository` for project tokens). In GitLab CI, a
90
+ > [project access token](https://docs.gitlab.com/ee/user/project/settings/project_access_tokens.html)
91
+ > with **Developer** role and `api` scope is sufficient.
92
+
93
+ ---
94
+
95
+ ## How it works
96
+
97
+ Each managed comment has a hidden HTML marker appended to its body:
98
+
99
+ ```
100
+ <!-- gitlab-mr-commenter id="plan-production" -->
101
+ ```
102
+
103
+ On every run the tool paginates through all notes on the MR looking for this
104
+ marker. If found, the note is updated in place. If not found, a new note is
105
+ created. The marker format is compatible with `gitlab-tofu-ctl mr-commenter`
106
+ so both tools can coexist on the same MR.
107
+
108
+ ---
109
+
110
+ ## CLI reference
111
+
112
+ ```
113
+ usage: gitlab-mr-commenter [-h] [--project-id ID] [--mr-iid IID]
114
+ [--token TOKEN] [--api-url URL]
115
+ COMMENT_ID
116
+
117
+ positional arguments:
118
+ COMMENT_ID Unique name for this comment slot, e.g. 'plan-production'.
119
+ A second run with the same value updates the existing comment.
120
+
121
+ options:
122
+ -h, --help show this help message and exit
123
+ --project-id ID GitLab project ID. Defaults to $CI_PROJECT_ID.
124
+ --mr-iid IID MR internal ID. Defaults to $CI_MERGE_REQUEST_IID.
125
+ --token TOKEN GitLab API token. Defaults to $GITLAB_MR_PLAN_TOKEN
126
+ or $GITLAB_TOKEN.
127
+ --api-url URL GitLab API base URL. Defaults to $CI_API_V4_URL.
128
+ ```
129
+
130
+ ---
131
+
132
+ ## Development setup
133
+
134
+ ```bash
135
+ git clone https://github.com/your-org/gitlab-mr-commenter
136
+ cd gitlab-mr-commenter
137
+
138
+ # Install dependencies (creates .venv automatically)
139
+ uv sync
140
+
141
+ # Run the CLI
142
+ uv run gitlab-mr-commenter --help
143
+ ```
144
+
145
+ ### Running the CLI locally against a real MR
146
+
147
+ ```bash
148
+ export GITLAB_MR_PLAN_TOKEN="glpat-xxxxxxxxxxxx"
149
+ export CI_API_V4_URL="https://gitlab.com/api/v4"
150
+ export CI_PROJECT_ID="12345678"
151
+ export CI_MERGE_REQUEST_IID="42"
152
+
153
+ echo "## Test comment $(date)" | uv run gitlab-mr-commenter test-local
154
+ ```
155
+
156
+ ---
157
+
158
+ ## Publishing
159
+
160
+ Releases are published to [PyPI](https://pypi.org/project/gitlab-mr-commenter/)
161
+ automatically when a version tag is pushed (see `.github/workflows/publish.yml`).
162
+ The workflow uses [Trusted Publishing (OIDC)](https://docs.pypi.org/trusted-publishers/)
163
+ so no API token needs to be stored in GitHub secrets — set it up once on PyPI's
164
+ side under your project's Publishing settings.
165
+
166
+ ```bash
167
+ # Bump the version in pyproject.toml, then:
168
+ git tag v0.2.0
169
+ git push origin v0.2.0
170
+ ```
171
+
172
+ To publish manually:
173
+
174
+ ```bash
175
+ uv build
176
+ uv publish
177
+ ```
178
+
179
+ ---
180
+
181
+ ## License
182
+
183
+ MIT
@@ -0,0 +1,34 @@
1
+ # Example: post a comment on a GitLab MR using gitlab-mr-commenter.
2
+ #
3
+ # Prerequisites
4
+ # -------------
5
+ # 1. Create a project access token (Developer role, `api` scope).
6
+ # 2. Add it as a masked CI/CD variable named GITLAB_MR_PLAN_TOKEN.
7
+ #
8
+ # The variables CI_PROJECT_ID, CI_MERGE_REQUEST_IID, and CI_API_V4_URL are
9
+ # provided automatically by GitLab in merge-request pipelines.
10
+
11
+ stages:
12
+ - comment
13
+
14
+ post-mr-comment:
15
+ stage: comment
16
+ image: python:3.12-slim
17
+ rules:
18
+ # Only run in merge-request pipelines so CI_MERGE_REQUEST_IID is set.
19
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
20
+ script:
21
+ - pip install uv
22
+
23
+ # Run whatever command you like and capture its output.
24
+ - OUTPUT=$(date -u)
25
+
26
+ # Pipe the output into gitlab-mr-commenter.
27
+ # Re-running this job updates the same comment rather than creating a new one.
28
+ # CI_PROJECT_ID, CI_MERGE_REQUEST_IID, and CI_API_V4_URL are picked up automatically.
29
+ - |
30
+ cat <<EOF | uvx gitlab-mr-commenter
31
+ ## Pipeline info
32
+
33
+ Triggered by job [#${CI_JOB_ID}](${CI_JOB_URL}) at ${OUTPUT}.
34
+ EOF
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env python3
2
+ """Example: call gitlab-mr-commenter from a Python script.
3
+
4
+ Run (env vars must be set, see README):
5
+
6
+ python examples/python-script.py
7
+ """
8
+
9
+ import os
10
+ import subprocess
11
+ from gitlab_mr_commenter import MRCommenter
12
+
13
+ output = subprocess.check_output(["date", "-u"], text=True).strip()
14
+
15
+ content = f"""\
16
+ ## Pipeline info
17
+
18
+ Triggered at {output} by job [{os.environ.get('CI_JOB_ID', 'local')}]({os.environ.get('CI_JOB_URL', '')})."""
19
+
20
+ commenter = MRCommenter() # token + api_url from env
21
+ commenter.post(
22
+ project_id=int(os.environ["CI_PROJECT_ID"]),
23
+ mr_iid=int(os.environ["CI_MERGE_REQUEST_IID"]),
24
+ comment_id="my-comment-slot",
25
+ content=content,
26
+ )
@@ -0,0 +1,190 @@
1
+ """gitlab-mr-commenter: post and idempotently update comments on GitLab MRs.
2
+
3
+ Each comment slot is identified by a hidden HTML marker embedded at the end
4
+ of the comment body. Re-running with the same ``comment_id`` updates the
5
+ existing comment rather than creating a duplicate.
6
+
7
+ Typical CI usage
8
+ ----------------
9
+ The following environment variables are read automatically when the
10
+ corresponding keyword argument is omitted:
11
+
12
+ * ``GITLAB_MR_PLAN_TOKEN`` (preferred) or ``GITLAB_TOKEN`` — API token
13
+ * ``CI_API_V4_URL`` — GitLab API base URL
14
+ * ``CI_PROJECT_ID`` — project ID
15
+ * ``CI_MERGE_REQUEST_IID`` — MR internal ID (project-scoped, visible in URLs)
16
+
17
+ Quick start
18
+ -----------
19
+ As a library::
20
+
21
+ from gitlab_mr_commenter import MRCommenter
22
+
23
+ commenter = MRCommenter() # reads token + URL from env
24
+ commenter.post("## Plan\\n\\nNo changes.") # project_id/mr_iid from env
25
+
26
+ # or explicitly:
27
+ commenter.post(
28
+ content="## Plan\\n\\nNo changes.",
29
+ project_id=123,
30
+ mr_iid=45,
31
+ comment_id="plan-production",
32
+ )
33
+
34
+ As a one-shot function::
35
+
36
+ from gitlab_mr_commenter import post_comment
37
+
38
+ post_comment("## Plan\\n\\nNo changes.") # all from env
39
+ post_comment("## Plan\\n\\nNo changes.", project_id=123, mr_iid=45, comment_id="plan-production")
40
+
41
+ From the command line::
42
+
43
+ echo "## Plan" | gitlab-mr-commenter plan-production
44
+ echo "## Plan" | gitlab-mr-commenter --project-id 123 --mr-iid 45 plan-production
45
+ """
46
+
47
+ from __future__ import annotations
48
+
49
+ import json
50
+ import os
51
+ from typing import Optional
52
+
53
+ import gitlab
54
+
55
+ __all__ = ["COMMENT_IDENTIFIER", "MRCommenter", "post_comment"]
56
+
57
+ # Hidden HTML marker embedded at the end of every managed comment.
58
+ # Matches the Go reference: `<!-- gitlab-mr-commenter id=%q -->` (%q → JSON string).
59
+ COMMENT_IDENTIFIER = "<!-- gitlab-mr-commenter id={} -->"
60
+
61
+
62
+ def _marker(comment_id: str) -> str:
63
+ """Return the full hidden marker for *comment_id*.
64
+
65
+ Uses ``json.dumps`` to produce a double-quoted, escaped string — the same
66
+ output as Go's ``%q`` verb for plain ASCII identifiers.
67
+ """
68
+ return COMMENT_IDENTIFIER.format(json.dumps(comment_id))
69
+
70
+
71
+ class MRCommenter:
72
+ """Post and idempotently update comments on a GitLab merge request.
73
+
74
+ Parameters
75
+ ----------
76
+ token:
77
+ GitLab personal / project / CI access token. Falls back to the
78
+ ``GITLAB_MR_PLAN_TOKEN`` or ``GITLAB_TOKEN`` environment
79
+ variables.
80
+ api_url:
81
+ GitLab API v4 base URL (e.g. ``https://gitlab.com/api/v4``). Falls
82
+ back to ``CI_API_V4_URL``.
83
+ """
84
+
85
+ def __init__(
86
+ self,
87
+ token: Optional[str] = None,
88
+ api_url: Optional[str] = None,
89
+ ) -> None:
90
+ resolved_token = (
91
+ token
92
+ or os.environ.get("GITLAB_MR_PLAN_TOKEN")
93
+ or os.environ.get("GITLAB_TOKEN")
94
+ )
95
+ if not resolved_token:
96
+ raise ValueError(
97
+ "No GitLab token found. Pass token= or set "
98
+ "GITLAB_MR_PLAN_TOKEN / GITLAB_TOKEN."
99
+ )
100
+
101
+ resolved_url = api_url or os.environ.get("CI_API_V4_URL")
102
+ if not resolved_url:
103
+ raise ValueError(
104
+ "No GitLab API URL found. Pass api_url= or set CI_API_V4_URL."
105
+ )
106
+
107
+ self._gl = gitlab.Gitlab(resolved_url, private_token=resolved_token)
108
+
109
+ # ------------------------------------------------------------------
110
+ # Public API
111
+ # ------------------------------------------------------------------
112
+
113
+ def post(
114
+ self,
115
+ content: str,
116
+ project_id: Optional[int] = None,
117
+ mr_iid: Optional[int] = None,
118
+ comment_id: str = "gitlab-mr-commenter",
119
+ ) -> None:
120
+ """Post or update a comment on a merge request.
121
+
122
+ If a note whose body contains the hidden marker for *comment_id*
123
+ already exists, it is updated in place. Otherwise a new note is
124
+ created.
125
+
126
+ Parameters
127
+ ----------
128
+ content:
129
+ Markdown body of the comment (without the trailing marker — that
130
+ is appended automatically).
131
+ project_id:
132
+ Numeric GitLab project ID. Falls back to ``$CI_PROJECT_ID``.
133
+ mr_iid:
134
+ Merge request **internal** ID — the project-scoped number shown
135
+ in the URL. Falls back to ``$CI_MERGE_REQUEST_IID``.
136
+ comment_id:
137
+ Opaque string that uniquely names this comment slot. Two calls
138
+ with the same *comment_id* on the same MR update the same note.
139
+ Defaults to ``"gitlab-mr-commenter"``.
140
+ """
141
+ if project_id is None:
142
+ raw = os.environ.get("CI_PROJECT_ID", "")
143
+ if not raw:
144
+ raise ValueError(
145
+ "project_id not provided and CI_PROJECT_ID is not set."
146
+ )
147
+ project_id = int(raw)
148
+
149
+ if mr_iid is None:
150
+ raw = os.environ.get("CI_MERGE_REQUEST_IID", "")
151
+ if not raw:
152
+ raise ValueError(
153
+ "mr_iid not provided and CI_MERGE_REQUEST_IID is not set."
154
+ )
155
+ mr_iid = int(raw)
156
+
157
+ marker = _marker(comment_id)
158
+ body = f"{content}\n\n{marker}"
159
+
160
+ project = self._gl.projects.get(project_id)
161
+ mr = project.mergerequests.get(mr_iid)
162
+
163
+ # Walk all pages lazily; stop as soon as we find a matching note.
164
+ for note in mr.notes.list(as_list=False):
165
+ if marker in note.body:
166
+ note.body = body
167
+ note.save()
168
+ return
169
+
170
+ # No existing comment — create one.
171
+ mr.notes.create({"body": body})
172
+
173
+
174
+ def post_comment(
175
+ content: str,
176
+ project_id: Optional[int] = None,
177
+ mr_iid: Optional[int] = None,
178
+ comment_id: str = "gitlab-mr-commenter",
179
+ *,
180
+ token: Optional[str] = None,
181
+ api_url: Optional[str] = None,
182
+ ) -> None:
183
+ """Convenience wrapper around :class:`MRCommenter`.
184
+
185
+ All parameters mirror :meth:`MRCommenter.post`; *token* and *api_url*
186
+ fall back to the same environment variables as the constructor.
187
+ """
188
+ MRCommenter(token=token, api_url=api_url).post(
189
+ content, project_id, mr_iid, comment_id
190
+ )
@@ -0,0 +1,115 @@
1
+ """CLI entry point for gitlab-mr-commenter.
2
+
3
+ Usage
4
+ -----
5
+ Minimal — all values from CI environment variables::
6
+
7
+ echo "## Plan" | gitlab-mr-commenter
8
+
9
+ With an explicit comment slot name::
10
+
11
+ echo "## Plan" | gitlab-mr-commenter plan-production
12
+
13
+ Fully explicit::
14
+
15
+ echo "## Plan" | gitlab-mr-commenter \\
16
+ --project-id 123 \\
17
+ --mr-iid 45 \\
18
+ --token "$GITLAB_TOKEN" \\
19
+ --api-url "https://gitlab.com/api/v4" \\
20
+ plan-production
21
+
22
+ Also callable as a module::
23
+
24
+ echo "## Plan" | python -m gitlab_mr_commenter
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import argparse
30
+ import sys
31
+
32
+ from . import MRCommenter
33
+
34
+
35
+ DEFAULT_COMMENT_ID = "gitlab-mr-commenter"
36
+
37
+
38
+ def main(argv: list[str] | None = None) -> None:
39
+ parser = argparse.ArgumentParser(
40
+ prog="gitlab-mr-commenter",
41
+ description=(
42
+ "Post or update a GitLab MR comment identified by COMMENT_ID. "
43
+ "Content is read from stdin. Re-running with the same COMMENT_ID "
44
+ "updates the existing comment in place."
45
+ ),
46
+ formatter_class=argparse.RawDescriptionHelpFormatter,
47
+ epilog=(
48
+ "environment variables (used when flags are omitted):\n"
49
+ " CI_PROJECT_ID GitLab project ID\n"
50
+ " CI_MERGE_REQUEST_IID MR internal ID\n"
51
+ " CI_API_V4_URL GitLab API base URL\n"
52
+ " GITLAB_MR_PLAN_TOKEN API token (preferred)\n"
53
+ " GITLAB_TOKEN API token (deprecated fallback)\n"
54
+ ),
55
+ )
56
+
57
+ parser.add_argument(
58
+ "comment_id",
59
+ metavar="COMMENT_ID",
60
+ nargs="?",
61
+ default=DEFAULT_COMMENT_ID,
62
+ help=(
63
+ f"Unique name for this comment slot (default: '{DEFAULT_COMMENT_ID}'). "
64
+ "A second run with the same value updates the existing comment."
65
+ ),
66
+ )
67
+ parser.add_argument(
68
+ "--project-id",
69
+ type=int,
70
+ default=None,
71
+ metavar="ID",
72
+ help="GitLab project ID. Defaults to $CI_PROJECT_ID.",
73
+ )
74
+ parser.add_argument(
75
+ "--mr-iid",
76
+ type=int,
77
+ default=None,
78
+ metavar="IID",
79
+ help="Merge request internal ID. Defaults to $CI_MERGE_REQUEST_IID.",
80
+ )
81
+ parser.add_argument(
82
+ "--token",
83
+ default=None,
84
+ metavar="TOKEN",
85
+ help=(
86
+ "GitLab API token. Defaults to $GITLAB_MR_PLAN_TOKEN " "or $GITLAB_TOKEN."
87
+ ),
88
+ )
89
+ parser.add_argument(
90
+ "--api-url",
91
+ default=None,
92
+ metavar="URL",
93
+ help="GitLab API base URL. Defaults to $CI_API_V4_URL.",
94
+ )
95
+
96
+ args = parser.parse_args(argv)
97
+
98
+ content = sys.stdin.read()
99
+ if not content.strip():
100
+ parser.error("stdin is empty — nothing to post.")
101
+
102
+ try:
103
+ MRCommenter(token=args.token, api_url=args.api_url).post(
104
+ content=content,
105
+ project_id=args.project_id,
106
+ mr_iid=args.mr_iid,
107
+ comment_id=args.comment_id,
108
+ )
109
+ except Exception as exc:
110
+ print(f"error: {exc}", file=sys.stderr)
111
+ sys.exit(1)
112
+
113
+
114
+ if __name__ == "__main__":
115
+ main()
File without changes
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "gitlab-mr-commenter"
7
+ version = "2.0.1"
8
+ description = "Post and idempotently update comments on GitLab merge requests"
9
+ requires-python = ">=3.9"
10
+ dependencies = [
11
+ "python-gitlab>=8.0",
12
+ ]
13
+ authors = [
14
+ {name = "Gian Klug", email = "gk@gk.wtf"}
15
+ ]
16
+
17
+ [project.scripts]
18
+ gitlab-mr-commenter = "gitlab_mr_commenter.__main__:main"
19
+
20
+ [tool.hatch.build.targets.wheel]
21
+ packages = ["gitlab_mr_commenter"]
22
+
23
+ [tool.semantic_release]
24
+ version_toml = ["pyproject.toml:project.version"]
25
+ branch = "main"
26
+ changelog_file = "CHANGELOG.md"
27
+ build_command = "uv build"
28
+ tag_format = "v{version}"
29
+
30
+ [tool.semantic_release.commit_parser_options]
31
+ # Conventional commits: feat → minor, fix/perf → patch, BREAKING CHANGE → major
32
+ allowed_tags = ["feat", "fix", "perf", "refactor", "docs", "chore", "ci"]
33
+ minor_tags = ["feat"]
34
+ patch_tags = ["fix", "perf"]