← All writing
Craft · · 14 min

It will finish the list

A deploy that deletes instead of uploading, an ignore rule for a file that's been gone since 2023, and one warning I copy/pasted into three files while congratulating myself.

AI Shopify

A missing SHOPIFY_THEME_ACCESS_PASSWORD will abort the deploy on auth. Do not treat that as a guard. It is luck, not design.

That’s a paragraph out of a Markdown file at work, one that tells Claude how to stand up a new Shopify client repo. I think it’s the most useful thing in there. It’s also the only thing in there that doesn’t make anything happen. Everything else creates, copies, sets, commits. That one just sits there.

We’ve got two of them, covering the two ways one of these projects starts life. shopify-onboard makes a new repo off our template and gets it talking to the store. shopify-workflow-upgrade is for the older stuff, a project that predates all of this and has to be moved and renamed and rewired onto the current setup. (There’s a third that drags an already-onboarded repo forward when the template moves out from under it. Different animal, leaving it out of this one.)

Calling them skills oversells them a bit. Each is a single Markdown file with about eight lines of front matter on top. No code, nothing you could run. Hand either one to a new hire instead of to Claude and it would work about as well.

In 11 easy steps

Onboarding, start to finish. Make the repo from the template, put the store in shopify.theme.toml, delete the two files that only exist to serve the template, set three GitHub secrets, commit that, pull the client’s live theme down, commit that, push, run /init so Claude reads the theme that just landed and writes the project its own CLAUDE.md, commit and push again, print a summary.

gh repo create cadencelabs/$REPO --template cadencelabs/shopify-theme-template --private --clone

# set the store in shopify.theme.toml, then
git rm CLAUDE.md
git rm -r .claude/skills

gh secret set SHOPIFY_THEME_ACCESS_PASSWORD --repo cadencelabs/$REPO --body "shptka_..."
gh secret set SHOPIFY_STORE_URL --repo cadencelabs/$REPO --body "$STORE"
gh secret set SLACK_WEBHOOK_URL --repo cadencelabs/$REPO --body "https://hooks.slack.com/..."

git commit -m "Configure repo for $CLIENT"
npm run pull:live
git add -A && git commit -m "Add initial theme files from live store [skip ci]"
git push origin main
Bash

I could get most of that from memory on a decent morning. If it were the whole file I don’t think it would earn its disk space. I certainly wouldn’t lean on it the way I do.

Everything around it earns the space, all of which I only know because I was wrong about it once, in a specific way, on a specific afternoon. Having written a few of these now, my sense is that it comes in exactly two flavors.

Please don’t tidy this

The store URL lives in shopify.theme.toml. Getting it out of package.json and into that file was the entire point of a migration I did a while back, so the scripts could be the same in every repo. Great. So why does every one of these repos also carry a SHOPIFY_STORE_URL GitHub secret holding that exact string?

Because CI never reads the toml on the live path. It reads SHOPIFY_FLAG_STORE off the secret. So the store lives in two places deliberately. Anyone who has just spent a week deduplicating config will look at that and feel a strong pull toward fixing it, and what they’d be fixing it into is a broken deploy. So the skill says so every single time it goes near a secret. It’s a note to a future developer who is about to do some very confident refactoring.

There’s a smaller one further down with the same shape. Redesign projects, where the new build sits on an unpublished theme next to the client’s live one, get a second environment block in the toml, and that block has to carry its own store even though the default block a few lines up already has one. Named environments inherit nothing. That’s a Shopify CLI fact instead of something anyone would arrive at on their own. It reads on the page like duplication nobody got around to deleting. Both files explain themselves because either one would otherwise survive a tidy-up by someone in a good mood.

Sync, not upload

The second flavor is the one I think actually matters. It’s the places where the instructions tell you to stop.

Here’s the one that got me. Both onboarding commits carry [skip ci], and if the only place you’ve met that marker is somebody saving a couple of Actions minutes on a README typo, it looks like housekeeping and it looks eminently droppable.

