Skip to content

feat(bitbucket): deliver to Bitbucket Cloud - #20

Merged
AlexeyShalaev merged 20 commits into
masterfrom
feat/bitbucket-read
Oct 8, 2026
Merged

AlexeyShalaev merged 20 commits into
masterfrom
feat/bitbucket-read

Conversation

@AlexeyShalaev

Copy link
Copy Markdown
Member

Bitbucket Cloud as a delivery platform: a hub on GitHub, GitLab or Gitea/Forgejo can now deliver to repositories on bitbucket.org. A hub that itself lives on Bitbucket (Bitbucket Pipelines) comes later.

Provider type bitbucket. url defaults to https://bitbucket.org, the API to https://api.bitbucket.org/2.0, and web URLs of repositories resolve. The reader and the writer are API tokens of two bot accounts. Logins are account UUIDs (writer: "{…}"), because Bitbucket nicknames are neither unique nor searchable. topics: on a Bitbucket entry fails check: Bitbucket has no topics, so use match: or repo:.

Driver (internal/platform/bitbucket).

  • Reads repositories, workspaces, files and pull requests. File metadata comes first, then the raw bytes, and the blob id is computed locally. Symlinks, submodules, directories and LFS files are not regular. A missing opt-in file is ErrNotFound only at a commit that exists; a missing repository or ref is never one.
  • Writes pull requests: create (with drafts), edit, comment, and close by decline. The marker is written before the decline, because a declined pull request can no longer be edited.
  • doctor checks the writer's access. Branch restrictions need admin rights to read, so they show at the push, where a refusal is rules:protected-branch.
  • Commits carry the writer's confirmed e-mail, so Bitbucket links them to the account.

Core.

New platform capabilities:

  • Caps.NoLabels. Bitbucket has no pull request labels: none are planned, set or estimated.
  • MarkerInRefDef. Bitbucket escapes HTML, so the marker is a Markdown reference definition, [touchmark]: # "touchmark:v1 …", which renders as nothing. The payload is unchanged, only the frame differs. Both frames are read everywhere, with CRLF and the backslash escapes of a web edit accepted. Descriptions on Bitbucket carry no HTML: no tick boxes, and a plain list in place of <details>.
  • Caps.ClosedImmutable. Declined pull requests cannot be edited, so decline memory needs no writes there:
    • an open pull request's marker keeps optin current (also while it is blocked);
    • a decline holds while the opt-in file keeps that state;
    • a forget_declines entry lifts it while the entry is present;
    • no ack, decline comment or revoke is ever written.

GitHub, GitLab, Gitea and Forgejo are unchanged, and tests prove their bodies and decisions are byte-identical.

Tests.

  • Driver unit tests on a recording HTTP server, with the shapes from Bitbucket's OpenAPI and real public responses.
  • A bitbucket flavor of the fake platform, delivery scenarios on it, a Bitbucket property simulation and new crash-matrix cases.
  • Marker fuzzing with the new frame.

An independent review found no regressions on the existing platforms; its three findings are fixed here.

Not verified live (no account). These are listed in the docs:

  • whether the reference-definition marker stays hidden and survives a web edit;
  • the permission and pull request query filters;
  • the 429 headers;
  • the push refusal text.

A provider of type bitbucket is Bitbucket Cloud: its url defaults to https://bitbucket.org and its API to https://api.bitbucket.org/2.0. Any other url needs an api_url (a test server); Bitbucket Data Center has another API and is not this type. Web URLs such as https://bitbucket.org/acme/billing resolve to the provider, workspace/repository as on the other platforms without nested namespaces, and exclude and match warn about paths deeper than that.

Bitbucket has no topics: check refuses topics on an entry whose provider is Bitbucket, with a hint to use match or repo. The JSON Schemas of hub.yml, the plan and doctor reports, and targets.yml follow, and so does the reference.
…quests

A new driver, internal/platform/bitbucket, serves plan for providers of type bitbucket; its write driver refuses with ClassUnsupported until the writer comes, so distribute and doctor stop at such a provider, and setup and the hub's key store say Bitbucket is not supported yet.

The reader authenticates with an API token of a bot account: Bearer for the REST API, Basic x-bitbucket-api-token-auth for git, both masked. Accounts are their uuids in braces (Lookup takes a uuid or an Atlassian account id and refuses anything else: Bitbucket finds no account by name). Probe asks nothing: flavor bitbucket, native drafts, closed_by as the closer, a 60 000-byte body, the marker as a Markdown link reference definition (platform.MarkerInRefDef, whose codec comes with the writer), branch restrictions known at push time only, and 15 reads a minute with two targets at once. Repo and Resolve read workspace/slug and a workspace's repositories through the next links (topics are refused); a repository without a main branch is empty.

ReadFile resolves the ref to a commit, reads the path's metadata, then its raw bytes at that commit, and computes the blob id. Symlinks, submodules, directories and Git LFS files are not regular. Only a path an existing commit lacks is ErrNotFound; a missing repository, ref or commit is ClassUnknown, never an opt-out. PRs lists the sync branches' pull requests in every state through q= (sorted by descending id), maps OPEN, MERGED, DECLINED and SUPERSEDED, takes the closer from closed_by and ClosedAt from updated_on, checks bases other than the default branch, and expands the short head of an open pull request through the commit API. OpenPRsBy sweeps every workspace of the identity by author.

Errors follow Bitbucket's {"type": "error"} body: 401 auth, 403 permission, 404 and 410 not found, 429 or a rate-limit message rate limited with the wait from Retry-After or x-ratelimit-reset, 5xx transient. Tests run on an HTTP test server with answers shaped after the OpenAPI description and anonymous answers of public repositories.
…ition

Bitbucket Cloud escapes HTML in descriptions, so the marker's HTML comment
would show as text. EncodeFrame writes the same payload in a link reference
definition, [touchmark]: # "touchmark:v1 ...", which renderers hide. Find,
Strip and Parse read both frames, with CRLF line endings and the backslash
escapes Bitbucket adds after a web edit undone. prbody accepts the new frame
as Render's marker, keeps it as a marker line and reserves it in the intro.
On a platform without labels the core asks for none: no labels on new pull
requests, none recorded as set in the marker, no AddLabels edits when
hub.yml gains one, no label writes in the estimates, and no advice about
labels in the auto-decline comment. The marker is written in the frame of
Caps.Marker (a reference definition on Bitbucket), and doctor reads markers
in both frames. A push Bitbucket refuses with "Permission denied to update
branch" is blocked:rules:protected-branch. A Bitbucket commit without the
account's email is authored by <uuid>@touchmark.invalid. The fake refuses
labels where its caps say there are none.
NewWriter builds a real writer. Target reads the repository by its uuid and
the writer's permission from GET /2.0/user/workspaces/{workspace}/
permissions/repositories (write or admin; read or none is ClassPermission
with rule contents or pull-requests). The TargetWriter pushes with the API
token's Basic credentials, opens pull requests (draft flag, the source
branch kept on merge, labels ignored), edits open ones with one PUT that
sends back title, description, draft flag and reviewers, declines after the
PUT so that the closed marker is written first, refuses to reopen or change
a declined pull request, comments, and has no labels to ensure. Probe sets
NoLabels; the writer's Self reads the account's primary confirmed address
for its commits. The Checker reports access per repository, branch
restrictions as unknown, and the token's expiry, scopes and 2fa as unknown.
setup's refusal now says that a hub on Bitbucket Pipelines comes later.
Bitbucket Cloud's declined pull requests can never be edited, and its descriptions show the HTML comments the tick boxes rest on. ClosedImmutable says the first (Bitbucket's Probe sets it); BodyControls derives the second from the marker store.
…ests are immutable

With MemoryConfig.ClosedImmutable the optin an open pull request's marker held stands for the ack: a decline holds while the opt-in file hashes the same and lapses when it changes. One without optin holds and is listed in Memory.Unanchored. A forget_declines entry lifts a decline while present (Memory.Forgotten), ticked repropose controls are ignored, and ToAck and ToRevoke stay empty, so no write to a closed pull request is planned.
Without Caps.BodyControls the body shows no control, the paused block asks for a recreate entry in .touchmark/operations.yml, and the footnote names forget_declines (also where closed pull requests are immutable). Only the Bitbucket goldens change.
CapsFor(Bitbucket) matches the driver's Probe: no labels, closed pull requests immutable, the marker as a reference definition, native drafts, a known closer, no workflow permission. EditPR of a declined pull request and its reopening are ClassUnsupported, a person cannot reopen one, a close counts as an edit and a decline.
… immutable

Memory gets Caps.ClosedImmutable and reads tick boxes only where descriptions carry them; the open pull request's marker keeps the current optin there (one edit when it is stale); an unanchored decline gets a target warning; a forget_declines entry that lifts a decline while present is reported as applying.
Decline, its lapse on an opt-in change, forget_declines while present, an unanchored decline, touchmark's own closes (no-diff, target-dropped), no labels and no tick boxes; bodies elsewhere stay byte for byte on an opt-in change. The property test runs 30 Bitbucket scenarios, the crash matrix three Bitbucket cases.
@AlexeyShalaev
AlexeyShalaev merged commit 73200b0 into master Oct 8, 2026
22 checks passed
@AlexeyShalaev
AlexeyShalaev deleted the feat/bitbucket-read branch October 8, 2026 23:46
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