Revyl Documentation

Builds

Build branches

How an app's builds are organized into branches, which build each run uses, and how to manage branches from the CLI and the dashboard.

A branch is a named line of builds in one app, such as main, staging, or feature-login. Every build belongs to one branch and never moves to another. Test runs, sessions, and Atlas evidence record the branch of the build they used.

Each app has its own branches, and its first upload sets its default branch. Use separate apps for builds that differ in platform or configuration, such as bundle ID or backend environment, and branches for lines of work on the same app.

What branches are for

  • Keep a source-of-truth branch current. Upload each new main or staging build to that branch and test or explore it, from CI or a cloud AI agent automation, so its results and Atlas map follow the app you ship. See Keep a source-of-truth branch current.
  • Keep feature work apart. Builds you upload from a feature branch go to a branch of the same name, so runs and links on main keep using main's builds.
  • Test a chosen branch once, or save it so every run uses it. See Test a branch.
  • Compare main with a feature branch in Atlas, or by running the same workflow on both. See Compare two branches.
  • Hold a steady release candidate. Pin staging to one build. See Pin a release candidate.

Default branch and default build

The app's default branch is the branch a run uses when nothing else picks one. A new app's default branch is the branch of its first upload, such as main; until its first upload it's base. Pull request preview builds from the Revyl GitHub and GitLab apps don't set it. Change it with revyl app branch default main --app "My App" or Make default branch… in App settings > Build branches. Changing it doesn't change saved choices that name a branch.

Each branch has a default build, the build a run on the branch uses when it doesn't pick one:

Default buildUsesSet it with
Latest buildThe branch's newest usable build. Every branch starts here.revyl app branch unpin, or Change default build… > Latest build. Deleting a branch's pinned build also returns the branch to Latest build.
Pinned buildA build you choose.revyl app branch pin, --pin on revyl build or revyl build upload, Pin “staging” to this build in a build's actions menu, or Change default build… in Build branches.

A usable build isn't deleted and is the kind the app runs: a simulator .app for iOS or an .apk for Android. Uploads still in progress don't count.

No upload changes a pin unless you pass --pin. A branch on Latest build runs each new upload because it's the newest; a pinned branch keeps its pin.

On an app's Builds page, the Default tag marks each branch's default build. In branch lists, Default with a house icon marks the app's default branch.

Git branch detection

Without --branch, uploads go to your Git branch, and runs try it first. The CLI, the terminal UI, the Revyl GitHub Actions (upload-build, run-test, run-workflow), and MCP read the Git branch of the code they run from:

Where it runsGit branch
Your machine, or CI that checks out a branchThe checked-out branch of the project, or of the folder you pass with -C, including linked worktrees.
GitHub Actions or Buildkite, for a pull requestThe pull request's source branch, such as feature-login. A source in another repository is owner:branch, such as contributor:feature-login.
GitHub Actions or Buildkite, other runsThe branch being built. A job without a checkout reads it from the job's environment.
revyl build --remoteThe branch of the source it submits.
MCPThe branch of the project in project_dir or REVYL_PROJECT_DIR, never the server's own folder. See MCP setup.
A tag, or a detached checkout outside GitHub Actions and Buildkite, as GitLab CI and Jenkins use by defaultNone, so the app's default branch is used. Pass --branch, for example --branch "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" on GitLab.

What happens with the detected branch:

  • Uploads go to the Git branch and create it if it doesn't exist. A Git branch name that can't be a Revyl branch fails with Detected Git branch is not a valid Revyl branch; use --branch <name>.
  • Runs, sessions, and explorations use the Git branch when it has a usable build. Otherwise they use the app's default branch and say so, with a notice such as Git branch "feature-login" has no usable build; using the app's default branch "main". A workflow decides this for each app. If the default branch has no usable build either, the run fails and names both branches.

Build lists, Atlas, the MCP build list, and the terminal UI's build browser show All branches until you pick one.

Detection never changes saved settings, and no setting turns it off. To opt out, pass --branch, or set the Revyl GitHub Actions' branch input.

How a run picks its build

A run uses the first of these that applies:

  1. A choice for this run, such as --branch staging, --build-id, --all-branches, or a branch or build picked in a run dialog. See Run options.
  2. The saved choice of the test, workflow, or embed, unless it's Default branch.
  3. Your Git branch, when it has a usable build. See Git branch detection.
  4. The app's default branch.

A choice has a branch part and a build part. In the dashboard, both live in one Build field.

BranchUses
Default branchNo saved branch, so the run goes on to steps 3 and 4: your Git branch, then the app's default branch as it is when the run starts.
A named branchThat branch. Saved choices store its ID, so they survive a rename.
All branchesThe newest usable build on any of the app's branches, always Latest build.
BuildUses
AutomaticThe branch's default build: its pinned build, or else its Latest build.
Latest buildThe branch's newest usable build, even when the branch is pinned.
Exact buildOne build, by ID. It doesn't pin the branch.

