Catching Deleted Theme Assets Before a Shopify Deploy

On a greige ground with confetti, a big headline reads Merged clean. File gone., with a Heads up ribbon at the top right. A small preview browser window winks and says looks fine. Below, a railway scene: Branch A carries a font file card marked with a cross that drops into a wastebasket, leaving a dashed empty card outline on the track; Branch B carries a preload tag with a curly arrow pointing at that empty spot. The two tracks join at a green no conflict marker, and on the merged track a striped barrier is down beside a stop sign and a fuchsia Missing asset tag.

In short: A merge can delete a theme asset on one branch and keep a preload for it on another, with no conflict. Run Theme Check’s MissingAsset check alone on the merged theme before deploying: it catches asset_url references to missing files when the file name is written out literally.

How do you stop a deploy that points at a deleted theme asset?

  1. Run Theme Check with one check only, on the merged theme. The MissingAsset check reports an asset_url whose file is not in the theme's assets folder, as long as the file name is written out literally and asset_url is the first filter, and its default severity is error[1][2]. A config file passed with --config can enable that check alone[3].
  2. Confirm that exactly one check is enabled. shopify theme check --list with that config should list MissingAsset and nothing else[3].
  3. Fail the pipeline on exit code 1. The --fail-level error option sets the minimum severity that makes the command exit with an error[3].
  4. If the theme already has missing assets, compare the branch with the main branch and fail only on new ones.
  5. Before you push to a theme that other people edit, look at the assets on the remote theme as well (see "What does the check not catch?").

The next two blocks do steps 1 to 4. They need Bash, Shopify CLI, jq and git. They read theme files and write one small config file and a temporary copy of the main branch.

These commands change no theme file, but test any change to a theme on an unpublished copy or a development store first, never on the live theme, and take a backup of the theme before editing it. You use this information at your own risk, and iLogr accepts no responsibility for any loss or damage.

Shell
mkdir -p .github
cat > .github/missing-asset-only.yml <<'EOF'
extends: nothing
MissingAsset:
  enabled: true
  severity: error
EOF

shopify theme check --list --config ./.github/missing-asset-only.yml
shopify theme check --path . --config ./.github/missing-asset-only.yml --fail-level error

Run it from the theme folder (the repository root, if the theme is connected to GitHub). The config sits in .github, which is not part of the theme structure, and Shopify's GitHub integration ignores folders that do not match it[4]. If you deploy with the CLI instead, keep the config outside the theme folder. The --config option overrides a .theme-check.yml in the theme[3], so a theme setting that switches the check off cannot hide a miss. The built-in nothing config that extends: nothing selects is in Theme Check's source[5], but the configuration page does not describe it[6], so keep the --list line and read its output once.

For a theme that already has misses, list the misses on the main branch and on your branch, and keep only the new ones:

Shell
(
set -euo pipefail
export LC_ALL=C
cfg=$PWD/.github/missing-asset-only.yml
w=$(mktemp -d)
trap 'rm -rf "$w"' EXIT
mkdir "$w/base"
git archive origin/main | tar -x -C "$w/base"
misses() {
  local root; root="$(cd "$1" && pwd -P)/"
  (cd "$1" && shopify theme check --path . --config "$cfg" --output json > "$2.json" || [ $? -eq 1 ])
  jq -e 'type == "array"' "$2.json" > /dev/null
  jq -r --arg root "$root" '.[] | .path as $p | .offenses[] | "\($p | ltrimstr($root)) \(.message)"' "$2.json" | sort > "$2"
}
misses "$w/base" "$w/old"
misses . "$w/current"
comm -13 "$w/old" "$w/current" > "$w/new"
if [ -s "$w/new" ]; then
  printf 'New missing assets:\n'
  cat "$w/new"
  exit 1
fi
)

