Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
cb83439
feat(pr): add pr update to change an open pull request
solrevdev Aug 5, 2026
cd6de83
docs: record the API coverage decision for every uncovered endpoint
solrevdev Aug 5, 2026
7d7813d
feat(repo): add repo update and fill the repo-group gaps
solrevdev Aug 5, 2026
6660934
feat(pr): edit and resolve comments, read conflicts, diffstat and mer…
solrevdev Aug 5, 2026
741d37c
feat(commit): add commit comments and read one build status
solrevdev Aug 5, 2026
c929d87
feat(access): read and set explicit repository and project permissions
solrevdev Aug 5, 2026
f388f0c
feat(pipelines): add config, SSH, deployments and the report writes
solrevdev Aug 5, 2026
6c0fd8b
feat(workspace): add snippet revisions, account reads and workspace s…
solrevdev Aug 5, 2026
c58c7fb
fix: correct four endpoints the docs get wrong, and say why a call fa…
solrevdev Aug 5, 2026
94ee7a4
docs: list every new command and the API behaviour the run uncovered
solrevdev Aug 5, 2026
e69c57d
chore: bump version to 1.1.0
solrevdev Aug 5, 2026
9cc86bc
docs(spec): pin the Bitbucket API spec and a script to refresh it
solrevdev Aug 5, 2026
7001348
fix(pipelines): post a change envelope to the environments changes en…
solrevdev Aug 6, 2026
7878215
feat(repo)!: drop deploy-keys update, which cannot succeed
solrevdev Aug 6, 2026
fae554d
fix(access)!: stop offering none as a permission
solrevdev Aug 6, 2026
aeb5367
docs: record what the live run settled about the three unverified com…
solrevdev Aug 6, 2026
1a2e30c
feat(pipelines)!: drop the repository-scoped oidc commands
solrevdev Aug 6, 2026
2e95f6c
ci: publish the version in the csproj, not a patch bump of it
solrevdev Aug 6, 2026
20a0282
docs: all eight permissions-config writes are confirmed live
solrevdev Aug 6, 2026
4311445
docs: correct four option lists and document bbx version
solrevdev Aug 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,12 @@ jobs:
PATCH=${BASH_REMATCH[3]}
BUMP=${REQUESTED_BUMP:-patch}

# The very first release ships the version already in the csproj.
# Every run after that bumps, so a later run can never try to push a
# version NuGet already has.
if git ls-remote --exit-code --tags origin 'refs/tags/v*' >/dev/null 2>&1; then
# The csproj version is the release number. If it has no tag yet it
# ships as it stands, so raising it by hand is how you choose a minor
# or a major. Only once that version has been released does a push
# bump, which is what stops a later run pushing a version NuGet
# already has.
if git ls-remote --exit-code --tags origin "refs/tags/v$CURRENT" >/dev/null 2>&1; then
case "$BUMP" in
major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;;
minor) MINOR=$((MINOR + 1)); PATCH=0 ;;
Expand All @@ -114,9 +116,10 @@ jobs:
;;
esac
VERSION="$MAJOR.$MINOR.$PATCH"
echo "v$CURRENT is already released; bumping $BUMP."
else
VERSION="$CURRENT"
echo "No release tags yet; publishing the initial version as-is."
echo "v$CURRENT has not been released; publishing the csproj version as-is."
fi

TAG="v$VERSION"
Expand Down
13 changes: 13 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,19 @@ Break these and something fails in production but not in the tests:
- Destructive verbs take `--yes` and confirm otherwise.
- New behaviour ships with a test.

## The API spec

`docs/spec/swagger.json` is a pinned copy of the Bitbucket Cloud spec. Read it
before guessing at an endpoint, and do not fetch the documentation site:

```bash
jq '.paths["/repositories/{workspace}/{repo_slug}/deploy-keys/{key_id}"]' docs/spec/swagger.json
```

`scripts/fetch-spec.sh` refreshes it and records the date beside it. The spec is
often wrong about request bodies and required fields, so confirm anything it
tells you against a real call before writing it down as fact.

## Live testing

If you test against real Bitbucket:
Expand Down
88 changes: 88 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,14 @@ src/Bbx/
Features/<Area>/<Verb>/ Request record + Handler, co-located
Composition/ ServiceRegistration (DI), JsonOptions
tests/Bbx.Tests/ FakeHttpMessageHandler, InMemoryCredentialStore, CaptureConsole
docs/spec/ pinned Bitbucket API spec; query it, do not fetch the docs site
```

Before guessing at an endpoint, read it: `jq '.paths["<path>"]' docs/spec/swagger.json`.
`scripts/fetch-spec.sh` refreshes the copy and records the date beside it. The
spec is often wrong about bodies and required fields, which is what most of the
rules below are; it is still the right place to start.

## Rules that bite

1. **Handlers hold the logic.** A file in `Commands/` parses arguments and calls
Expand Down Expand Up @@ -78,6 +84,88 @@ tests/Bbx.Tests/ FakeHttpMessageHandler, InMemoryCredentialStore, CaptureCons
compile-time check that each bound symbol matches its handler parameter.
A broken command definition still compiles, so parse-level behaviour is
covered by `CommandSurfaceTests`.
12. **An absent array option parses to an empty array, not null.** A handler that
keys off null therefore treats "flag not given" as "set this to nothing".
`UpdatePullRequestHandler` only sends `reviewers` when the array is non-empty,
because the null check shipped a bug that stripped the reviewers off every
pull request it touched.
13. **`PUT pullrequests/{id}` merges, and drops `close_source_branch` on its own.**
Fields left out of the body keep their value, so send only what changed.
`close_source_branch` is the exception: Bitbucket applies it 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, it is discarded, and the 200 response
still echoes the value you sent. `UpdatePullRequestHandler` reads the pull
request first and refuses rather than report a change that did not land.
Only open pull requests can be updated at all.

14. **A write that answers 204 has no body**, so the client deserializes it to
the default `JsonElement`. Serializing that throws "Operation is not valid
due to the current state of the object", which turns a call that worked
into an unexplained error. `CommandRunner` prints null instead;
`override-settings update` reads the settings back, because the caller
wanted to see them anyway.
15. **Bitbucket resolves several payloads by a `type` discriminator.** A report
needs `"type": "report"`, an annotation `"report_annotation"`, a known host
`"pipeline_known_host"` with `"pipeline_ssh_public_key"` nested inside. A
body without one is answered with a 400 carrying no message at all.
16. **A report needs `details`.** The spec marks nothing required and the field
reads as optional; Bitbucket answers "Cannot build Report, some of required
attributes are not set [details]". `--details` is therefore required.
17. **`DELETE pipelines-config/caches` is not "clear everything".** It takes
`?name=` and clears every cache with that name whatever its UUID, and
answers a bare 400 without it.
18. **`/user/workspaces` returns `workspace_access` records, not workspaces.**
The slug and uuid sit under a nested `workspace`; the top level carries only
whether the caller is an administrator. This is also the working
replacement for the withdrawn `/2.0/workspaces`.
19. **`PUT deploy-keys/{id}` cannot succeed, and the command is gone.** Without
`key` Bitbucket says the key is invalid; with it, that you may not change a
key's contents. Eight bodies were tried on 2026-08-06, including the key
with its comment appended exactly as `ssh-keygen` wrote it, an empty key, a
different key and a `type` discriminator. All 400. The web UI offers no
rename control either, only view and delete. Do not add the verb back.
Delete and re-add.
20. **Some endpoints refuse an API token**, answering 403 "This resource does
not support authentication using the provided token". Pull request and file
conflicts and the *workspace* OIDC discovery endpoints are the ones found so
far. It is not a scope problem and no scope fixes it. Do not confuse this
with a path that does not exist: the repository-scoped
`repositories/{ws}/{repo}/pipelines-config/identity/oidc/...` answers 404
"There is no API hosted at this URL", because only the workspace form is
real. `bbx pipeline oidc` called it and has been removed; do not add it
back. `bbx workspace pipelines oidc` is the one that reaches a real path.
21. **Errors carry `detail` and `data.arguments` as well as the message.**
Bitbucket often answers a bare "Bad request" and puts the reason in an
argument, which is how "SSH for this hostname is already configured by
Bitbucket" stayed invisible until `EnsureSuccessAsync` printed it.
22. **`POST environments/{uuid}/changes/` takes a change envelope.** The body is
`{"change": {...}}`, not a set of fields, and it answers 202 with an empty
body because the change is queued. The spec documents no body at all; this
was read off the web UI, which posts exactly that. Only `name` and
`restrictions.admin_only` can be changed. `lock`, `rank`, `hidden`,
`environment_type` and `environment_lock_enabled` are all answered with 400
`deploy-service.environment.change-not-supported`, and there is no lock
resource anywhere in 2.0. A body without `change` gets a different 400,
`deploy-service.request.validation-error`, which is how you tell a wrong
envelope from an unchangeable field. The trailing slash is optional.
23. **`none` is not a permission.** `permissions-config` takes `read`, `write`,
`admin`, and `create-repo` on projects. `none` is answered with 400 "none is
not a valid permission". Use `DELETE` to clear a grant.
24. **The permissions-config spec text about app passwords is stale.** All eight
operations claim "The only authentication method for this endpoint is via
app passwords". App passwords were withdrawn on 28 July 2026 and an API
token drives them fine; all eight were run live on 2026-08-06. A user-keyed
grant cannot name the workspace owner: Bitbucket answers 400 "This user is
linked to this workspace, so their access ... cannot be modified or
removed", so a second member is the only way to test those four. The
selector may be an account UUID with its braces or an account ID; both
work, and a username does not.
25. **Groups live only in the 1.0 API.** No 2.0 path mentions groups outside
`permissions-config`, so there is no way to list a group slug from 2.0.
`GET /1.0/groups/{workspace}/` still answers 200 with an API token and is
the only way to read one. `GET /1.0/users/{workspace}/invitations` answers
too, and lists invitations that have been sent but not accepted. Both are
probes, not features: do not build on them.

## Auth

Expand Down
116 changes: 102 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ bbx auth status # who am I, which workspace
bbx auth token # print email:token (for curl -u)
bbx auth set-workspace myws # default for -w
bbx auth logout # clear stored credentials
bbx version # print the installed bbx version
```
</details>

Expand All @@ -198,11 +199,22 @@ bbx repo permissions myrepo -w myws
bbx repo watchers -w myws -r myrepo
bbx repo forks list -w myws -r myrepo

bbx repo update myrepo -w myws --description "..." --main-branch trunk
bbx repo file-conflicts 'feat/x..main' -w myws -r myrepo
bbx repo override-settings view|update -w myws -r myrepo

bbx repo hooks list|view|create|update|delete -w myws -r myrepo
bbx repo deploy-keys list|view|add|delete -w myws -r myrepo
bbx repo default-reviewers list|add|remove|effective -w myws -r myrepo
bbx repo branching-model view|settings|update -w myws -r myrepo
bbx repo deploy-keys list|view|add|update|delete -w myws -r myrepo
bbx repo default-reviewers list|view|add|remove|effective -w myws -r myrepo
bbx repo branching-model view|settings|update|effective -w myws -r myrepo
bbx repo access groups list|view|set|remove -w myws -r myrepo
bbx repo access users view|set|remove -w myws -r myrepo
```

`repo update` changes only the fields you name, like `pr update`. Bitbucket
rejects a `--language` it does not recognise. There is no `repo deploy-keys
update`: Bitbucket refuses to change a key's contents and refuses a body
without them, so delete and re-add instead.
</details>

<details>
Expand All @@ -212,6 +224,8 @@ bbx repo branching-model view|settings|update -w myws -r myrepo
bbx pr list -w myws -r myrepo --state OPEN --limit 25
bbx pr view 42 -w myws -r myrepo
bbx pr create --title "Fix" --source feat/x --dest main -w myws -r myrepo
bbx pr update 42 --title "Better title" -w myws -r myrepo
bbx pr update 42 --dest release/next -w myws -r myrepo # retarget
bbx pr merge 42 --strategy squash --yes -w myws -r myrepo
bbx pr decline 42 --reason "superseded" -w myws -r myrepo

Expand All @@ -221,15 +235,27 @@ bbx pr commits 42 -w myws -r myrepo
bbx pr activity 42 -w myws -r myrepo
bbx pr statuses 42 -w myws -r myrepo

bbx pr diffstat 42 -w myws -r myrepo # per-file line counts
bbx pr conflicts 42 -w myws -r myrepo
bbx pr merge-status 42 --task-id <id> -w myws -r myrepo
bbx pr activity -w myws -r myrepo # no ID: the whole repository

bbx pr comment 42 --body "LGTM" -w myws -r myrepo
bbx pr comments 42 -w myws -r myrepo
bbx pr comment-view|comment-update|comment-delete 42 --comment-id <id> -w myws -r myrepo
bbx pr comment-resolve|comment-unresolve 42 --comment-id <id> -w myws -r myrepo
bbx pr approve|unapprove 42 -w myws -r myrepo
bbx pr request-changes|unrequest-changes 42 -w myws -r myrepo
bbx pr tasks list|add|update|complete|delete 42 -w myws -r myrepo
bbx pr tasks list|view|add|update|complete|delete 42 -w myws -r myrepo
```

