LUCENTCOMMERCEGET A FREE STORE AUDITFREE AUDIT

TECHNICAL · THEME · OPERATIONS · 25 NOVEMBER 2025 · 9 MIN READ

Theme CI: testing a Shopify theme before it ships

Lint, preview and a performance budget, run on every pull request. Three stages, all of them built from tools Shopify already ships.

A request and its response, side by side

Set up CI for a Shopify theme in three stages, each a step harder than the last. Lint every pull request with shopify theme check and a --fail-level your team agrees on. Push each pull request to its own unpublished theme with shopify theme push --unpublished --json so reviewers get a real, password-protected preview URL instead of a diff. Then run the Shopify Lighthouse CI action against that preview with minimum score thresholds, so a regression in performance blocks the merge. All three authenticate with a Theme Access password in SHOPIFY_CLI_THEME_TOKEN, and none of them needs a tool Shopify does not already publish.

IN SHORT

  • Theme CI is three stages: lint, a real preview per pull request, and a performance budget that can fail a build.
  • `shopify theme check` is the linter; `.theme-check.yml` sets which checks run and at what severity, and `--fail-level` decides which severities break the build.
  • `shopify theme push --unpublished --json` creates a throwaway theme per branch and returns the preview URL, which is the single highest-value thing CI can do for a theme.
  • Shopify CLI authenticates in CI with a Theme Access password in `SHOPIFY_CLI_THEME_TOKEN`, with `SHOPIFY_FLAG_STORE` for the store and `SHOPIFY_FLAG_FORCE=1` to stop it waiting on a prompt.
  • The hard part is not the pipeline. It is that merchandisers edit JSON templates and settings in the admin, so your repository is not the only writer.
  • Never let a deploy push `settings_data.json` blindly — use `--ignore` or `--nodelete` deliberately, or you will overwrite a week of merchandising.

What theme CI can and cannot do

A theme is not an application, and pretending otherwise produces a pipeline nobody trusts. There is no unit test for a section rendering correctly at 375px with a 40-character product title, and there will not be one. What CI can do for a theme is narrower and still worth a great deal: it can catch the errors a linter catches, it can put a working preview in front of a human on every change, and it can stop a change that makes the store measurably slower.

That is the whole scope. Everything else — does this look right, does the merchandising make sense, does the mobile layout survive a long title — is a person looking at a page. CI’s job is to make sure that person is looking at the right page, quickly, without asking anyone to build them one.

It also has a second job that only becomes obvious after the first incident: it stops the store being deployed to by hand. Most theme disasters we are called about are not bad code. They are a theme push run from a laptop against the wrong store, or a live theme overwritten with a branch that was three weeks behind.

Stage zero: decide who owns the repository

Before any pipeline, settle the awkward fact underneath Shopify theme development: the repository is not the only thing writing to the theme.

Shopify’s GitHub integration connects one branch to one theme, and the sync runs both ways. Pushing commits updates the theme; editing the theme in the admin — theme editor, code editor, or a theme app — produces commits back onto the branch, made by a Shopify bot, with edits saved within about ten seconds of each other batched into a single commit. That sync cannot be disabled. Only folders matching the default theme structure sync, and anything else in the repository is ignored.

The consequence is a genuine ordering problem. Two writers, no locking. The documentation is blunt about who wins in a conflict: in the code editor, "the version of the file in the code editor overwrites the GitHub version of the file", and a commit coming from Shopify can be rejected by GitHub as outdated when both sides move at once.

One more thing worth knowing before you start: a branch cannot be reconnected to a theme after it has been disconnected. Reconnecting adds it as a new theme instead. Plan the branch-to-theme mapping once rather than experimenting with it on a live store.

So choose the model deliberately. Either the integration is on and you accept that merchandisers commit to your repository, or it is off and CI is the only thing that writes to the store. Both work. Discovering which one you have during an incident does not.

Stage one: lint, with a fail level someone chose

