Merge changes

Promote main to other branches

Move a branch such as staging only to a checked commit from main, and see which commit is ready.

A promotion moves a branch such as staging or production to a commit that is already on main. [[target]] in .bay/config.toml names each such branch and the rule it moves by. The rule names the checks that must pass, and how old the commit must be. [[freeze]] holds a branch until a set time, except for fixes and undos.

bay has no promote command. A person promotes with a plain git push to the branch. Or, with advance = true, the executor moves the branch for you. bay shows each branch's state in bay status, and each move in bay watch. On a host from bay host init, the hook refuses a push that breaks the rule. On any other host, bay sees a bad move only after the push. bay deploys nothing. Your deploy tool reads the branch.

When to use promotion

  • Stage a release: staging takes only main commits whose required checks passed.
  • Soak before production: production takes a commit from staging only once it is old enough.
  • Freeze for a holiday: until the freeze ends, only fixes and undos reach the branch.
  • See what is ready: bay status names the newest commit you can promote now.
  • Follow green main: with advance = true, the executor moves staging to each new promotable commit.

Declare a target

Add one [[target]] per branch to .bay/config.toml on main. name names the target, and ref defaults to refs/heads/<name>. from is main by default, or the name of another target, such as staging. requires lists check ids from .bay/policy.toml, for example your post checks. soak_s is the minimum age of the commit in seconds, by its committer time. It defaults to 0.

toml
[[target]]
name = "staging"
requires = ["integration"]         # check ids from .bay/policy.toml

[[target]]
name = "production"
from = "staging"                   # promoted from staging, not from main
requires = ["integration"]
soak_s = 86400                     # the commit is at least one day old

Freeze a target

[[freeze]] names a target, an end time in UTC to the second, and a reason. Until end, the target takes only a commit with a Fixes: or Reverts: trailer. bay start --fixes and bay undo write those trailers on the main commit. bay reads freezes from main's tip only, so a freeze you remove there ends at once. A [[freeze]] whose target names main holds main itself. Configuration explains that hold.

toml
[[freeze]]
target = "production"
end = "2026-12-27T00:00:00Z"       # RFC 3339 UTC, to the second
reason = "holiday freeze"

Promote a commit

Read the target's row in bay status, then push its promotable.commit to the target's ref. behind_from counts the commits between that commit and the source's tip. When no commit is ready, promotable.blocked_by says why the newest one fails.

sh
commit=$(bay status --json | jq -r '.detail.targets[] | select(.name == "staging") | .promotable.commit')
git push origin "$commit:refs/heads/staging"

Let the executor promote

Set advance = true on a target, and each pass of bay executor run moves it for you. It moves the target to its newest promotable commit, the one bay status names. Before the push, it judges that commit again by the same rule as the host hook. It pushes under a lease on the tip it read, so it never goes backwards or over another push. A target promoted from another target moves after its source, in the same pass. A target with advance = true needs requires, or the config does not load.

toml
[[target]]
name = "staging"
requires = ["integration"]
advance = true                     # each executor pass moves staging to its newest promotable commit

See each target

bay status fetches the target refs, then lists one row per target under detail.targets. The fleet map lists the same rows from its last fetch. A row bay cannot judge says why under unread, and never reads as ready.

sh
bay status --json | jq '.detail.targets'
FieldMeaning
tipThe commit the target's ref points at, or null when the remote has no such ref.
from, from_tipThe source, main or another target, and its tip.
on_fromWhether the tip is on the source's first-parent line, or is a bay merge that a merge outside bay put off it.
driftCommits on the target that the source does not reach: count, and up to 5 in commits.
lagHow many first-parent commits of the source the target is behind.
requires, soak_sThe rule in force, with the config at the target's tip added.
checksEach required check at the tip: pass, product_failure, flaky, unknown, pending, unknown_check or no_policy.
freezeThe freeze in force, with end and reason, else null.
tip_failuresThe rules the tip breaks: off the source's line, or a required check that does not pass.
promotableThe newest commit you can promote now, with behind_from. Else blocked_by, or at_from when the target is at the source's tip.
fetchedfalse when bay read the copies from its last fetch.

Watch targets move

Outside a view, bay watch lists one event each time a target's tip changes. target_moved means the new tip meets its rule. target_unpromotable means the tip is off its source's line, or a required check does not pass. Each event names target, ref, commit, from and failures. Without a cursor, bay watch lists target_unpromotable for each target whose tip fails now.

sh
bay watch --json | jq '.detail.events[] | select(.kind | startswith("target_"))'

How bay judges a promotion

  • The commit is on the first-parent line of the source's tip, and fast-forwards the target.
  • Every check that requires names passes on the commit's tree, judged as a post check.
  • A check id that the policy does not declare never counts as a pass.
  • The commit's committer time is at least soak_s old.
  • No freeze is in force, or the commit carries Fixes: or Reverts:.
  • bay adds the requires and soak_s from the config at the target's tip, so a rule never weakens.

What promotion cannot do

  • bay moves a target only when it sets advance = true. Otherwise a person or your tooling pushes it.
  • bay deploys nothing. Your deploy tool reads the target branch.
  • bay reads the check records that exist. It starts no check to make a commit ready.
  • Off a host from bay host init, bay cannot stop a bad push. It reports it afterwards.
  • target_unpromotable judges the line and the checks only. Soak and freeze apply when you push.

Refusals

On a host from bay host init, the hook refuses the whole push with PROMOTION_REFUSED. The message names the target's ref and each rule the commit breaks. The hook judges a target push with or without a key registry. It also refuses any push that deletes a target. With Claude Code hooks, the hook refuses an agent's own git push of a target with SHELL_DENIED. The Results page lists what to do next.

RuleMeaning
not_on_fromThe commit is not on the first-parent line of the source's tip, nor a bay merge that a merge outside bay put off it.
not_fast_forwardThe push does not move the target forward from its old tip.
checkA required check does not pass on the commit.
soakThe commit is younger than soak_s.
freezeA freeze is in force, and the commit carries no Fixes: or Reverts:.

Limits

  • Search: promotable reads at most 50 first-parent commits of the source, newest first.
  • Drift: a row names at most 5 drift commits, and counts them all.
  • Clock: the hook judges soak and freeze by the host's clock, and bay status by yours.