Build, Deploy, and Ship Intermediate 15 min read

CI/CD Pipeline with GitHub Actions

Automate your React Shopify theme deployment with GitHub Actions. Set up continuous integration, testing, and deployment to preview and production themes.

Manual deployments are error-prone and time-consuming. A CI/CD pipeline automates building, testing, and deploying your React Shopify theme—ensuring every change is validated before it reaches customers. In this lesson, we’ll build a complete pipeline using GitHub Actions.

Pipeline Overview

Our CI/CD pipeline will:

┌─────────────────────────────────────────────────────────────────┐
│ CI/CD PIPELINE FLOW │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Push to Branch │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Type Check │ ──▶ Fail? ──▶ Stop & Report │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Lint Code │ ──▶ Fail? ──▶ Stop & Report │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Run Tests │ ──▶ Fail? ──▶ Stop & Report │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Build Assets │ ──▶ Fail? ──▶ Stop & Report │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Check Bundles │ ──▶ Over budget? ──▶ Warn │
│ └────────┬────────┘ │
│ │ │
│ ├───────────────────────────────┐ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Deploy Preview │ │ Deploy to Prod │ │
│ │ (feature branch)│ │ (main branch) │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘

Setting Up GitHub Secrets

First, add your Shopify credentials to GitHub:

  1. Go to your repository → Settings → Secrets and variables → Actions
  2. Add these secrets:
SHOPIFY_STORE_URL = your-store.myshopify.com
SHOPIFY_ACCESS_TOKEN = shpat_xxxxxxxxxxxxx (Theme Access token)
SHOPIFY_THEME_ID = 123456789 (Production theme ID)
SHOPIFY_PREVIEW_THEME_ID = 987654321 (Preview/staging theme ID)

Get a Theme Access token from Shopify Admin → Online Store → Themes → Theme access.

Basic CI Workflow

Start with a workflow that runs on every push:

.github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build:
name: Build and Test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Type check
run: npm run type-check
- name: Lint
run: npm run lint
- name: Run tests
run: npm run test:run
- name: Build
run: npm run build
- name: Check bundle size
run: npm run check-bundle
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: theme-assets
path: |
dist/
theme/
retention-days: 7

Complete Deployment Workflow

A full workflow with preview and production deployments:

.github/workflows/deploy.yml
name: Build and Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
NODE_VERSION: '20'
jobs:
# Job 1: Build and Test
build:
name: Build and Test
runs-on: ubuntu-latest
outputs:
should-deploy: ${{ steps.check.outputs.deploy }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Type check
run: npm run type-check
- name: Lint
run: npm run lint
- name: Run tests
run: npm run test:run -- --coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
fail_ci_if_error: false
- name: Build for production
run: npm run build
env:
NODE_ENV: production
- name: Validate build
run: npm run validate-build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-output
path: |
dist/
theme/assets/
retention-days: 7
- name: Check if should deploy
id: check
run: |
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" == "refs/heads/main" ]]; then
echo "deploy=true" >> $GITHUB_OUTPUT
elif [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo "deploy=preview" >> $GITHUB_OUTPUT
else
echo "deploy=false" >> $GITHUB_OUTPUT
fi
# Job 2: Deploy to Preview (Pull Requests)
deploy-preview:
name: Deploy to Preview
needs: build
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
environment:
name: preview
url: https://${{ secrets.SHOPIFY_STORE_URL }}?preview_theme_id=${{ secrets.SHOPIFY_PREVIEW_THEME_ID }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: build-output
- name: Setup Shopify CLI
uses: shopify/shopify-cli-action@v1
with:
version: latest
- name: Deploy to preview theme
run: |
shopify theme push \
--store=${{ secrets.SHOPIFY_STORE_URL }} \
--theme=${{ secrets.SHOPIFY_PREVIEW_THEME_ID }} \
--path=theme \
--allow-live
env:
SHOPIFY_CLI_THEME_TOKEN: ${{ secrets.SHOPIFY_ACCESS_TOKEN }}
- name: Comment on PR
uses: actions/github-script@v7
with:
script: |
const previewUrl = `https://${{ secrets.SHOPIFY_STORE_URL }}?preview_theme_id=${{ secrets.SHOPIFY_PREVIEW_THEME_ID }}`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## 🚀 Preview Deployed!\n\nYour changes have been deployed to the preview theme.\n\n🔗 [View Preview](${previewUrl})`
});
# Job 3: Deploy to Production (Main Branch)
deploy-production:
name: Deploy to Production
needs: build
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment:
name: production
url: https://${{ secrets.SHOPIFY_STORE_URL }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: build-output
- name: Setup Shopify CLI
uses: shopify/shopify-cli-action@v1
with:
version: latest
- name: Deploy to production theme
run: |
shopify theme push \
--store=${{ secrets.SHOPIFY_STORE_URL }} \
--theme=${{ secrets.SHOPIFY_THEME_ID }} \
--path=theme \
--allow-live
env:
SHOPIFY_CLI_THEME_TOKEN: ${{ secrets.SHOPIFY_ACCESS_TOKEN }}
- name: Create release tag
run: |
VERSION=$(node -p "require('./package.json').version")
git tag -a "v$VERSION" -m "Release v$VERSION"
git push origin "v$VERSION"
continue-on-error: true

Theme Check Integration

Add Shopify’s Theme Check for Liquid linting:

.github/workflows/theme-check.yml
name: Theme Check
on: [push, pull_request]
jobs:
theme-check:
name: Run Theme Check
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run Theme Check
uses: shopify/theme-check-action@v1
with:
theme_root: theme
flags: --fail-level error

Bundle Size Monitoring

Track bundle sizes over time:

# Part of ci.yml
- name: Analyze bundle size
uses: preactjs/compressed-size-action@v2
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
pattern: 'dist/**/*.{js,css}'
build-script: 'build'

Or create a custom check:

- name: Check bundle sizes
id: bundle-check
run: |
# Get sizes
MAIN_SIZE=$(stat -c%s dist/main.js 2>/dev/null || echo 0)
VENDOR_SIZE=$(stat -c%s dist/vendor-react*.js 2>/dev/null || echo 0)
# Convert to KB
MAIN_KB=$((MAIN_SIZE / 1024))
VENDOR_KB=$((VENDOR_SIZE / 1024))
echo "main_size=${MAIN_KB}KB" >> $GITHUB_OUTPUT
echo "vendor_size=${VENDOR_KB}KB" >> $GITHUB_OUTPUT
# Check against budget
if [ $MAIN_KB -gt 200 ]; then
echo "::warning::Main bundle (${MAIN_KB}KB) exceeds 200KB budget"
fi
- name: Comment bundle sizes
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## 📦 Bundle Sizes\n\n| Bundle | Size |\n|--------|------|\n| main.js | ${{ steps.bundle-check.outputs.main_size }} |\n| vendor.js | ${{ steps.bundle-check.outputs.vendor_size }} |`
});

Scheduled Health Checks

Run periodic checks on the live site:

.github/workflows/health-check.yml
name: Site Health Check
on:
schedule:
- cron: '0 */6 * * *' # Every 6 hours
workflow_dispatch:
jobs:
health-check:
name: Check Site Health
runs-on: ubuntu-latest
steps:
- name: Check site is up
run: |
STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://${{ secrets.SHOPIFY_STORE_URL }})
if [ $STATUS -ne 200 ]; then
echo "::error::Site returned status $STATUS"
exit 1
fi
- name: Run Lighthouse
uses: treosh/lighthouse-ci-action@v10
with:
urls: |
https://${{ secrets.SHOPIFY_STORE_URL }}
https://${{ secrets.SHOPIFY_STORE_URL }}/collections/all
https://${{ secrets.SHOPIFY_STORE_URL }}/products/sample-product
budgetPath: ./lighthouse-budget.json
uploadArtifacts: true

Create a Lighthouse budget:

lighthouse-budget.json
[
{
"path": "/*",
"resourceSizes": [
{ "resourceType": "script", "budget": 300 },
{ "resourceType": "stylesheet", "budget": 100 },
{ "resourceType": "total", "budget": 1000 }
],
"resourceCounts": [
{ "resourceType": "third-party", "budget": 10 }
],
"timings": [
{ "metric": "first-contentful-paint", "budget": 2000 },
{ "metric": "largest-contentful-paint", "budget": 3000 },
{ "metric": "cumulative-layout-shift", "budget": 0.1 },
{ "metric": "total-blocking-time", "budget": 300 }
]
}
]

Rollback Strategy

Quick rollback if something goes wrong:

.github/workflows/rollback.yml
name: Rollback
on:
workflow_dispatch:
inputs:
version:
description: 'Version to rollback to (e.g., v1.2.3)'
required: true
type: string
jobs:
rollback:
name: Rollback to Previous Version
runs-on: ubuntu-latest
environment: production
steps:
- name: Checkout specific version
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.version }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install and build
run: |
npm ci
npm run build
- name: Deploy rollback
run: |
shopify theme push \
--store=${{ secrets.SHOPIFY_STORE_URL }} \
--theme=${{ secrets.SHOPIFY_THEME_ID }} \
--path=theme \
--allow-live
env:
SHOPIFY_CLI_THEME_TOKEN: ${{ secrets.SHOPIFY_ACCESS_TOKEN }}
- name: Notify team
run: |
echo "::notice::Rolled back to ${{ github.event.inputs.version }}"

Branch Protection Rules

Configure GitHub to require CI passing before merge:

  1. Go to Settings → Branches → Add rule
  2. Branch name pattern: main
  3. Enable:
    • Require a pull request before merging
    • Require status checks to pass before merging
      • Select: build, theme-check
    • Require branches to be up to date before merging
    • Require conversation resolution before merging

Notification Setup

Get notified on deployment:

- name: Notify Slack
if: always()
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
fields: repo,message,commit,author,action,eventName,ref,workflow
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

Key Takeaways

  1. Automate everything: Build, test, and deploy should happen without manual intervention.

  2. Fail fast: Type check and lint before running expensive tests.

  3. Preview deployments: Let reviewers see changes before they hit production.

  4. Protect main branch: Require CI to pass before merging.

  5. Monitor bundle size: Track changes over time to catch bloat.

  6. Plan for rollback: Have a quick way to revert bad deployments.

  7. Health checks: Periodically verify the live site is working.

  8. Notify the team: Everyone should know when deployments happen.

In the final lesson, we’ll walk through a complete theme from start to finish, bringing together everything you’ve learned in this course.

Finished this lesson?

Mark it complete to track your progress.

Discussion

Loading comments...