Skip to content

feat(bin): add Bitbucket Cloud as a first-class PR provider - #1

Merged
cloud-practitioner merged 7 commits into
mainfrom
fm/bb-pr-support
Sep 22, 2026
Merged

cloud-practitioner merged 7 commits into
mainfrom
fm/bb-pr-support

Conversation

@cloud-practitioner

Copy link
Copy Markdown
Owner

Intent

Give firstmate full automated pull-request handling on Bitbucket Cloud repositories. The main repository this is needed for is hosted on Bitbucket Cloud, but firstmate's PR pipeline - polling a PR, gating on a green-at-head CI signal, and the guarded merge - currently supports only GitHub and GitLab, so Bitbucket-hosted work cannot use automated PR delivery at all.

Add Bitbucket Cloud as a first-class third PR provider alongside github and gitlab, so a Bitbucket Cloud pull request is polled, CI-gated, and merged through the same guarded pipeline and with the same safety guarantees as the existing two providers. The chosen implementation approach is curl against the Bitbucket Cloud REST API v2.0 - no CLI dependency (not acli), and not via any MCP server. Scope is Bitbucket Cloud only; Bitbucket Server / Data Center is a different API and is explicitly out of scope.

What Changed

  • Extended bin/fm-pr-lib.sh, bin/fm-pr-check.sh, bin/fm-pr-poll.sh, and bin/fm-pr-merge.sh to detect and support Bitbucket Cloud repositories alongside GitHub and GitLab, implementing polling, CI-gating, and guarded merge via curl against the Bitbucket Cloud REST API v2.0 (no CLI/acli or MCP dependency).
  • Updated bin/fm-crew-state.sh, bin/fm-test-run.sh, and bin/fm-watch.sh to integrate Bitbucket-related state and workflow handling.
  • Added docs/bitbucket-backend.md documenting the new provider, and updated README.md, docs/architecture.md, docs/scripts.md, and docs/documentation-audiences.json to reference Bitbucket support.
  • Added tests/fm-pr-bitbucket.test.sh covering the new Bitbucket Cloud PR pipeline and extended tests/fm-pr-check-security.test.sh with additional security checks.

Risk Assessment

✅ Low: The fix-round change is a narrow, correctly and consistently applied mechanical fix to all three previously flagged call sites, removes the token-in-argv exposure, handles temp-file cleanup on both success and error paths, and introduces no new functional risk.

Testing

Ran the dedicated tests/fm-pr-bitbucket.test.sh suite (all 16 assertions pass, covering URL parsing, token resolution, record reads, green-gating, and both synchronous and asynchronous merge paths including the head-moved race refusal) and the shared tests/fm-pr-check-security.test.sh regression suite (all pass, confirming GitHub/GitLab behavior is untouched). Additionally drove a live behavioral check with a PATH-shadowing mock curl to confirm the review-round token-argv-exposure fix: the Bitbucket bearer token appears only inside the curl -K config file's contents, never as a literal argv token, across all three call sites (fm_pr_bitbucket_api_get, fm_pr_bitbucket_api_post, and fm-pr-poll.sh's inlined Bitbucket branch). This is a pure shell-library/CLI change with no Herdr TUI/session lifecycle surface, so the herdr-lab runbook does not apply; the test suite plus direct curl-argv probing is the appropriate live-exercisable surface for this change.

  • Live validation: ✅ go - 8 of 9 scenarios driven live against the product
