You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Two related baseline-resolution behaviours in getParentCommits are not configurable, and on a high-velocity squash-merge master they cost us a full TurboSnap rebuild on most mainline builds. We currently run a one-commit fork of the CLI to turn them off, and we would like to get off that fork.
This has been asked for before. #332 ("Add a (hidden) flag to skip squash/rebase merge PR check") ended with @tmeasday building the requester a disable-squash-merge-check branch and writing:
I'll also discuss adding this as a permanent flag to the CLI.
The flag was never added, and the issue was closed because the underlying GitHub 502s got fixed — i.e. it was closed for the symptom that motivated it, not because nobody needed the knob. This is a second, independent reason to want it: not GitHub API flakiness, but baseline quality on a squash-merge trunk.
The problem
Our master is squash-merge only: 298 of 300 mainline commits have a single parent. Every PR is squashed, so a merged PR's head-branch build commits are not ancestors of master HEAD.
At the tail of getParentCommits, the MergeCommitsQuery / mergedPullRequests block adds each merged PR's lastHeadBuild.commit as a baseline candidate. On our trunk those commits are consistently newer than the correct mainline baseline, so they rank first and win.
Concretely, in the build we used as the canonical case (Chromatic build #176855 in our project — happy to share the project on request):
Chromatic picked baseline de26f7655, the head of the PR branch whose squash merge created the build's HEAD commit.
Diffing HEAD against that PR-branch head produced a 1,304-file diff (429 frontend files plus package.json and yarn.lock), instead of the correct 67 files / 3 frontend files against the previous mainline build.
We audited every baseline Chromatic chose over a sample window: all 23 of them were non-ancestors of HEAD, so all 23 entered through merged-PR injection. (We initially suspected the unrestricted ancestry walk in nextCommits, but on a squash-merge trunk --first-parent changes almost nothing — the merged-PR block was the whole effect.)
After deploying the fork (2026-07-23), measured across master builds:
before
after
TurboSnap bail rate on master
57.3%
4.7%
captured snapshots per build
~2,800
~385
Baseline selection is now a tight forward-moving chain: e.g. build 177203 baselines on e6cc2165, the HEAD of build 177190, the immediately preceding snapshot-producing master build. Intermediate builds 177191–177202 had no frontend changes, correctly skipped Chromatic, and correctly produced no baselines.
Proposed solution
Two opt-in flags, both defaulting to today's behaviour, both branch-glob scoped exactly like --ignore-last-build-on-branch (so they can be enabled on the trunk only, and PR builds keep the default base-branch baseline):
--ignore-merged-pr-builds [branch] — skip the MergeCommitsQuery / mergedPullRequests block, so builds on merged PR head branches are not baseline candidates. This is the load-bearing one, and it is the same knob Add a (hidden) flag to skip squash/rebase merge PR check. #332 asked for.
--first-parent-baseline [branch] — pass --first-parent to the git rev-list ancestry walk in nextCommits, so traversal stays on the mainline. A no-op for us in practice; included because it is the correct companion for a linear-trunk workflow and it is what we originally shipped.
Plumbed the standard way: help text, flags, Flags/Options, getOptions, the config-file Zod schema, action.yml, action-src/main.ts, and resolved through git.matchesBranch(...) in the gitInfo task so node-src/git/ keeps taking plain booleans.
We are happy to be told these should be named differently or scoped differently — alternatives we considered: --ignore-merged-pr-baselines, and --skip-squash-merge-check (which matches #332's language). We'd like to confirm naming and semantics with you before you spend time reviewing code.
Safety, and the tradeoff
The merged-PR block exists so that squash/rebase merges don't lose baselines (docs: squash and rebase merging), and @tmeasday flagged exactly that on #332:
This means that if you are using squash or rebase merges they will not be detected and you may lose baselines, unless you use --auto-accept-changes on your main branch.
That is why this is opt-in and branch-scoped, and we document the caveat in the flag's help text. For a trunk build specifically the exposure is small: the trunk build inherits the complete snapshot set from the previous trunk build, so a merged-PR head build cannot contribute snapshots that are absent from that baseline, and the trunk build still recaptures every story changed since the prior trunk build. We run --auto-accept-changes on master, as the docs recommend for this case.
No backend change is required
Noting this pre-emptively against the compat policy in the README ("any new features will have to be on Chromatic production before they could be used in this package"): neither flag adds or depends on any new GraphQL field or Chromatic production behaviour. --ignore-merged-pr-buildsremoves a call to an existing query, and --first-parent-baseline only changes local git rev-list arguments. Both are pure CLI-side changes and are backward compatible with any Chromatic version that works with the CLI today.
A question about the else branch
While tracing how a commit that isn't in the repository ended up as our top-ranked baseline, we noticed this in the merged-PR loop:
if(awaitcommitExists(deps,lastHeadBuildCommit)){commitsWithBuilds.push(lastHeadBuildCommit);}else{log.debug(`Merged PR build commit ${lastHeadBuildCommit} not in index, blindly appending to parents`);extraParentCommits.push(lastHeadBuildCommit);}
extraParentCommits is prepended to the returned ancestor list, so a commit that is provably absent from the clone becomes the first candidate. In our case that is precisely how it won.
We assume this is deliberate for shallow clones, where "not in index" doesn't mean "not an ancestor". But in a full clone, "not in index" is much stronger evidence. Should this branch be conditional on the clone actually being shallow (or on a git fetch for that commit having failed), rather than unconditional? Same question applies to the identical else for lastBuild.commit a little further up. We're raising it as a question, not proposing a change — happy to file separately if you'd rather.
Additional context
Working proof-of-concept, running in our CI since 2026-07-23: Greenbax/chromatic-cli@2f89949 (env-var gated; the upstream version is properly plumbed as the two flags above).
Feature request
Two related baseline-resolution behaviours in
getParentCommitsare not configurable, and on a high-velocity squash-mergemasterthey cost us a full TurboSnap rebuild on most mainline builds. We currently run a one-commit fork of the CLI to turn them off, and we would like to get off that fork.This has been asked for before. #332 ("Add a (hidden) flag to skip squash/rebase merge PR check") ended with @tmeasday building the requester a
disable-squash-merge-checkbranch and writing:The flag was never added, and the issue was closed because the underlying GitHub 502s got fixed — i.e. it was closed for the symptom that motivated it, not because nobody needed the knob. This is a second, independent reason to want it: not GitHub API flakiness, but baseline quality on a squash-merge trunk.
The problem
Our
masteris squash-merge only: 298 of 300 mainline commits have a single parent. Every PR is squashed, so a merged PR's head-branch build commits are not ancestors ofmasterHEAD.At the tail of
getParentCommits, theMergeCommitsQuery/mergedPullRequestsblock adds each merged PR'slastHeadBuild.commitas a baseline candidate. On our trunk those commits are consistently newer than the correct mainline baseline, so they rank first and win.Concretely, in the build we used as the canonical case (Chromatic build #176855 in our project — happy to share the project on request):
de26f7655, the head of the PR branch whose squash merge created the build's HEAD commit.package.jsonandyarn.lock), instead of the correct 67 files / 3 frontend files against the previous mainline build.package.json/yarn.lockdelta trippedfindChangedDependencies, disabling TurboSnap entirely: 0 inherited, 3,865 captured snapshots.We audited every baseline Chromatic chose over a sample window: all 23 of them were non-ancestors of HEAD, so all 23 entered through merged-PR injection. (We initially suspected the unrestricted ancestry walk in
nextCommits, but on a squash-merge trunk--first-parentchanges almost nothing — the merged-PR block was the whole effect.)After deploying the fork (2026-07-23), measured across master builds:
Baseline selection is now a tight forward-moving chain: e.g. build 177203 baselines on
e6cc2165, the HEAD of build 177190, the immediately preceding snapshot-producing master build. Intermediate builds 177191–177202 had no frontend changes, correctly skipped Chromatic, and correctly produced no baselines.Proposed solution
Two opt-in flags, both defaulting to today's behaviour, both branch-glob scoped exactly like
--ignore-last-build-on-branch(so they can be enabled on the trunk only, and PR builds keep the default base-branch baseline):--ignore-merged-pr-builds [branch]— skip theMergeCommitsQuery/mergedPullRequestsblock, so builds on merged PR head branches are not baseline candidates. This is the load-bearing one, and it is the same knob Add a (hidden) flag to skip squash/rebase merge PR check. #332 asked for.--first-parent-baseline [branch]— pass--first-parentto thegit rev-listancestry walk innextCommits, so traversal stays on the mainline. A no-op for us in practice; included because it is the correct companion for a linear-trunk workflow and it is what we originally shipped.Plumbed the standard way: help text,
flags,Flags/Options,getOptions, the config-file Zod schema,action.yml,action-src/main.ts, and resolved throughgit.matchesBranch(...)in thegitInfotask sonode-src/git/keeps taking plain booleans.We are happy to be told these should be named differently or scoped differently — alternatives we considered:
--ignore-merged-pr-baselines, and--skip-squash-merge-check(which matches #332's language). We'd like to confirm naming and semantics with you before you spend time reviewing code.Safety, and the tradeoff
The merged-PR block exists so that squash/rebase merges don't lose baselines (docs: squash and rebase merging), and @tmeasday flagged exactly that on #332:
That is why this is opt-in and branch-scoped, and we document the caveat in the flag's help text. For a trunk build specifically the exposure is small: the trunk build inherits the complete snapshot set from the previous trunk build, so a merged-PR head build cannot contribute snapshots that are absent from that baseline, and the trunk build still recaptures every story changed since the prior trunk build. We run
--auto-accept-changeson master, as the docs recommend for this case.No backend change is required
Noting this pre-emptively against the compat policy in the README ("any new features will have to be on Chromatic production before they could be used in this package"): neither flag adds or depends on any new GraphQL field or Chromatic production behaviour.
--ignore-merged-pr-buildsremoves a call to an existing query, and--first-parent-baselineonly changes localgit rev-listarguments. Both are pure CLI-side changes and are backward compatible with any Chromatic version that works with the CLI today.A question about the
elsebranchWhile tracing how a commit that isn't in the repository ended up as our top-ranked baseline, we noticed this in the merged-PR loop:
extraParentCommitsis prepended to the returned ancestor list, so a commit that is provably absent from the clone becomes the first candidate. In our case that is precisely how it won.We assume this is deliberate for shallow clones, where "not in index" doesn't mean "not an ancestor". But in a full clone, "not in index" is much stronger evidence. Should this branch be conditional on the clone actually being shallow (or on a
git fetchfor that commit having failed), rather than unconditional? Same question applies to the identicalelseforlastBuild.commita little further up. We're raising it as a question, not proposing a change — happy to file separately if you'd rather.Additional context
Greenbax/chromatic-cli@2f89949(env-var gated; the upstream version is properly plumbed as the two flags above).