feat: cover the remaining API surface, and record where API tokens are refused - #3
Merged
Conversation
Wraps PUT /repositories/{ws}/{repo}/pullrequests/{id}, the one mutating
pull request endpoint bbx did not cover. Takes --title, --body, --dest,
--reviewers and --close-source-branch/--no-close-source-branch.
The endpoint merges rather than replaces, so the handler sends only the
fields the caller named and a retitle leaves the description alone.
Two things the API does not document, both found against the live API:
close_source_branch is applied only when the same call also moves another
field to a new value. On its own, or beside a field set to what it already
holds, Bitbucket discards it and still answers 200 with the value echoed
back. The handler reads the pull request first and exits 1 rather than
report a change that did not land.
An absent array option parses to an empty array, not null, so keying the
reviewers off null sent "reviewers": [] on every edit and would have
stripped the reviewers from any pull request it touched.
CommandBinding gains a ninth-arity SetHandler for the wider option set.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffing the published OpenAPI spec against the endpoints bbx calls leaves 142 live gaps once the deprecated operations are dropped. Deciding each one is the slow part of closing them, and the decision is worth keeping whether or not the code lands, so it goes in its own file and its own commit. 109 are worth covering. 33 are not: Connect app properties need an app key a CLI cannot hold, pipelines runners need real hardware, account credential writes would mutate the only real account on this machine, and three are already covered or duplicated.
The tool created and deleted well and rarely updated or fetched one item. There was no way at all to change a repository's settings, so renaming one or moving it to another project meant the web UI. Adds repo update, override-settings view and update, file-conflicts, branching-model effective, deploy-keys update, default-reviewers view, branch restrictions view and update, and branch refs. src ls no longer requires --ref: without one Bitbucket serves the root of the main branch, which saves looking up whether it is called main or master. repo update carries fifteen bound symbols, more than SetHandler's nine, so it reads the parse result directly rather than growing the overload set for one caller.
…ge status bbx could add and list pull request comments but not change one, so fixing a typo in a review meant the web UI. It also had no way to see which files a merge would conflict on, what a pull request touches without pulling the whole diff, or whether an asynchronous merge had finished. Adds comment-view, comment-update, comment-delete, comment-resolve, comment-unresolve, conflicts, diffstat, merge-status and tasks view. pr activity now takes an optional ID: without one it reads the repository-wide feed, which is a separate endpoint rather than the same one with a blank ID. Resolve and reopen are the same resource under POST and DELETE, so one builder makes both commands.
bbx could list commit comments but not write one, and could write a build status but not read a single one back by key. Both matter for CI, which is the main reason anything talks to these endpoints. Adds commit comment, comment-view, comment-update, comment-delete and status view. commit list gains --include and --exclude: Bitbucket takes those two lists in a POST body only, so passing either switches the verb, and the next link that comes back is followed with a plain GET because the walk is carried in its query string. --line without --path is refused. Bitbucket accepts that body, drops the anchor, and leaves the comment sitting unanchored as though it had worked.
repo permissions summarised the user grants and there was no way to see the group grants at all, or to change either. Access is the one thing a script cannot fall back to the web UI for, because the person running it often has no browser session. Adds repo access groups list/view/set/remove and users view/set/remove, and the same under workspace project access with users list as well. Permissions are checked before the call because Bitbucket answers an unknown one with a bare 400; projects also take create-repo, repositories do not. The write verbs are unit-tested only. Granting a permission needs a second Bitbucket account or a group and neither exists on this machine, so they were never run against the live API.
The pipelines group could list and create but rarely read one item back or change it. There was no way to turn Pipelines on or off, set the next build number, manage the SSH key a pipeline authenticates with, or publish a report from a CI step, which is the main reason anything scripts this API. Adds pipeline config view/update/build-number, schedules view/update/ executions, variables view/update, ssh key-pair and known-hosts, caches content-uri and clear --all, step, test-case-reasons, deployments create/ delete/changes and its variables, deploys list/view, and reports update/delete with the four annotation verbs. Two shapes are worth knowing. A known host needs a type discriminator on both the outer object and the nested key, or Bitbucket answers "An invalid field was found in the JSON payload". The annotations create endpoint takes a bare JSON array rather than an object, so --annotations passes raw JSON through and checks it is an array first. `deploys` is separate from `deployments`: deployments manages the environments, deploys reads the records of what was released to them. The existing naming made a clean split impossible without renaming a shipped command.
…ettings Three gaps in one commit because they share a shape: each group could create and delete but not read one item back or pin a write. Snippets take --revision on view, update, delete and files, which is also how Bitbucket does optimistic concurrency: an update against a stale revision is refused rather than clobbering the newer one. Adds snippet commits, diff and patch, and --update on comments. user gains workspaces, gpg-keys list and view, emails --email, and two permission reads scoped to one workspace. `bbx user workspaces` matters particularly: `workspace list` calls /2.0/workspaces, which Bitbucket withdrew, and this is the endpoint that replaced it. workspace gains member, repo-permissions, gpg-key, pullrequests and a pipelines group for the workspace-wide variables and OIDC discovery. Projects gain update, branching-model settings and default-reviewers view. GPG key writes are left out on purpose: adding or deleting one would mutate the only real account on this machine.
…iled Found by running the new surface against a throwaway repository. Each of these looked right and failed live. A report needs details. The spec marks nothing required; Bitbucket answers "Cannot build Report, some of required attributes are not set [details]", so --details is now required rather than optional. Reports and annotations also need a type discriminator, without which the 400 carries no message at all. DELETE on the caches collection is not "clear everything". It takes a name in the query string and clears every cache with that name whatever its UUID, and answers a bare 400 without it. --all is now --name. PUT on override-settings answers 204 with no body, so the handler read back an empty JsonElement and the serializer threw "Operation is not valid due to the current state of the object" on a call that had worked. It now reads the settings back. CommandRunner prints null rather than throwing if any other write does the same. /user/workspaces returns workspace_access records, not workspaces: the slug and uuid sit one level down. Reading them flat gave a list of nulls. Errors now carry error.detail and error.data.arguments. Adding a known host for bitbucket.org failed with a bare "Bad request" until those were surfaced; the reason was there all along, in an argument saying Bitbucket already configures SSH for that hostname.
README and docs/llm-guide.md carry the new verbs, group by group. The guide matters most because it is what agents are pointed at, so each row names the endpoint and the flags that are not obvious from the verb. CLAUDE.md gains eight rules from the live run: the 204 that crashes the serializer, the type discriminators, the report that needs details, the caches delete that is really a delete-by-name, the nested workspace record, the deploy key PUT that cannot succeed, the endpoints that refuse an API token, and the error detail that was there all along.
A minor bump rather than a patch: this adds 109 endpoints across every command group. Nothing was removed and no existing output shape changed, so it is not a major.
Every endpoint verdict in docs/api-coverage.md was made against a spec nobody could check without a network call. This puts the document in the repository, so a claim about what Bitbucket documents can be read rather than trusted. Bitbucket serves the spec as a single 921 KB line, which no editor, grep or agent can read. scripts/fetch-spec.sh normalises it through jq -S, so the file is searchable and a refresh diffs as content rather than one changed line. The script also rewrites the date, hash and operation counts in docs/spec/README.md, because provenance that has to be updated by hand goes stale. The copy matches the OpenAPI 3 spec the coverage audit used: same 331 operations, descriptions, deprecation flags and request bodies, different serialisation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…dpoint
The handler sent a flat body with a lock object. Bitbucket answered 400 to
that and to every other shape tried, so the command shipped unverified.
Watching the Bitbucket web UI rename an environment settled it. The body is
an envelope, {"change": {...}}, posted to the path with a trailing slash, and
it answers 202 with no body because the change is queued rather than applied.
Only name and restrictions.admin_only can be changed. The lock, rank, hidden
flag, type and environment_lock_enabled are all refused with a different 400,
change-not-supported, and there is no lock resource anywhere in the 2.0 API.
So --lock and --unlock could never have worked and are gone. --admin-only and
--no-admin-only replace them, which is the switch the UI actually offers.
--reason went with them: the change_request wrapper it fed is not a thing.
Also fixes the view projection, which emitted the field as "lock_" because
lock is a C# keyword and the escape was spelled with an underscore.
Confirmed live against a throwaway repository on 2026-08-06.
PUT deploy-keys/{key_id} demands the key field and then refuses it. Eight
bodies went out live: the key alone, the key with its comment appended
exactly as ssh-keygen wrote it, the comment sent as its own field, an empty
key, a label with no key, a second freshly generated key, and a type
discriminator. Every one answered 400, either "you can't modify the contents
of an access key" or "that access key is invalid".
The web UI offers no rename control either, only view and delete, so this is
not a body nobody has guessed. It is a call nobody can make.
A verb that always fails costs a user a round trip to find that out, so it is
better gone. repo deploy-keys delete followed by add already does the job.
BREAKING CHANGE: bbx repo deploy-keys update is removed. Delete and re-add.
All four permissions-config set verbs accepted none. The schema does not list it and Bitbucket answers 400 "none is not a valid permission", so the option could only ever fail. Clearing a grant is what the remove verbs do. Found while running these endpoints live for the first time, against a disposable empty group in the personal solrevdev workspace. BREAKING CHANGE: --permission none is rejected at parse time. Use the matching remove verb instead.
…mands
The environments changes endpoint, the deploy key update and the eight
permissions-config writes were all carrying guesses. They now carry results.
Coverage moves by one: 109 cover and 33 skip become 108 and 34, because
PUT deploy-keys/{key_id} moved from a command to a documented dead end.
Two things the spec gets wrong are now written down. It claims all eight
permissions-config operations take app passwords only, which is stale, as
those were withdrawn on 28 July 2026 and an API token drives them fine. And
it defines no repository-scoped OIDC path, which is why bbx pipeline oidc
answers 404 rather than the 403 rule 20 describes: two different faults that
looked like one.
Groups turn out to exist only in the 1.0 API. GET /1.0/groups/{workspace}/
still answers with an API token, and it is the only way to read a group slug
from the command line.
pipeline oidc config and keys called
repositories/{ws}/{repo}/pipelines-config/identity/oidc/..., which the spec
does not define. Both answered 403 and rule 20 read that as the API token
limitation that stops pr conflicts and file-conflicts working.
It is not. Aimed at a repository the path answers 404 "There is no API hosted
at this URL"; only the workspace form answers 403. Two faults that looked like
one, and one of them is a path that does not exist.
workspace pipelines oidc already calls the real path, so nothing is lost.
The coverage counts do not move: these rows were never in the table, because
the endpoint they called is not in the spec.
BREAKING CHANGE: bbx pipeline oidc is removed. Use bbx workspace pipelines
oidc, which is also the only form in Atlassian's OIDC guidance.
The tag check asked whether any release tag existed, not whether this version had been released. So every push to master took the bump branch, and a push event carries no bump input, which defaults to patch. Merging this branch would therefore have published 1.1.1 and skipped 1.1.0 entirely, overwriting the deliberate bump commit, and a release that adds 108 endpoints would have gone out signalled as a patch. The only way to ship a minor was workflow_dispatch, which the push trigger beats to it on merge. Checking for v$CURRENT instead makes the csproj version the release number, which is what its own comment always said it wanted. The guard survives: once that version is tagged a push bumps, so no run can push a version NuGet already has. Verified against the real origin tags: 1.1.0 with no bump input now publishes 1.1.0, while 1.0.2 still bumps to 1.0.3, 1.1.0 or 2.0.0 for patch, minor and major.
The four user-keyed ones were the last thing on this branch running on unit tests alone. A second workspace member now exists, so they have been run: PUT and DELETE, repository and project, read, write, admin and create-repo. Both selectors were exercised, an account UUID with its braces and an account ID. Also records why the owner cannot be the target, which is what made this awkward to test at all, and that the spec's "app passwords only" sentence is wrong for every one of the eight.
A pass over every command, walking the tree from --help and checking each
leaf against the docs, found the option lists had drifted. None of this came
from this branch; they were wrong before it.
repo create claimed --scm, --language, --main-branch and --project-key.
It takes --private, --description, --project, --fork-policy.
repo fork claimed --workspace-target. It is --to-workspace.
repo clone claimed --protocol https|ssh. It is a bare --ssh.
pr create claimed a repeatable --reviewer. It is --reviewers.
bbx version was the only command missing from the README.
All 246 leaf commands are now named in both the README and the agent guide,
and every flag either doc attributes to a command is one that command
accepts.
solrevdev
force-pushed
the
feat/pr-update
branch
from
August 6, 2026 09:23
41e6fab to
4311445
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Started as
pr updateand grew into closing the whole gap between whatBitbucket Cloud's API offers and what
bbxcould reach.Coverage
bbxDiffing the published OpenAPI spec against the endpoints the source calls left
142 gaps once the deprecated operations were dropped. 108 were worth covering
and all 108 now have a command.
docs/api-coverage.mdrecords a verdict and areason for every one, including the 34 skipped.
The 151 and 259 are counted by hand-checking what the matching script cannot
see. The script itself reports 149 and 231, because it looks for one string
literal per path and a handler often builds the path across a local or two. Both
counts move by the same 108.
What is new
Read-and-update halves for groups that could only create, list and delete:
update,file-conflicts,override-settings,branching-model effective,default-reviewers view, and anaccessgroup for the explicit user and group permissions.
diffstat,conflicts,merge-status,tasks view, the fivecomment verbs, and
activitywith no ID for the repository-wide feed.status view, and--include/--excludeon
list.refs,restrictions viewandrestrictions update.config,ssh,step,test-case-reasons, thedeploymentswrites and their variables,
deploys, and the report and annotation writes CIneeds to publish its findings.
commits,diff,patch,--revisionon the writes, andcomments --update.workspaces,gpg-keys,emails --email, two workspace-scopedpermission reads.
member,repo-permissions,gpg-key,pullrequests, apipelinesgroup, andproject update,branching-model settings,default-reviewers viewandaccess.bbx user workspacesis worth calling out:workspace listreads/2.0/workspaces, which Bitbucket withdrew, and this is the endpoint thatreplaced it.
src lsno longer requires--ref. Without one it lists the root of the mainbranch, which saves looking up whether it is called main or master.
The spec is now in the repository
docs/spec/swagger.jsonis a pinned copy of the Bitbucket Cloud spec, so everyverdict in
docs/api-coverage.mdcan be checked without a network call.scripts/fetch-spec.shrefreshes it and records the date, hash and countsbeside it.
Bitbucket serves the spec as a single 921 KB line, which no editor,
greporagent can read, so the copy is pretty printed and key sorted. That is 34,421
lines and most of this pull request's diff. It is documentation, not code.
What the API does that the docs do not say
Found by running everything against a throwaway repository. Each is now handled
and written up in
CLAUDE.md.details. The spec marks nothing required; Bitbucket answers"Cannot build Report, some of required attributes are not set [details]".
typediscriminator. Without onethe 400 carries no message at all.
DELETE pipelines-config/cachesis not "clear everything". It takes?name=and clears every cache with that name.
PUT override-settingsanswers 204, so the handler read back an emptyJsonElementand the serializer threw on a call that had worked./user/workspacesreturnsworkspace_accessrecords, so the slug sits onelevel down. Reading it flat gave a list of nulls.
PUT deploy-keys/{id}cannot succeed, though the spec documents both a bodyand a worked curl example for it. Without the key Bitbucket calls it invalid;
with it, it refuses to change a key's contents.
Errors now carry
error.detailanderror.data.arguments. Adding a known hostfor
bitbucket.orgfailed with a bare "Bad request" until those were surfaced;the reason was there all along.
What the second live run settled
Three commands shipped unverified. A run on 2026-08-06 against a throwaway
repository, a disposable empty group and a new project in the personal
solrevdevworkspace closed two of them and half of the third. Two were fixedby watching the Bitbucket web UI make the call, which turned a guessing game
into a read.
pipeline deployments changesworks, and the command was wrong. The web UIposts
{"change":{"name":"..."}}to the path with a trailing slash and gets a202 with no body, because the change is queued rather than applied. The
Terraform lead was right. Two error keys tell you where you are:
request.validation-errormeans the envelope is wrong,environment.change-not-supportedmeans the envelope parsed and the field isnot changeable. Only
nameandrestrictions.admin_onlyare changeable, andthere is no lock resource anywhere in 2.0, so
--lockand--unlockcouldnever have worked. They are replaced by
--admin-onlyand--no-admin-only.--reasonis gone with them.repo deploy-keys updatecannot succeed, and is removed. Eight bodies wentout, including the one this PR previously hoped for: the key with its comment
appended exactly as
ssh-keygenwrote it. Same 400. The Access keys screenoffers only view and delete, no rename, so it is not a body nobody has guessed.
It is a call nobody can make.
bbx pipeline oidcis removed too. It was two faults wearing one hat. Therepository path answers 404 "There is no API hosted at this URL"; only the
workspace form answers the 403 that rule 20 describes.
workspace pipelines oidcalready calls the real path.All eight
permissions-configwrites are confirmed. Group and user forms,repository and project,
read,write,adminandcreate-repo, PUT andDELETE. The user forms take an account UUID or an account ID; both were
exercised. Three spec claims fall over: the "app passwords only" sentence is
stale, since an API token drives all eight;
noneis not a permission, so--permission noneis gone andremovedoes that job; and a user-keyed grantcannot name the workspace owner, which is why these needed a second member
rather than a self-grant.
What is still not verified
Stated plainly rather than implied away. None of it blocks a merge.
pr conflicts,repo file-conflictsand the workspace OIDC discovery pair all report "Thisresource does not support authentication using the provided token".
variables exist. Add, update and delete all work.
key a CLI cannot hold, pipelines runners need real hardware, and account
credential writes would mutate the only real account on this machine.
Verification
dotnet buildanddotnet testclean. 409 tests, up from 274.solrevdevworkspace was deleted andthe deletion confirmed. No probe repositories, groups, projects or snippets
remain.
1.1.0was neverpublished and only one of the three,
pipeline oidc, exists in a releasedversion, where it has answered 404 since the day it shipped. No working script
can break.
whether any
v*tag existed rather than whether this version had shipped, soevery push to
mastertook the bump branch, and a push event carries no bumpinput and defaults to patch. Merging would have published 1.1.1, skipped
1.1.0, and overwritten the bump commit. Checking
refs/tags/v$CURRENTinsteadmakes the csproj version the release number, which is what the comment above
it always said. Once a version is tagged a later push still bumps, so no run
can push a version NuGet already has. Confirmed by running the workflow's own
version script against the real origin tags: 1.1.0 now publishes 1.1.0, while
1.0.2 still gives 1.0.3, 1.1.0 and 2.0.0 for patch, minor and major.