Pre-merge QA workflow
For teams running trunk-based development, develop (or main) has to be release-ready at all times. The only way to keep it that way is to QA every change before it lands, not after. buildtree is built for that loop.
This guide describes the pattern. The product features that support it (env + branch hierarchy, always-latest folder URLs, install access gating) all exist today; together they replace the CI plumbing most teams hand-roll on top of generic distribution tools.
The problem
Trunk-based development has one rule that everything else hangs on: the trunk is always shippable. To keep it shippable, you cannot merge anything that has not been tested.
Most teams enforce this with a manual ritual:
- Developer opens a PR for
feature/payments. - They trigger a custom GitHub Action that builds and uploads the artifact somewhere.
- The action posts a Slack message with a link.
- The developer moves the Linear / Jira ticket to "QA".
- A tester picks it up, installs the build, tests it.
- If it passes, the ticket goes to "Ready to merge". The PR is approved and merged.
- If it fails, the developer fixes the bug, re-triggers the action, and step 4 repeats.
The pain is not the test itself; it is the plumbing. Each team rebuilds the same five-step glue between CI, distribution, and ticket tracking. The build artifact ends up at a different URL each time. Testers chase Slack messages to find the latest one. Old branches accumulate stale links nobody trusts.
How buildtree fits
buildtree organises every build by (env, branch). The CLI accepts both as flags:
buildtree upload ./app.apk --env dev --branch feature/payments
The CI invocation never changes. It uploads under the same (env, branch) pair every time. The dashboard groups uploads by branch and serves a stable URL per branch:
/install/folder/<project>/dev/feature/payments
That URL always points at the latest build for that environment and branch. Bookmark it once at the start of QA, and every subsequent CI run replaces what it returns. The tester does not chase Slack messages. The developer does not paste new links into the ticket. The ticket's link to QA stays the same for the life of the branch.
The flow, end to end
1. Developer opens PR for feature/payments.
2. CI runs (or developer runs locally) and uploads:
buildtree upload ./app.apk --env dev --branch feature/payments
buildtree upload ./app.ipa --env dev --branch feature/payments
3. Developer pastes one URL into the ticket:
https://buildtree.sh/install/folder/<project>/dev/feature/payments
(or scans the QR for testers in person)
4. QA installs from that URL, tests, marks the ticket as approved
or "needs fixes" in Linear / Jira.
5. If fixes are needed: developer pushes a commit, CI re-uploads to
the same (env, branch) pair, QA refreshes the same URL.
6. Once approved, PR merges to develop. CI uploads the merged build
as the develop checkpoint:
buildtree upload ./app.apk --env dev
(no --branch flag; this is the env's checkpoint, see below)
7. /install/folder/<project>/dev now serves the latest release-ready
build. That URL is what you give to internal stakeholders, demo
reviewers, anyone who wants "the latest from develop".
The first six steps are the per-feature loop. Step 7 is the continuous deliverable: develop's install URL is always the freshest passing build.
Checkpoint vs branch uploads
The model has two upload modes, distinguished by whether you pass --branch:
- Branched upload (
--env dev --branch feature/payments): a pre-merge preview build for that branch. Lives in the Branches section of the dashboard. Never affects the env's "latest". - Checkpoint upload (
--env dev, no--branch): the env's latest release-ready build. Most-recent branchless iOS + most-recent branchless Android together form the "Latest dev" pair surfaced at the top of the env tab.
This maps directly onto trunk-based development: feature branches are branched uploads, every merge to develop produces a new checkpoint upload. The branch URL gets QA in the feature loop; the checkpoint URL gets internal stakeholders the latest passing build.
What you stop maintaining
This pattern replaces a stack of small custom scripts that most mobile teams own:
- The GitHub Action that builds + uploads per-branch
- The Slack message that posts the link
- The README section explaining where QA finds builds
- The "stale branch artifact" cleanup script
The CI invocation collapses to a single buildtree upload line with --branch ${GITHUB_HEAD_REF} (or the equivalent on Bitrise, Codemagic, Fastlane). The URL convention is enforced by the platform. Old branches roll off as the branches themselves get deleted.
CI snippet
GitHub Actions example, branch-aware:
- name: Distribute to buildtree
env:
BUILDTREE_TOKEN: ${{ secrets.BUILDTREE_TOKEN }}
run: |
npm install -g @buildtree/cli
BRANCH="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}"
if [ "$BRANCH" = "develop" ] || [ "$BRANCH" = "main" ]; then
buildtree upload ./build/app.apk --env dev
else
buildtree upload ./build/app.apk --env dev --branch "$BRANCH"
fi
Pull requests upload as branched; merges to develop or main upload as the env checkpoint. Full example workflows: see GitHub Actions.
What is coming
The flow described above is fully supported today. Two pieces that are on the roadmap and would compress the loop further:
- Per-build QA status (in_qa / approved / needs_fixes), settable from the install page so testers can mark a build's verdict in-product instead of in a separate ticket.
- Per-build feedback so testers can leave notes attached to a specific build, surfaced on the developer's dashboard.
Both belong in this workflow and would replace the Slack / Linear / Jira step in the loop above. If your team would use these, tell us so we prioritise correctly.
See also
- Concepts: the env + branch + checkpoint model in detail.
- Install URLs: pinned, folder, and release URL flavours.
- Install access: scoping branch URLs to internal testers when the project is private.
- GitHub Actions: full CI workflow examples.