Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/labels-and-capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <id>` 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`) |
Expand Down
36 changes: 22 additions & 14 deletions tools/bitbucket/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <id>` 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.

Expand All @@ -72,6 +74,7 @@ Implemented commands and compatibility stubs:
- `magpie-bitbucket pr list-open`
- `magpie-bitbucket pr get <id>`
- `magpie-bitbucket pr commits <id>`
- `magpie-bitbucket pr jira-issues <id>` (Data Center-only linked Jira discovery)
- `magpie-bitbucket pr diff <id>`
- `magpie-bitbucket pr discussion <id>`
- `magpie-bitbucket pr comment <id> --body-file <path>` (Cloud-only write)
Expand All @@ -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,
Expand Down Expand Up @@ -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:
Expand All @@ -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 <id>` | 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 <id>` | 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 <id>` | 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 <id>` | 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 <id>` | 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 <id> --body-file <path>` | 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. |
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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.
19 changes: 19 additions & 0 deletions tools/bitbucket/src/magpie_bitbucket/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.")

Expand Down Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions tools/bitbucket/src/magpie_bitbucket/cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
40 changes: 40 additions & 0 deletions tools/bitbucket/src/magpie_bitbucket/datacenter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

Expand Down Expand Up @@ -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"))
Expand Down
31 changes: 31 additions & 0 deletions tools/bitbucket/src/magpie_bitbucket/normalize.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading