Skip to content

feat: cover the remaining API surface, and record where API tokens are refused - #3

Merged
solrevdev merged 20 commits into
masterfrom
feat/pr-update
Aug 6, 2026
Merged

feat: cover the remaining API surface, and record where API tokens are refused#3
solrevdev merged 20 commits into
masterfrom
feat/pr-update

Conversation

@solrevdev

@solrevdev solrevdev commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Started as pr update and grew into closing the whole gap between what
Bitbucket Cloud's API offers and what bbx could reach.

Coverage

Before After
Spec operations 331 331
Called by bbx 151 259
Live gaps 142 34

Diffing 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.md records a verdict and a
reason 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:

  • repo: update, file-conflicts, override-settings, branching-model effective, default-reviewers view, and an access
    group for the explicit user and group permissions.
  • pr: diffstat, conflicts, merge-status, tasks view, the five
    comment verbs, and activity with no ID for the repository-wide feed.
  • commit: the four comment verbs, status view, and --include/--exclude
    on list.
  • branch: refs, restrictions view and restrictions update.
  • pipeline: config, ssh, step, test-case-reasons, the deployments
    writes and their variables, deploys, and the report and annotation writes CI
    needs to publish its findings.
  • snippet: commits, diff, patch, --revision on the writes, and
    comments --update.
  • user: workspaces, gpg-keys, emails --email, two workspace-scoped
    permission reads.
  • workspace: member, repo-permissions, gpg-key, pullrequests, a
    pipelines group, and project update, branching-model settings,
    default-reviewers view and access.

bbx user workspaces is worth calling out: workspace list reads
/2.0/workspaces, which Bitbucket withdrew, and this is the endpoint that
replaced it.

src ls no longer requires --ref. Without one it lists the root of the main
branch, which saves looking up whether it is called main or master.

The spec is now in the repository

docs/spec/swagger.json is a pinned copy of the Bitbucket Cloud spec, so every
verdict in docs/api-coverage.md can be checked without a network call.
scripts/fetch-spec.sh refreshes it and records the date, hash and counts
beside it.

Bitbucket serves the spec as a single 921 KB line, which no editor, grep or
agent 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.

  • A report needs details. The spec marks nothing required; Bitbucket answers
    "Cannot build Report, some of required attributes are not set [details]".
  • Reports, annotations and known hosts need a type discriminator. Without one
    the 400 carries no message at all.
  • DELETE pipelines-config/caches is not "clear everything". It takes ?name=
    and clears every cache with that name.
  • PUT override-settings answers 204, so the handler read back an empty
    JsonElement and the serializer threw on a call that had worked.
  • /user/workspaces returns workspace_access records, so the slug sits one
    level down. Reading it flat gave a list of nulls.
  • PUT deploy-keys/{id} cannot succeed, though the spec documents both a body
    and 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.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.

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
solrevdev workspace closed two of them and half of the third. Two were fixed
by watching the Bitbucket web UI make the call, which turned a guessing game
into a read.

pipeline deployments changes works, and the command was wrong. The web UI
posts {"change":{"name":"..."}} to the path with a trailing slash and gets a
202 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-error means the envelope is wrong,
environment.change-not-supported means the envelope parsed and the field is
not changeable. Only name and restrictions.admin_only are changeable, and
there is no lock resource anywhere in 2.0, so --lock and --unlock could
never have worked. They are replaced by --admin-only and --no-admin-only.
--reason is gone with them.

repo deploy-keys update cannot succeed, and is removed. Eight bodies went
out, including the one this PR previously hoped for: the key with its comment
appended exactly as ssh-keygen wrote it. Same 400. The Access keys screen
offers only view and delete, no rename, so it is not a body nobody has guessed.
It is a call nobody can make.

bbx pipeline oidc is removed too. It was two faults wearing one hat. The
repository path answers 404 "There is no API hosted at this URL"; only the
workspace form answers the 403 that rule 20 describes. workspace pipelines oidc already calls the real path.

All eight permissions-config writes are confirmed. Group and user forms,
repository and project, read, write, admin and create-repo, PUT and
DELETE. 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; none is not a permission, so
--permission none is gone and remove does that job; and a user-keyed grant
cannot 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.

  • Three commands answer 403 that no scope changes. pr conflicts, repo file-conflicts and the workspace OIDC discovery pair all report "This
    resource does not support authentication using the provided token".
  • The deployment variables list endpoint answers an empty page even when
    variables exist. Add, update and delete all work.
  • 34 endpoints were skipped on purpose. Connect app properties need an app
    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 build and dotnet test clean. 409 tests, up from 274.
  • Every throwaway created in the personal solrevdev workspace was deleted and
    the deletion confirmed. No probe repositories, groups, projects or snippets
    remain.
  • Version stays at 1.1.0. Three commands are removed, but 1.1.0 was never
    published and only one of the three, pipeline oidc, exists in a released
    version, where it has answered 404 since the day it shipped. No working script
    can break.
  • The release workflow would not have shipped 1.1.0. Its tag check asked
    whether any v* tag existed rather than whether this version had shipped, so
    every push to master took the bump branch, and a push event carries no bump
    input and defaults to patch. Merging would have published 1.1.1, skipped
    1.1.0, and overwritten the bump commit. Checking refs/tags/v$CURRENT instead
    makes 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.

solrevdev and others added 11 commits August 5, 2026 17:02
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.
@solrevdev solrevdev changed the title feat(pr): add pr update feat: cover the remaining Bitbucket Cloud API surface Aug 5, 2026
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>
@solrevdev solrevdev changed the title feat: cover the remaining Bitbucket Cloud API surface feat: cover the remaining API surface, and record where API tokens are refused Aug 5, 2026
…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
solrevdev merged commit ae49972 into master Aug 6, 2026
2 checks passed
@solrevdev
solrevdev deleted the feat/pr-update branch August 6, 2026 09:25
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