Merge strategies: `merge_commit` (default), `squash`, `fast_forward`.
`merge`, `fast-forward` and `ff` are accepted as aliases.

`pr update` changes only the fields you name and works on open pull requests
only. `--body ""` clears the description. `--close-source-branch` and
`--no-close-source-branch` need another real change in the same call, because
Bitbucket silently drops the setting otherwise.
</details>

<details>
Expand All @@ -241,8 +267,11 @@ bbx branch view main -w myws -r myrepo
bbx branch create feat/x --target main -w myws -r myrepo
bbx branch delete feat/x --yes -w myws -r myrepo

bbx branch restrictions list -w myws -r myrepo
bbx branch refs -w myws -r myrepo # branches and tags together

bbx branch restrictions list|view -w myws -r myrepo
bbx branch restrictions add --kind push --pattern main -w myws -r myrepo
bbx branch restrictions update 12345 --pattern 'release/*' -w myws -r myrepo
bbx branch restrictions delete 12345 --yes -w myws -r myrepo

bbx branch tag list|view|create|delete -w myws -r myrepo
Expand All @@ -261,15 +290,25 @@ bbx commit approve|unapprove <hash> -w myws -r myrepo
bbx commit filehistory main path/to/file -w myws -r myrepo
bbx commit merge-base 'feat/x..main' -w myws -r myrepo

bbx commit list --include feat/x --exclude main -w myws -r myrepo

bbx commit comment <hash> --body "..." --path src/a.cs --line 3 -w myws -r myrepo
bbx commit comment-view|comment-update|comment-delete <hash> --comment-id <id> -w myws -r myrepo

bbx commit status create <hash> --key ci --state SUCCESSFUL --url https://ci/1 -w myws -r myrepo
bbx commit status update <hash> --key ci --state FAILED --url https://ci/1 -w myws -r myrepo
bbx commit status view <hash> --key ci -w myws -r myrepo
```

`--include` and `--exclude` walk a commit range. Bitbucket takes them in a POST
body only, so passing either switches the verb.
</details>

<details>
<summary><strong>src</strong>: browse and write files</summary>

```bash
bbx src ls -w myws -r myrepo # root of the main branch
bbx src ls --ref main -w myws -r myrepo
bbx src ls src/ --ref main -w myws -r myrepo
bbx src cat --ref main README.md -w myws -r myrepo
Expand All @@ -290,17 +329,40 @@ bbx pipeline logs '{pipeline-uuid}' '{step-uuid}' -w myws -r myrepo
bbx pipeline trigger --branch main -w myws -r myrepo
bbx pipeline stop '{uuid}' --yes -w myws -r myrepo

bbx pipeline variables list|add|delete -w myws -r myrepo
bbx pipeline schedules list|create|delete -w myws -r myrepo
bbx pipeline caches list|clear -w myws -r myrepo
bbx pipeline deployments list|view -w myws -r myrepo
bbx pipeline reports list|view|annotations <hash> -w myws -r myrepo
bbx pipeline step '{pipeline-uuid}' '{step-uuid}' -w myws -r myrepo
bbx pipeline logs '{pipeline}' '{step}' --log-uuid '{log}' -w myws -r myrepo

bbx pipeline config view|update|build-number -w myws -r myrepo
bbx pipeline variables list|view|add|update|delete -w myws -r myrepo
bbx pipeline schedules list|view|create|update|delete|executions -w myws -r myrepo
bbx pipeline caches list|clear|content-uri -w myws -r myrepo
bbx pipeline caches clear --name node --yes -w myws -r myrepo
bbx pipeline ssh key-pair view|set|delete -w myws -r myrepo
bbx pipeline ssh known-hosts list|view|add|update|delete -w myws -r myrepo

bbx pipeline deployments list|view|create|delete|changes -w myws -r myrepo
bbx pipeline deployments variables list|add|update|delete -e '{env}' -w myws -r myrepo
bbx pipeline deploys list|view -w myws -r myrepo

