chromaui/chromatic-cli
github.com/chromaui/chromatic-cli ↗ · 647 files · 1.9 MB
Chromatic CLI Publishes your Storybook to Chromatic and kicks off tests if they're enabled.
Overview
Chromatic CLI builds or accepts prebuilt Storybooks, uploads their artifacts to Chromatic, and reports hosted visual-test results to developers and CI. The same TypeScript pipeline serves command-line aliases, a GitHub Action, and a Node API, with additional paths for test archives, React Native artifacts, and temporary Storybook sharing. Distinctive mechanisms include content-hash upload deduplication, Git-based TurboSnap selection, and a newer hash-manifest collection path that still defers story selection to TurboSnap v1 at this commit.
System map
The product is primarily a Node client for hosted Chromatic services; its Storybook and Vite configuration renders the CLI's own presentation examples. CLI, Action, and programmatic callers converge on a shared context and an explicitly ordered pipeline. Local artifacts and diagnostic files complement remote build state, while transport, task logic, and rendering occupy separate modules. Packaging and release configuration describe CommonJS npm bundles and a separately bundled GitHub Action.
- The npm package maps chroma, chromatic, and chromatic-cli to dist/bin.cjs; its root export is isChromatic, while ./node exposes dist/node.cjs and declarations.
- bin-src/main.ts calls run({argv}); action-src/main.ts calls the same run with translated flags and inAction:true, so both adapters share node-src/index.ts orchestration.
- runBuild invokes renderAuth through renderSnapshot directly; node-src/tasks/index.ts now supplies only an optional JUnit report task, with a separate runShare task list.
- Context owns mutable Git, Storybook, announced-build, result-build, artifact, upload, and TurboSnap state; Deps narrows shared services and exposes Readonly<Options> plus mutable Runtime.forceRebuild.
- setupContext creates HTTPClient and GraphQLClient against CHROMATIC_INDEX_URL/graphql, attaches session/version headers, and loads configuration; authentication and sharing can override the endpoint to /api.
- The inspected persistence boundaries are build files, local diagnostics/JUnit output, and remote upload targets: writeManifest writes through ProjectFiles, generateReport writes XML, and uploadFiles posts file bytes to negotiated formAction URLs.
- tsdown emits split CommonJS CLI/Node bundles with Node declarations and a separate unsplit Action bundle; its node20 targets differ from package.json's Node >=22 requirement and action.yml's node24 runtime.
- Repository scripts define build, coverage tests, lint, and type checking; release.yml triggers PR canaries and selected main-branch source releases through yarn run release with publishing permissions.
Feature catalog
Publishing supports locally built web Storybooks and prebuilt output, with hosted verification and visual-result reporting. Users can constrain stories, apply branch policies, initialize archive integrations, or build React Native artifacts. Auxiliary commands expose dependency tracing and manifest inspection, while Node consumers can reserve temporary share URLs. Diagnostics, metadata, and JUnit output provide operational visibility alongside terminal messages and Action outputs.
- The main publishing path runs the discovered build script or a custom buildCommand; storybookBuildDir bypasses the web build and sends existing artifacts through preparation and upload.
- Options expose branch-scoped auto acceptance, exit-zero-on-changes, exit-once-uploaded, skip, force-rebuild, patch builds, and story file/name filters; incompatible filter combinations and malformed patch pairs are rejected.
- TurboSnap's onlyChanged mode selects affected story files, with externals and untraced controls; background v2 hash collection also runs when onlyChanged is unset unless explicitly disabled.
- chromatic init offers Storybook, Playwright, Cypress, and Vitest setup, adds a chromatic package script, and installs framework archive dependencies for the three archive integrations; buildCommand selection routes E2E options through getE2EBuildCommand.
- React Native publishing can reuse artifacts plus manifest.json or build missing artifacts and generate the manifest; standalone react-native-build and generate-manifest commands expose those operations, and initialization rejects TurboSnap for native apps.
- JUnit reporting substitutes {buildNumber}, queries tests in 1,000-item pages, and writes XML; diagnostics and uploadMetadata provide local context output and hosted debugging files.
- Utility commands include trace for changed-file dependency paths, trim-stats-file for simplified stats JSON, and turbosnap-manifest for serialized v2 hashes on stdout; trace rejects package manifests because it cannot diff their contents.
- Programmatic run returns build URLs and test/capture counts that the Action exports; share returns a reserved URL and optional expiry with progress/error/cancellation callbacks, while isChromatic detects the browser user agent or chromatic=true URL marker.
Data flow
Inputs enter through argv, Action fields, Node options, configuration, and selected process environment variables. A shared Context accumulates normalized metadata and task outputs, while extract/apply adapters mediate most state changes. Artifact paths and hashes become upload descriptors, remote publication becomes build-result state, and result counts leave through the Node return object or Action outputs. Diagnostics and TurboSnap manifests form additional persistence and upload channels.
- getPartialOptions merges defaults, configurationOptions, optionsFromFlags, then extraOptions; it subsequently computes interactive mode from CI, debug, the CLI flag, stdout TTY, and NODE_ENV.
- getConfiguration reads JSON5-compatible content from the first existing explicit/default candidate, validates a strict partial Zod schema, and returns the selected configFile path; a missing default config yields an empty object.
- runAuth exchanges userToken plus projectId for a CLI token or projectToken for an app token, then installs that token as GraphQLClient's default Bearer authorization; publishBuild overrides it with reportToken.
- gatherGitInfo combines local Git/CI commit information, uncommitted hashes, root paths, and clone diagnostics; announceBuild transforms that state into API input, hashes gitUserEmail, converts committedAt to Date, and requests baselines when TurboSnap has not bailed.
- validateFiles recursively enumerates artifact sizes, excludes the root .chromatic directory, normalizes names to POSIX, separates preview stats from upload paths, and can replace sourceDir with an output directory found in the build log.
- prepareProject traces affected files before calculating artifact hashes; v2 separately rolls transitive story dependencies and Storybook-wide categories into hash maps, writes a JSON manifest, and sends only aggregate/config/story hashes through BuildUploadHashes.
- buildFileList maps validated relative paths into localPath/targetPath/contentLength/contentHash descriptors; uploadBuild negotiates batches and joins returned targets to local descriptors, then uploadFiles streams multipart POSTs with concurrency 10 and per-file retries.
- Completed build state supplies run's output counts and optional JUnit XML; after runBuild, diagnostics redact projectToken/reportToken/userToken, metadata is conditionally uploaded, and implicit log/diagnostic files are removed unless persistence was requested.
Behavior traces
Source walks followed startup, normal publishing, sharing, and patch-workspace restoration through their concrete function calls. The publishing sequence has early-stop branches for skipped commits and fully TurboSnapped builds, plus self-skips for dry-run stages. Sharing reserves its URL before uploading and reports a terminal status afterward. Post-pipeline diagnostics and metadata handling occur after build teardown, with persistence controlled separately from artifact publication.
- Startup: register.js dispatches by process.argv[2] or falls back to main; main calls run, which parses arguments, checks Node compatibility, discovers package.json, constructs InitialContext, and calls runAll.
- Context setup: runAll calls setupContext, getPartialOptions, the credential-free branch-skip preflight, and getOptions; it seeds Runtime.forceRebuild and launches runBuild alongside checkForUpdates.
- Publish preparation: renderAuth runs runAuth; runBuild then calls renderGitInfo, renderStorybookInfo, and renderInitialize, whose initialize function gathers environment/runtime metadata, calls announceBuild, and optionally sets component owners.
- Artifact preparation: buildProject selects native or web handling; the web path resolves sourceDir, setBuildCommand, and buildStorybook unless prebuilt, then prepareProject validates files and Android artifacts, traces changes, and calculates hashes.
- Upload and completion: uploadProject calls uploadBuild and waits for deduplicated sentinels, retrying without hashes on failure; a server SKIPPED result returns partial and halts downstream tasks, otherwise verifyProject publishes/verifies and snapshotProject waits for completion and maps status to an exit code.
- Sharing: share calls setupShareContext and reserveShare, invokes onUrl immediately, runs renderBuild/renderPrepare and the Listr uploadShare task, uploads supporting files before index.html, then confirms complete/error/cancelled; confirmation failures warn without replacing the upload outcome.
- Patch builds: renderPrepareWorkspace precedes authentication; runPrepareWorkspace checks cleanliness/currentness, finds and checks out the merge base, and installs dependencies. runBuild's finally calls runRestoreWorkspace despite pipeline skips, which resets, checks out the previous branch, reinstalls, and resets again.
- Post-run persistence: runAll writes diagnostics when requested or needed for metadata, uploads metadata when a published build URL permits it, cleans implicit files, and calls checkPackageJson only for non-E2E exit codes 0/1; that function can interactively add a script using originalArgv.
Testing signals
Tests and CI were inspected, not executed; their existence establishes intended checks, not passing results. Vitest covers task logic, orchestration, renderer semantics, and deterministic hashing through extensive mocks and fixtures. CI also defines packed-package smoke runs and hosted visual checks, which exercise different boundaries from unit tests. This is a bounded inspection, and configured coverage excludes the Action adapter and several presentation/fixture categories.
- package.json defines vitest run --coverage; vitest.config.mts includes bin-src, node-src, and isChromatic helpers in coverage, excluding stories, frames, fake adapters, mocks, and selected test utilities. action-src is outside that configured coverage include.
- node-src/index.test.ts mocks subprocesses, DNS, node-fetch, and writes; its orchestration assertions include merge-base checkout before getCommit and restoration after a later failure, so those tests do not demonstrate real Git restoration.
- upload.test.ts checks GraphQL descriptors, multipart target uploads, and upload tallies with mocked HTTP/files; its fallback test asserts a second mutation omits contentHash after a sentinel failure.
- share.test.ts mocks reservation, confirmation, file upload, and filesystem boundaries; assertions cover early onUrl delivery, complete/error/cancelled confirmation, nonfatal confirmation errors, and optional log-file configuration.
- Renderer-engine tests distinguish skip from skip-self: both succeed visually, but only skip sets ctx.skip; these assertions protect downstream pipeline behavior independently of a terminal renderer.
- manifest.golden.test.ts pins exact whole-Storybook, per-story, and config hashes against a frozen fixture, supplementing relational hash tests; its comments explain that changing the recipe affects comparisons against older baseline hashes.
- smoke-test.yml packs the CLI and defines Yarn variants, pnpm, npx, Node API, Windows, and Action runs; it copies fixtures outside the repository to avoid fallback to repository node_modules. The inspected matrix lists Ubuntu and Windows, without establishing macOS behavior.
- Lint-and-test CI specifies type checking, linting, build inspection, Vitest, and Codecov; separate Chromatic workflows publish the CLI's own Storybook with exitOnceUploaded:true, so their upload job does not wait for final snapshot results.
Risk map
These are source observations and explicitly labeled hypotheses, not confirmed vulnerabilities. Sensitive boundaries include credential exchange, shell command construction, repository mutation, filesystem traversal, and diagnostic uploads. Compatibility hotspots include shared Context access and persistent hash contracts across CLI versions. Mock-based tests provide useful behavioral specifications but do not validate these boundaries against a live service or real user workspace in this investigation.
- execGit uses shell:true while Git helpers interpolate revisions, branch references, and paths into command strings; findMergeBase and checkout therefore require careful input/escaping review at callers. No exploit was tested.
- Patch restoration executes git reset --hard before and after dependency installation; preparation checks a clean workspace, but isUpToDate returns true on remote-update or revision lookup errors, and restoration failures are logged without masking the original pipeline error.
- Interactive checkPackageJson builds a persisted npm script from originalArgv.join(' '); if a project token was supplied on argv, the resulting package.json script can retain it. This path is gated by interactive mode and confirmation.
- TurboSnap enables metadata upload by default once a build Storybook URL exists; the upload list includes logs, diagnostics, main/preview configuration, trimmed stats, and the manifest. Diagnostics redact three named token fields, but that redaction is not applied by uploadMetadataFiles to every listed file.
- readMainConfig first evaluates the project's main configuration through require before falling back to AST parsing; metadata discovery is consequently a code-execution boundary, even before the explicit Storybook build command.
- Artifact enumeration uses statSync, which follows symlinks, without the ancestor-cycle tracking present in the v2 ProjectFiles tree walker; user-selected output/config/static paths can consequently involve filesystem locations beyond a repository-local artifact tree.
- Hypothesis: a build that never receives completion may wait indefinitely during snapshot monitoring. The inspected GraphQL completion loop has delay-and-recurse behavior without a local deadline or abort check, and the notify function accepts no AbortSignal; this was not reproduced. (inferred)
- Upload and TurboSnap preparation still thread the live Context through nominal task seams, and share stubs ctx.git with empty values; meanwhile v2 hash recipes persist across backend baselines. These are concrete coupling and compatibility hotspots for seemingly local refactors.
Extension points
Extension points are explicit TypeScript tables, interfaces, adapters, and task seams. CLI commands use a dispatch table, configuration uses a strict schema, and renderers consume a common lifecycle interface. Programmatic callbacks expose task progress and cancellation without a separate plugin loader in the inspected flow. TurboSnap's filesystem abstraction and manifest serialization provide particularly clear seams for deterministic implementation and testing.
- New CLI utilities plug into the commands table in bin-src/register.js, following its lazy-import main(argv) handlers and argument slicing; unknown commands currently fall back to the publishing main handler.
- A new shared option spans Flags/Options, parseArguments' meow definitions, getPartialOptions normalization, and possibly configurationSchema; the schema is strict, so unknown configuration keys are rejected.
- Action-facing options require matching action.yml input declarations and getInput/maybe mapping in action-src/main.ts; return-object changes also require synchronization with declared Action outputs.
- TaskConfig defines transitions, extractInput, run, applyOutput, and applyPartial; runTask adapts that business logic to any TaskRenderer implementing start/update/succeed/fail.
- Programmatic integrations can provide experimental_onTaskStart/onTaskError/onTaskProgress/onTaskComplete and experimental_abortSignal; makeReporter fans numeric progress to callbacks and synchronizes reported build state onto Context.
- builders and viewLayers are lookup registries for Storybook builder/framework identities; getStorybookMetadata reads through MainConfigReader and combines independently resolved version, builder, refs, and static-directory information.
- TurboSnap v2 requires an injected ProjectFiles interface for file tests, version resolution, hashing, tree listing, and writes; realProjectFiles is an explicit production adapter, and serializeManifest is shared by disk output and the inspection command.
- Presentation examples extend through .storybook/main.ts's node-src story glob and HTML/Vite framework; preview decorators render ANSI CLI messages or HTML, allowing task/message states to be exercised separately from live publishing.
Contributor handoff
Begin with the shared pipeline and its adapters before editing individual tasks. Preserve task-result semantics, input precedence, Action synchronization, and the distinction between v1 selection and v2 hash collection. Tests and CI were inspected, not executed, and this bounded brief cannot establish passing checks or comprehensive coverage. Any later executable validation needs separate authorization because setup, publishing, initialization, and patch builds have material side effects.
- Read package.json, bin-src/register.js, node-src/index.ts, context.ts, and types.ts first: they define exported surfaces, dispatch, pipeline order, client setup, and state contracts.
- Use the repository's Yarn 4 toolchain in an authorized development session; packageManager pins yarn@4.18.0, and existing scripts provide yarn test, yarn typescript:check, yarn lint, and yarn build. This investigation executed none of them.
- Prefer edits at extractInput/run/applyOutput task seams; keep Options readonly to task dependencies and mutate Runtime for effective forceRebuild changes. Account for the explicitly documented live-Context exceptions in upload and preparation.
- Preserve partial/skip versus skip-self behavior: partial and skip halt downstream tasks, while skip-self permits continuation. Patch restoration must remain outside the skippable render pipeline.
- For option changes, inspect precedence and validation before extending CLI, config, Node, or Action interfaces; retain dependent-option checks, strict schema behavior, and the Action's merge-queue-specific exitZeroOnChanges default.
- For TurboSnap changes, read the dispatcher and golden hashes first: v2 returns fallback after writing/uploading hashes, v1 still supplies story selection, and exact hash constants protect comparisons with older baselines.
- Treat token handling, metadata uploads, and workspace operations as side-effect boundaries: diagnostics redact named tokens, metadata includes configuration/log files, init installs dependencies, and patch restoration invokes hard resets.
- For later authorized verification, start with adjacent Vitest cases and then the configured CI checks; preserve the distinction between mocked orchestration tests and live packed-package smoke tests. Reconcile Node >=22, bundle node20 targets, and Action node24 when changing runtime compatibility.
Languages
| Language | Files | Share of source |
|---|---|---|
| TypeScript | 541 | 94% |
| JSON | 35 | docs/config |
| JavaScript | 32 | 6% |
| YAML | 19 | docs/config |
| Markdown | 9 | docs/config |
| HTML | 3 | docs/config |
| CSS | 1 | docs/config |
Start reading here
Probable source entrypoint: node-src/ — inferred from the directory tree and manifest, not the upstream docs.
Source roots
node-src/ (561 files) bin-src/ (21 files) smoke-tests/ (13 files)
Source layout — node-src/
| Path | Files |
|---|---|
node-src/__mocks__/ | 19 |
node-src/ui/ | 245 |
Repository structure
| Top-level path | Files |
|---|---|
node-src/ | 561 |
bin-src/ | 21 |
.github/ | 16 |
smoke-tests/ | 13 |
action-src/ | 4 |
.storybook/ | 4 |
scripts/ | 3 |
static/ | 1 |
Docs found: README.mddocs/DEVELOPMENT.md
Package
package.json — chromatic v18.10.2, 80 declared dependencies.
@actions/core@actions/github@antfu/ni@auto-it/slack@chromatic-com/cypress@chromatic-com/playwright@chromatic-com/vitest@clack/prompts@discoveryjs/json-ext@eslint-community/eslint-plugin-eslint-comments@expo/config-plugins@expo/config-types
CI & config entrypoints
README.md— Repository readme.env.example— Required env vars.github/workflows/smoke-test.yml— GitHub Actions workflow.github/workflows/chromatic-main-and-prs.yml— GitHub Actions workflow.github/workflows/release.yml— GitHub Actions workflow.github/workflows/chromatic-prod.yml— GitHub Actions workflow.github/workflows/pull-request-workflow.yml— GitHub Actions workflow.github/workflows/package-size.yml— GitHub Actions workflow.github/workflows/package-size-main.yml— GitHub Actions workflow.github/workflows/check-labels.yml— GitHub Actions workflow
Structural health signals
Average score 8.5/10 across 96 analyzed source files; 1 hotspot surfaced.
node-src/lib/uploadFiles.ts— 77 lines, score 6.4 (Deep nesting, Moderate churn)node-src/tasks/gitInfo.ts— 495 lines, score 6.7 (Large file, Deep nesting)node-src/io/httpClient.ts— 115 lines, score 7.1 (Deep nesting, High churn)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
9e2606a2dbc12026-10-02 — Bump version to: 18.10.2 [skip ci]f86cef5c48e72026-10-02 — Update CHANGELOG.md [skip ci]9af4ad27ead72026-10-02 — Merge pull request #1511 from chromaui/cody/cap-5180-v1890-always-on-turbosnap-v2-hash-collection-runs-out-of564ea89dce822026-10-01 — Correct the stale comment on shouldCollectHashes1949e36dc8862026-09-30 — Merge pull request #1509 from chromaui/cody/cap-5179-hash-single-file-staticdirs-entries-in-the-turbosnap-v2