Baselines and branches
Baseline
A baseline is the approved image of a snapshot on one branch. Each build compares its screenshots with their baselines. A screenshot that matches is Not changed, one that differs is Changed, and a snapshot with no baseline is New. See Builds and snapshots for what a snapshot and a build are.
A baseline changes only when a review is complete:
- Approving a New or Changed screenshot makes it the baseline on the build’s branch.
- Approving a Removed screenshot retires the baseline on the build’s branch.
Nothing else writes a baseline. Uploading screenshots doesn’t, and neither does a staged Approve before you select Finish review.
Fallback to the default branch
Every project has a default branch, set as Default branch when you create it (usually main). Its baselines are the ones every other branch starts from.
To find the baseline for a screenshot, pixmoat looks in this order:
- The baseline on the build’s own branch.
- If there isn’t one, the baseline on the default branch.
- If neither exists, the screenshot is New.
So the first build on a feature branch is compared with the default branch. After you approve a change on that branch, the branch has its own baseline for that snapshot, and later builds on the branch compare with it. Snapshots you didn’t change keep falling back to the default branch.
Two consequences:
- Approving on a feature branch never changes the default branch’s baselines. Other branches keep comparing with the default branch until your change is merged and promoted.
- Fallback only goes to the default branch. A branch created from another feature branch doesn’t see that branch’s baselines.
The branch comes from PIXMOAT_BRANCH in CI. Set it to the source branch of the merge request, as the GitLab example does with $CI_COMMIT_REF_NAME. If every job reports the same branch name, all builds share one set of baselines.
Baselines during a capture
When a new build starts, pixmoat saves the visual baselines on its branch and the default branch. Every screenshot in that build uses those saved images, even if another developer finishes a review while your tests run. Hash confirmations and missing-screenshot detection use the same saved baselines.
A screenshot without a baseline when the build starts stays New in that build. A screenshot added to the default branch later does not become an unexpected Removed item in an already running build.
Retrying a finished build refreshes its saved baselines and discards its previous capture results. An upload still in progress when a build finishes or is retried fails with capture_changed; start a new capture to upload it. Builds created before this behavior was introduced keep their earlier comparison behavior until retried. Performance, accessibility, and console comparisons still resolve their own baselines during upload.
When the target baselines change
A branch review remains Approved after the default branch’s baselines change. Its saved target baselines are now stale. In blocking mode, the build summary returns ci_status: "blocked", including for builds that previously had no changes. Capture again against the current target before using that build to pass CI. Report-only mode keeps its informational CI behavior.
GET /api/builds/{id} and the agent summary include baseline_freshness. Its state is current, stale, unverifiable, or not_applicable. A stale result includes a reason and the snapshot keys whose target image, version, or comparison settings changed. Changing the default branch also makes an earlier branch capture stale. Legacy builds without saved baselines are unverifiable and keep their existing CI behavior. Builds on the default branch are not_applicable because approving them advances their own baselines.
When finishing an approval, pixmoat checks the saved target baselines again under the same project lock as baseline writes. A stale blocking-mode branch review, or a stale or unverifiable integration review, returns HTTP 409 with capture_ineligible and baseline_freshness. The session stays open, staged decisions remain, and no baselines or automatic decisions are written by the failed finish. Capture again against the current target. Rejections can still be recorded. Retrying Finish on a historical approval also checks current eligibility; a conflict preserves the earlier approved result. Report-only branch reviews and legacy captures retain their existing review behavior.
The agent-summary verdict gate returns FAIL when stale baselines would otherwise produce PASS, regardless of blocking mode. Conditional polling also changes its digest when freshness changes.
An ordinary branch capture does not verify that CI captured combined branch code, or that the target commit is unchanged. Configured Git status delivery now queues a non-passing update when target baseline changes make a blocking capture stale. The server also rechecks verified integration captures for missed source and target commit changes. Delivery can lag during provider outages. Re-run the gate to read the current result, and update your branch before capturing changes to shared screens. See Commit status.
Integration capture identity
A GitLab merged-result capture records the source commit, target branch and commit, captured merge commit, pipeline ID, and the saved baseline snapshot. The Playwright client checks the local Git HEAD before creating the build. Pixmoat checks the merge request, target branch head, pipeline identity, and merge commit parents through the project’s configured GitLab connection. CI and its screenshot inputs must still be trusted.
Integration captures compare screenshots, hash confirmations, and removals with the saved target baselines. An approved baseline on the source branch cannot hide a difference from the target. A complete, accepted target build for the exact target commit must match those saved images. If that build is missing, still awaiting review, deleted, or retried, the capture cannot pass. Capture again once the target build is accepted.
GET /api/builds/{id} and the agent summary include capture_identity. It records the initial verification result. baseline_freshness gives the current eligibility result. Source or target commit movement makes the capture stale even if no images changed. Provider errors and replaced candidate pipelines make it unverifiable. A capture that was not verified when created needs a new capture. Pending reviews and stale or unverifiable integration captures cannot pass CI, including in report-only mode.
Each explicitly identified capture creates a new build and preserves previous review history. Requests without capture metadata keep the ordinary branch workflow. Existing captures have no verified integration identity. Enabled blocking performance, accessibility, or console checks make an integration capture unverifiable until those comparison inputs also support frozen target snapshots.
See Capture the proposed merge for setup. Configured status delivery revokes obsolete published checks. The opt-in current merge candidate policy also verifies GitLab semi-linear merge rules and blocks ordinary branch captures from passing its gate. Live GitLab validation is required before production rollout.
Merge promotion
When a merge request is merged, its approved baselines can be copied to the branch it was merged into. This is merge promotion. It needs an inbound merge webhook from your Git host. GitLab CI, Bitbucket and other Git hosts show how to set one up. GitHub doesn’t send merge events to pixmoat.
When the webhook arrives, pixmoat looks for the build for the head commit of the merged branch. If the webhook identifies a commit with no build, pixmoat does not promote an older approved build instead. For each approved screenshot merged into the default branch, pixmoat checks that the target baseline still has the version and image saved when that build started. This target check is separate from the branch’s own comparison baseline.
- If it is, the approved screenshot becomes the target branch’s baseline. An approved removal retires the target branch’s baseline.
- If the target branch’s baseline changed in the meantime, for example because another merge request changed the same snapshot and merged first, pixmoat skips that snapshot. The next build on the target branch compares the merged screenshots with its current baseline. Review any remaining changes there.
If two branches add the same new snapshot, the second merge cannot overwrite the first under the assumption that the snapshot is still new. Checks and baseline writes happen together, so simultaneous promotions cannot both replace the same saved target version. Retiring and recreating a baseline also changes its version, even if its image is identical.
Older builds and promotions to other target branches use their captured comparison image for the target check. They do not have a separate saved target version.
An integration capture is promoted only if it was verified when created and the webhook’s actual merged commit equals its captured commit. A different squash or rebase result needs a capture on the target branch. Manual promotion cannot establish that equality for an integration capture and does not promote it.
Screenshots that were never approved aren’t promoted. After promotion, the builds list shows a Merged badge on the build.
These checks protect baseline promotion after a merge. Polling can detect stale target baselines before merging, but a previously published Git status can remain passing. Branch screenshots do not prove how combined changes will render. Update your branch with the current target and capture again before merging changes to shared screens.
Without the webhook, nothing is promoted. The next build on the default branch compares with the old default-branch baseline, so the screenshots you already approved on the feature branch show as Changed or New again. Approve them in that build to move the default branch forward.
Manual promotion
There’s no button for promoting a build by hand. If a merge webhook delivery was lost, a project admin can promote an approved build through the API:
curl --fail -X POST \
-H "Authorization: Bearer $PERSONAL_TOKEN" \
-H "Content-Type: application/json" \
-d '{"target_branch": "main"}' \
"https://app.pixmoat.com/api/projects/<project-slug>/builds/<build-id>/promote"
PERSONAL_TOKEN holds a personal access token (it starts with pxgp_) from Account → Personal Access Tokens → Create token, created by a project admin. Project access tokens (pxg_) can’t promote, so the CI token can’t do this.
Promotion runs the same checks as merge promotion. The build must show Approved, otherwise the request fails with 422. Promoting the same build to the same branch again changes nothing.
If you can’t use the API, run CI on the target branch and approve the changed screenshots in that build. The result is the same.
Baseline history
Every baseline change is recorded. On the build review page, select a screenshot and open More actions → History to see its Baseline History: each approved version, the branch it came from, and a Retired badge where a removal retired it.
Pruned historical images
Old images don’t stay forever. What pixmoat keeps depends on your plan and on the project’s Build history settings (Settings → Build history):
- Finished builds are deleted with their screenshots once a branch has more than Keep latest builds builds or a build is older than Maximum build age (days). Builds that are still running, uploaded or in review are never deleted. Deleted builds can’t be recovered.
- Old baseline versions keep their history entry, but their image is removed once it’s older than your plan’s retention period. The Baseline History panel shows a Pruned tile instead of the image, with the tooltip “Image removed by retention”. You can still see who approved that version and when.
- The current baseline is never pruned, however old it is. Comparisons always have an image to compare with.
Images saved as comparison inputs for a build stay available until that build is deleted or retried, even if their old baseline-history entries are pruned.
Project settings can shorten how long builds are kept, but not extend it past your plan’s limit.
Branch baselines aren’t deleted when a branch is merged or abandoned. They stop being used once no builds report that branch.
GitLab Free queue freshness
The opt-in GitLab Free merge queue checks an exact source pipeline whose checkout includes current main. Its integration capture records both source and target commits. A finished review becomes stale when main moves. The coordinator rebases the next source branch and waits for a new capture and review. Fast-forward merging rejects a target change after the final check. The queue also requires a complete accepted target capture at that exact main commit, even when its screenshots did not change.