Skip to content

Engineering stories · 05

Shipping safely to a co-edited live Shopify store

  • GitHub
  • Delivery

Designing a delivery workflow around two Shopify-specific truths: branches map to themes and merges auto-publish, and a content team co-edits the same files through the Customizer all day.

Shopify OS 2.0 · GitHub integration · Production delivery · Shared-state management · Release safety

This is not a story about knowing Git. Git is a given. The story is what Git does to a live Shopify store when you use it the ordinary way, because two platform behaviors turn a routine workflow into a live-content shredder. I learned both the expensive way, then designed the delivery process around them. That process is why every other part of this project could ship one change at a time and revert in a click.

The setup that makes normal Git dangerous

The store runs Shopify's GitHub integration. Three themes are bound to three branches: a production theme on the live branch, an integration theme on a dev branch, and a backup. The binding is the important part. When something merges to the live branch, Shopify auto-publishes it to the storefront. There is no separate deploy step and no confirmation dialog. Merge is deploy.

So the first rule wrote itself: direct pushes to the live branch are closed, and changes reach it only through a pull request. A merged PR is a production release whether you meant it that way or not.

Each developer works on a personal, unpublished duplicate theme for preview. You point the Shopify CLI at your own theme through a gitignored .env, run shopify theme dev, and get a live local-to-theme sync that never touches anyone else's preview or the storefront. Your experiments stay yours until they become a PR.

That handles code. The second behavior is the one that bites, and it has nothing to do with code.

The content team edits the store every day, and Shopify commits their edits back to Git. Merchandisers change copy, swap images, and reorder sections through the Shopify Customizer as part of their normal work. Shopify's GitHub integration writes each of those edits back to the repo as a commit, so the branch history carries a steady stream of "Update from Shopify" commits. In this repo, 317 of 484 commits are those automatic Customizer round-trips. Two thirds of the history was written by people who never opened a code editor.

The consequence is easy to state and easy to forget: a handful of JSON files are shared state, not local files I own. They hold the team's live merchandising. My local copy of them is stale the moment a merchandiser touches the store. The files that carry this content, the ones I came to treat as a danger zone, are:

  • templates/*.json (which sections a page shows, in what order, with what settings)
  • sections/header-group.json and sections/footer-group.json (the header and footer content: logo, promo bar, menu selection)
  • config/settings_data.json (global theme settings)

Everything else in the theme, the section and snippet .liquid files and the assets, is authored in code. Those I own and can push freely. The danger-zone JSON I do not own alone. Treating the two categories the same is exactly the mistake.

Two incidents that taught the rule

I did not derive the danger zone from first principles. Two production incidents drew the boundary for me.

Incident one: a stale template overwrote sections that only existed in the editor. I pushed a product template JSON from my local branch. My copy was older than the store, because in between, the content team had inserted new sections into that page through the Customizer. My push replaced the live template with my stale version and those newly inserted sections vanished. They were never in Git. They existed only in the store's live state, which my push had just overwritten. There was nothing to revert to, because Git had never held the good version. The team's work was gone, not rolled back.

Incident two: a go-live full push reverted the header. During a launch I ran a full theme push to move a batch of work up at once. It reverted the promo-bar text, the header logo, and the active menu selection, all live content the team had set through the Customizer. The header section-group JSON was one of the files in the push, and I had not pulled the current version first. A full push does not touch one file. It clobbers every co-edited JSON on the theme in a single motion, so a launch that was supposed to add features quietly rolled back the storefront's masthead at the same time.

Both share one root: I pushed my version of a file that was really the team's version, without first capturing what they had done. The first incident taught me which files are shared state. The second taught me that a full push multiplies the risk across all of them at once.

The workflow I designed in response

The fix is not a tool. Nothing in the platform enforces this, so the discipline has to be the design.

Pull before push on any danger-zone file. Before I touch one of those JSON files, I pull the current live version first: shopify theme pull --only <file>. That captures whatever the content team has done since I last synced. Then I layer my change on top of their current state, and only then push. My edit becomes an addition to their live content instead of a replacement for it. In the repo this shows up as small capture commits that record the Customizer state right before a change goes in.

Back up settings before any full push, and prefer never doing a full push at all. The go-live incident was a full push. The safe default is to move only the specific files a change needs, and to treat a whole-theme push as a rare operation that starts with pulling and backing up the shared JSON.

Never push while theme dev is running. The live sync and a manual push fight over the same files, and the loser is usually the live content.

Push code freely, treat content carefully. Section and snippet .liquid and the assets are local-authored. I push those directly without ceremony. The whole discipline concentrates on the four JSON categories that hold state I do not own. Knowing exactly which files those are is what keeps the process fast instead of paranoid.

Two details that matter at deploy time

Order matters when a section and its template change together. If I change a section's schema and also change a template that uses it, I deploy the section first, then the template. Shopify strips any setting from a template when that setting is absent from the section's current schema. Push the template first and the template arrives referencing settings the live section does not yet define, so Shopify silently drops them. Section first, template second, and the settings survive.

Every change ships as one isolated, revertable PR. This is the line every other article in this project leans on, and this is why it is true. Each feature is a single PR that does one thing. If it goes wrong in production, it reverts in one action: the GitHub Revert button, or git revert -m 1 <merge-sha> on the merge commit. Because a merge to the live branch auto-publishes, a revert auto-publishes too. The rollback path is the same one-step mechanism as the release. Small, single-purpose PRs are not a style preference here. They are what makes "revert in a click" real on a store where merge equals deploy.

The same lesson in a different system

The danger-zone JSON is one case of a broader rule: know which state you do not own. Inventory is the other. Stock levels are mastered by Veeqo, an external fulfillment and inventory system that pushes quantities into Shopify. Edit stock by hand in Shopify Admin and Veeqo overwrites it on the next sync. Same shape as the Customizer problem, different system: a source of truth lives outside the surface you are editing, so a local write is temporary and will be clobbered. It is also the reason the storefront's "Only 8 left" style lines are static content the content team sets in the Customizer, not live stock reads. Wiring them to real inventory would have meant fighting Veeqo for authority over a number Veeqo owns.

One operational note that belongs with all of this: push access to the repo is limited to a specific account. Using the right one is part of the routine, and getting it wrong is its own small class of failure.

Outcome

The verified outcome is process, not a metric. After the workflow was in place, no further Customizer content was lost. Danger-zone files are pulled before they are pushed, full pushes are backed up first, and releases go out as single revertable PRs. The two incidents that shaped the rules did not recur.

The expected business value follows from that, stated as protection rather than measured lift. The content team's daily work is no longer collateral damage in a code deploy, which protects live merchandising and the team's trust that engineering will not quietly undo their work. Revenue-impacting regressions get a one-step rollback, because every change is isolated. And launches got faster at the same time: a new landing page is a template copy with only its product bindings swapped, and the content team fills in the rest through the Customizer, on a path that no longer treats their edits as a merge conflict waiting to happen.

What I would carry to the next store

  • On a co-edited Shopify theme, safe delivery is a design problem, not a Git skill. The platform gives you auto-publish on merge and a content team writing to the same files. The workflow has to assume both.
  • Name your shared state explicitly. A short, memorized list of danger-zone files (templates/*.json, the two section-group files, settings_data.json) is worth more than a general sense of caution.
  • Pull before you push the files you do not own, and never full-push without a backup. Most of the risk lives in whole-theme operations.
  • Make revert a first-class feature by keeping every change to one PR. Rollback safety is bought at authoring time, not at incident time.
  • Ask, for every value on the page, who owns its source of truth. The Customizer owns content, Veeqo owns stock. Writing over either from the wrong place is the same mistake twice.

Technical references

  • Platform: Shopify Online Store 2.0, GitHub integration (branch-to-theme binding, auto-publish on merge to the live branch), Shopify CLI.
  • Preview: personal unpublished duplicate dev themes; shopify theme dev for one-way local-to-theme sync; --theme-editor-sync to pull Customizer JSON back into local files.
  • Shared-state files: templates/*.json, sections/header-group.json, sections/footer-group.json, config/settings_data.json.
  • Core commands: shopify theme pull --only <file> (capture Customizer state before editing); git revert -m 1 <merge-sha> or the PR Revert button (one-step rollback).
  • Deploy order: when a section schema and its template change together, deploy the section before the template so Shopify does not strip template settings absent from the section schema.
  • Related shared state: inventory mastered externally by Veeqo; manual Shopify stock edits are overwritten on the next sync.
Back to the caseLumway

Need a Shopify engineer who can work across the full commerce path?

I work where storefront UX, product logic, platform constraints, measurement, and production delivery meet.

Start a conversation