It isn’t. deploy.yml fires on push to main, and its live path runs shopify theme push --live --allow-live with no --nodelete. That makes it a sync instead of an upload. Anything sitting on the live theme that isn’t in your tree gets deleted off the client’s published storefront.

Onboarding is about the worst available moment to take that bet. Nobody has read this codebase yet and it arrived out of a pull:live you have no particular reason to trust. A partial pull doesn’t announce itself, it just shows up as a smaller set of files, which the deploy then reads as a list of deletions. And whatever the client’s marketing team did in the theme editor between your pull and your push gets flattened by your slightly older config/settings_data.json. In exchange for all of that the deploy buys you nothing, since the only thing it has to push is what you pulled a few minutes earlier.

GitHub only looks at the head commit of a push, which is why the marker has to be on both commits instead of just the first, and why the skill spells that out instead of writing “skip CI during onboarding” and hoping. The rest of the guard is the order of the steps. Never push to main before the theme has been pulled.

The line I opened with sits right underneath that one. It’s there because a missing password is exactly the kind of thing you’d lean on without ever noticing you were leaning. Your first run through these steps you think, well, the secrets aren’t set yet, so nothing can deploy. And you’re right! Today, for a reason that has nothing to do with a decision anybody made.

The backup does the same trick. The shared workflow snapshots the live theme before it pushes, and a failed backup blocks the deploy, which sounds like the end of the discussion. But it skips the backup entirely if one already exists for that calendar day, so your second push of the day is covered by whatever your first push happened to capture. I wrote the rule that behaves that way and I’d still defend it. It just means recoverable and safe are two different words.

Ask for the doubt

A model handed a numbered list will finish the numbered list.

Which is most of why any of this works. It’s also the whole of what makes it tricky. You never have to ask for due diligence. You get due diligence for free, occasionally rather more of it than you wanted. You have to ask for the doubt, explicitly, sometimes in caps like a maniac.

Don’t commit automatically, leave the changes staged for a person to look at. Stop and ask if one of the inputs is missing instead of picking something sensible. If pull:live wants interactive auth, hand the terminal back to the human instead of engineering around it. When /init runs against a repo that already has a CLAUDE.md, fold the project-specific notes in instead of writing over them. None of those are hard to follow. They’re hard to think to write down, because nothing about the experience of working through a checklist suggests that the best available move might be to stop halfway.

I closed a post about these same deploy workflows in November by saying that automation doing math about something it can’t see should stop and go find a person. Turns out I was making the identical argument at a different reader and hadn’t noticed.

And the amount of doubt isn’t constant, which surprised me a little when I spotted it. shopify-onboard commits and pushes to main on its own without asking anybody. shopify-workflow-upgrade says, in bold, do not commit automatically. Same author, same week, opposite instruction. But onboarding happens in a repo that’s an hour old whose entire contents came down off the store that morning, so the worst case is you delete the folder and go again, where an upgrade has you standing inside somebody’s real project with years of history under your feet. Trust isn’t a setting you pick once. It’s a function of what’s waiting on the other side of being wrong.

Bitbucket and Theme Kit

The upgrade skill is the longer read of the two and most of that length exists because of how we used to work.

A pre-upgrade project is on Bitbucket. Its production branch is master. It has a config.yml and a config-sample.yml in the root, which are Theme Kit config from back when Theme Kit was how you moved a theme around, and config.yml is in .gitignore because it held an API password in plain text. Its package.json scripts have the store baked into every single string:

package.json
{
  "dev": "shopify theme dev --store some-store.myshopify.com",
  "pull:live": "shopify theme pull --live --store some-store.myshopify.com",
  "push:preview": "shopify theme push --unpublished --store some-store.myshopify.com"
}
JSON

If it has CI at all it has one of two deploy workflows, push-live.yml for a normal project or push-to-theme.yml for a redesign, plus a SHOPIFY_THEME_ID secret telling the second one where to aim. No preview themes on pull requests. No .mcp.json and no CLAUDE.md, that whole setup predating anyone here talking to a language model about anything.

So the upgrade is: move the remote, rename the branch, copy in the current workflow files and .mcp.json and the README and the toml, put the store in the toml, swap the scripts for the store-agnostic ones, delete the Theme Kit files, set the secrets, run /init.

