Repository navigation
feat(tools/gitlab): add read-only GitLab bridge foundation (Part of #305) - #1369
Conversation
2753b66 to
6f4b548
Compare
|
Hi @potiuk, I've implemented the GitLab bridge (
Whenever you have time, I'd really appreciate your initial thoughts or guidance on whether this direction looks good or if anything needs adjusting. Thank you! |
Resolves apache#305 Phase 1: Python CLI package skeleton, workspace integration, and read-only API client.
Resolves apache#305 Component 2: 100% offline deterministic test coverage for client, issues, MRs, pipelines, and CLI using pytest and mock.
Resolves apache#305 Component 3: Detailed operations catalogue, prerequisites, usage guide, and issue template schemas for GitLab adapter.
Resolves apache#305 Component 4: Finalize the adapter integration by adding it to the official capability taxonomy map and replacing its tracked extension point references with its shipped status in the registries.
Also fixes gitlab README prerequisites formatting to satisfy validator strict mode
- Add GitLab API client, CLI, issues, merge requests, and pipeline inspection - Add 100% offline unit tests with deterministic HTTP mock responses - Add Prerequisites, Configuration, Kind, and Vendor declarations in README.md - Register GitLab capabilities in docs/vendor-neutrality.md
- Use path.as_posix() for cross-platform forward-slash skip path matching - Add UTF-8 encoding and replace error handling in _git_show subprocess calls - Support Git-on-Windows pointer files in skill discovery and capability resolution - Gracefully skip directory symlink creation test when lacking OS privileges (WinError 1314)
- Reduce combined description and when_to_use length to 1489 characters to comply with the 1536 character truncation limit
ddc6f85 to
9339331
Compare
…tion, and diff overflow
|
Done with the GitLab integration. |
onlyarnav
left a comment
There was a problem hiding this comment.
Thank you for putting this together! Adding first-class GitLab support to Magpie (#305) is a great step forward for forge neutrality. The stdlib-only implementation (urllib.request), zero runtime dependencies, offline unit test suite with mock responses, and safe-redirect credential protection (_SafeRedirectHandler) are very well designed.
However, there are several blocking architectural and functional issues that need to be resolved before this can be merged:
1. Overstated Capabilities & Artificial Vendor-Neutrality Inflation [Blocking]
-
Files:
-
Problem:
tools/gitlab/README.mddeclares:**Capability:** contract:tracker + contract:source-control + contract:change-request **Kind:** implementation **Vendor:** GitLab
This causes
vendor-neutrality-scoreto count GitLab as a complete, selectable backend vendor for all three contracts indocs/vendor-neutrality.md.
However,magpie-gitlabcurrently implements only 5 read-only GET operations:contract:tracker: Onlyissue listandissue get(no issue creation, commenting, labeling, assignment, or closing).contract:source-control: Onlyrepo get/ project metadata (no branch listing, commit fetching, diffs, or VCS operations).contract:change-request: Onlymr list,mr get,mr diff,mr commits, and pipeline status (no MR creation, review, commenting, approval, or merging).
Furthermore, the PR description states:
"HITL — All state-mutating actions (commenting, labelling, approving, merging) are gated on explicit user confirmation."
In reality, none of these state-mutating actions exist in
tools/gitlab/src/magpie_gitlab(there are zeroPOST,PUT,PATCH, orDELETErequests in the package). -
Required Fix:
Follow the standard established bytools/bitbucket:- Add
**Coverage:** partialtotools/gitlab/README.md. - In
docs/labels-and-capabilities.md, mark it asCoverage: partialfoundation that must not be counted as a complete/selectable backend until write operations (review, comments, merge, issue mutation) are implemented. - Revert the complete-backend vendor additions in
docs/vendor-neutrality.md(or list as partial foundation).
- Add
2. Mandatory Token Requirement Breaks Unauthenticated Public Project Reads [Blocking]
- File:
tools/gitlab/src/magpie_gitlab/client.py - Problem:
Line 118 unconditionally enforces:Becausetoken = require(config.token, "GITLAB_TOKEN or CI_JOB_TOKEN")
get_json()andget_paged_json()always call_auth_headers(), any attempt to query public projects, public issues, or public MRs ongitlab.comor self-hosted instances (e.g.salsa.debian.org, GNOME GitLab) without an environment token immediately fails with:
GitLabError: GITLAB_TOKEN or CI_JOB_TOKEN is required. - Required Fix:
GitLab REST API v4 allows unauthenticated reads on public projects. Allowconfig.tokento be optional for read operations:def _auth_headers(config: GitLabConfig) -> dict[str, str]: headers: dict[str, str] = {"Accept": "application/json"} if not config.token: return headers if config.token_type == "job_token": headers["JOB-TOKEN"] = config.token else: headers["Authorization"] = f"Bearer {config.token}" return headers
3. URL Scheme Validation Logic Flaw on Localhost [Security / Bug]
- File:
tools/gitlab/src/magpie_gitlab/client.py - Problem:
Because the condition is
parsed = urllib.parse.urlparse(url) if parsed.scheme != "https" and parsed.hostname not in ( "localhost", "127.0.0.1", "::1", ): raise GitLabError(f"Insecure instance URL scheme '{parsed.scheme}': HTTPS is required")
parsed.scheme != "https" and parsed.hostname not in (...), wheneverparsed.hostnameis"localhost", any scheme (e.g.ftp://localhost,file://localhost) evaluates toTrue and False == Falseand bypasses validation without error. - Required Fix:
Explicitly allow only HTTPS generally, and HTTP strictly for localhost:if parsed.scheme == "https": return if parsed.scheme == "http" and parsed.hostname in ("localhost", "127.0.0.1", "::1"): return raise GitLabError(f"Insecure instance URL scheme '{parsed.scheme}': HTTPS is required")
4. list_mr_pipelines Implemented but Unexposed in CLI
- Files:
- Problem:
pipelines.pyimplementslist_mr_pipelines(project, mr_iid, config), butcli.pyhas no subcommand for it (onlypipeline statusis registered). - Required Fix:
Add a command incli.pyundermr(e.g.,magpie-gitlab mr pipelines <project> <mr_iid>) to exposelist_mr_pipelines.
5. Unbounded Pagination in get_paged_json
- File:
tools/gitlab/src/magpie_gitlab/client.py - Problem:
get_paged_json()followsX-Next-Pageindefinitely. On repositories with thousands of issues or merge requests,magpie-gitlab issue list <project>will execute dozens or hundreds of sequential HTTP calls and buffer all records in memory. - Suggestion:
Add an optional--limitormax_pagesparameter to avoid unbounded fetches in automated agent loops.
6. Support Canonical GitLab PRIVATE-TOKEN Header
- File:
tools/gitlab/src/magpie_gitlab/client.py - Problem:
Personal, Project, and Group Access Tokens (glpat-...) are canonically authenticated in GitLab REST API v4 using thePRIVATE-TOKEN: <token>header. WhileAuthorization: Beareris supported on gitlab.com, older self-hosted instances (e.g. Debian Salsa) or enterprise reverse proxies may reject Bearer headers for PATs. - Suggestion:
UsePRIVATE-TOKEN: <token>whentoken.startswith("glpat-")or support it as the standard header for non-job tokens.
7. Bundled Unrelated Changes
- Files:
plugins/magpie-release-management/skills/prepare/SKILL.md(unrelated description edits)docs/mode-economics.md(generated token count churn fromprepare/SKILL.md)tools/gitlab/uv.lock(redundant nested lockfile; Magpie uses a single workspace rootuv.lock)tools/skill-and-tool-validator/src/skill_and_tool_validator/__init__.py(Windows symlink pointer resolution)
- Suggestion:
- Delete
tools/gitlab/uv.lock(the workspace rootuv.lockalready includesmagpie-gitlab). - Revert the unrelated changes to
prepare/SKILL.mdanddocs/mode-economics.md. - Isolate the cross-platform Windows validator fixes into a separate, dedicated PR.
- Delete
…direct, and pagination
|
Thanks for the detailed review @onlyarnav! I have addressed all requested changes across the 7 points:
Ready for another look! |
potiuk
left a comment
There was a problem hiding this comment.
Thanks for the quick turnaround. Points 2, 3, 6 and 7 from @onlyarnav's review are fixed and I checked them against the diff. Point 1, the blocking one, is not: the reply says Coverage: partial was added and the docs switched to the partial-foundation form, but no file in the current head (5a00631) carries that marker. GitLab is still presented as a complete backend in four places. There are also a few code-level issues, detailed below and inline.
Blocking — GitLab is still counted as a complete backend
The
partialqualifier means this tool implements named contract operations but does not satisfy the complete contract and must not be counted as a complete/selectable backend.
—docs/labels-and-capabilities.md,tools/bitbucketrow
The adapter has seven read-only GETs and no writes. Still:
tools/gitlab/README.mdhas no**Coverage:** partialline (comparetools/bitbucket/README.md).- The
docs/labels-and-capabilities.mdrow describes a full forge bridge with noCoverage: partialqualifier. docs/adapters/registry.mdlistsgitlabwithout thepartial-read-onlymarker Bitbucket carries, and drops GitLab #305 from the open-extension column.docs/vendor-neutrality.mdputs GitLab in "Backends today" as a complete backend, removes it from the open extension points, and the regenerated contract table counts it as a fifth / sixth / fourth vendor.
Please mirror the Bitbucket precedent: add the Coverage line to the README, add the partial qualifier to the capabilities row and the registry, keep GitLab #305 listed as open coverage work, and list GitLab in vendor-neutrality.md as a partial foundation excluded from complete-backend counts (the same wording used for Bitbucket). For the same reason, the PR should say Part of #305 rather than Closes #305.
Unvalidated path and query parameters
issue_iid, mr_iid and pipeline_id are interpolated into the URL path as-is, and --state goes into the query string without encoding. An ID taken from issue or MR text (which is external content an agent reads) can walk to another API endpoint with the caller's token, e.g. mr get grp/proj '1/../../../../user'. A state like opened&per_page=1 injects parameters. The fix is small: type=int on the ID positionals, choices= on --state (opened/closed/all, plus merged/locked for MRs), and urllib.parse.urlencode to build the query.
mr diff uses the deprecated /changes endpoint
GET /projects/:id/merge_requests/:iid/changes has been deprecated since GitLab 15.7 and is slated for removal in API v5. Its replacement, GET .../merge_requests/:iid/diffs, is paginated. Moving to it also fixes the current behaviour of failing outright on large MRs (overflow: true): the paginated endpoint returns the whole diff instead.
Silent truncation in get_paged_json
DEFAULT_MAX_PAGES = 10 caps every list at 1,000 items, but the result gives no sign that anything was cut. An agent that runs issue list on a large project gets back a partial list and treats it as complete. get_mr_diff already raises on overflow; pagination should be just as explicit (raise, or return a truncation marker the CLI prints to stderr). Also, --limit slices the result after all pages have been fetched, so it doesn't reduce the number of requests. Pass it through as max_pages (ceil(limit / 100)). Separately, if a later page returns a non-list, return [data] throws away every item collected so far.
Smaller observations
client.py,_SafeRedirectHandler: the origin check compareshostnameonly, so a same-host redirect to a different port (gitlab.example.com:8443) keeps the token. Compare(scheme, hostname, port).client.py,_auth_headers: theglpat-prefix check wins overGITLAB_AUTH_SCHEME, soGITLAB_AUTH_SCHEME=bearercan't force Bearer for a PAT, and unknown values are silently ignored.token_type="bearer"is also recorded for tokens that end up sent asPRIVATE-TOKEN. Suggest letting an explicit scheme win and rejecting unknown values.client.py:require()is no longer used outside the tests since the token became optional; drop it.hasattr(response, "headers")is always true.cli.py: the subparsers aren'trequired=True, somagpie-gitlab issueprints the top-level help instead of theissuehelp.- Tests:
conftest.pypatchesurllib.request.build_openerglobally, so nothing checks that_build_opener()actually installs_SafeRedirectHandler. A regression to plainurlopenwould still pass. Add one un-patched assertion on the opener's handlers.test_get_paged_json_multi_pageuses a URL with no query string, so rebuilding the page-2 URL withstate=…&per_page=100&page=2is never checked.- Several tests set
GITLAB_TOKENwithout clearingCI_JOB_TOKEN/GITLAB_AUTH_SCHEME, so they depend on the developer's environment.
tool.md: the reply says it was updated formr pipelines, but the catalogue still lists only five operations.repo get,mr commitsandmr pipelinesare missing.README.md: the prerequisites still say a token is required. It is now optional for public projects.GITLAB_AUTH_SCHEMEis not documented.- The PR description is out of date. It still describes HITL-gated "commenting, labelling, approving, merging" (the adapter has no writes) and Windows validator fixes (reverted in this push).
This review was drafted by an AI-assisted tool and
confirmed by an Apache Magpie maintainer. After you've
addressed the points above and pushed an update, an Apache Magpie
maintainer — a real person — will take the next look
at the PR. The findings cite the project's review criteria;
if you think one of them is mis-applied, please reply on the
PR and a maintainer will weigh in.More on how Apache Magpie handles maintainer review:
CONTRIBUTING.md.
# Conflicts: # docs/vendor-neutrality.md
…n, auth, and input validation
|
Thanks @potiuk for the detailed review and guidance. All points from your review have been addressed in the latest push (1efbfea):
|
potiuk
left a comment
There was a problem hiding this comment.
Most of the last round is fixed, including the blocking point: GitLab is now presented as a partial-read-only foundation on every surface. Three things still need to land before merge: a documented auth-scheme value the code rejects, one remaining silent-truncation path, and a rebase to regenerate .github/labeler.yml.
I checked the previous points against 1efbfea rather than the reply:
- Fixed: the Coverage / capabilities / registry / vendor-neutrality wording;
type=intandchoices=withurlencode;/diffs; non-list later pages; the(scheme, host, port)redirect check; explicit-scheme precedence;required=True; the un-patched opener test; the page-2 query-string test; and thetool.md/ README additions. All seven ofonlyarnav's points are fixed as well. - Still open, although the reply lists them as done:
require()and thehasattrcheck,token_type, and the shared test fixture (inline).
GITLAB_AUTH_SCHEME=JobToken is rejected (tools/gitlab/README.md:45)
The README lists PrivateToken, Bearer, JobToken. _auth_headers lowercases the value and accepts private-token/privatetoken, bearer, and job-token/job_token, so "JobToken".lower() → jobtoken raises Unsupported GITLAB_AUTH_SCHEME: 'JobToken'. The tests use Job-Token, so nothing catches it.
elif scheme in ("job-token", "job_token"): # ← "jobtoken" (README spelling) falls through to the raise
headers["JOB-TOKEN"] = config.token
else:
raise GitLabError(f"Unsupported GITLAB_AUTH_SCHEME: '{config.auth_scheme}'")Please accept jobtoken (client.py:166) and add a test for the README spelling, or change the README to the spellings the code accepts.
--limit above 1000 is still silently truncated (client.py:249)
With --limit 2000, line 220 clamps target_pages to min(20, DEFAULT_MAX_PAGES) = 10, so the loop stops at 1000 items. The notice only prints when limit is None, so the caller gets half of what they asked for and nothing on stderr. The default-cap notice also says "use --limit to fetch more", but --limit can never exceed that cap.
if target_pages is not None and pages_fetched >= target_pages:
if has_more and limit is None: # ← an explicit --limit that hit max_pages stays silentEither derive target_pages from limit alone when it is given, or print the notice whenever has_more is true and fewer than limit items came back. Please also reword the default notice so it names a remedy that works, and add a limit > 1000 test.
Rebase needed: .github/labeler.yml is generated from tool READMEs
.github/labeler.yml landed on main after this branch was cut; it is generated from every tools/<name>/README.md **Capability:** line. It is missing from the branch, which is why PR CI is green. Merging this branch into main and running tools/dev/generate-labeler-config.py --check fails, so main's whole-tree prek would go red after merge.
Before opening or updating a PR, run
prek run --all-files… the PR's CI run is scoped to the PR's diff, so a file the branch did not touch but broke anyway … goes green on the PR and fails on themainbuild after merge.
—AGENTS.md
Please rebase onto current main, run tools/dev/generate-labeler-config.py (or prek run --all-files), and commit the regenerated file.
Smaller observations
Inline: require() / hasattr (client.py:144), token_type (client.py:121), the mock_env fixture (conftest.py:63), the user.md aside (README.md:52), mr list missing from tool.md, and the README intro wording.
This review was drafted by an AI-assisted tool and
confirmed by an Apache Magpie maintainer. After you've
addressed the points above and pushed an update, an Apache Magpie
maintainer — a real person — will take the next look
at the PR. The findings cite the project's review criteria;
if you think one of them is mis-applied, please reply on the
PR and a maintainer will weigh in.More on how Apache Magpie handles maintainer review:
CONTRIBUTING.md.
…on, fixtures, and docs
…ble robust against skill iteration order
|
Thanks for the review, @potiuk. Here are the updates pushed in response to the feedback:
Ready for another look! |
…r-1369 # Conflicts: # tools/dev/tests/test_check_duplication.py
A negative --limit sliced items off the end of the result silently (items[:-5]), and 0 made a request only to return nothing. Generated-by: Claude Code (Opus 5.5)
potiuk
left a comment
There was a problem hiding this comment.
Everything from the last round is fixed and verified against 3bcfa26 — JobToken accepted, --limit above 1000 fetches the pages it needs, labeler.yml regenerated, require/hasattr gone, token_type matches the header, mock_env clears the env, and the docs are cleaned up. I've resolved all my threads, and onlyarnav's earlier points were confirmed fixed in the previous round.
I pushed two small commits on top so this can land now: a merge of main that drops the unrelated tools/dev/tests/test_check_duplication.py change (that test was fixed on main in #1428), and a fixup that makes every --limit reject values below 1 — a negative value used to slice items off the end silently — with a test.
Smaller observations (follow-up, not blocking)
client.py— with an explicitlimit, a short page that leaves fewer thanlimititems whileX-Next-Pageis still set breaks the loop with no notice. Largely theoretical since GitLab fills pages;test_get_paged_json_with_limit_fewer_than_page's docstring also says "emits notice" while the test assertserr == "".tools/gitlab/issue-template.mdputs the doctoc TOC above the SPDX header and ends with "Ensure that the adapter correctly parses these specific constructs" — the adapter returns raw JSON. Worth rewording as descriptive reference.
This review was drafted by an AI-assisted tool and
confirmed by an Apache Magpie maintainer. The maintainer
approving this PR has read the findings and signed off. If
something feels off, please reply on the PR and a maintainer
will follow up.More on how Apache Magpie handles maintainer review:
CONTRIBUTING.md.
…r-1369 # Conflicts: # docs/adapters/registry.md
|
Thanks a lot for the thorough review, guidance, and merging this in, @potiuk! I've noted the two follow-up observations on |
Summary
tools/gitlab/) as apartial-read-onlyfoundation for Apache Magpie (Part of feat(tools/gitlab): add tracker + forge bridge for GitLab-hosted projects #305).magpie-gitlab) supporting read-only operations acrosscontract:tracker(issue list/get),contract:source-control(repo metadata context), andcontract:change-request(MR get/list/diffs/commits/pipelines, pipeline status).type=intfor issue/MR/pipeline IDs) and state query parameters (urllib.parse.urlencodewith strict choices) to prevent injection and path traversal./diffsendpoint for full diff retrieval without overflow limits, safe origin-preserving redirect handling (_SafeRedirectHandler), and bounded pagination with advisory output on truncation.GITLAB_AUTH_SCHEMEconfiguration (PrivateToken,Bearer,JobToken), and self-hosted instances viaGITLAB_INSTANCE_URL.tools/gitlab/README.md(**Coverage:** partial),docs/adapters/registry.md,docs/labels-and-capabilities.md, anddocs/vendor-neutrality.md(excluded from complete backend counts per Bitbucket parity and PR fix(vendor-neutrality-score): never count partial-coverage tools as backends #1381).Type of change
tools/<system>/*.md)tools/*/withpyproject.toml)docs/,README.md)Test plan
uv run --directory tools/gitlab pytest -v(56/56 unit tests passing in ~0.35s covering client auth, redirects, pagination, diffs, issues, pipelines, and CLI validation)uv run --directory tools/gitlab mypy(Clean pass: 0 issues in 13 source files)uv run --directory tools/gitlab ruff check(Clean pass: 0 lint errors)uv run --directory tools/gitlab ruff format --check(Clean pass: 16 files formatted)uv run --project tools/vendor-neutrality-score vendor-neutrality-score(docs/vendor-neutrality.mdtable verified in sync with partial foundation status)RFC-AI-0004 compliance
urllibonly); network access explicitly declared forgitlab.comand configured self-hosted instances.**Coverage:** partial; full issue/MR mutations and write operations remain open under feat(tools/gitlab): add tracker + forge bridge for GitLab-hosted projects #305.GITLAB_INSTANCE_URL.(scheme, hostname, port)origin equivalence before forwarding auth headers.Linked issues
Part of #305