Theme Check is Shopify’s linter for the Liquid and JSON in a theme, and it runs from the CLI as shopify theme check. It catches syntax errors, missing templates, unused variables and snippets, unknown and deprecated tags, and a set of performance issues — the last group being the one that earns its place in CI, because those are exactly the problems nobody spots in review.

Two flags do the work in a pipeline. --fail-level sets the severity at which the command exits non-zero, and --output writes the results as json or text if you want to attach them to the pull request. There is also --auto-correct for the checks that can be fixed mechanically, which is better run locally by a developer than in CI, where an auto-commit pushing to a branch mid-review is a nuisance.

The configuration lives in .theme-check.yml. extends points at a preset such as theme-check:recommended, ignore excludes paths, require loads custom checks, and each check can be named directly to turn it off or change its severity:

  • extends: theme-check:recommended — start here rather than enumerating checks by hand.
  • TemplateLength: with enabled: false or severity: warning — the per-check override. Severities are error, warning and info.
  • {% # theme-check-disable CheckName %} and {% # theme-check-disable-next-line CheckName %} — the escape hatch in the Liquid itself, for the one place the rule is genuinely wrong.
  • shopify theme check --print prints the resolved configuration, and --list prints the active checks. Run both once so the team knows what it has actually agreed to.

Stage two: a real preview on every pull request

This is the stage that changes how a team reviews, and it is the least work of the three.

shopify theme push --unpublished creates a new unpublished theme and pushes to it. Add --json and the command returns the result as JSON, including the preview URL, which your workflow can post as a pull request comment. Reviewers open a link and look at the store. Nobody sets up a local environment to check a spacing change.

Authentication is the part people get stuck on. Shopify documents the CI path clearly: generate a password from the Theme Access app and pass it as SHOPIFY_CLI_THEME_TOKEN, set the store in SHOPIFY_FLAG_STORE, and set SHOPIFY_FLAG_FORCE=1 if the step is timing out because the CLI is waiting for a prompt that will never come. The documentation is explicit that the password should be masked or stored as a secret — it is a credential with write access to your storefront.

One habit worth adopting early: give the throwaway theme a name that includes the branch and the build number, and delete it when the pull request closes. A store carrying forty abandoned preview themes is its own small problem, and the theme list is where a nervous person looks during an incident.

The flags that decide whether a deploy is safe

The deploy step is where a theme pipeline either protects the store or becomes the thing that broke it.

  • --strict requires Theme Check to pass without errors before pushing. Belt and braces if lint already runs as its own job, and worth having on the deploy step specifically, because that is the one that touches a real theme.
  • --allow-live allows a push to the live theme in a non-interactive environment. Treat adding this flag as a design decision, not a fix for a failing job. Most teams should deploy to an unpublished theme and publish separately.
  • --nodelete prevents deleting remote files that do not exist locally. Useful when apps add files to the theme that your repository has never seen.
  • --ignore (-x) skips files on upload. This is how you stop a deploy overwriting settings_data.json and the JSON templates that merchandisers have been editing all week. Decide which files the repository owns and which the admin owns, and encode that decision here.
  • --only (-o) uploads just the files you name — the right tool for a hotfix to one snippet on a live theme, and a much smaller blast radius than a full push.

Stage three: a performance budget that can fail the build

Shopify publishes a Lighthouse CI action for exactly this. It runs Lighthouse audits against a theme on pull requests and reports back as a status check, and it takes a store, credentials, and optional product_handle and collection_handle so the audit covers a product and a collection page rather than only the home page — which is the version of this check that actually tells you something, because the home page is rarely the template a customer lands on.

The thresholds are the point. lhci_min_score_performance defaults to 0.6 and lhci_min_score_accessibility to 0.9, both on a 0 to 1 scale, and both are the numbers to argue about as a team. A budget nobody agreed to is a budget somebody will disable the first time it goes red. Authenticate with the action’s client_id and client_secret inputs rather than a legacy custom app token, and note that the tokens it fetches are valid for 24 hours, so this is not a credential to cache.

Two honest caveats. Lighthouse in CI is a lab measurement on a machine that is not a phone on a train, so treat it as a regression detector rather than a score to optimise. And it will not see the apps that only load on the live theme, which is a large share of real-world storefront weight. The field data in Shopify’s own web performance reports — Largest Contentful Paint, Interaction to Next Paint and Cumulative Layout Shift from your real traffic — is the check on whether the lab number means anything.

One more tool worth knowing even though it does not belong in CI: shopify theme profile profiles Liquid performance on a given page. When the Lighthouse number moves and nobody knows why, that is where to start, because it distinguishes a slow template from a slow page.

What we would build first, and what we would leave

If you have none of this today, build it in this order: preview on pull request, then lint, then the performance budget. That is deliberately not the order of increasing rigour. Previews change reviewer behaviour on day one and cost an afternoon; lint produces a backlog of findings on an inherited theme that somebody has to triage; the performance budget is only useful once the first two have stopped the obvious regressions.

Leave visual regression testing alone unless you have a specific, repeated failure it would have caught. Screenshot diffing a storefront where prices, stock badges and recommendations change hourly generates false positives faster than anyone will triage them, and a check people routinely override is worse than no check, because it teaches the team that red is normal.

The place this stops being a theme problem is when the pipeline needs something the CLI does not do: seeding a test store, reconciling data between the theme and a back office, or exercising an integration that the storefront depends on. That is application work with its own deployment story, and it belongs in a [custom app or back end](/services/integrate/custom-shopify-apps-and-back-ends) with a real test suite rather than bolted onto a theme workflow. Knowing where the boundary is saves a lot of time spent trying to make a theme pipeline behave like a product one.

Questions this raises

What is Theme Check and do I need it in CI?

Theme Check is Shopify’s linter for the Liquid and JSON in a theme, run as `shopify theme check`. It detects syntax errors, missing templates, unused variables and snippets, deprecated tags and a set of performance issues. In CI it is worth having because those are the classes of problem a human reviewer reliably misses — but set `--fail-level` deliberately, because failing on every severity in an inherited theme will produce a red build on day one and a disabled check on day two.

How does Shopify CLI authenticate in a CI pipeline?

With a password generated from the Theme Access app, passed as the `SHOPIFY_CLI_THEME_TOKEN` environment variable, plus `SHOPIFY_FLAG_STORE` for the store. Set `SHOPIFY_FLAG_FORCE=1` if the step hangs waiting for an interactive prompt. Shopify’s documentation is explicit that the password should be masked or stored as a secret; it grants write access to your storefront, so it belongs in your CI provider’s secret store and nowhere else.

Should we use the Shopify GitHub integration or deploy from CI?

Pick one and know why. The GitHub integration syncs both ways — commits update the theme, and admin edits are committed back by a Shopify bot — which is convenient and means merchandisers write to your repository. CI-only deployment gives you one writer and full control of the deploy step, at the cost of admin edits not being captured anywhere. The failure mode in both cases is the same: two writers and no agreement about who owns which files.

How do I stop a deploy overwriting merchandising settings?

Use `--ignore` (`-x`) on `shopify theme push` to skip the files the admin owns — typically `settings_data.json` and the JSON templates that the theme editor writes. `--nodelete` additionally stops the push removing remote files that do not exist locally, which matters when apps have added files your repository has never seen. Decide the ownership boundary once and put it in the deploy script, not in someone’s head.

Can you run automated tests against a Shopify theme?

You can run linting, Lighthouse audits and end-to-end browser tests against a preview theme, and all three are useful. What you cannot reasonably automate is whether a page looks right, because the content underneath it changes constantly. Visual regression testing on a storefront produces false positives from price changes, stock badges and recommendations faster than most teams will triage them.

Does theme CI work on a development theme?

It can, but an unpublished theme is usually the better target for a pull request preview. Development themes created by Shopify CLI are temporary and hidden, do not count towards your theme limit, and are deleted after seven days of inactivity — ideal for local development, less ideal for a preview link a reviewer might open next Tuesday. `shopify theme push --unpublished` creates a theme that persists until you delete it.

NEXT STEP

Free store audit

A senior Shopify engineer reviews your storefront, theme performance and checkout, then sends a prioritised list of fixes.