From 79008fae39531ab79dfbc1734a9122c5c729050b Mon Sep 17 00:00:00 2001 From: Kavya Katal Date: Sat, 17 Oct 2026 17:58:54 +0530 Subject: [PATCH 1/3] feat(bitbucket): add Data Center Jira issue handoff --- docs/labels-and-capabilities.md | 2 +- tools/bitbucket/README.md | 30 ++-- tools/bitbucket/src/magpie_bitbucket/cli.py | 19 +++ tools/bitbucket/src/magpie_bitbucket/cloud.py | 12 ++ .../src/magpie_bitbucket/datacenter.py | 38 +++++ .../src/magpie_bitbucket/normalize.py | 30 ++++ tools/bitbucket/tests/test_bitbucket.py | 152 ++++++++++++++++++ tools/spec-loop/specs/adapters.md | 5 +- 8 files changed, 274 insertions(+), 14 deletions(-) diff --git a/docs/labels-and-capabilities.md b/docs/labels-and-capabilities.md index 59d68c26e..21b9a9a74 100644 --- a/docs/labels-and-capabilities.md +++ b/docs/labels-and-capabilities.md @@ -331,7 +331,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 ` now 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. Linked Jira tracker handoff and remaining pull-request coverage remain 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..4e887798f 100644 --- a/tools/bitbucket/README.md +++ b/tools/bitbucket/README.md @@ -72,6 +72,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 +94,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 +130,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 +155,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 +201,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 +327,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..42eb92177 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,37 @@ 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" + + 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..89b3d6bf5 100644 --- a/tools/bitbucket/src/magpie_bitbucket/normalize.py +++ b/tools/bitbucket/src/magpie_bitbucket/normalize.py @@ -245,6 +245,36 @@ 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 + + key = _string(item.get("key") or item.get("issueKey")) + + 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..ed269190f 100644 --- a/tools/bitbucket/tests/test_bitbucket.py +++ b/tools/bitbucket/tests/test_bitbucket.py @@ -4720,3 +4720,155 @@ 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"}, + ], + } + + +@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"}, + {}, + ], + }, + ) + + 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 7be4e86ff..67ef2bf3f 100644 --- a/tools/spec-loop/specs/adapters.md +++ b/tools/spec-loop/specs/adapters.md @@ -116,8 +116,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 From aa9fac3446b566af5b278efe144a5a947f3419fb Mon Sep 17 00:00:00 2001 From: Kavya Katal Date: Mon, 19 Oct 2026 17:22:23 +0530 Subject: [PATCH 2/3] fix(bitbucket): canonicalize linked Jira issue keys --- tools/bitbucket/src/magpie_bitbucket/normalize.py | 3 ++- tools/bitbucket/tests/test_bitbucket.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tools/bitbucket/src/magpie_bitbucket/normalize.py b/tools/bitbucket/src/magpie_bitbucket/normalize.py index 89b3d6bf5..792d88c53 100644 --- a/tools/bitbucket/src/magpie_bitbucket/normalize.py +++ b/tools/bitbucket/src/magpie_bitbucket/normalize.py @@ -260,7 +260,8 @@ def pull_request_jira_issues( if not isinstance(item, dict): continue - key = _string(item.get("key") or item.get("issueKey")) + 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) diff --git a/tools/bitbucket/tests/test_bitbucket.py b/tools/bitbucket/tests/test_bitbucket.py index ed269190f..0411beebf 100644 --- a/tools/bitbucket/tests/test_bitbucket.py +++ b/tools/bitbucket/tests/test_bitbucket.py @@ -4794,9 +4794,10 @@ def test_normalize_pull_request_jira_issues() -> None: { "pull_request_id": "7", "values": [ - {"key": "MAGPIE-101"}, + {"key": " magpie-101 "}, {"issueKey": "MAGPIE-102"}, {"key": "MAGPIE-101"}, + {"issueKey": " magpie-102 "}, {}, ], }, From 0d960f9b52e259a442261d435f28417aacd0a1a5 Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Mon, 19 Oct 2026 21:35:10 +0200 Subject: [PATCH 3/3] fix(bitbucket): test the jira-issues fail-closed paths and settle its docs The two fail-closed branches of `pr jira-issues` (a body that is not JSON, such as a login page served with a 200, and a JSON object where an array is expected) and the filtering of non-object items had no tests; they do now. A comment says why this one JSON read goes through get_text: the endpoint returns an array, which get_json rejects. The labels-doc row no longer says the handoff exists and then that it "remains incomplete", and the README intro now names the Data Center handoff instead of saying no tracker operation exists. Generated-by: Claude Opus 5 --- docs/labels-and-capabilities.md | 2 +- tools/bitbucket/README.md | 6 ++- .../src/magpie_bitbucket/datacenter.py | 2 + tools/bitbucket/tests/test_bitbucket.py | 39 +++++++++++++++++++ 4 files changed, 46 insertions(+), 3 deletions(-) diff --git a/docs/labels-and-capabilities.md b/docs/labels-and-capabilities.md index d0eb3e921..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. 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 ` now 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. 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 4e887798f..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. diff --git a/tools/bitbucket/src/magpie_bitbucket/datacenter.py b/tools/bitbucket/src/magpie_bitbucket/datacenter.py index 42eb92177..b07273e9d 100644 --- a/tools/bitbucket/src/magpie_bitbucket/datacenter.py +++ b/tools/bitbucket/src/magpie_bitbucket/datacenter.py @@ -194,6 +194,8 @@ def get_pull_request_jira_issues( 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, diff --git a/tools/bitbucket/tests/test_bitbucket.py b/tools/bitbucket/tests/test_bitbucket.py index 0411beebf..4529e6a6c 100644 --- a/tools/bitbucket/tests/test_bitbucket.py +++ b/tools/bitbucket/tests/test_bitbucket.py @@ -4769,6 +4769,45 @@ def test_datacenter_get_pull_request_jira_issues( } +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,