To deploy a Shopify theme with GitHub Actions, install Shopify CLI in the job, store a Theme Access password and the store URL as repository secrets, and run shopify theme push against a named theme ID. Pushing to the published theme also needs --allow-live, and a list of files the push must leave alone.
The command is the small part. A push uploads the repository’s copy of every file, including the files the theme editor writes, so before the workflow goes anywhere near a published theme you have to decide which files Git owns and which the merchant owns.
What does the CLI need to run without a login prompt?
Two values. Shopify’s CI/CD guide for themes names them: SHOPIFY_CLI_THEME_TOKEN, a password generated by the Theme Access app, and SHOPIFY_FLAG_STORE, the store to talk to. A third, SHOPIFY_FLAG_FORCE=1, turns off interactive prompts.
The password comes from the Theme Access app, which the store owner installs, or a staff member or collaborator with the Themes permission. It emails the developer a link. The link expires after 7 days or as soon as it has been opened, and the password is shown once, so copy it straight into the repository secret. It carries the write_themes scope and nothing else, and deleting it in the app revokes it. That narrow scope is the reason to use it in CI instead of an Admin API token with wider access.
What does a workflow to deploy a Shopify theme from GitHub Actions look like?
Start from the workflow in Shopify’s guide (checkout, Node, npm install -g @shopify/cli, one push) and split it in two: pull requests go to a staging theme, merges to main go to the live one. Theme IDs are not secret, so they sit in repository variables.
name: Theme deploy
on:
pull_request:
push:
branches: [main]
env:
SHOPIFY_FLAG_STORE: ${{ secrets.SHOPIFY_FLAG_STORE }}
SHOPIFY_CLI_THEME_TOKEN: ${{ secrets.SHOPIFY_CLI_THEME_TOKEN }}
SHOPIFY_FLAG_FORCE: 1
jobs:
staging:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm install -g @shopify/cli
- name: Push to the staging theme
run: |
shopify theme push --json --strict \
--theme "${{ vars.STAGING_THEME_ID }}"
live:
if: github.event_name == 'push'
runs-on: ubuntu-latest
concurrency: theme-live
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm install -g @shopify/cli
- name: Push code to the live theme
run: |
shopify theme push --json --strict --allow-live \
--theme "${{ vars.LIVE_THEME_ID }}" \
--ignore "templates/*.json" \
--ignore "config/settings_data.json"
Every flag there is in the shopify theme push reference. --allow-live is documented as “Required in non-interactive environments” when the target is the live theme, so without it the second job fails instead of asking a question nobody can answer. --strict makes the push “require theme check to pass without errors” first. If you want finer control over which offences fail a build, run the linter as its own step, which is what our post on Theme Check in CI covers.
--json prints the result in a form a later step can read: the theme id, name, role, an editor_url and a preview_url. On the staging job, that preview_url is the link a reviewer wants in the pull request.
concurrency is a GitHub Actions key, nothing to do with the CLI. It stops two merges in quick succession from pushing to the live theme at the same moment.
Why would a deploy delete the merchant’s changes?
Because a theme holds two kinds of file, and shopify theme push treats them the same.
Liquid, CSS, JavaScript and section schemas are code. They change when a developer commits. But JSON templates are, in Shopify’s words, “data files that store a list of sections to be rendered, and their associated settings”, and merchants add, remove and reorder those sections in the theme editor. config/settings_data.json holds the theme’s setting values and is rewritten whenever one changes in the editor. Those files change when the merchant clicks Save.
A push uploads your local copy of everything. If the repository’s templates/index.json is from last month, the live homepage goes back to last month. The push also removes remote files that do not exist locally unless you pass --nodelete, so a template the merchant created in the admin and nobody pulled is simply gone.
The two --ignore flags in the live job are the fix: the reference describes --ignore as skipping the upload of the files it matches, so the repository’s stale JSON never reaches the published theme. The reference does not say what happens to an ignored file that exists remotely and not in the repository, so push to a duplicate of the live theme once and check that a merchant-created template survives before you rely on it. You can put the same patterns in a .shopifyignore file at the theme root (it accepts file names, wildcards and regular expressions, and the CLI respects it alongside --ignore), but I would keep them on the command. A .shopifyignore applies to every push and pull from that directory, and the staging job should receive the JSON so reviewers see a full theme.
How do you get editor changes back into Git?
Pull them, on purpose, from the live theme. The shopify theme pull reference has --live and a repeatable --only, and asks you to quote wildcards.
shopify theme pull --live --nodelete \
--only "templates/*.json" \
--only "config/settings_data.json"
git add templates config/settings_data.json
git commit -m "chore(theme): sync editor content from live"
Run that before cutting a release branch, or on a schedule. The repository then carries the content the shop is actually running, and the staging theme previews new code against it.
There is a cost to ignoring JSON on the live push. A new template, or a new default in an existing one, no longer ships by merging. Somebody pushes that one file deliberately with --only "templates/page.landing.json", having checked it does not already exist on the live theme. I would keep it that way, because the one operation that can overwrite content is then something a person ran against a named file.
Should you use Shopify’s GitHub integration instead?
For a theme with no build step, often yes. The GitHub integration connects a branch to a theme and syncs both ways. Commits update the theme, and edits made in the admin are committed back to the branch by Shopify, with saves that land within about 10 seconds of each other batched into one commit. Editor changes arrive in Git without anyone running a pull.
It has conditions. Only branches that match the default theme folder structure can be connected, which Shopify describes as a buildless theme or one that has already been through its file transformations. A theme with a bundling step therefore needs a built branch, and something has to produce it. A branch cannot be reconnected to a theme once it has been disconnected. And nothing sits between a commit on the connected branch and the theme, so any checks have to happen before the merge.
My recommendation: for a buildless theme maintained by one or two developers, connect the branch and protect it with required checks. Reach for the Actions workflow when the theme has a build step, when a deploy has to wait for tests to pass, or when one repository feeds several stores. In either setup, do not let a pipeline write merchant-owned JSON to a published theme. If the storefront is Hydrogen there is no theme to push, and deploys go through Oxygen, which we cover in the post on Oxygen environment variables and deploys.
Whoooop builds and maintains Shopify themes, and the deployment pipeline is part of that work. If your deploys are still a theme push from someone’s laptop, our Shopify development team can set this up alongside the theme itself.