Run it in Bash from a theme at the repository root, after git fetch origin main. The explicit --path . makes each run inspect its own folder even if SHOPIFY_FLAG_PATH is set in the environment. The subshell keeps exit 1 and the strict settings away from your own shell. It stops with an error if the checkout, the Theme Check run or the JSON report fails, because a check that cannot run must not count as a pass. Only exit status 1 from Theme Check, which means it found errors, is accepted. It then exits 1 when the branch adds a new miss and 0 when it adds none. In a script or a CI step, make that exit status a required check. In a test, it printed exactly one new miss, the dead preload on a merged theme, and exited with an error when Theme Check crashed, printed nothing or printed something other than a JSON list, or when the branch name for the baseline did not exist. The plain gate passed once the preload was removed.

Why does a clean merge still leave a reference to a deleted asset?

Git merges lines, not references. One branch removes a file from assets. The other adds a line, in a different file, that names it. Neither change touches the other's lines, so git reports no conflict. A different sync problem, where Shopify rejects a file and the old code stays live, is covered in Shopify GitHub Theme Sync Rejected a File and Left the Old Code Live.

Field note (test, Shopify CLI 4.9.4). In a test on a small theme, one branch deleted a font file and the other added a font preload to the layout. Each branch alone passed Theme Check. Git merged them without a conflict, left the preload in the layout and no file in assets, and the gate then failed with 'assets/<name>' does not exist. A gate that runs on each branch passes both. Run it on the merge result: the merge commit, the pull request's merge ref, or the branch after the merge.

A diagram of where a merge leaves a hole. Branch A removes a font file from the assets folder. Branch B adds a preload for it in the layout. The two meet in a merge with no conflict, and a gate on the merge result reads MissingAsset: stop. From the merge, two checks follow. A dashed preview theme card, a possible case, may retain an earlier copy of the file and looks fine. A fresh theme built from the repository has the file missing and returns a 404.
Where a merge leaves a hole: one branch removes a font file, the other adds a preload for it, and git reports no conflict. The preview card is a possible case, drawn dashed, not a measured one.

In the test, and in the real theme below, the preload sat between a theme-check-disable AssetPreload and a theme-check-enable AssetPreload comment. That pair silences the check about preloads, not this one, and MissingAsset still fired.

Why can the preview still look fine?

Three things can hide the miss.

  • The page can render without the file. In a headless Chrome 155 test, a page with a preload for a font file that did not exist had a heading of the same dimensions as the same page with the file present. The browser logged one failed request (404) and a warning that the preload was not used. An unused font preload can fail without changing the visible page; if the page does use the missing asset, its appearance or function may change. For a theme asset, the URL comes from asset_url, which Shopify documents as returning the CDN address of a file in the theme's assets[7]; the page does not say what it returns for a file that does not exist. On a Shopify demo storefront, a request for a theme asset that did not exist returned 404 with cache-control: public, max-age=60.
  • The preview theme can still hold the file. shopify theme push removes remote files that no longer exist locally unless you pass --nodelete[8], and the CLI source shows the deletion running after the uploads finish and skipping remote files that match --ignore[9]. shopify theme dev does the same for files deleted locally[10]. If the preview theme already holds the file, a push with --nodelete, or no push since the deletion, leaves it there. That preview then loads an asset that a fresh upload from the repository would not contain. Shopify's GitHub integration page does not say how deleted files are handled[4], so check the theme's file list after a sync rather than assume it matches the branch.
  • Nobody opens the network panel. The failed request is quiet, and the page looks the same.

In the real case below, the history does not record how the theme was pushed or previewed, so we cannot say which of these applied.

What happened on a real theme?

Field note. A Shopify theme kept in Git, reconstructed from its history. One branch replaced a self-hosted font with a font from Shopify's own library and deleted the old font files. A performance branch, started earlier, had added a preload for one of those files to the layout. The two touched different files, so merging them gave no conflict. After the merge the theme had no font files and the preload was still there. A follow-up commit a few hours later pointed the preload at the font chosen in the theme settings. The history does not show whether the merged state reached a live theme.

