Skip to content

Baselines & branches

View as Markdown

For each story and viewport, StoryShelf picks a baseline:

  1. The accepted baseline on the current branch, if one exists.
  2. Otherwise, the default branch’s baseline.
  3. If neither exists, the story is new.
flowchart TD
    A[New Snapshot] --> B{Baseline on<br/>Current Branch?}
    B -->|Yes| C[Use Branch Baseline]
    B -->|No| D{Baseline on<br/>Default Branch?}
    D -->|Yes| E[Use Default Branch Baseline]
    D -->|No| F[Mark as New]
    C --> G[Diff Against Baseline]
    E --> G
    F --> G
    G --> H[Store Result]

The three-step algorithm above runs for every snapshot during capture. This means:

  • Feature branches get their own baselines once changes are accepted
  • Default branch (usually main) acts as the fallback for all branches
  • New stories on any branch start as “new” until accepted

Baseline lookup is a pure key lookup on (project, story, viewport, browser, branch) with the default-branch fallback above. StoryShelf never walks git history or ancestor builds, so:

  • Rebasing or force-pushing a branch changes its commit shas but not its branch name — the accepted baselines still apply and you are not asked to re-accept the same changes.
  • Squash-merging (or rebase-merging) to main needs no ancestry between the feature commits and the resulting commit; the next default-branch build simply writes the default baselines.

This is locked in by tests in baseline.test.ts. Tools that resolve baselines by ancestor build document rebase/squash troubleshooting for the case where that lookup breaks; here there is nothing to break. (Affected capture does use git, but only to decide which stories to re-render, never which baseline applies.)

Builds on the default branch auto-approve and become the baselines. Pushing to main re-baselines the project — every snapshot on main updates the default-branch baseline.

ConfigDefaultDescription
defaultBranchmainBranch that auto-approves and provides fallback baselines

Accepting a change on a feature branch records a baseline for that branch, so the next upload doesn’t re-flag the same change. Merging to main promotes the baselines on the next default-branch build.

sequenceDiagram
    participant Dev as Developer
    participant CI as CI/CD
    participant SS as StoryShelf
    Dev->>CI: Push feature branch
    CI->>SS: Upload build (branch: feature-x)
    SS-->>Dev: Snapshots show as "changed"
    Dev->>SS: Accept changed snapshots
    SS->>SS: Store branch baselines
    Dev->>CI: Push new revision to feature-x
    CI->>SS: Upload build
    SS-->>Dev: Same snapshots now "unchanged" (branch baseline)
    Dev->>CI: Merge to main
    CI->>SS: Upload build (branch: main)
    SS->>SS: Auto-approve, update default baselines

A build carrying the persistent label is never purged. On git checkouts the CLI attaches it automatically for revisions with git tags, so release builds survive retention.

Terminal window
# Tag a release → persistent label auto-attached
git tag v1.0.0
git push origin v1.0.0
# StoryShelf CLI detects tag → adds "persistent" label

Persistent builds are excluded from purgeTtlDays retention. Their snapshots remain as baselines indefinitely.

Feature-branch baselines are GC’d after branchTtlDays (default 30, null = disabled) of inactivity via a daily sweep (branchGcIntervalMs 24h). Default-branch baselines are never GC’d.

ConfigDefaultDescription
branchTtlDays30Days of inactivity before feature-branch baselines GC’d
branchGcIntervalMs86400000 (24h)How often to run the GC sweep
purgeTtlDays30Days before terminal builds purged (separate from baselines)
timeline
    title Retention vs Baselines Timeline
    section Build Upload
    Day 0 : Build uploaded, capture starts
    Day 1 : Build status: reviewing/approved
    section Retention (purgeTtlDays=30)
    Day 30 : Build purged (screenshots, diffs, metadata)
    section Baselines (outlive builds)
    Day 30+ : Baselines persist
    Day 60 : Feature-branch baseline GC'd (branchTtlDays=30)
    Day 90+ : Default-branch baseline never GC'd

Most teams never promote manually — pushing an accepted feature branch to main auto-promotes on the next default-branch build. If you need to pin a branch as a long-lived baseline (e.g., release/2.0), set that branch as the default in project settings or copy its baselines via the API.

API for manual promotion:

Terminal window
POST /api/v1/projects/:slug/baselines/promote
{ "sourceBranch": "release/2.0", "targetBranch": "main" }

On the build detail page each snapshot shows Baseline: <branch>@<buildId> or New when no baseline exists. If a story is unexpectedly New on a PR, check:

  1. No accepted baseline exists on the PR branch yet (accept once)
  2. The default branch has a baseline for that story/viewport — otherwise the PR correctly has no ancestor

Builds are transient and purged by purgeTtlDays (default 30); baselines outlive their builds and are stored separately. Purging a build does not delete its baseline — the next build on that branch still compares against it until branchTtlDays GC.

WhatRetentionSurvives purge?
Build metadatapurgeTtlDaysNo
ScreenshotspurgeTtlDaysNo
Diff overlayspurgeTtlDaysNo
Branch baselinesbranchTtlDaysUntil GC
Default-branch baselinesNeverYes
Persistent buildsNeverYes

Baselines can leave and enter StoryShelf freely: export a project’s baselines (optionally one branch) as a zip of manifest.json + PNGs and import the same archive into any instance, for backups, migrations, or staging → prod promotion. Import upserts by story, mode, browser, and branch, and needs the approver role. See Baseline portability.

  • GET /api/v1/projects/:slug/baselines — List baselines
  • POST /api/v1/projects/:slug/baselines/promote — Promote branch baselines
  • GET /api/v1/projects/:slug/builds/:id/snapshots/:snapshotId/baseline — Get baseline for snapshot
  • GET /api/v1/projects/:slug/baselines/export?branch= — Export baselines as a zip
  • POST /api/v1/projects/:slug/baselines/import — Import (upsert) baselines from a zip
ScenarioExpected behavior
New story on feature branchStatus: new (no baseline anywhere)
Changed story on feature branch, first timeStatus: changed (diff vs default branch)
Changed story on feature branch, after acceptStatus: unchanged (diff vs branch baseline)
Merge feature branch to mainDefault branch baselines updated on next main build
Delete feature branchBaselines GC’d after branchTtlDays inactivity
Tag release (v1.0.0)Build gets persistent label, never purged
Story deleted from StorybookBaseline remains until GC (orphaned baseline)