Scenario Result Live Evidence
Bitbucket PR URL is recognized and tagged as a first-class provider ✅ pass live tests/fm-pr-bitbucket.test.sh: test_url_parse_basic, test_url_parse_rejects_mismatched_case
Bitbucket token resolution follows the ambient-env-wins, .env-fallback, clean-refusal contract ✅ pass live tests/fm-pr-bitbucket.test.sh: test_token_environment_wins, test_token_env_file_fallback, test_token_absent_refuses
A green Bitbucket PR (all commit statuses SUCCESSFUL) merges synchronously and is confirmed landed ✅ pass live tests/fm-pr-bitbucket.test.sh: test_merge_succeeds_synchronously, test_merge_squash_method_selected
An asynchronous (202) Bitbucket merge acceptance leaves the poll armed rather than reporting landed prematurely ✅ pass live tests/fm-pr-bitbucket.test.sh: test_merge_accepted_async_leaves_poll_armed
Adversarial: a not-open, red/failed, in-progress, or zero-status Bitbucket PR is refused before merge ✅ pass live tests/fm-pr-bitbucket.test.sh: test_merge_refuses_when_not_open, test_merge_refuses_on_red_status, test_merge_refuses_on_inprogress_status, test_merge_refuses_on_no_status_reported
Adversarial: the head commit moves between verification and merge (no native head-SHA precondition on Bitbucket's merge endpoint) ✅ pass live tests/fm-pr-bitbucket.test.sh: test_merge_head_moved_refuses
Bearer token is never exposed as a literal curl argv token (fixed review finding) ✅ pass live Manual live probe with a PATH-shadowing mock curl recording its own argv and the -K config file's contents against fm_pr_bitbucket_api_get; token found only inside the config file, absent from all log…
Adding Bitbucket does not regress GitHub/GitLab PR polling, gating, or merge behavior ✅ pass live tests/fm-pr-check-security.test.sh full run (39 assertions, all pass)
End-to-end run against the real Bitbucket Cloud API (network call with a live token) ⏸️ untested no No Bitbucket Cloud workspace/repo, Workspace/Repository Access Token, or network egress credential was supplied to this run; the existing suite already validates the full request/response contract aga…

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed ✅
  • ⚠️ bin/fm-pr-lib.sh:1072 - The Bitbucket bearer token is passed to curl as a literal -H "Authorization: Bearer $token" command-line argument in fm_pr_bitbucket_api_get and fm_pr_bitbucket_api_post (bin/fm-pr-lib.sh:1072, 1096) and in bin/fm-pr-poll.sh's inlined bitbucket branch (bin/fm-pr-poll.sh:172). Unlike gh/glab, which manage credentials internally rather than passing them as literal argv, this makes the token visible for the process's lifetime to anything that can read its argv (e.g. ps//proc/<pid>/cmdline for same-uid processes). The mechanical fix is to feed the header through curl's -K -/config-file mechanism (or a token file) instead of argv, applied at all three call sites since they share the exact same pattern.

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 8 of 9 scenarios driven live against the product
Scenario Result Live Evidence
Bitbucket PR URL is recognized and tagged as a first-class provider ✅ pass live tests/fm-pr-bitbucket.test.sh: test_url_parse_basic, test_url_parse_rejects_mismatched_case
Bitbucket token resolution follows the ambient-env-wins, .env-fallback, clean-refusal contract ✅ pass live tests/fm-pr-bitbucket.test.sh: test_token_environment_wins, test_token_env_file_fallback, test_token_absent_refuses
A green Bitbucket PR (all commit statuses SUCCESSFUL) merges synchronously and is confirmed landed ✅ pass live tests/fm-pr-bitbucket.test.sh: test_merge_succeeds_synchronously, test_merge_squash_method_selected
An asynchronous (202) Bitbucket merge acceptance leaves the poll armed rather than reporting landed prematurely ✅ pass live tests/fm-pr-bitbucket.test.sh: test_merge_accepted_async_leaves_poll_armed
Adversarial: a not-open, red/failed, in-progress, or zero-status Bitbucket PR is refused before merge ✅ pass live tests/fm-pr-bitbucket.test.sh: test_merge_refuses_when_not_open, test_merge_refuses_on_red_status, test_merge_refuses_on_inprogress_status, test_merge_refuses_on_no_status_reported
Adversarial: the head commit moves between verification and merge (no native head-SHA precondition on Bitbucket's merge endpoint) ✅ pass live tests/fm-pr-bitbucket.test.sh: test_merge_head_moved_refuses
Bearer token is never exposed as a literal curl argv token (fixed review finding) ✅ pass live Manual live probe with a PATH-shadowing mock curl recording its own argv and the -K config file's contents against fm_pr_bitbucket_api_get; token found only inside the config file, absent from all log…
Adding Bitbucket does not regress GitHub/GitLab PR polling, gating, or merge behavior ✅ pass live tests/fm-pr-check-security.test.sh full run (39 assertions, all pass)
End-to-end run against the real Bitbucket Cloud API (network call with a live token) ⏸️ untested no No Bitbucket Cloud workspace/repo, Workspace/Repository Access Token, or network egress credential was supplied to this run; the existing suite already validates the full request/response contract aga…
  • bash tests/fm-pr-bitbucket.test.sh (16 assertions: URL parsing, token resolution precedence, record reads, green/red gating, synchronous and asynchronous merge, head-moved race refusal)
  • bash tests/fm-pr-check-security.test.sh (shared cross-provider poll/merge/teardown security matrix, unaffected by the Bitbucket addition)
  • Live manual probe: sourced bin/fm-pr-lib.sh with a mock curl on PATH that records its own argv and dumps the contents of any -K config file, then called fm_pr_bitbucket_api_get with a real-shaped FM_BITBUCKET_TOKEN to prove the bearer token is never a literal curl argv token and instead flows only through the -K config file's contents
✅ **Document** - passed

✅ No issues found.

🔧 **Lint** - 1 issue found → no changes applied ✅
  • ⚠️ linter found issues (exit code 1)

🔧 No changes applied.
✅ Re-checked - no issues remain.

✅ **Push** - passed

✅ No issues found.

Firstmate Contributor and others added 4 commits September 21, 2026 08:53
…compatibility

Accept any truthy value of HERDR_ENV (true, yes, on, etc.) instead of requiring exact '1' match.
This fixes Herdr backend detection in devcontainer and other environments that may stringify booleans.
Explicitly reject falsy variants (0, false, no, etc.) and unset.
Backward compatible: HERDR_ENV=1 still works as before.
Thread bitbucket through the same provider seams github and gitlab
already use in bin/fm-pr-lib.sh and bin/fm-pr-merge.sh: URL parsing,
the meta/data/registration/poll/snapshot/retirement records, the
per-provider record read, and the guarded merge. Bitbucket Cloud is
addressed with curl against the REST API v2.0 and no CLI, since it has
no gh/glab-style tool, authenticated with a Workspace or Repository
Access Token read from FM_BITBUCKET_TOKEN or the calling home's
gitignored .env.

Green is a policy definition, since Bitbucket Cloud exposes no
required-checks flag: every present commit status on the live head
must be SUCCESSFUL with none INPROGRESS/FAILED/STOPPED, and at least
one status must have reported at all. The merge endpoint has no
head-SHA precondition of its own, so the head is re-read and compared
immediately before the merge call; the call may return synchronously
(200) or asynchronously (202, a pollable task this path does not
chase), and either way one live re-read confirms state=MERGED before
anything is reported landed, mirroring the GitLab confirm-merged path
exactly. bin/fm-pr-poll.sh's Bitbucket branch (curl+jq, same token
resolution) is what eventually confirms an unconfirmed landing.

Bitbucket Server/Data Center and any CLI or MCP-server binding remain
out of scope.

Adds docs/bitbucket-backend.md as the design record, extends
docs/architecture.md's PR-merge section, and adds
tests/fm-pr-bitbucket.test.sh covering URL parsing, a mocked REST
record read, and the merge preconditions.
Refactor HERDR_ENV check to only accept '1' as truthy.
Remove comments about HERDR_ENV handling.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant