Skip to content

Use bb in a CI Pipeline

A pipeline runs bb with nobody to answer a question, and branches on what it returns. What that takes, in one place.

The credential

Give the job BITBUCKET_URL and BITBUCKET_TOKEN from the CI system's secret store. bb reads them from the environment and writes nothing to disk, so no bb auth login is needed. On a runner that other jobs share, set BB_DISABLE_STORED_CONFIG=1 as well: a configuration file another job left behind is then not read.

No flag takes a token: a value on the command line is visible to other processes on the runner.

Nobody to ask

With no terminal, bb never waits for an answer: a command missing a value fails and names it. --no-input makes that explicit wherever a pipeline might have a terminal attached. A command that changes something, such as a merge, is not refused for want of a person; one that deletes asks for --yes.

Exit codes

Every failure exits with the code of its kind, so a step can tell them apart without reading the message:

Exit code Kind What to do
0 It went through
2 validation Fix the command
3 authentication, authorization Check the token and its rights
4 not_found Check the project, repository or id
5 conflict Read the state again, then decide
10 transient Retry
11 not_implemented Stop: it is not implemented
12 cancelled It was interrupted: do not retry it automatically
13 unknown_outcome Do not retry: read the state first, since the change may have been made
14 unsupported This Bitbucket release cannot do it; a newer one can
1 permanent, internal Stop

unknown_outcome is the one to handle on purpose. The request was sent, and whether Bitbucket applied it is unknown, so running it again may do it twice. Machine Mode and Diagnostics lists every kind.

Under --json a step gets one document on stdout: the result under data, or the failure under error, with its kind.

Wait for the builds

bb pr checks reports the builds through its exit code, as gh pr checks does: 1 when one failed, 8 when none failed and one is still running or has no result, and 0 otherwise. A pipeline that waits for them:

status=0
while bb pr checks 42 --repo PAY/payments --no-input || status=$?; [ "$status" -eq 8 ]; do
  status=0
  sleep 30
done
[ "$status" -eq 0 ] || { echo "a build of pull request 42 failed" >&2; exit 1; }

Check before changing

--dry-run says what the real run would do, and in text it exits as the real run would. So a step can refuse to go on unless the change would go through:

bb pr merge 42 --repo PAY/payments --dry-run --no-input
bb pr merge 42 --repo PAY/payments --no-input

Under --json a dry run exits 0 whenever it reached a verdict, and the verdict is in the document: preview.error is there exactly when the run would fail. Dry-Run Planning says what each command checks.

A whole step

#!/usr/bin/env bash
set -euo pipefail
# BITBUCKET_URL and BITBUCKET_TOKEN come from the CI system's secret store.
export BB_DISABLE_STORED_CONFIG=1

repo=PAY/payments
pr=42

# Wait while a build is still running.
status=0
while bb pr checks "$pr" --repo "$repo" --no-input || status=$?; [ "$status" -eq 8 ]; do
  status=0
  sleep 30
done
[ "$status" -eq 0 ] || { echo "a build of pull request $pr failed" >&2; exit 1; }

# Merge once the preview says it would go through.
bb pr merge "$pr" --repo "$repo" --dry-run --no-input

status=0
bb pr merge "$pr" --repo "$repo" --no-input || status=$?
if [ "$status" -eq 13 ]; then
  # Sent, and whether it merged is unknown: read the state rather than retry.
  state=$(bb pr get "$pr" --repo "$repo" --json --no-input | jq -r '.data.pullRequest.state')
  [ "$state" = MERGED ] || { echo "pull request $pr is $state after an unknown outcome" >&2; exit 1; }
elif [ "$status" -ne 0 ]; then
  exit "$status"
fi