Skip to content

feat(snapshots): Add CI image export via SNAPSHOTS_EXPORT_DIR env var#252

Open
cameroncooke wants to merge 8 commits intomainfrom
cameroncooke/snapshot-ci
Open

feat(snapshots): Add CI image export via SNAPSHOTS_EXPORT_DIR env var#252
cameroncooke wants to merge 8 commits intomainfrom
cameroncooke/snapshot-ci

Conversation

@cameroncooke
Copy link
Copy Markdown

@cameroncooke cameroncooke commented Apr 1, 2026

Add filesystem-based snapshot export for CI pipelines that need direct access to rendered PNGs and their metadata without extracting XCTest result bundles.

When the SNAPSHOTS_EXPORT_DIR environment variable is set, SnapshotCIExportCoordinator writes each snapshot as a PNG plus a JSON sidecar (containing preview context, device info, color scheme, etc.) to the specified directory. PNG encoding and file I/O are dispatched to a concurrent background queue so the test runner isn't blocked. A barrier-based drain runs automatically via XCTestObservation at bundle finish to guarantee all writes complete before the process exits.

Also adds:

  • colorSchemes() class override on SnapshotTest — return e.g. [.light, .dark] to render every preview in multiple schemes with suffixed filenames.
  • Smarter file-name disambiguation when multiple previews share the same display name within a group (falls back to line number or index).

The existing XCTest attachment path is unchanged when the env var is absent.

Includes unit tests for the coordinator, sanitization, sidecar content, drain semantics, and file-name resolution.

Testing

You can test via this HackerNews PR which is using this version of SnapshotPreviews and exercises the export to sentry functionality:
EmergeTools/hackernews#780

cameroncooke and others added 7 commits April 2, 2026 08:17
Underscores are valid filename characters and common in Swift type names.
Previously they were treated as unsafe and collapsed with adjacent
non-kept characters. Now they are preserved as-is.

Update test to verify unsafe char collapsing separately from underscore
preservation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The _shared static var was read/written without any isolation while
hasDrained used NSLock protection. Both production callers
(discoverPreviews, testPreview) are already @mainactor, so this makes
the contract explicit and prevents future data races.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The init already creates the directory or calls preconditionFailure, so
the directory is guaranteed to exist. The guard also had the side-effect
of skipping the drain while operations could still be in-flight.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace manual semaphore+barrier pattern with the built-in
OperationQueue API that does the same thing. Blocks the calling
thread until all enqueued operations complete.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move the color scheme reset into a defer block so the override is
always cleared even if the render loop exits early due to an unexpected
error. Previously the reset only ran after normal loop completion.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The grouping key in discoverPreviews and testPreview used
fileID ?? typeName, skipping the typeDisplayName fallback that
canonicalGroup uses. This could cause filename collisions when a
PreviewProvider has no fileId but a typeDisplayName differing from
typeName.

Extract canonicalGroup to accept raw parameters and make it internal
so both SnapshotTest and the coordinator share the same fallback chain:
fileId -> typeDisplayName -> typeName.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move filename sanitization to the point where baseFileName is created
in testPreview, so SnapshotContext.baseFileName and the sidecar
image_file_name field are always consistent. Previously baseFileName
stored the raw unsanitized name while image_file_name was sanitized,
creating a confusing mismatch for sidecar consumers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants