Skip to content
Open
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 @@ -273,7 +273,7 @@ it implements multiple contracts (e.g. `tools/gmail` provides both
| [`tools/dev`](../tools/dev/) | `substrate:framework-dev` | Framework dev-loop helpers |
| [`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/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` | Coverage: `partial-read-only`. 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, read-only merge-check context fetching, and read-only status fetching. The `partial-read-only` qualifier means this tool implements named read-only contract operations but does not satisfy the complete contract and must not be counted as a complete/selectable backend. `contract:tracker` remains absent until Bitbucket issue operations or linked Jira handoff coverage exist. |
| [`tools/bitbucket`](../tools/bitbucket/) | `contract:change-request` + `contract:tracker` | Coverage: `partial-read-only`. 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, read-only merge-check context fetching, and read-only status fetching, and Cloud-only issue listing/fetching. The `partial-read-only` qualifier means this tool implements named read-only contract operations but does not satisfy the complete contract and must not be counted as a complete/selectable backend. `contract:tracker` coverage is partial and limited to Cloud issue reads until Bitbucket issue writes or linked Jira handoff coverage exist. |
| [`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` | 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/)) and the pull-request review/merge gate (`change-request`; the ASF default backend, alongside `tools/jira-patch/` and `tools/mail-patch/` for SVN-first projects) |
| [`tools/github-body-field`](../tools/github-body-field/) | `contract:tracker` | Read or rewrite one `### Field` section of a GitHub issue body without bringing the body into agent context — substrate helper for the security-sync skills |
Expand Down
46 changes: 28 additions & 18 deletions tools/bitbucket/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

# Bitbucket forge bridge

**Capability:** contract:change-request
**Capability:** contract:change-request + contract:tracker

**Coverage:** `partial-read-only`

Expand All @@ -36,17 +36,18 @@ that use Bitbucket as a forge, pull-request review surface, or Jira-paired Atlas

This initial bridge implements a `partial-read-only` profile for
repository metadata context and pull-request discovery/fetching under
`contract:change-request`. Partial adapters may implement named
`contract:change-request`, with partial Cloud-only issue reads under
`contract:tracker`. Partial adapters may implement named
contract verbs, but they do not satisfy the complete 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. `contract:tracker` is also intentionally absent until
Bitbucket issue operations or linked Jira handoff coverage exist.
backend. `contract:tracker` coverage is partial and currently limited to
Bitbucket Cloud issue listing/fetching where the repository issue tracker is enabled.
#606 remains open for the remaining Bitbucket/Jira workflow coverage.
Later PRs can extend the same adapter with write operations,
Bitbucket Issues, linked Jira handoff, branch permissions, and
linked Jira handoff, issue write operations, and
fuller Pipelines run/log/retry coverage.

## Partial read-only roadmap
Expand All @@ -60,6 +61,8 @@ Implemented read-only commands:
- `magpie-bitbucket auth-check`
- `magpie-bitbucket repo get`
- `magpie-bitbucket repo restrictions`
- `magpie-bitbucket issue list-open`
- `magpie-bitbucket issue get <id>`
- `magpie-bitbucket pr list-open`
- `magpie-bitbucket pr get <id>`
- `magpie-bitbucket pr commits <id>`
Expand All @@ -72,7 +75,6 @@ Implemented read-only commands:
Remaining candidate read-only gaps include:

- broader repository permission context
- Bitbucket Issues read-only listing and fetching, where enabled
- linked issue or Jira handoff context, if a repository exposes it through
supported APIs
- deeper Pipelines/build run, log, and artifact read coverage
Expand All @@ -97,19 +99,21 @@ surfaces, and maintainer review.

## Features

This first implementation covers read-only operations:
This implementation covers read-only operations:

1. **Authentication preflight:** verify the configured Bitbucket backend and credentials can reach the selected repository.
2. **Repository metadata:** fetch normalized repository details from Bitbucket Cloud or Data Center.
3. **Repository branch restrictions:** fetch known read-only repository branch restriction / branch permission policy context.
4. **Pull-request listing:** list open pull requests as `contract:change-request` proposal summaries.
5. **Pull-request fetch:** fetch one pull request as a normalized proposal object.
6. **Pull-request commits fetch:** fetch commits associated with a pull request as normalized read-only output.
7. **Pull-request diff fetch:** fetch the pull request unified diff as normalized read-only output.
8. **Pull-request discussion fetch:** fetch a comments-only pull request discussion subset as normalized read-only output.
9. **Pull-request review-state fetch:** fetch reviewers, approvals, change-request signals, pending review requests, and normalized review activity.
10. **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.
11. **Pull-request status fetch:** fetch build/status checks for the pull request as normalized read-only output.
4. **Cloud issue listing:** list open Bitbucket Cloud issues where the repository issue tracker is enabled.
5. **Cloud issue fetch:** fetch one Bitbucket Cloud issue as partial read-only tracker context.
6. **Pull-request listing:** list open pull requests as `contract:change-request` proposal summaries.
7. **Pull-request fetch:** fetch one pull request as a normalized proposal object.
8. **Pull-request commits fetch:** fetch commits associated with a pull request as normalized read-only output.
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. **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.
13. **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 @@ -133,7 +137,7 @@ surface:
| Change requests | `post_review` | Not implemented | Follow-up work for #606. |
| Change requests | `land` | Not implemented | Follow-up work for #606. |
| Change requests | `reject` | Not implemented | Follow-up work for #606. |
| Tracker | issue operations | Not implemented | `contract:tracker` remains absent until Bitbucket issue operations or linked Jira handoff coverage exist. |
| Tracker | `issue list-open` / `issue get <id>` | Partial read-only, Cloud only | Lists and fetches Bitbucket Cloud issues where the repository issue tracker is enabled. Bitbucket Data Center native issue reads are unsupported; linked Jira handoff remains separate follow-up work. |
| CI | `pr status <id>` | Partial read-only | Fetches build/status checks for a pull request. This does not trigger, retry, or mutate Pipelines/builds. |

## Invocation
Expand All @@ -148,6 +152,12 @@ uv run --project tools/bitbucket magpie-bitbucket repo get
# Fetch repository branch restrictions
uv run --project tools/bitbucket magpie-bitbucket repo restrictions

# List open Bitbucket Cloud issues
uv run --project tools/bitbucket magpie-bitbucket issue list-open

# Fetch one Bitbucket Cloud issue
uv run --project tools/bitbucket magpie-bitbucket issue get 123

# List open pull requests
uv run --project tools/bitbucket magpie-bitbucket pr list-open

Expand Down Expand Up @@ -199,7 +209,7 @@ injected by the caller as `BITBUCKET_TOKEN` / `BITBUCKET_CLOUD_USER`.
Every successful command emits JSON to stdout. Failures return a
non-zero exit code with a human-readable error on stderr.

Fetched repository branch restriction policy, branch matcher patterns, users, groups, access keys,
Fetched repository branch restriction policy, branch matcher patterns, users, groups, access keys, issue titles/descriptions, issue reporter/assignee names, issue links,
pull request descriptions, commit messages, diff hunks, file paths, comments,
reviewer names, review decisions/events, approval/change-request activity,
merge-check decisions/blockers, status descriptions, CI URLs, and raw Bitbucket
Expand All @@ -225,7 +235,7 @@ then invoke the write command.

Follow-up PRs can extend this bridge with:

- Bitbucket issue read/write operations, which will add tracker coverage.
- Bitbucket issue write operations and additional tracker fields.
- Linked Jira issue handoff through `tools/jira/`.
- Pull-request comment creation, review, approve, decline, and merge operations.
- Broader repository permission reads.
Expand Down
15 changes: 15 additions & 0 deletions tools/bitbucket/src/magpie_bitbucket/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ def _build_parser() -> argparse.ArgumentParser:
repo_subparsers.add_parser("get", help="Fetch repository metadata.")
repo_subparsers.add_parser("restrictions", help="Fetch repository branch restrictions.")

issue_parser = subparsers.add_parser("issue", help="Interact with Bitbucket issues.")
issue_subparsers = issue_parser.add_subparsers(dest="issue_action", required=True)
issue_subparsers.add_parser("list-open", help="List open issues.")

issue_get = issue_subparsers.add_parser("get", help="Fetch one issue.")
issue_get.add_argument("issue_id", help="Issue ID to fetch.")

pr_parser = subparsers.add_parser("pr", help="Interact with Bitbucket pull requests.")
pr_subparsers = pr_parser.add_subparsers(dest="pr_action", required=True)
pr_subparsers.add_parser("list-open", help="List open pull requests.")
Expand Down Expand Up @@ -102,6 +109,14 @@ def _dispatch(args: argparse.Namespace, config: BitbucketConfig) -> dict[str, An
raw = backend.get_repository_restrictions(config)
return normalize.repository_restrictions(config.kind, raw)

if args.subcommand == "issue" and args.issue_action == "list-open":
raw = backend.list_open_issues(config)
return normalize.issue_list(config.kind, raw)

if args.subcommand == "issue" and args.issue_action == "get":
raw = backend.get_issue(config, args.issue_id)
return normalize.issue(config.kind, raw)

if args.subcommand == "pr" and args.pr_action == "list-open":
raw = backend.list_open_pull_requests(config)
return normalize.pull_request_list(config.kind, raw)
Expand Down
35 changes: 35 additions & 0 deletions tools/bitbucket/src/magpie_bitbucket/cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,41 @@ def get_repository_restrictions(config: BitbucketConfig) -> dict[str, Any]:
return combined


def list_open_issues(config: BitbucketConfig) -> dict[str, Any]:
"""List open issues from a Bitbucket Cloud repository."""
workspace = quote_path(require(config.workspace, "BITBUCKET_WORKSPACE"))
repo_slug = quote_path(require(config.repo_slug, "BITBUCKET_REPO_SLUG"))
url = f"{CLOUD_API_BASE}/repositories/{workspace}/{repo_slug}/issues?q=state%3D%22new%22%20OR%20state%3D%22open%22%20OR%20state%3D%22on%20hold%22"

combined: dict[str, Any] = {
"values": [],
"paginated": True,
"pages": [],
}

seen_urls = {url}
while url:
page = get_json(url, config)
combined["pages"].append(page)

values = page.get("values")
if isinstance(values, list):
combined["values"].extend(item for item in values if isinstance(item, dict))

url = _validated_next_url(page.get("next"), seen_urls)

return combined


def get_issue(config: BitbucketConfig, issue_id: str) -> dict[str, Any]:
"""Fetch one issue from a Bitbucket Cloud repository."""
workspace = quote_path(require(config.workspace, "BITBUCKET_WORKSPACE"))
repo_slug = quote_path(require(config.repo_slug, "BITBUCKET_REPO_SLUG"))
issue = quote_path(issue_id)
url = f"{CLOUD_API_BASE}/repositories/{workspace}/{repo_slug}/issues/{issue}"
return get_json(url, config)


def list_open_pull_requests(config: BitbucketConfig) -> dict[str, Any]:
"""List all open pull requests from Bitbucket Cloud."""
workspace = quote_path(require(config.workspace, "BITBUCKET_WORKSPACE"))
Expand Down
14 changes: 14 additions & 0 deletions tools/bitbucket/src/magpie_bitbucket/datacenter.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,20 @@ def get_repository_restrictions(config: BitbucketConfig) -> dict[str, Any]:
return combined


def list_open_issues(config: BitbucketConfig) -> dict[str, Any]:
"""Reject native Bitbucket issue listing for Data Center."""
_ = config
msg = "Bitbucket Data Center native issue reads are not supported; use linked Jira coverage instead."
raise BitbucketError(msg)


def get_issue(config: BitbucketConfig, issue_id: str) -> dict[str, Any]:
"""Reject native Bitbucket issue fetch for Data Center."""
_ = (config, issue_id)
msg = "Bitbucket Data Center native issue reads are not supported; use linked Jira coverage instead."
raise BitbucketError(msg)


def list_open_pull_requests(config: BitbucketConfig) -> dict[str, Any]:
"""List all open pull requests from Bitbucket Data Center."""
project_key = quote_path(require(config.project_key, "BITBUCKET_PROJECT_KEY"))
Expand Down
77 changes: 77 additions & 0 deletions tools/bitbucket/src/magpie_bitbucket/normalize.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,63 @@ def repository_restrictions(kind: str, raw: dict[str, Any]) -> dict[str, Any]:
}


def issue_summary(kind: str, raw: dict[str, Any]) -> dict[str, Any]:
"""Normalize one Bitbucket issue as read-only tracker context."""
if kind == "cloud":
return {
"backend": "bitbucket-cloud",
"id": _string(raw.get("id")),
"title": raw.get("title"),
"state": _normalize_issue_state(raw.get("state")),
"kind": _string(raw.get("kind")),
"priority": _string(raw.get("priority")),
"assignee": _cloud_user(raw.get("assignee")),
"reporter": _cloud_user(raw.get("reporter")),
"created": _cloud_timestamp(raw.get("created_on")),
"updated": _cloud_timestamp(raw.get("updated_on")),
"permalink": _cloud_link(raw, "html"),
"labels": ["bitbucket", "read-only", "partial-tracker"],
}

return {
"backend": "bitbucket-datacenter",
"id": _string(raw.get("id")),
"title": raw.get("title"),
"state": "unsupported",
"kind": None,
"priority": None,
"assignee": None,
"reporter": None,
"created": None,
"updated": None,
"permalink": None,
"labels": ["bitbucket", "read-only", "unsupported-tracker"],
}


def issue(kind: str, raw: dict[str, Any]) -> dict[str, Any]:
"""Normalize one Bitbucket issue."""
summary = issue_summary(kind, raw)
content = raw.get("content")
summary["description"] = _cloud_issue_content(content)
summary["raw"] = raw
return summary


def issue_list(kind: str, raw: dict[str, Any]) -> dict[str, Any]:
"""Normalize a Bitbucket issue list response."""
values = raw.get("values")
if not isinstance(values, list):
values = []

return {
"backend": "bitbucket-cloud" if kind == "cloud" else "bitbucket-datacenter",
"coverage": "partial-read-only",
"issues": [issue_summary(kind, item) for item in values if isinstance(item, dict)],
"raw": raw,
}


def pull_request_summary(kind: str, raw: dict[str, Any]) -> dict[str, Any]:
"""Normalize one pull request as a read-only change-request summary."""
if kind == "cloud":
Expand Down Expand Up @@ -967,6 +1024,26 @@ def _datacenter_inline(raw: object) -> dict[str, Any] | None:
return inline or None


def _normalize_issue_state(value: object) -> str:
state = _string(value)
if not state:
return "unknown"

normalized = state.lower().replace(" ", "_").replace("-", "_")
if normalized in {"new", "open"}:
return "open"
if normalized in {"resolved", "closed", "duplicate", "invalid", "wontfix", "wont_fix"}:
return "closed"
return normalized


def _cloud_issue_content(raw: object) -> str | None:
if isinstance(raw, dict):
value = raw.get("raw") or raw.get("html") or raw.get("markup")
return _string(value)
return _string(raw)


def _string(value: object) -> str | None:
"""Convert a value to string while preserving missing values as None."""
if value is None:
Expand Down
Loading