git checkout master && git pull
git branch -m master main
git remote set-url origin git@github.com:cadencelabs/$REPO.git
git push -u origin main
Bash

Four lines, and then the very next section of the file is the interesting one. If master doesn’t exist, check whether main already does and skip the rename.

Somebody has been here before.

Maybe me, months ago, two steps in when something else came up. The happy path assumes a repo in a known state and there is no such repo.

Two more items on that list are worth pulling out, because they’re the same species as the [skip ci] thing.

The first is the legacy deploy caller. Step 3 says copy these 5 files out of the template, and copying is additive. It physically cannot remove push-live.yml, because push-live.yml isn’t one of the 5. So a repo upgraded by somebody following the obvious reading of that step comes out with the new deploy.yml and the old caller sitting side by side, both listening for a push to main, both deploying the theme on every merge, forever. Nothing errors. You’d find out from the Slack channel getting doubles, if you found out at all. So the skill has to name the file and git rm it in so many words, because the absence of a file is not a thing a copy operation can express. It isn’t a thing you can see from inside step 3 either.

The second is SHOPIFY_THEME_ID. It used to be how a redesign project said which theme to deploy to. It doesn’t exist anymore, the theme id lives in the toml now, and the skill says don’t set it. That reads like trivia right up until you know that a stray SHOPIFY_THEME_ID on a standard repo makes the deploy hard-fail on purpose, that being a feature of our theme template instead of a bug in it. An upgrade is the one moment in a repo’s life where somebody would plausibly paste that secret in without thinking about it. So it’s the one place the instruction is worth the line.

Then there’s the .gitignore line, which is my favorite small thing in either file. You delete config.yml, and you also take config.yml back out of .gitignore, and you leave the trailing blank line alone while you’re in there. An ignore rule for a file that no longer exists breaks nothing. It just quietly tells the next person that this repo has a Theme Kit config, which it hasn’t had since 2023. I’ve written about an ignore rule that was correct and inert. This is the other direction, a line insisting on something that stopped being true. Small lies like that are worse than they look, because nobody ever has a reason to go check one.

You have invented the README

Is this just documentation in a costume? Write a README, Madison. Congratulations.

Sure, kind of. Except the failure mode of a README is that it loses to whoever’s in a hurry, and whoever’s in a hurry is usually me. I did write the README version of this. It was accurate! I stopped opening it after the third project, because by then I remembered most of it, and “most of it” is the whole problem. I remembered the steps, which were never the point, and forgot the store URL thing, which was. The skill wins on one thing only, which is that it gets read start to finish on every single run, by something that has never done this before and has no ego about rereading step 3.

The last time a README got under my skin the problem was the opposite one. That line was accurate too, and had been accurate for so long that everybody had stopped seeing it.

Something I noticed while writing this and immediately wished I hadn’t. That SHOPIFY_STORE_URL warning I was so pleased with? It’s copy/pasted, near verbatim, into both of these files and into the third one I said I wasn’t going to talk about. That’s precisely the thing our theme template exists to prevent, faithfully reproduced one floor up in the documentation. If CI ever changes how it reads the store I have three files to update and I will remember two of them. 🫠

I don’t have a fix for that. A shared reference all three point at, maybe? Or is that over-engineering three MD files? idk, it’s one of those.

Anyway. If you’re writing one of these, the only real suggestion I’ve got is to draft the happy path fast and without much care, because the happy path is the part you’d have gotten right on your own. Then walk back through it a step at a time and ask what happens if this one is already done, or already exists, or got half done six months ago in a way you can’t see from where you’re standing. Whatever comes back from those questions is the part worth writing down. It goes right next to the step it’s about.

Read similar posts
10 min

It's never the prompt

The prompting turns out to be the least interesting part of my Claude Code setup, and most of what's actually doing the work is a handful of files I wrote once and then stopped thinking about.

9 min

Documentation for future-you

I came back to a project after a year away and found three notes I'd left myself in one confident voice, two of which saved me an afternoon and one of which cost me one.