Skip to content

feat(bitbucket): add guarded cloud PR merge - #1471

Open
KatalKavya96 wants to merge 4 commits into
apache:mainfrom
KatalKavya96:feat-bitbucket-cloud-pr-merge
Open

KatalKavya96 wants to merge 4 commits into
apache:mainfrom
KatalKavya96:feat-bitbucket-cloud-pr-merge

Conversation

@KatalKavya96

Copy link
Copy Markdown
Contributor

Summary

  • Adds guarded Bitbucket Cloud pull-request merge support via magpie-bitbucket pr merge <id> --strategy {merge,squash,rebase}.
  • Maps the Cloud merge path onto the change-request land(id, strategy) -> landed_ref contract shape, including extraction of the resulting merge commit when available.
  • Keeps Bitbucket Data Center merge writes explicitly fail-closed and preserves queued/submitted merge state instead of reporting an incomplete merge as already landed.

Type of change

  • Skill change (.claude/skills/<name>/) — eval fixtures updated below
  • Tool / bridge contract (tools/<system>/*.md)
  • Python package (tools/*/ with pyproject.toml)
  • Groovy reference impl
  • Cross-cutting (RFC, AGENTS.md, sandbox, privacy-LLM)
  • Documentation (docs/, README.md, CONTRIBUTING.md)
  • Project template (projects/_template/)
  • CI / dev loop (prek, workflows, validators)
  • Other:

Test plan

  • prek run --all-files passes
  • For Python packages touched: uv run pytest / ruff check / mypy passes
  • For Groovy bridges touched: command-line invocation tested end-to-end
  • For skill changes: eval suite passes for the affected skill
    (PYTHONPATH=tools/skill-evals/src python3 -m skill_evals.runner tools/skill-evals/evals/<skill>/)
  • For skill behaviour changes: a new or updated eval fixture is included in this PR
    (a regression test for the bug fixed / the behaviour added — see CONTRIBUTING.md)
  • Other:
    • focused merge tests cover strategy payload, completed merge normalization, queued merge normalization, landed_ref, Data Center fail-closed behavior, and CLI dispatch
    • full Bitbucket test suite passes
    • git diff --check passes
    • workspace pytest passes with a short macOS temp path to avoid the unrelated AF_UNIX pathname-length limitation

RFC-AI-0004 compliance

  • HITL — the merge mutation remains gated on explicit caller-side user confirmation
  • Sandbox — no new unrestricted host access; the implementation reuses the existing guarded HTTPS write path
  • Vendor neutrality — the Bitbucket-specific command is mapped onto the generic change-request land contract rather than introducing a new framework-level vendor

@KatalKavya96

Copy link
Copy Markdown
Contributor Author

Hi @potiuk — next narrow #606 follow-up after #1194.

This adds guarded Bitbucket Cloud pr merge <id> --strategy {merge,squash,rebase}, maps it onto the change-request land(id, strategy) -> landed_ref contract shape, preserves queued merge state, and keeps Data Center merge writes fail-closed.

Focused/full Bitbucket tests, Ruff, mypy, repository hooks, and workspace pytest are green.

Would appreciate your review when you get a chance.

@github-actions github-actions Bot added contract:tracker Tool capability: issue / board / label backend contract:change-request Tool capability: proposed-change review + merge gate (PR / MR / Gerrit change) labels Sep 29, 2026

@Kaap10 Kaap10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this @KatalKavya96!

The overall structure and Data Center fail-closed handling look solid. However, there are two discrepancies between the documented contract shape and the implementation:

  1. Missing --strategy in CLI & Payload:

    • README.md and the PR summary document pr merge <id> --strategy {merge,squash,rebase}, but --strategy is missing from _build_parser() in cli.py.
    • cloud.merge_pull_request() currently hardcodes payload={"type": "pullrequest"} without accepting or mapping the strategy parameter to Bitbucket's API values (merge_commit, squash, fast_forward).
  2. Missing landed_ref normalization:

    • The documentation mentions returning the merge commit as landed_ref when available, but in normalize.py (merged_pull_request), landed_ref is not extracted from result_data.get("merge_commit", {}).get("hash").

Could you add --strategy to cli.py / cloud.py and extract landed_ref in normalize.py along with matching tests?

@KatalKavya96

KatalKavya96 commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks @Kaap10

I updated the PR to:

  • add required --strategy {merge,squash,rebase} CLI handling

  • map those values to Bitbucket Cloud merge strategies

  • pass the selected strategy through the merge request

  • extract merge_commit.hash as landed_ref

  • preserve queued merges without reporting a landed ref prematurely

  • add matching strategy, normalization, CLI, and Data Center fail-closed tests

Thanks for catching the contract/implementation mismatch.

@potiuk potiuk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — --strategy is now wired end to end and landed_ref comes from merge_commit.hash, so the earlier review points are addressed. A merge is the highest-blast-radius write this bridge exposes, though, and a few things need tightening before it's approvable (details inline):

  • Async merges (major). Bitbucket Cloud answers a slow merge with 202 and a Location pointing at the merge task-status resource. write_request drops the headers, an empty-body 202 makes this raise after the merge was actually submitted, and a finished task carries the hash under merge_result.merge_commit.hash, which the normalizer doesn't read.
  • Head pinning, the rebase mapping, README consistency, and failure-path tests (minor).

One contract question to settle in the README either way: pr merge performs no pre-merge gating — it doesn't check approvals, build status or merge checks, and Bitbucket Cloud only enforces merge checks server-side on Premium with the setting enabled. Either the adapter should check change_request.gates before POSTing, or the README row should say plainly that the caller must run pr merge-checks first.


This review was drafted by an AI-assisted tool and
confirmed by an Apache Magpie maintainer. The findings
below are observations, not blockers; an Apache Magpie
maintainer — a real person — will take the next look at the
PR. If you think a finding is mis-applied, please reply on
the PR and a maintainer will weigh in.

More on how Apache Magpie handles maintainer review:
CONTRIBUTING.md.

"merge_strategy": merge_strategy,
},
)
if result is None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

major — Bitbucket Cloud's merge endpoint isn't always synchronous: past its synchronous window (or with ?async=true) it answers 202 with a Location header pointing at .../pullrequests/{id}/merge/task-status/{task_id}, and that resource carries task_status and merge_result (with merge_commit.hash). Today write_request drops headers so the task URL is lost; an empty-body 202 raises here and the CLI exits non-zero after the merge was submitted (inviting a confused retry); the 30s client timeout sits right at the synchronous window; and normalize.merged_pull_request reads merge_commit only at top level, so a finished task reports landed_ref: None. Please surface status + Location from the merge call, treat 202 as submitted (returning the task URL, or polling it with a bounded loop), read merge_result.merge_commit.hash, and add tests for the 202-with-Location and empty-body cases — the current queued-merge test uses a hand-built dict that no endpoint returns.

"pull_request_id",
help="Pull request ID to merge.",
)
pr_merge.add_argument(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor — Nothing pins the head the maintainer confirmed: if the author pushes between confirmation and this call, the new commits land unreviewed. Bitbucket's merge API has no expected-hash parameter, so consider an --expected-source-commit <sha> that GETs the PR first and fails closed if source.commit.hash doesn't match (prefix-compare — Cloud returns short hashes). That narrows the window from human think-time to the GET→POST gap and makes "guarded" concrete.

strategy_map = {
"merge": "merge_commit",
"squash": "squash",
"rebase": "fast_forward",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor — fast_forward isn't a rebase: it fails unless the source already contains the destination tip, whereas rebase replays the commits. Bitbucket Cloud now appears to offer rebase strategies (rebase_fast_forward / rebase_merge) — please check the current API reference and map to the closer one. Separately, the output echoes the requested strategy; the land contract asks backends to report the strategy they actually used when they can't honour the request.

Comment thread tools/bitbucket/README.md
and pull-request approve/unapprove, request-changes/remove-request-changes, and decline actions after the calling skill has obtained
and pull-request approve/unapprove, request-changes/remove-request-changes, decline, and merge actions after the calling skill has obtained
explicit user confirmation. Other writes, such as editing/deleting comments,
merging, creating/updating issues, changing branches, or triggering

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor — This paragraph now lists merge as supported and "merging" as out of scope two lines later. Please drop "merging," here (or say "Data Center merging"). The Write-path discipline section further down still lists only the older mutations and says "All other Bitbucket mutations remain out of scope", and the Invocation block has no pr merge example.



@patch("magpie_bitbucket.client.urllib.request.build_opener")
def test_cloud_merge_pull_request_posts_default_payload(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor — All new tests are happy-path (plus the Data Center refusal). Please add: a 409/400 from the merge endpoint surfacing as a non-zero CLI exit with Bitbucket's message; the result is None branch; argparse rejecting a missing or invalid --strategy before any request (assert no opener call); and pr merge via the CLI with BITBUCKET_KIND=datacenter exiting non-zero without an outbound request. The ..._posts_default_payload name is also stale now that --strategy is required.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contract:change-request Tool capability: proposed-change review + merge gate (PR / MR / Gerrit change) contract:tracker Tool capability: issue / board / label backend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants