Skip to content

Auto-merge when CI is green

This recipe requires an approval and an explicit list of CI check keys. It reads every status page with bb api --paginate, then checks that the PR head still matches the commit inspected.

Each required key must have exactly one status with state: SUCCESSFUL. Every returned status must also be successful. Missing checks, zero checks, duplicate keys, INPROGRESS, STOPPED and unknown states never permit a merge. A failed check aborts the run.

Set REQUIRED_CHECK_KEYS to the exact keys your CI publishes, separated by spaces. These are status keys, not display names. Keep keys free of whitespace for this script.

bb pr checks 42 --watch waits for running checks and exits non-zero when one failed or stopped, but it does not know which keys you require, so this recipe reads the statuses itself.

Terminal window
bb api "/repositories/$WORKSPACE/$REPO/pullrequests/42/statuses" --paginate --json \
| jq --arg keys 'build test' '
.values as $statuses
| ($keys | split(" ")) as $required
| ($statuses | length) > 0
and all($statuses[]; .state == "SUCCESSFUL")
and ([$statuses[].key] | length == (unique | length))
and all($required[];
. as $key
| [$statuses[] | select(.key == $key)]
| length == 1)
'

Save as auto-merge-on-green.sh. It exits 0 after a successful merge, 1 on errors or timeout, and 2 when --once finds a PR that is not ready.

#!/bin/bash
set -euo pipefail
WORKSPACE="${WORKSPACE:?set WORKSPACE}"
REPO="${REPO:?set REPO}"
PR_ID="${1:?Usage: $0 <pr-id> [--once]}"
REQUIRED_CHECK_KEYS="${REQUIRED_CHECK_KEYS:?set required CI check keys}"
TIMEOUT_SECONDS="${TIMEOUT_SECONDS:-1800}"
POLL_INTERVAL="${POLL_INTERVAL:-30}"
once="${2:-}"
path="/repositories/$WORKSPACE/$REPO/pullrequests/$PR_ID"
required=$(printf '%s' "$REQUIRED_CHECK_KEYS" | jq -Rc 'split(" ") | map(select(length > 0)) | unique')
jq -e 'length > 0' <<< "$required" >/dev/null
deadline=$(( $(date +%s) + TIMEOUT_SECONDS ))
while true; do
pr=$(bb api "$path" --json)
[ "$(jq -r '.state' <<< "$pr")" = OPEN ] || exit 2
head=$(jq -er '.source.commit.hash | select(type == "string" and length > 0)' <<< "$pr")
statuses=$(bb api "$path/statuses" --paginate --json)
jq -e '.values | type == "array"' <<< "$statuses" >/dev/null
if jq -e 'any(.values[]; .state == "FAILED")' <<< "$statuses" >/dev/null; then
echo "PR #$PR_ID has a failed check." >&2
exit 1
fi
if jq -e --argjson required "$required" '
.values as $statuses
| ($statuses | length) > 0
and all($statuses[]; .state == "SUCCESSFUL")
and ([$statuses[].key] | length == (unique | length))
and all($required[];
. as $key
| [$statuses[] | select(.key == $key)]
| length == 1)
' <<< "$statuses" >/dev/null; then
current=$(bb api "$path" --json)
if jq -e --arg head "$head" '
.state == "OPEN"
and .source.commit.hash == $head
and any(.participants[]?; .approved == true)
' <<< "$current" >/dev/null; then
bb pr merge "$PR_ID" -w "$WORKSPACE" -r "$REPO" \
--strategy squash --close-source-branch
exit 0
fi
fi
[ "$once" != --once ] || exit 2
if [ "$(date +%s)" -ge "$deadline" ]; then
echo "Timed out waiting for PR #$PR_ID checks and approval." >&2
exit 1
fi
sleep "$POLL_INTERVAL"
done
Terminal window
WORKSPACE=myworkspace REPO=myrepo REQUIRED_CHECK_KEYS="build test" \
./auto-merge-on-green.sh 42

Save the script above alongside this one. Run it on a schedule to attempt each ready PR once. A failed check, API error or rejected merge makes the scan exit nonzero after processing the remaining PRs.

#!/bin/bash
set -euo pipefail
WORKSPACE="${WORKSPACE:?set WORKSPACE}"
REPO="${REPO:?set REPO}"
export WORKSPACE REPO
export REQUIRED_CHECK_KEYS="${REQUIRED_CHECK_KEYS:?set required CI check keys}"
script_dir=$(cd "$(dirname "$0")" && pwd)
open_prs=$(bb pr list -w "$WORKSPACE" -r "$REPO" --all --json --jq '.pullRequests[].id')
failed=0
for pr_id in $open_prs; do
if "$script_dir/auto-merge-on-green.sh" "$pr_id" --once; then
echo "Merged PR #$pr_id"
else
status=$?
if [ "$status" -eq 2 ]; then
echo "PR #$pr_id: not ready"
else
echo "PR #$pr_id: check or merge failed" >&2
failed=1
fi
fi
sleep 2
done
exit "$failed"

Both scripts need Bash and external jq. --all makes the PR scan include older open PRs rather than only the default first 25.