We ran Theme Check (Shopify CLI 4.9.4) on three copies of that history: the side with the deletion, the merge, and the follow-up. Each copy reported hundreds of errors and warnings, so a gate that fails on any error failed all three, and shopify theme push --strict, which requires Theme Check to pass without errors[8], would have blocked every push of this theme.

MissingAsset alone told the story. It reported two errors on the deletion side, three at the merge and two after the follow-up. The extra one at the merge was the font preload. The other two were references to image files that have no file in the repository at any point in its history, so they were there before the merge and after the fix. Compared with the main branch, the merge added one new miss and the follow-up none.

What does the check not catch?

  • Names built at run time. The check inspects only an asset_url that is the first filter on a literal string[2]. In a test, 'brand' | append: '.woff2' | asset_url passed.
  • References outside Liquid. In a test, url('font.woff2') inside a plain .css file passed, while asset_url inside a .css.liquid file, a snippet or a section was caught. The check also treats base.css as present when base.css.liquid exists[2].
  • A disable comment. A theme-check-disable MissingAsset comment hides the miss in a test. Inline disable comments work for any check[6], although Shopify's page for this one says disabling it is not recommended[1]. The gate trusts whoever reviews the code, so search for that comment in review.
  • The remote theme. Theme Check reads the folder you give it. It cannot tell you that an asset exists only on a remote theme, or that a push is about to delete one. Before pushing to a theme that others also edit, pull its assets into an empty folder, never into your working theme, because shopify theme pull deletes local files that do not exist remotely unless you pass --nodelete[11]. Use --theme for the remote theme, --path for the empty folder and --only "assets/*", then compare the two file lists. We could not run this step without a store login, so it comes from the documentation.

Which tool should run the check?

Shopify CLI and Theme Check are free to use, and a CI provider's own pricing applies if you run them there. Run the commands above on a local merge of the two branches, or as a step in whatever CI you already use, on the merge result. Shopify's theme-check-action for GitHub Actions has a flags input for passing CLI options[12]; we did not test it with this config. --strict on a push suits a theme with no errors, not one with hundreds. The GitHub integration alone does nothing about references, so the check has to run before the merge.

References

Numbers in square brackets in the text link to these sources.

  1. Shopify.dev, Theme Check "MissingAsset" (opens in a new tab).Used for: what the check reports, its default error severity and the advice not to disable it.
  2. Shopify/theme-tools on GitHub, missing-asset/index.ts (opens in a new tab).Used for: only literal names with asset_url first are checked, with a liquid fallback.
  3. Shopify.dev, Shopify CLI "theme check" (opens in a new tab).Used for: the config, fail-level, list and output options.
  4. Shopify.dev, "Shopify GitHub integration for themes" (opens in a new tab).Used for: folders outside the theme structure being ignored, and deleted files not being described.
  5. Shopify/theme-tools on GitHub, theme-check-node/configs (opens in a new tab).Used for: the built-in configs, including one with no checks enabled.
  6. Shopify.dev, "Theme Check configuration" (opens in a new tab).Used for: per-check settings, inline disable comments and the theme's own config file.
  7. Shopify.dev, Liquid filter "asset_url" (opens in a new tab).Used for: what the filter returns, with nothing said about missing files.
  8. Shopify.dev, Shopify CLI "theme push" (opens in a new tab).Used for: the nodelete and strict options and what each does.
  9. Shopify/cli on GitHub, theme-uploader.ts (opens in a new tab).Used for: remote-only files being deleted after uploads unless nodelete is set.
  10. Shopify.dev, Shopify CLI "theme dev" (opens in a new tab).Used for: files deleted locally being deleted remotely unless nodelete is set.
  11. Shopify.dev, Shopify CLI "theme pull" (opens in a new tab).Used for: the only, theme and path options, and pull deleting local files.
  12. Shopify/theme-check-action on GitHub (opens in a new tab).Used for: the action's flags input for passing options to Theme Check.

Leave a Reply

Your email address will not be published. Required fields are marked * See the privacy policy for how comments are handled.