diff --git a/.github/actions/shopify-theme-problem-matchers/action.yml b/.github/actions/shopify-theme-problem-matchers/action.yml new file mode 100644 index 0000000..4d31f29 --- /dev/null +++ b/.github/actions/shopify-theme-problem-matchers/action.yml @@ -0,0 +1,20 @@ +name: "Register Shopify Theme Problem Matchers" +description: "Registers or removes problem matchers for Shopify Theme Check" + +inputs: + action: + description: "Whether to add or remove matchers" + required: false + default: "add" + +runs: + using: composite + steps: + - if: inputs.action == 'add' + shell: bash + run: | + echo "::add-matcher::${{ github.action_path }}/theme-check.json" + - if: inputs.action == 'remove' + shell: bash + run: | + echo "::remove-matcher owner=theme-check::" diff --git a/.github/actions/shopify-theme-problem-matchers/theme-check.json b/.github/actions/shopify-theme-problem-matchers/theme-check.json new file mode 100644 index 0000000..0ae2681 --- /dev/null +++ b/.github/actions/shopify-theme-problem-matchers/theme-check.json @@ -0,0 +1,18 @@ +{ + "problemMatcher": [ + { + "owner": "theme-check", + "pattern": [ + { + "regexp": "^(.+):(\\d+):(\\d+):\\s+(error|warning|info)\\s+\\[(.+?)\\]\\s+(.+)$", + "file": 1, + "line": 2, + "column": 3, + "severity": 4, + "code": 5, + "message": 6 + } + ] + } + ] +} diff --git a/.github/workflows/shopify-theme-pr.yml b/.github/workflows/shopify-theme-pr.yml new file mode 100644 index 0000000..43a706d --- /dev/null +++ b/.github/workflows/shopify-theme-pr.yml @@ -0,0 +1,179 @@ +name: 🎨 Shopify Theme PR Checks + +on: + workflow_call: + inputs: + working-directory: + description: 'Working directory for the theme' + required: false + type: string + default: '.' + fail-level: + description: 'Theme Check fail level (error, suggestion, style)' + required: false + type: string + default: 'error' + shopify-store: + description: 'Shopify store URL (e.g. example.myshopify.com). Required for preview deploy.' + required: false + type: string + deploy-preview: + description: 'Deploy an unpublished preview theme per PR' + required: false + type: boolean + default: false + pr-number: + description: 'The pull request number from the caller. Pass github.event.pull_request.number.' + required: true + type: string + secrets: + SHOPIFY_CLI_THEME_TOKEN: + description: 'Theme Access app password for preview deploys' + required: false + +jobs: + theme-check: + name: Theme Check + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: 'lts/*' + + - name: Cache npm + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + with: + path: ~/.npm + key: ${{ runner.os }}-npm-shopify-cli + + - name: Install Shopify CLI + run: npm install -g @shopify/cli@latest + + - name: Register problem matcher + uses: aligent/workflows/.github/actions/shopify-theme-problem-matchers@main + + - name: Run Theme Check + working-directory: ${{ inputs.working-directory }} + env: + INPUTS_FAIL_LEVEL: ${{ inputs.fail-level }} + run: | + shopify theme check --output json > theme-check-report.json 2>/dev/null || true + + # Transform JSON report into a format the problem matcher can parse + # Theme Check offenses use 0-indexed rows/columns; annotations need 1-indexed + jq -r ' + .[] | .path as $path | + .offenses[] | + "\($path):\(.start_row + 1):\(.start_column + 1): \(.severity) [\(.check)] \(.message)" + ' theme-check-report.json + + error_count=$(jq '[.[].errorCount] | add // 0' theme-check-report.json) + warning_count=$(jq '[.[].warningCount] | add // 0' theme-check-report.json) + echo "Theme Check: ${error_count} error(s), ${warning_count} warning(s)" + + fail_level="${INPUTS_FAIL_LEVEL}" + if [ "$fail_level" = "error" ] && [ "$error_count" -gt 0 ]; then + exit 1 + elif [ "$fail_level" = "warning" ] && [ $((error_count + warning_count)) -gt 0 ]; then + exit 1 + elif [ "$fail_level" = "suggestion" ] || [ "$fail_level" = "style" ]; then + total=$(jq '[.[].offenses | length] | add // 0' theme-check-report.json) + if [ "$total" -gt 0 ]; then + exit 1 + fi + fi + + - name: Remove problem matcher + if: always() + uses: aligent/workflows/.github/actions/shopify-theme-problem-matchers@main + with: + action: remove + + preview: + name: Preview Theme + needs: theme-check + if: inputs.deploy-preview + runs-on: ubuntu-latest + env: + # zizmor: ignore[secrets-outside-env] caller controls environment scoping + SHOPIFY_CLI_THEME_TOKEN: ${{ secrets.SHOPIFY_CLI_THEME_TOKEN }} + SHOPIFY_FLAG_STORE: ${{ inputs.shopify-store }} + PR_THEME_NAME: "PR-#${{ inputs.pr-number }}" + steps: + - name: Validate inputs + run: | + if [ -z "${SHOPIFY_FLAG_STORE}" ]; then + echo "::error::shopify-store input is required when deploy-preview is enabled" + exit 1 + fi + if [ -z "${SHOPIFY_CLI_THEME_TOKEN}" ]; then + echo "::error::SHOPIFY_CLI_THEME_TOKEN secret is required when deploy-preview is enabled" + exit 1 + fi + + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: 'lts/*' + + - name: Cache npm + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + with: + path: ~/.npm + key: ${{ runner.os }}-npm-shopify-cli + + - name: Install Shopify CLI + run: npm install -g @shopify/cli@latest + + - name: Push unpublished theme + working-directory: ${{ inputs.working-directory }} + run: | + shopify theme push \ + --unpublished \ + --json \ + --theme "$PR_THEME_NAME" \ + > push.json + + - name: Extract preview URL + id: preview + env: + WORKING_DIRECTORY: ${{ inputs.working-directory }} + run: | + url=$(jq -r '.theme.preview_url // empty' "$WORKING_DIRECTORY/push.json") + echo "url=$url" >> "$GITHUB_OUTPUT" + + - name: Comment preview link on PR + env: + GH_TOKEN: ${{ github.token }} + PREVIEW_URL: ${{ steps.preview.outputs.url }} + PR_NUMBER: ${{ inputs.pr-number }} + run: | + MARKER="" + BODY="${MARKER} + **Preview theme:** ${PR_THEME_NAME} + + **Preview URL:** ${PREVIEW_URL}" + + # Find an existing comment with the marker + COMMENT_ID=$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" \ + --jq ".[] | select(.body | startswith(\"${MARKER}\")) | .id" \ + | head -n1) + + if [ -n "$COMMENT_ID" ]; then + gh api "repos/${GITHUB_REPOSITORY}/issues/comments/${COMMENT_ID}" \ + --method PATCH \ + --field body="$BODY" + else + gh pr comment "$PR_NUMBER" --body "$BODY" + fi diff --git a/.github/workflows/shopify-theme-preview-cleanup.yml b/.github/workflows/shopify-theme-preview-cleanup.yml new file mode 100644 index 0000000..84e5601 --- /dev/null +++ b/.github/workflows/shopify-theme-preview-cleanup.yml @@ -0,0 +1,44 @@ +name: 🧹 Shopify Theme Preview Cleanup + +on: + workflow_call: + inputs: + shopify-store: + description: 'Shopify store URL (e.g. example.myshopify.com)' + required: true + type: string + pr-number: + description: 'The pull request number from the caller. Pass github.event.pull_request.number.' + required: true + type: string + secrets: + SHOPIFY_CLI_THEME_TOKEN: + description: 'Theme Access app password' + required: true + +jobs: + cleanup: + name: Cleanup preview + runs-on: ubuntu-latest + env: + # zizmor: ignore[secrets-outside-env] caller controls environment scoping + SHOPIFY_CLI_THEME_TOKEN: ${{ secrets.SHOPIFY_CLI_THEME_TOKEN }} + SHOPIFY_FLAG_STORE: ${{ inputs.shopify-store }} + PR_THEME_NAME: "PR-#${{ inputs.pr-number }}" + steps: + - name: Setup Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: 'lts/*' + + - name: Cache npm + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + with: + path: ~/.npm + key: ${{ runner.os }}-npm-shopify-cli + + - name: Install Shopify CLI + run: npm install -g @shopify/cli@latest + + - name: Delete preview theme (ignore if missing) + run: shopify theme delete --theme "$PR_THEME_NAME" --force || true diff --git a/docs/shopify-theme-pr.md b/docs/shopify-theme-pr.md new file mode 100644 index 0000000..f08972c --- /dev/null +++ b/docs/shopify-theme-pr.md @@ -0,0 +1,74 @@ +# Shopify Theme PR Checks + +A reusable workflow for running quality checks on Shopify theme pull requests, with optional preview theme deployment. + +#### **Features** +- **Theme Check linting**: Runs Shopify Theme Check with configurable fail levels and GitHub problem matchers for inline annotations +- **Preview deployments**: Optionally deploys an unpublished preview theme per PR, with a comment linking to the preview URL +- **Idempotent PR comments**: Preview URL comments are updated in place rather than duplicated on subsequent pushes + +#### **Inputs** +| Name | Required | Type | Default | Description | +|------|----------|------|---------|-------------| +| working-directory | :x: | string | `.` | Working directory for the theme | +| fail-level | :x: | string | `error` | Theme Check fail level (`error`, `warning`, `suggestion`, `style`) | +| shopify-store | :x: | string | | Shopify store URL (e.g. `example.myshopify.com`). Required when `deploy-preview` is enabled | +| deploy-preview | :x: | boolean | `false` | Deploy an unpublished preview theme per PR | +| pr-number | :heavy_check_mark: | string | | The pull request number from the caller. Pass `github.event.pull_request.number` | + +#### **Secrets** +| Name | Required | Description | +|------|----------|-------------| +| SHOPIFY_CLI_THEME_TOKEN | :x: | Theme Access app password for preview deploys. Required when `deploy-preview` is enabled | + +#### **Jobs** +| Job | Description | +|-----|-------------| +| `theme-check` | Installs Shopify CLI, runs Theme Check, and surfaces offences as GitHub annotations via problem matchers | +| `preview` | Pushes the theme as an unpublished preview and comments the preview URL on the PR. Only runs when `deploy-preview` is `true` and `theme-check` passes | + +#### **Example Usage** + +**Basic theme linting only:** +```yaml +on: + pull_request: + +jobs: + theme-checks: + uses: aligent/workflows/.github/workflows/shopify-theme-pr.yml@main + with: + pr-number: ${{ github.event.pull_request.number }} +``` + +**With preview deployment:** +```yaml +on: + pull_request: + +jobs: + theme-checks: + uses: aligent/workflows/.github/workflows/shopify-theme-pr.yml@main + with: + pr-number: ${{ github.event.pull_request.number }} + deploy-preview: true + shopify-store: example.myshopify.com + secrets: + SHOPIFY_CLI_THEME_TOKEN: ${{ secrets.SHOPIFY_CLI_THEME_TOKEN }} +``` + +**Monorepo with stricter fail level:** +```yaml +on: + pull_request: + paths: + - 'themes/storefront/**' + +jobs: + theme-checks: + uses: aligent/workflows/.github/workflows/shopify-theme-pr.yml@main + with: + working-directory: themes/storefront + fail-level: warning + pr-number: ${{ github.event.pull_request.number }} +``` diff --git a/docs/shopify-theme-preview-cleanup.md b/docs/shopify-theme-preview-cleanup.md new file mode 100644 index 0000000..1d9bd34 --- /dev/null +++ b/docs/shopify-theme-preview-cleanup.md @@ -0,0 +1,66 @@ +# Shopify Theme Preview Cleanup + +A reusable workflow for deleting preview themes created by the [Shopify Theme PR Checks](shopify-theme-pr.md) workflow when a pull request is closed or merged. + +#### **Features** +- **Automatic cleanup**: Removes the unpublished preview theme associated with a PR +- **Safe deletion**: Silently succeeds if the theme has already been removed + +#### **Inputs** +| Name | Required | Type | Default | Description | +|------|----------|------|---------|-------------| +| shopify-store | :heavy_check_mark: | string | | Shopify store URL (e.g. `example.myshopify.com`) | +| pr-number | :heavy_check_mark: | string | | The pull request number from the caller. Pass `github.event.pull_request.number` | + +#### **Secrets** +| Name | Required | Description | +|------|----------|-------------| +| SHOPIFY_CLI_THEME_TOKEN | :heavy_check_mark: | Theme Access app password | + +#### **Example Usage** + +**Cleanup on PR close:** +```yaml +on: + pull_request: + types: [closed] + +jobs: + cleanup-preview: + uses: aligent/workflows/.github/workflows/shopify-theme-preview-cleanup.yml@main + with: + shopify-store: example.myshopify.com + pr-number: ${{ github.event.pull_request.number }} + secrets: + SHOPIFY_CLI_THEME_TOKEN: ${{ secrets.SHOPIFY_CLI_THEME_TOKEN }} +``` + +**Combined PR and cleanup workflows:** +```yaml +# .github/workflows/shopify-theme.yml +name: Shopify Theme + +on: + pull_request: + types: [opened, synchronize, reopened, closed] + +jobs: + pr-checks: + if: github.event.action != 'closed' + uses: aligent/workflows/.github/workflows/shopify-theme-pr.yml@main + with: + pr-number: ${{ github.event.pull_request.number }} + deploy-preview: true + shopify-store: example.myshopify.com + secrets: + SHOPIFY_CLI_THEME_TOKEN: ${{ secrets.SHOPIFY_CLI_THEME_TOKEN }} + + cleanup-preview: + if: github.event.action == 'closed' + uses: aligent/workflows/.github/workflows/shopify-theme-preview-cleanup.yml@main + with: + shopify-store: example.myshopify.com + pr-number: ${{ github.event.pull_request.number }} + secrets: + SHOPIFY_CLI_THEME_TOKEN: ${{ secrets.SHOPIFY_CLI_THEME_TOKEN }} +```