Once a run starts, its branch and build stay fixed, including retries.

Choices you make fail strictly. If a branch or build chosen for the run, or saved on the test, workflow, or embed, can't be used, the run fails instead of using another one, for example Branch "staging" on "My App" has no usable build. Upload a build to that branch or choose another. Only a detected Git branch falls back.

New saved choices

  • Tests created in the dashboard, and workflow app overrides when you choose an app, start on All branches · Latest build, the newest build on any branch, so a pull request job can run another pull request's upload. To test the pull request's own build, pass the upload's build ID to the run, or save it on Default branch so it runs on the pull request's own branch.
  • Tests saved from a session, tests created with the CLI, YAML, MCP, or the API, and embeds start on Default branch with Automatic.

Pull request builds

Pull request jobs on GitHub Actions and Buildkite upload to the pull request's source branch. The Revyl GitHub and GitLab apps upload their preview builds there too, as ordinary builds that can become that branch's Latest build. The apps skip pull requests from forks. If a source branch name can't be a Revyl branch, the app's Revyl configuration check fails and no preview is built.

Every source branch you upload from becomes a Revyl branch. To choose between per-PR branches and one shared branch, see Choose where CI uploads go.

Common tasks

Keep a source-of-truth branch current

Choose the branch you want to stay current, usually main or staging, and keep feeding it new builds.

Upload each new build to the branch

Upload with --branch and keep the build ID:

revyl build upload --file ./build/app.apk --app "$APP_ID" \
  --branch main --yes --json > build.json
BUILD_ID=$(jq -er '.build.build_id' build.json)

Test or explore that build

Run your tests on it, explore it to keep the branch's Atlas map current, or both:

revyl workflow run smoke-tests --android-app "$APP_ID" --android-build-id "$BUILD_ID"
revyl explore run "$APP_ID" --build-id "$BUILD_ID"

Repeat for every new build

Run it however suits your team: in CI after every merge, on a daily or weekly schedule, or from a cloud AI agent automation, such as a Cursor automation. Keep main or staging current has GitHub Actions workflows to copy for after merges and weekly.

Merging a pull request doesn't move its builds or Atlas evidence to main, so each new build needs its own run.

Make it the app's default branch. Choices saved on Default branch then use it in dashboard runs, and in runs from your code when your Git branch has no usable build:

revyl app branch default main --app "My App"

Test a branch

Run once on a branch without changing saved settings:

revyl test run login --branch staging
revyl workflow run smoke --branch staging

A workflow's --branch applies to every app it runs; it fails if one of them doesn't have that branch or has no usable build there. In the dashboard, use Run on a branch… on a workflow or a selection of tests: type a branch name or choose All branches, then Automatic or Latest build. On one test, use Run with a build… (Run async with a build… in the editor).

To make every run use a branch, save it:

revyl test build set login --branch staging
revyl workflow app set smoke --ios <ios-app-id> --ios-branch staging

In the dashboard, set the Build field in the test's settings, or turn on App Override in the workflow's Settings tab and set iOS build or Android build. For every option, see Run options and Saved test and workflow choices.

Pin a release candidate

Pin staging to one build so runs on it keep that build while newer builds arrive:

revyl build list --app "My App" --branch staging
revyl app branch pin staging <build-id> --app "My App"

Runs that choose Latest build still use the branch's newest build. Unpin with revyl app branch unpin staging --app "My App".

Compare two branches

  • Atlas. Pick a branch in Atlas's scope control, or open each map with revyl atlas open --app "My App" --branch <name>; links keep the branch, so you can open two side by side. revyl atlas brief and revyl atlas graph take --branch too. Branches filter evidence, not screens: merging or hiding screens applies to the whole app.
  • Test results. Run the same workflow with --branch main and --branch feature-login and compare the reports. A test's history page can be filtered by branch; workflow history shows each run's build and branch.

Check which build a run uses

revyl build which shows the branch and build a run on the app would use, and why, without starting one:

revyl build which --app "My App"
revyl build which --app "My App" --branch staging
revyl build which --app "My App" --json

It uses --branch, otherwise your Git branch when it has a usable build, otherwise the app's default branch, then that branch's default build. It prints the app, branch, reason, build, and default build, and prints the fallback notice on stderr when your Git branch couldn't be used.

  • It reads the app's selection, not a test's or workflow's saved choice.
  • HEAD here is a literal branch name.
  • A failed selection exits nonzero without a result.

Manage branches

TaskCLIApp settings > Build branches
Listrevyl app branch list --app "My App"The branch list
Createrevyl app branch create staging --app "My App"New branch
Renamerevyl app branch rename staging release --app "My App"Rename…
Set the default branchrevyl app branch default staging --app "My App"Make default branch…
Pin or unpinrevyl app branch pin staging <build-id> --app "My App", revyl app branch unpin staging --app "My App"Change default build…, then Latest build or Pinned build

Each row in Build branches also offers View builds and Open in Atlas.

Uploads also create branches: an upload to a branch name or Git branch that doesn't exist yet, or a new name typed in the dashboard's upload sheet. Creating a branch that exists returns it. A pin must be a build on that branch. MCP tools can't rename or pin branches or set the default branch.

Branch names are case-sensitive, unique within the app, 1 to 255 characters, with no leading or trailing spaces and no control characters. A rename keeps the branch's ID, so saved choices follow it; scripts that pass the old name need the new one, and an upload from a Git branch with the old name creates a new branch. Branches can't be deleted or archived. Lists show the default branch first, then the rest by latest activity: creating a branch, a new build, a rename, or a pin change moves it up.

From the CLI:

revyl app branch list --app "My App" --page 2 --page-size 100 --json
  • list marks the default branch (default) and shows 50 branches per page (--page-size up to 100). Its columns are BRANCH, BRANCH ID, DEFAULT BUILD, and BUILD ID.
  • DEFAULT BUILD reads Latest build (v4), Latest build (no builds), Pinned build (v1.2), or Pinned build (v1.2, unavailable). BUILD ID is the build a run on the branch uses now, or - when there is none.
  • pin takes the ID of a build on that branch, not a version label. List IDs with revyl build list --app "My App" --branch staging.
  • The other commands print the branch as Branch: <name> (<id>), or JSON with --json.

CLI reference

The branch and build options, output, and JSON fields of each CLI command.

Upload and list builds

revyl build --profile preview --platform ios --branch staging
revyl build upload --file ./app.apk --app "My App" --branch staging --pin
revyl build list --app "My App"
revyl build list --app "My App" --branch staging --page 2 --page-size 50
revyl build list --app "My App" --branch HEAD
  • --branch chooses the upload's branch and creates it if it doesn't exist. Without it, the upload goes to your Git branch, created if needed, or the app's default branch when none is detected. When --repo and --commit name a different repository or commit than the checkout, the checkout doesn't choose the branch. Pass --branch when the artifact wasn't built from this checkout.
  • revyl init chooses its uploads' branch the same way, so the first build it uploads to an app it created sets that app's default branch.
  • --pin pins the branch to the new build. If the version label already exists, the upload reuses that build and doesn't pin; the warning prints the revyl app branch pin command to run.
  • Version labels are unique within an app among builds that aren't deleted. A label used on another branch fails with This version already belongs to another branch; use a unique version.
  • The output prints App:, Build Version:, Build ID:, and Branch: with what a run on the branch uses now: staging (Latest build: runs use this upload), staging (Latest build: runs use v4), staging (pinned to v1.2; runs that don't choose a build keep using v1.2), or staging (pinned to this build). Keep the build ID to run that exact build later; a remote build job ID isn't a build ID.

build list shows every branch of the app. --branch <name> narrows it to one branch. When no Revyl branch has that name, it lists builds built from a Git branch of that name instead. --branch HEAD means your checked-out Git branch and fails outside Git or on a detached checkout, as in pull request CI. The heading says which filter matched: Builds (Revyl branch: main): or Builds (Git branch: main):. Tables show GIT BRANCH, the branch the build was built from, and across branches REVYL BRANCH, the dashboard's Branch column.

Run options

These apply to one run and don't change saved settings:

revyl test run login --branch staging
revyl test run login --branch staging --build-selection latest
revyl test run login --build-id <build-id>
revyl test run login --all-branches
revyl workflow run smoke --branch staging
revyl workflow run smoke --ios-app <app-id> --ios-build-id <build-id>
revyl explore run "My App" --branch staging
revyl device start --app-id <app-id> --branch staging
revyl device install --app-id <app-id> --all-branches
OptionUses
--branch <name>That branch. Fails if it doesn't exist or has no usable build.
--build-selection inheritAutomatic, the default: the branch's default build.
--build-selection latestLatest build: the branch's newest usable build, even when it's pinned.
--all-branchesAll branches · Latest build: the newest usable build on any of the app's branches. It can't be combined with --branch, an exact build, or another --build-selection.
--build-id <id>One exact build, on test run, explore run, test create, and test build set.
--build-version-id <id>One exact build, on device start, device install, and revyl dev.
--ios-build-id, --android-build-idOne exact build per platform on workflow run, with --ios-app or --android-app. --ios-build and --android-build take a version label instead.
  • A workflow's --branch and --build-selection apply to every app it runs. Each app needs that branch and, unless you give it an exact build, a usable build there; otherwise the workflow doesn't start and the error names the apps. Without them, each app falls back from your Git branch on its own.
  • With test or workflow --build, the new build uploads to --branch, or your Git branch, and the run uses that exact build. Don't combine --build with --build-selection.
  • --ios-app and --android-app replace the workflow's saved app for one run; platforms you don't override use each test's saved choice.
  • Output names the branch of each build, such as Build: v1.4 from branch feature-login.
  • revyl device history --app-id <id> --branch staging lists sessions that started with a build from that branch.
  • revyl dev keeps one branch for startup and every rebuild, and doesn't take --all-branches. See Dev Loop for how it picks the branch and build.

Saved test and workflow choices

revyl test build show login
revyl test build set login --branch staging --build-selection latest
revyl test build set login --build-id <build-id>
revyl test build set login --all-branches
revyl test build set login --build-selection inherit
revyl test build clear login
revyl test create login --from-file ./login.yaml --branch staging
revyl workflow app set smoke --ios <app-id> --ios-branch staging --ios-build-selection inherit
revyl workflow app set smoke --ios-automatic
revyl workflow app set smoke --ios-all-branches
revyl workflow app show smoke
revyl workflow app disable smoke
revyl workflow app enable smoke
revyl workflow app clear smoke
  • test build show prints the saved Branch (Default branch, All branches, or staging (<branch-id>)) and Build (Automatic, Latest build, or Exact build v1.4 (<build-id>)); --json gives the saved IDs.
  • test build set saves a branch and build. --build-selection alone keeps the saved branch, so on a test saved on All branches it saves Default branch. To move a test saved on a named branch back to Default branch, choose Default branch in its dashboard settings, or push YAML with build_selection: inherit and no branch_id.
  • test build clear resets a test to All branches · Latest build.
  • test create takes the same --branch, --build-selection, --build-id, and --all-branches, also with --from-file or --from-session. Without them or a choice in the YAML, a new test starts on Default branch.
  • Workflows save iOS and Android separately with the --ios-* and --android-* flags. --ios-automatic saves Default branch with Automatic for iOS, and --ios-all-branches saves All branches · Latest build.
  • workflow app show names each app and branch with its ID. disable turns the app override off and keeps its choices, enable turns it back on, and clear removes the choices, like turning the override off in the dashboard.

Terminal UI

  • The app build browser opens on All branches, including when you return to it. Its branch filter narrows the list and never chooses an upload's destination or a run's build.
  • Run, session, and upload pickers start with your Git branch when one is detected: Git branch (feature-login) · falls back to base for runs and sessions, or Git branch (feature-login) for uploads, which create it. Pickers that save a choice, and pickers with no Git branch, start with Default branch (base) and its default build. The other branches follow, most recently active first, with the app's default marked (default).
  • Pickers for runs, sessions, and saved choices list All branches · Latest build after the first row. Upload pickers and the build browser don't.
  • After a branch, run, session, and saved pickers offer Automatic, Latest build, or Exact build. A workflow run takes a branch name and offers Automatic or Latest build; with no name, it offers Keep saved settings or All branches · Latest build.
  • Lists load one page at a time: m loads more, / searches, R retries a failed page, and n names a new branch in an upload picker.

JSON fields

  • Uploads and builds: branch_id and branch_name, with the build ID at .build.build_id for build upload --file and .version_id for build upload --url.
  • Runs (test run, workflow run, workflow status, workflow report, explore run, device start, device install): branch_name for each build, branch_source for where that branch came from, and git_branch_name:
    • branch_source is git_branch (your Git branch), explicit_branch (--branch, or the branch saved on the test or workflow), or default_branch (the app's default branch). It's left out when an exact build or All branches · Latest build chose the build.
    • git_branch_name is the Git branch the run sent, present only when the run consulted it: with git_branch, and with default_branch when your Git branch couldn't be used (it doesn't exist, has no usable build, or isn't a valid Revyl branch name). It's left out for explicit_branch, an exact build, All branches · Latest build, and a run with no Git branch. For example, a fallback reports {"branch_name": "base", "branch_source": "default_branch", "git_branch_name": "feature-login"}.
  • build which: app_id, app_name, branch_id, branch_name, branch_source (explicit_branch, git_branch, or default_branch, which a fallback also reports), git_branch_name (as for runs: present with git_branch, and with default_branch after a fallback), build_id, build_version, and branch_build_selection (latest or pinned: the branch's default build).
  • build list: versions, count, total, page, page_size, and has_next; with --branch, also branch_filter and branch_filter_kind (revyl_branch or git_branch).
  • app branch list: branches, each with is_default, pinned_build_id, pinned_build_version, pinned_build_available, and latest_build (absent when the branch has no builds), plus total, page, page_size, and total_pages.

Deprecated flags

--set-current on revyl build and revyl build upload pins like --pin. --set-current=false and --no-set-current do nothing. Each prints a warning on stderr, --json output is unchanged, and none appears in --help.

On this page