diff --git a/docs/labels-and-capabilities.md b/docs/labels-and-capabilities.md index 8c9ec82b8..a0bb2b9b7 100644 --- a/docs/labels-and-capabilities.md +++ b/docs/labels-and-capabilities.md @@ -332,7 +332,7 @@ or a contract-free mix of substrates (e.g. `tools/spec-inventory` is | [`tools/egress-gateway`](../tools/egress-gateway/) | `substrate:sandbox` | Egress-allowlist forward proxy (proxy.py plugin); host-level egress chokepoint — defence-in-depth for RFC-AI-0003 §4.4 | | [`tools/container-gateway`](../tools/container-gateway/) | `substrate:sandbox` | Per-project policy proxy for the podman / docker API; label-scoped, mount- and privilege-checked container access from inside the sandbox | | [`tools/forwarder-relay`](../tools/forwarder-relay/) | `contract:report-relay` | Adapter contract for inbound-relay backends (ASF Security relay, huntr.com, HackerOne triagers). Pure interface spec; adapters declare detection + credit-extraction + reporter-addressing rules. | -| [`tools/bitbucket`](../tools/bitbucket/) | `contract:change-request` + `contract:tracker` | Coverage: `partial`. Bitbucket Cloud and Bitbucket Data Center bridge foundation for repository metadata context, branch restriction context for PR-management decisions, pull-request discovery/fetching, read-only commit fetching, read-only diff fetching, comments-only discussion fetching, read-only review-state fetching, Cloud-only pull-request task listing/fetching, read-only merge-check context fetching, and read-only status fetching, plus narrowly scoped Cloud pull-request comment creation and approve/unapprove actions. No `contract:tracker` operations are currently implemented: Atlassian removed the native Bitbucket Cloud Issue Tracker API on 2026-08-20 and Data Center native issues were never supported, so the historical issue CLI surface fails closed before any request and points callers to the Jira bridge (`tools/jira`); the label stays for the linked Jira handoff tracked in #606. The `partial` qualifier means this tool implements named contract operations but does not satisfy the complete contract and must not be counted as a complete/selectable backend. Linked Jira tracker handoff and remaining pull-request coverage remain incomplete. | +| [`tools/bitbucket`](../tools/bitbucket/) | `contract:change-request` + `contract:tracker` | Coverage: `partial`. Bitbucket Cloud and Bitbucket Data Center bridge foundation for repository metadata context, branch restriction context for PR-management decisions, pull-request discovery/fetching, read-only commit fetching, read-only diff fetching, comments-only discussion fetching, read-only review-state fetching, Cloud-only pull-request task listing/fetching, read-only merge-check context fetching, and read-only status fetching, plus narrowly scoped Cloud pull-request comment creation and approve/unapprove actions. Native Bitbucket issue operations are not implemented: Atlassian removed the native Bitbucket Cloud Issue Tracker API on 2026-08-20 and Data Center native issues were never supported, so the historical issue CLI surface fails closed before any request. Data Center `pr jira-issues ` provides read-only discovery of Jira keys associated with pull-request commits and hands them to the Jira bridge (`tools/jira`) for tracker operations. The `partial` qualifier means this tool implements named contract operations but does not satisfy the complete contract and must not be counted as a complete/selectable backend. Broader linked Jira coverage and the remaining pull-request coverage are incomplete. | | [`tools/forgejo`](../tools/forgejo/) | `contract:tracker` + `contract:source-control` + `contract:change-request` | Coverage: `partial`. Forgejo / Gitea REST API and `tea` CLI forge bridge foundation for issue listing/fetching, confirmed issue creation, body edits, comments, labels, and milestones under `contract:tracker`, git-backed branch/commit/push operations under `contract:source-control`, and pull-request creation via REST API / compare URL and label edits under `contract:change-request`. Project boards are unsupported (`no-op`) due to absence of REST card/column endpoints. The `partial` qualifier means this tool implements named contract operations but does not satisfy the complete contract and must not be counted as a complete/selectable backend. | | [`tools/fossil`](../tools/fossil/) | `contract:tracker` + `contract:source-control` | Fossil SCM forge bridge: integrates local SQLite-backed ticket tracking, wiki, and forum reads with the version-control shim | | [`tools/github`](../tools/github/) | `contract:tracker` + `contract:source-control` + `contract:change-request` + `contract:people` | GitHub REST / GraphQL tracker substrate (called by every lifecycle phase) plus the Git source-control binding documented in [`source-control.md`](../tools/github/source-control.md) (runnable backend in [`tools/vcs`](../tools/vcs/)), the pull-request review/merge gate (`change-request`; the ASF default backend, alongside `tools/jira-patch/` and `tools/mail-patch/` for SVN-first projects), and account profiles / collaborators / team membership (`people`) | diff --git a/tools/bitbucket/README.md b/tools/bitbucket/README.md index a042255ca..c6dd01b01 100644 --- a/tools/bitbucket/README.md +++ b/tools/bitbucket/README.md @@ -45,11 +45,13 @@ contract and must not be advertised as complete/selectable backends. Repository metadata reads are currently bridge context for Bitbucket pull-request workflows, not a complete `contract:source-control` -backend. No `contract:tracker` operations are currently implemented: +backend. No native issue operations are implemented: native Bitbucket Cloud issue operations are unavailable after Atlassian's 2026-08-20 removal, and Data Center native issue operations are also unsupported; use the Jira bridge (`tools/jira`). -#606 remains open for linked Jira tracker coverage and the remaining +On Data Center, `pr jira-issues ` lists the Jira keys linked to a +pull request's commits as the handoff to that bridge. +#606 remains open for broader linked Jira coverage and the remaining Bitbucket workflow gaps, including broader repository permissions and fuller Pipelines run/log/retry coverage. @@ -72,6 +74,7 @@ Implemented commands and compatibility stubs: - `magpie-bitbucket pr list-open` - `magpie-bitbucket pr get ` - `magpie-bitbucket pr commits ` +- `magpie-bitbucket pr jira-issues ` (Data Center-only linked Jira discovery) - `magpie-bitbucket pr diff ` - `magpie-bitbucket pr discussion ` - `magpie-bitbucket pr comment --body-file ` (Cloud-only write) @@ -93,8 +96,7 @@ Implemented commands and compatibility stubs: Remaining candidate read-only gaps include: - broader repository permission context -- linked issue or Jira handoff context, if a repository exposes it through - supported APIs +- broader Jira handoff context beyond Data Center PR-linked issue discovery - deeper Pipelines/build run, log, and artifact read coverage The `pr reviews` command provides partial read-only review-state coverage, @@ -130,14 +132,15 @@ This implementation primarily covers read-only operations, plus narrowly scoped 5. **Pull-request listing:** list open pull requests as `contract:change-request` proposal summaries. 6. **Pull-request fetch:** fetch one pull request as a normalized proposal object. 7. **Pull-request commits fetch:** fetch commits associated with a pull request as normalized read-only output. -8. **Pull-request diff fetch:** fetch the pull request unified diff as normalized read-only output. -9. **Pull-request discussion fetch:** fetch a comments-only pull request discussion subset as normalized read-only output. -10. **Pull-request review-state fetch:** fetch reviewers, approvals, change-request signals, pending review requests, and normalized review activity. -11. **Cloud pull-request approve:** approve one pull request as the authenticated user after explicit caller-side confirmation. -12. **Cloud pull-request unapprove:** withdraw the authenticated user's approval after explicit caller-side confirmation. -13. **Pull-request tasks fetch:** list Bitbucket Cloud pull-request tasks and fetch one task as partial read-only change-request context. -14. **Pull-request merge-check context fetch:** fetch known read-only mergeability, conflict, status-check, and review blocker context while preserving unknown values where the backend does not expose a clear signal. -15. **Pull-request status fetch:** fetch build/status checks for the pull request as normalized read-only output. +8. **Data Center Jira handoff discovery:** fetch Jira issue keys associated with commits in a Bitbucket Data Center pull request and identify `tools/jira` as the tracker handoff. Bitbucket does not proxy Jira reads or writes. +9. **Pull-request diff fetch:** fetch the pull request unified diff as normalized read-only output. +10. **Pull-request discussion fetch:** fetch a comments-only pull request discussion subset as normalized read-only output. +11. **Pull-request review-state fetch:** fetch reviewers, approvals, change-request signals, pending review requests, and normalized review activity. +12. **Cloud pull-request approve:** approve one pull request as the authenticated user after explicit caller-side confirmation. +13. **Cloud pull-request unapprove:** withdraw the authenticated user's approval after explicit caller-side confirmation. +14. **Pull-request tasks fetch:** list Bitbucket Cloud pull-request tasks and fetch one task as partial read-only change-request context. +15. **Pull-request merge-check context fetch:** fetch known read-only mergeability, conflict, status-check, and review blocker context while preserving unknown values where the backend does not expose a clear signal. +16. **Pull-request status fetch:** fetch build/status checks for the pull request as normalized read-only output. The bridge supports two Bitbucket API flavours behind one command surface: @@ -154,6 +157,7 @@ surface: | Change requests | `list_open` / `pr list-open` | Supported read-only | Lists open pull requests with pagination. | | Change requests | `get` / `pr get ` | Partial read-only | Fetches PR metadata only. Commits, diff, discussion, review state, and status are fetched separately through dedicated read-only commands; mergeability remains incomplete. | | Change requests | `commits[]` supplement / `pr commits ` | Partial read-only | Fetches the commit list associated with a pull request so partial Bitbucket `get` coverage can expose proposal commits. This does not mutate branches, refs, or repository history. | +| Change requests / tracker handoff | `pr jira-issues ` | Partial read-only, Data Center only | Fetches Jira issue keys associated by Bitbucket Data Center with commits in the pull request. Output identifies `tools/jira` as the handoff tool; this command does not fetch or mutate Jira issues. Bitbucket Cloud has no equivalent bridge endpoint here and fails closed. | | Change requests | `diff` supplement / `pr diff ` | Partial read-only | Fetches the pull request unified diff so partial Bitbucket `get` coverage can expose proposal diffs. This does not mutate files, branches, refs, or repository history. | | Change requests | `get_discussion` / `pr discussion ` | Partial read-only | Fetches a comments-only discussion subset with pagination. Participants beyond comment authors and unresolved-thread accounting remain incomplete. | | Change requests | `pr comment --body-file ` | Partial write, Cloud only | Creates one top-level Bitbucket Cloud pull-request comment from a caller-supplied body file after explicit caller-side confirmation. Data Center PR comment writes remain unsupported in this command. | @@ -199,6 +203,10 @@ uv run --project tools/bitbucket magpie-bitbucket pr get 123 # Fetch pull request commits uv run --project tools/bitbucket magpie-bitbucket pr commits 123 +# Discover Jira issues associated with a Bitbucket Data Center pull request. +# Use tools/jira for subsequent tracker reads or confirmed writes. +uv run --project tools/bitbucket magpie-bitbucket pr jira-issues 123 + # Fetch pull request diff uv run --project tools/bitbucket magpie-bitbucket pr diff 123 @@ -321,8 +329,8 @@ then invoke the write command. Follow-up PRs can extend this bridge with: -- Linked Jira issue/tracker handoff through `tools/jira/`, replacing the - removed native Bitbucket Cloud Issue Tracker surface. +- Broader linked Jira handoff through `tools/jira/`; Data Center PR-linked + Jira issue discovery is implemented, while Jira remains the tracker backend. - Remaining pull-request review operations and broader merge coverage. - Broader repository permission reads. - Fuller Bitbucket Pipelines run/log/retry coverage beyond read-only pull-request status reads. diff --git a/tools/bitbucket/src/magpie_bitbucket/cli.py b/tools/bitbucket/src/magpie_bitbucket/cli.py index e3fc687ec..02ccae5a7 100644 --- a/tools/bitbucket/src/magpie_bitbucket/cli.py +++ b/tools/bitbucket/src/magpie_bitbucket/cli.py @@ -124,6 +124,15 @@ def _build_parser() -> argparse.ArgumentParser: pr_commits = pr_subparsers.add_parser("commits", help="Fetch pull request commits.") pr_commits.add_argument("pull_request_id", help="Pull request ID to fetch commits for.") + pr_jira_issues = pr_subparsers.add_parser( + "jira-issues", + help="Fetch Jira issues associated with a pull request.", + ) + pr_jira_issues.add_argument( + "pull_request_id", + help="Pull request ID whose linked Jira issues to fetch.", + ) + pr_diff = pr_subparsers.add_parser("diff", help="Fetch pull request diff.") pr_diff.add_argument("pull_request_id", help="Pull request ID to fetch diff for.") @@ -331,6 +340,16 @@ def _dispatch(args: argparse.Namespace, config: BitbucketConfig) -> dict[str, An raw = backend.get_pull_request_commits(config, args.pull_request_id) return normalize.pull_request_commits(config.kind, raw) + if args.subcommand == "pr" and args.pr_action == "jira-issues": + raw = backend.get_pull_request_jira_issues( + config, + args.pull_request_id, + ) + return normalize.pull_request_jira_issues( + config.kind, + raw, + ) + if args.subcommand == "pr" and args.pr_action == "diff": raw = backend.get_pull_request_diff(config, args.pull_request_id) return normalize.pull_request_diff(config.kind, raw) diff --git a/tools/bitbucket/src/magpie_bitbucket/cloud.py b/tools/bitbucket/src/magpie_bitbucket/cloud.py index 89e550389..99fcf14d0 100644 --- a/tools/bitbucket/src/magpie_bitbucket/cloud.py +++ b/tools/bitbucket/src/magpie_bitbucket/cloud.py @@ -182,6 +182,18 @@ def get_pull_request(config: BitbucketConfig, pull_request_id: str) -> dict[str, return get_json(url, config) +def get_pull_request_jira_issues( + config: BitbucketConfig, + pull_request_id: str, +) -> dict[str, Any]: + """Reject the Data Center-specific PR-to-Jira issue lookup on Cloud.""" + _ = (config, pull_request_id) + raise BitbucketError( + "PR-linked Jira issue lookup through Bitbucket is supported only " + "for Data Center; use the configured Jira tracker through tools/jira." + ) + + def get_pull_request_commits(config: BitbucketConfig, pull_request_id: str) -> dict[str, Any]: """Fetch commits from a Bitbucket Cloud pull request.""" workspace = quote_path(require(config.workspace, "BITBUCKET_WORKSPACE")) diff --git a/tools/bitbucket/src/magpie_bitbucket/datacenter.py b/tools/bitbucket/src/magpie_bitbucket/datacenter.py index 71ea1fbb8..b07273e9d 100644 --- a/tools/bitbucket/src/magpie_bitbucket/datacenter.py +++ b/tools/bitbucket/src/magpie_bitbucket/datacenter.py @@ -19,6 +19,7 @@ from __future__ import annotations +import json from typing import Any from magpie_bitbucket.client import BitbucketConfig, BitbucketError, get_json, get_text, quote_path, require @@ -30,6 +31,12 @@ def _api_base(config: BitbucketConfig) -> str: return f"{base_url}/rest/api/1.0" +def _jira_api_base(config: BitbucketConfig) -> str: + """Return the Bitbucket Data Center Jira-integration API base URL.""" + base_url = require(config.base_url, "BITBUCKET_BASE_URL").rstrip("/") + return f"{base_url}/rest/jira/latest" + + def _next_page_start(page: dict[str, Any], start: int) -> int | None: """Return the validated next page offset, or None when pagination must stop. @@ -176,6 +183,39 @@ def get_pull_request(config: BitbucketConfig, pull_request_id: str) -> dict[str, return get_json(url, config) +def get_pull_request_jira_issues( + config: BitbucketConfig, + pull_request_id: str, +) -> dict[str, Any]: + """Fetch Jira issues associated with commits in a Data Center pull request.""" + project_key = quote_path(require(config.project_key, "BITBUCKET_PROJECT_KEY")) + repo_slug = quote_path(require(config.repo_slug, "BITBUCKET_REPO_SLUG")) + pr_id = quote_path(pull_request_id) + + url = f"{_jira_api_base(config)}/projects/{project_key}/repos/{repo_slug}/pull-requests/{pr_id}/issues" + + # This endpoint answers with a JSON array, which get_json rejects (it + # requires an object), so the body is read as text and parsed here. + response = get_text( + url, + config, + accept="application/json", + ) + + try: + parsed = json.loads(response["body"]) + except json.JSONDecodeError as exc: + raise BitbucketError(f"Failed to parse Jira integration JSON response from {url}") from exc + + if not isinstance(parsed, list): + raise BitbucketError(f"Expected JSON array from Jira integration endpoint {url}") + + return { + "pull_request_id": pull_request_id, + "values": [item for item in parsed if isinstance(item, dict)], + } + + def get_pull_request_commits(config: BitbucketConfig, pull_request_id: str) -> dict[str, Any]: """Fetch commits from a Bitbucket Data Center pull request.""" project_key = quote_path(require(config.project_key, "BITBUCKET_PROJECT_KEY")) diff --git a/tools/bitbucket/src/magpie_bitbucket/normalize.py b/tools/bitbucket/src/magpie_bitbucket/normalize.py index ccebf1b45..792d88c53 100644 --- a/tools/bitbucket/src/magpie_bitbucket/normalize.py +++ b/tools/bitbucket/src/magpie_bitbucket/normalize.py @@ -245,6 +245,37 @@ def pull_request(kind: str, raw: dict[str, Any]) -> dict[str, Any]: return summary +def pull_request_jira_issues( + kind: str, + raw: dict[str, Any], +) -> dict[str, Any]: + """Normalize Jira issue keys associated with one pull request.""" + values = raw.get("values") + if not isinstance(values, list): + values = [] + + issue_keys: list[str] = [] + + for item in values: + if not isinstance(item, dict): + continue + + raw_key = _string(item.get("key") or item.get("issueKey")) + key = raw_key.strip().upper() if raw_key else None + + if key and key not in issue_keys: + issue_keys.append(key) + + return { + "backend": ("bitbucket-cloud" if kind == "cloud" else "bitbucket-datacenter"), + "coverage": "partial-read-only", + "pull_request_id": _string(raw.get("pull_request_id")), + "jira_issue_keys": issue_keys, + "handoff_tool": "tools/jira", + "raw": raw, + } + + def pull_request_list(kind: str, raw: dict[str, Any]) -> dict[str, Any]: """Normalize a Bitbucket pull-request list response.""" values = raw.get("values") diff --git a/tools/bitbucket/tests/test_bitbucket.py b/tools/bitbucket/tests/test_bitbucket.py index b55a21c0e..4529e6a6c 100644 --- a/tools/bitbucket/tests/test_bitbucket.py +++ b/tools/bitbucket/tests/test_bitbucket.py @@ -4720,3 +4720,195 @@ def test_cli_pr_reject_datacenter_fails_without_request( stderr = capsys.readouterr().err assert "reject writes are not supported" in stderr + + +@patch("magpie_bitbucket.client.urllib.request.build_opener") +def test_datacenter_get_pull_request_jira_issues( + mock_build_opener: MagicMock, + datacenter_env: None, +) -> None: + expected_url = ( + "https://bitbucket.example.test/rest/jira/latest/projects/MAGPIE/repos/magpie/pull-requests/7/issues" + ) + + response = make_mock_text_response( + json.dumps( + [ + {"key": "MAGPIE-101"}, + {"key": "MAGPIE-102"}, + ] + ), + content_type="application/json", + url=expected_url, + ) + + opener = MagicMock() + opener.open.return_value = MagicMock( + __enter__=MagicMock(return_value=response), + __exit__=MagicMock(return_value=None), + ) + mock_build_opener.return_value = opener + + result = datacenter.get_pull_request_jira_issues( + load_config(), + "7", + ) + + request = opener.open.call_args.args[0] + + assert request.get_method() == "GET" + assert request.full_url == expected_url + assert request.get_header("Accept") == "application/json" + + assert result == { + "pull_request_id": "7", + "values": [ + {"key": "MAGPIE-101"}, + {"key": "MAGPIE-102"}, + ], + } + + +def _jira_issues_with_body(mock_build_opener: MagicMock, body: str) -> dict[str, Any]: + response = make_mock_text_response(body, content_type="application/json") + opener = MagicMock() + opener.open.return_value = MagicMock( + __enter__=MagicMock(return_value=response), + __exit__=MagicMock(return_value=None), + ) + mock_build_opener.return_value = opener + return datacenter.get_pull_request_jira_issues(load_config(), "7") + + +@pytest.mark.parametrize( + ("body", "match"), + [ + ("Log in", "Failed to parse Jira integration JSON"), + ('{"key": "MAGPIE-101"}', "Expected JSON array"), + ], +) +@patch("magpie_bitbucket.client.urllib.request.build_opener") +def test_datacenter_get_pull_request_jira_issues_fails_closed_on_unexpected_body( + mock_build_opener: MagicMock, + datacenter_env: None, + body: str, + match: str, +) -> None: + with pytest.raises(BitbucketError, match=match): + _jira_issues_with_body(mock_build_opener, body) + + +@patch("magpie_bitbucket.client.urllib.request.build_opener") +def test_datacenter_get_pull_request_jira_issues_drops_non_object_items( + mock_build_opener: MagicMock, + datacenter_env: None, +) -> None: + result = _jira_issues_with_body(mock_build_opener, json.dumps([{"key": "MAGPIE-101"}, "junk", 3, None])) + + assert result["values"] == [{"key": "MAGPIE-101"}] + + +@patch("magpie_bitbucket.client.urllib.request.build_opener") +def test_cloud_get_pull_request_jira_issues_fails_without_request( + mock_build_opener: MagicMock, + cloud_env: None, +) -> None: + with pytest.raises( + BitbucketError, + match="supported only for Data Center", + ): + cloud.get_pull_request_jira_issues( + load_config(), + "7", + ) + + mock_build_opener.assert_not_called() + + +def test_normalize_pull_request_jira_issues() -> None: + from magpie_bitbucket.normalize import pull_request_jira_issues + + normalized = pull_request_jira_issues( + "datacenter", + { + "pull_request_id": "7", + "values": [ + {"key": " magpie-101 "}, + {"issueKey": "MAGPIE-102"}, + {"key": "MAGPIE-101"}, + {"issueKey": " magpie-102 "}, + {}, + ], + }, + ) + + assert normalized["backend"] == "bitbucket-datacenter" + assert normalized["coverage"] == "partial-read-only" + assert normalized["pull_request_id"] == "7" + assert normalized["jira_issue_keys"] == [ + "MAGPIE-101", + "MAGPIE-102", + ] + assert normalized["handoff_tool"] == "tools/jira" + + +@patch("magpie_bitbucket.datacenter.get_pull_request_jira_issues") +def test_cli_pr_jira_issues_datacenter( + mock_get_jira_issues: MagicMock, + datacenter_env: None, + capsys: pytest.CaptureFixture[str], +) -> None: + mock_get_jira_issues.return_value = { + "pull_request_id": "7", + "values": [ + {"key": "MAGPIE-101"}, + {"key": "MAGPIE-102"}, + ], + } + + exit_code = main( + [ + "pr", + "jira-issues", + "7", + ] + ) + + assert exit_code == 0 + + mock_get_jira_issues.assert_called_once_with( + load_config(), + "7", + ) + + output = json.loads(capsys.readouterr().out) + + assert output["backend"] == "bitbucket-datacenter" + assert output["pull_request_id"] == "7" + assert output["jira_issue_keys"] == [ + "MAGPIE-101", + "MAGPIE-102", + ] + assert output["handoff_tool"] == "tools/jira" + + +@patch("magpie_bitbucket.client.urllib.request.build_opener") +def test_cli_pr_jira_issues_cloud_fails_without_request( + mock_build_opener: MagicMock, + cloud_env: None, + capsys: pytest.CaptureFixture[str], +) -> None: + exit_code = package_main( + [ + "pr", + "jira-issues", + "7", + ] + ) + + assert exit_code == 1 + mock_build_opener.assert_not_called() + + stderr = capsys.readouterr().err + assert "supported only for Data Center" in stderr + assert "tools/jira" in stderr diff --git a/tools/spec-loop/specs/adapters.md b/tools/spec-loop/specs/adapters.md index 94a8b3d5f..ecf63dc84 100644 --- a/tools/spec-loop/specs/adapters.md +++ b/tools/spec-loop/specs/adapters.md @@ -166,8 +166,11 @@ by swapping the adapter, not the skill. Atlassian fully removed the native Bitbucket Cloud Issue Tracker API on 2026-08-20, so the historical `issue` CLI commands now fail closed before any request and direct callers toward linked Jira coverage. + Data Center `pr jira-issues ` provides read-only Jira-key discovery for + issues associated with pull-request commits and hands those keys to + `tools/jira`; Bitbucket does not proxy Jira tracker operations. It is not a complete `contract:change-request` or `contract:tracker` - backend yet; deeper Jira handoff, Data Center + backend yet; broader Jira handoff, Data Center review/merge writes, broader repository permissions, and fuller Pipelines run/log/retry coverage remain tracked in #606. - `tools/gitlab/` (`magpie-gitlab`) — partial, read-only GitLab REST API v4