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:
- Go to your repository → Settings → Secrets and variables → Actions
- Add these secrets:
SHOPIFY_STORE_URL = your-store.myshopify.comSHOPIFY_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:
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: 7Complete Deployment Workflow
A full workflow with preview and production deployments:
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: trueTheme Check Integration
Add Shopify’s Theme Check for Liquid linting:
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 errorBundle 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:
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: trueCreate a Lighthouse budget:
[ { "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:
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:
- Go to Settings → Branches → Add rule
- Branch name pattern:
main - Enable:
- Require a pull request before merging
- Require status checks to pass before merging
- Select:
build,theme-check
- Select:
- 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
-
Automate everything: Build, test, and deploy should happen without manual intervention.
-
Fail fast: Type check and lint before running expensive tests.
-
Preview deployments: Let reviewers see changes before they hit production.
-
Protect main branch: Require CI to pass before merging.
-
Monitor bundle size: Track changes over time to catch bloat.
-
Plan for rollback: Have a quick way to revert bad deployments.
-
Health checks: Periodically verify the live site is working.
-
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...