bbx pipeline reports list|view|update|delete <hash> -w myws -r myrepo
bbx pipeline reports annotations|annotations-create <hash> <report-id> -w myws -r myrepo
bbx pipeline reports annotation-view|annotation-update|annotation-delete <hash> <report-id> <ann-id> -w myws -r myrepo
bbx pipeline test-reports '{pipeline}' '{step}' -w myws -r myrepo
bbx pipeline test-cases '{pipeline}' '{step}' -w myws -r myrepo
bbx pipeline oidc config|keys -w myws -r myrepo
bbx pipeline test-case-reasons '{pipeline}' '{step}' '{test-case}' -w myws -r myrepo
```

UUIDs include the braces. Quote them so your shell doesn't expand them.

`deployments` manages the environments; `deploys` reads the records of what was
released to them. `deployments changes` takes `--name`, `--admin-only` and
`--no-admin-only`, and nothing else: Bitbucket refuses to change an
environment's lock, rank, type or hidden flag through it. `caches clear` takes a
cache UUID, or `--name` to clear every cache with that name. `reports update`
needs `--details`: Bitbucket refuses a report without it whatever the docs say.
`pipeline config` answers 404 until Pipelines has been enabled on the repository
at least once. There is no `pipeline oidc`: only the
workspace form of that path exists, and `workspace pipelines oidc` reaches it.
</details>

<details>
Expand All @@ -320,16 +382,26 @@ bbx download delete build.zip --yes -w myws -r myrepo
```bash
bbx workspace view myws
bbx workspace members -w myws
bbx workspace member '{account-uuid}' -w myws
bbx workspace permissions -w myws
bbx workspace repo-permissions -w myws [--repo myrepo]
bbx workspace gpg-key -w myws
bbx workspace pullrequests '{account-uuid}' -w myws --state OPEN
bbx workspace hooks list|view|create|update|delete -w myws

bbx workspace pipelines variables list|view|add|update|delete -w myws
bbx workspace pipelines oidc config|keys -w myws

bbx workspace project list -w myws
bbx workspace project view KEY -w myws
bbx workspace project create --key KEY --name "Name" -w myws
bbx workspace project update KEY --description "..." -w myws
bbx workspace project delete KEY --yes -w myws
bbx workspace project default-reviewers list|add|remove --project-key KEY -w myws
bbx workspace project default-reviewers list|view|add|remove --project-key KEY -w myws
bbx workspace project deploy-keys list|view|add|delete --project-key KEY -w myws
bbx workspace project branching-model view|update --project-key KEY -w myws
bbx workspace project branching-model view|settings|update --project-key KEY -w myws
bbx workspace project access groups list|view|set|remove --project-key KEY -w myws
bbx workspace project access users list|view|set|remove --project-key KEY -w myws
```
</details>

Expand All @@ -338,10 +410,17 @@ bbx workspace project branching-model view|update --project-key KEY -w myws

```bash
bbx snippet list|view|create|update|delete -w myws
bbx snippet view|update|delete <id> --revision <rev> -w myws
bbx snippet files|watch|comments <id> -w myws
bbx snippet commits <id> -w myws
bbx snippet diff|patch <id> <revision> -w myws
bbx snippet comments <id> --update <comment-id> --content "..." -w myws

bbx user view # the authenticated account
bbx user emails
bbx user emails [--email me@x.com]
bbx user workspaces # replaces the withdrawn workspace list
bbx user permissions workspace|workspace-repositories -w myws
bbx user gpg-keys list|view
bbx user ssh-keys list|view|add|delete

bbx issue list|view|create|update|delete -w myws -r myrepo
Expand All @@ -361,6 +440,15 @@ bbx issue comment|comments <id> -w myws -r myrepo
> **HTTP 410 Gone**. Atlassian removed the cross-workspace discovery endpoints
> under CHANGE-2770. Nothing in `bbx` can bring them back. Name the workspace,
> or set one with `bbx auth set-workspace`.
>
> `bbx user workspaces` is the account-scoped replacement for `workspace list`
> and still works.

A few endpoints refuse an API token outright, answering **HTTP 403 "This
resource does not support authentication using the provided token"**. `bbx pr
conflicts`, `bbx repo file-conflicts` and the OIDC discovery commands are the
ones we found. Nothing in `bbx` can work around it; it needs a different
credential type.

## Recipes

Expand Down
Loading