climaybe 1.0.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +64 -12
  3. package/package.json +45 -2
  4. package/src/commands/add-store.js +77 -4
  5. package/src/commands/init.js +150 -16
  6. package/src/commands/update-workflows.js +4 -2
  7. package/src/index.js +15 -3
  8. package/src/lib/config.js +17 -0
  9. package/src/lib/git.js +21 -3
  10. package/src/lib/github-secrets.js +263 -0
  11. package/src/lib/prompts.js +116 -6
  12. package/src/lib/workflows.js +23 -3
  13. package/src/workflows/build/build-pipeline.yml +57 -0
  14. package/src/workflows/build/create-release.yml +52 -0
  15. package/src/workflows/build/reusable-build.yml +52 -0
  16. package/src/workflows/multi/main-to-staging-stores.yml +44 -3
  17. package/src/workflows/multi/multistore-hotfix-to-main.yml +84 -0
  18. package/src/workflows/multi/pr-to-live.yml +82 -8
  19. package/src/workflows/multi/stores-to-root.yml +10 -3
  20. package/src/workflows/preview/pr-close.yml +63 -0
  21. package/src/workflows/preview/pr-update.yml +120 -0
  22. package/src/workflows/preview/reusable-cleanup-themes.yml +71 -0
  23. package/src/workflows/preview/reusable-comment-on-pr.yml +76 -0
  24. package/src/workflows/preview/reusable-extract-pr-number.yml +35 -0
  25. package/src/workflows/preview/reusable-rename-theme.yml +73 -0
  26. package/src/workflows/preview/reusable-share-theme.yml +94 -0
  27. package/src/workflows/shared/ai-changelog.yml +82 -24
  28. package/src/workflows/shared/version-bump.yml +22 -7
  29. package/src/workflows/single/nightly-hotfix.yml +38 -2
  30. package/src/workflows/single/post-merge-tag.yml +68 -15
  31. package/src/workflows/single/release-pr-check.yml +16 -6
  32. package/src/workflows/multi/hotfix-backport.yml +0 -100
@@ -1,7 +1,7 @@
1
- # climaybe — Main to Staging Stores (Multi-store)
1
+ # climaybe — Main to Staging <store> (Multi-store)
2
2
  # When a PR is merged into main (from staging), this workflow
3
3
  # opens and auto-merges PRs from main to each staging-<alias> branch.
4
- # Skips hotfix backport commits to prevent recursive triggers.
4
+ # Skips [hotfix-backport] and version-bump commits so multistore-hotfix-to-main syncs are not re-pushed to stores.
5
5
 
6
6
  name: Main to Staging Stores
7
7
 
@@ -72,6 +72,7 @@ jobs:
72
72
  permissions:
73
73
  contents: write
74
74
  pull-requests: write
75
+ actions: write
75
76
  env:
76
77
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
77
78
  steps:
@@ -112,7 +113,47 @@ jobs:
112
113
 
113
114
  # Auto-merge
114
115
  PR_NUM=$(echo "$PR_URL" | grep -oP '\d+$')
115
- gh pr merge "$PR_NUM" --merge --admin 2>/dev/null || echo "Auto-merge failed — manual review may be needed."
116
+ if gh pr merge "$PR_NUM" --merge --admin 2>/dev/null; then
117
+ echo "Auto-merge succeeded for $BRANCH"
118
+
119
+ # Explicitly trigger Stores to Root to avoid missing downstream runs
120
+ # when merges are performed by GITHUB_TOKEN-based automation.
121
+ gh workflow run "Stores to Root" --ref "$BRANCH" 2>/dev/null || \
122
+ echo "Failed to trigger Stores to Root for $BRANCH"
123
+
124
+ # Wait for the latest Stores to Root run on this branch to complete,
125
+ # then trigger PR to Live to keep ordering deterministic.
126
+ for i in $(seq 1 30); do
127
+ RUN_STATUS=$(gh run list \
128
+ --workflow "Stores to Root" \
129
+ --branch "$BRANCH" \
130
+ --limit 1 \
131
+ --json status,conclusion \
132
+ --jq '.[0].status + "|" + (.[0].conclusion // "")' 2>/dev/null || echo "")
133
+
134
+ if [ -z "$RUN_STATUS" ]; then
135
+ sleep 5
136
+ continue
137
+ fi
138
+
139
+ STATUS="${RUN_STATUS%%|*}"
140
+ CONCLUSION="${RUN_STATUS##*|}"
141
+
142
+ if [ "$STATUS" = "completed" ]; then
143
+ if [ "$CONCLUSION" = "success" ]; then
144
+ gh workflow run "PR to Live" --ref "$BRANCH" 2>/dev/null || \
145
+ echo "Failed to trigger PR to Live for $BRANCH"
146
+ else
147
+ echo "Stores to Root did not succeed for $BRANCH (conclusion: $CONCLUSION)"
148
+ fi
149
+ break
150
+ fi
151
+
152
+ sleep 5
153
+ done
154
+ else
155
+ echo "Auto-merge failed — manual review may be needed."
156
+ fi
116
157
  else
117
158
  echo "No changes to sync for $BRANCH"
118
159
  fi
@@ -0,0 +1,84 @@
1
+ # climaybe — Multistore Hotfix to Main
2
+ # Automatically syncs direct hotfixes from staging-<store> or live-<store> back to main.
3
+ # Triggers: push to staging-* or live-*; also after Root to Stores completes (live-*).
4
+ # Merges the store branch into main with [hotfix-backport] so post-merge-tag runs patch bump.
5
+ # main-to-staging-stores skips [hotfix-backport] commits so hotfixes are not re-pushed to stores.
6
+
7
+ name: Multistore Hotfix to Main
8
+
9
+ on:
10
+ push:
11
+ branches:
12
+ - 'staging-*'
13
+ - 'live-*'
14
+ workflow_run:
15
+ workflows: ["Root to Stores"]
16
+ types: [completed]
17
+ branches:
18
+ - 'live-*'
19
+
20
+ concurrency:
21
+ group: multistore-hotfix-to-main-${{ github.event_name == 'push' && github.ref_name || github.event.workflow_run.head_branch }}
22
+ cancel-in-progress: false
23
+
24
+ jobs:
25
+ sync-to-main:
26
+ if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success'
27
+ runs-on: ubuntu-latest
28
+ permissions:
29
+ contents: write
30
+ env:
31
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
32
+ steps:
33
+ - name: Resolve source branch ref
34
+ id: ref
35
+ run: |
36
+ if [ "${{ github.event_name }}" = "workflow_run" ]; then
37
+ echo "branch=${{ github.event.workflow_run.head_branch }}" >> $GITHUB_OUTPUT
38
+ else
39
+ echo "branch=${{ github.ref_name }}" >> $GITHUB_OUTPUT
40
+ fi
41
+
42
+ - uses: actions/checkout@v4
43
+ with:
44
+ fetch-depth: 0
45
+ token: ${{ secrets.GITHUB_TOKEN }}
46
+
47
+ - name: Check if backport is needed
48
+ id: check
49
+ run: |
50
+ SOURCE="${{ steps.ref.outputs.branch }}"
51
+
52
+ MERGE_BASE=$(git merge-base origin/main origin/$SOURCE 2>/dev/null || echo "")
53
+
54
+ if [ -z "$MERGE_BASE" ]; then
55
+ echo "needs_backport=false" >> $GITHUB_OUTPUT
56
+ exit 0
57
+ fi
58
+
59
+ COMMITS=$(git log --oneline ${MERGE_BASE}..origin/$SOURCE -- . ':!stores/' 2>/dev/null | \
60
+ grep -v "\[stores-to-root\]" | grep -v "\[root-to-stores\]" | grep -v "chore(release)" || true)
61
+
62
+ if [ -n "$COMMITS" ]; then
63
+ echo "needs_backport=true" >> $GITHUB_OUTPUT
64
+ echo "Commits to sync to main:"
65
+ echo "$COMMITS"
66
+ else
67
+ echo "needs_backport=false" >> $GITHUB_OUTPUT
68
+ echo "No new commits to sync."
69
+ fi
70
+
71
+ - name: Merge into main and push
72
+ if: steps.check.outputs.needs_backport == 'true'
73
+ run: |
74
+ SOURCE="${{ steps.ref.outputs.branch }}"
75
+
76
+ git config user.name "github-actions[bot]"
77
+ git config user.email "github-actions[bot]@users.noreply.github.com"
78
+
79
+ git fetch origin
80
+ git checkout main
81
+ git merge origin/$SOURCE --no-ff -m "Merge $SOURCE into main [hotfix-backport]"
82
+ git push origin main
83
+
84
+ echo "Synced $SOURCE → main"
@@ -10,34 +10,78 @@ on:
10
10
  types: [completed]
11
11
  branches:
12
12
  - 'staging-*'
13
+ workflow_dispatch:
13
14
 
14
15
  jobs:
15
16
  create-pr:
16
- # Only run if the triggering workflow succeeded
17
- if: github.event.workflow_run.conclusion == 'success'
17
+ # Run on successful Stores to Root completion, or explicit dispatch.
18
+ if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success'
18
19
  runs-on: ubuntu-latest
19
20
  permissions:
20
21
  contents: read
21
22
  pull-requests: write
22
23
  env:
23
24
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
25
+ SHOPIFY_CLI_THEME_TOKEN: ${{ secrets.SHOPIFY_CLI_THEME_TOKEN }}
24
26
  steps:
25
27
  - uses: actions/checkout@v4
26
28
 
27
29
  - name: Extract store alias
28
30
  id: alias
29
31
  run: |
30
- BRANCH="${{ github.event.workflow_run.head_branch }}"
32
+ if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
33
+ BRANCH="${{ github.ref_name }}"
34
+ else
35
+ BRANCH="${{ github.event.workflow_run.head_branch }}"
36
+ fi
37
+
38
+ if [[ "$BRANCH" != staging-* ]]; then
39
+ echo "Branch $BRANCH is not a staging-* branch, skipping."
40
+ exit 0
41
+ fi
42
+
31
43
  ALIAS="${BRANCH#staging-}"
44
+ ALIAS_SECRET=$(echo "$ALIAS" | tr '[:lower:]-' '[:upper:]_')
32
45
  echo "alias=$ALIAS" >> $GITHUB_OUTPUT
46
+ echo "alias_secret=$ALIAS_SECRET" >> $GITHUB_OUTPUT
33
47
  echo "staging_branch=$BRANCH" >> $GITHUB_OUTPUT
34
48
  echo "live_branch=live-${ALIAS}" >> $GITHUB_OUTPUT
35
49
 
50
+ - name: Resolve store domain from package.json
51
+ id: store
52
+ env:
53
+ SHOPIFY_STORE_URL_SCOPED: ${{ secrets[format('SHOPIFY_STORE_URL_{0}', steps.alias.outputs.alias_secret)] }}
54
+ SHOPIFY_STORE_URL_DEFAULT: ${{ secrets.SHOPIFY_STORE_URL }}
55
+ run: |
56
+ ALIAS="${{ steps.alias.outputs.alias }}"
57
+ DOMAIN_CONFIG=$(node -e "
58
+ const fs = require('fs');
59
+ const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
60
+ const domain = pkg?.config?.stores?.['${ALIAS}'] || '';
61
+ process.stdout.write(domain);
62
+ ")
63
+ DOMAIN="${SHOPIFY_STORE_URL_SCOPED:-}"
64
+ if [ -z "$DOMAIN" ]; then
65
+ DOMAIN="${SHOPIFY_STORE_URL_DEFAULT:-}"
66
+ fi
67
+ if [ -z "$DOMAIN" ]; then
68
+ DOMAIN="$DOMAIN_CONFIG"
69
+ fi
70
+ DOMAIN=$(echo "$DOMAIN" | sed -E 's#^https?://##; s#/.*$##')
71
+ echo "domain=$DOMAIN" >> $GITHUB_OUTPUT
72
+
36
73
  - name: Create PR to live branch
74
+ env:
75
+ SHOPIFY_CLI_THEME_TOKEN_SCOPED: ${{ secrets[format('SHOPIFY_CLI_THEME_TOKEN_{0}', steps.alias.outputs.alias_secret)] }}
76
+ SHOPIFY_CLI_THEME_TOKEN_DEFAULT: ${{ secrets.SHOPIFY_CLI_THEME_TOKEN }}
37
77
  run: |
38
78
  STAGING="${{ steps.alias.outputs.staging_branch }}"
39
79
  LIVE="${{ steps.alias.outputs.live_branch }}"
40
80
  ALIAS="${{ steps.alias.outputs.alias }}"
81
+ DOMAIN="${{ steps.store.outputs.domain }}"
82
+ STAGING_THEME_ID=""
83
+ REPO_NAME="${GITHUB_REPOSITORY#*/}"
84
+ SHOPIFY_TOKEN="${SHOPIFY_CLI_THEME_TOKEN_SCOPED:-$SHOPIFY_CLI_THEME_TOKEN_DEFAULT}"
41
85
 
42
86
  # Check if live branch exists
43
87
  if ! git ls-remote --heads origin "$LIVE" | grep -q "$LIVE"; then
@@ -54,15 +98,45 @@ jobs:
54
98
  fi
55
99
 
56
100
  # Create PR
101
+ BODY=$(printf "Deployment PR for **%s** store.\n\nReview the changes and merge to deploy." "$ALIAS")
102
+
103
+ if [ -n "$DOMAIN" ]; then
104
+ # Try to resolve a staging/non-live theme ID for this store.
105
+ if [ -n "$SHOPIFY_TOKEN" ]; then
106
+ if ! command -v shopify >/dev/null 2>&1; then
107
+ npm install -g @shopify/cli @shopify/theme >/dev/null 2>&1 || true
108
+ fi
109
+
110
+ THEME_JSON=$(shopify theme list \
111
+ --store "$DOMAIN" \
112
+ --password "$SHOPIFY_TOKEN" \
113
+ --json 2>/dev/null || echo "[]")
114
+
115
+ STAGING_THEME_ID=$(echo "$THEME_JSON" | jq -r '
116
+ # Prefer exact branch theme naming: "<repo>/<staging-branch>"
117
+ (map(select(.role != "main" and .name == ($repo + "/" + $branch)))[0].id)
118
+ # Fallback: branch suffix match (still branch-specific)
119
+ // (map(select(.role != "main" and (.name | endswith("/" + $branch))))[0].id)
120
+ // empty
121
+ ' --arg repo "$REPO_NAME" --arg branch "$STAGING")
122
+ fi
123
+
124
+ if [ -n "$STAGING_THEME_ID" ]; then
125
+ PREVIEW_URL="https://${DOMAIN}?preview_theme_id=${STAGING_THEME_ID}"
126
+ CUSTOMIZE_URL="https://${DOMAIN}/admin/themes/${STAGING_THEME_ID}/editor"
127
+ BODY="${BODY}"$'\n\n'"**Customize URL (Staging Theme):** ${CUSTOMIZE_URL}"$'\n'"**Preview URL (Staging Theme):** ${PREVIEW_URL}"
128
+ else
129
+ BODY="${BODY}"$'\n\n'"⚠️ Staging theme link not found for branch \`${STAGING}\`."$'\n'"Expected naming: \`${REPO_NAME}/${STAGING}\` (or suffix match \`/${STAGING}\`)."
130
+ fi
131
+ fi
132
+
133
+ BODY="${BODY}"$'\n\n'"*Generated by climaybe*"
134
+
57
135
  PR_URL=$(gh pr create \
58
136
  --base "$LIVE" \
59
137
  --head "$STAGING" \
60
138
  --title "Deploy to ${ALIAS}" \
61
- --body "Deployment PR for **${ALIAS}** store.
62
-
63
- Review the changes and merge to deploy.
64
-
65
- *Generated by climaybe*" 2>/dev/null || echo "")
139
+ --body "$BODY" 2>/dev/null || echo "")
66
140
 
67
141
  if [ -n "$PR_URL" ]; then
68
142
  echo "Created PR: $PR_URL"
@@ -8,6 +8,7 @@ on:
8
8
  push:
9
9
  branches:
10
10
  - 'staging-*'
11
+ workflow_dispatch:
11
12
 
12
13
  # Prevent concurrent runs per branch
13
14
  concurrency:
@@ -27,10 +28,16 @@ jobs:
27
28
  - name: Skip if this is a sync commit
28
29
  id: gate
29
30
  run: |
30
- COMMIT_MSG="${{ github.event.head_commit.message }}"
31
- if echo "$COMMIT_MSG" | grep -q "\[stores-to-root\]"; then
32
- echo "skip=true" >> $GITHUB_OUTPUT
31
+ if [ "${{ github.event_name }}" = "push" ]; then
32
+ COMMIT_MSG="${{ github.event.head_commit.message }}"
33
+ if echo "$COMMIT_MSG" | grep -q "\[stores-to-root\]"; then
34
+ echo "skip=true" >> $GITHUB_OUTPUT
35
+ else
36
+ echo "skip=false" >> $GITHUB_OUTPUT
37
+ fi
33
38
  else
39
+ # Manual/API dispatch should always run for the selected branch ref.
40
+ echo "workflow_dispatch detected, bypassing sync-commit gate."
34
41
  echo "skip=false" >> $GITHUB_OUTPUT
35
42
  fi
36
43
 
@@ -0,0 +1,63 @@
1
+ # climaybe — PR Close (Optional Preview Package)
2
+ # Cleans preview themes for the PR number and comments cleanup details.
3
+
4
+ name: PR Close
5
+
6
+ on:
7
+ pull_request:
8
+ types: [closed]
9
+ branches: [main, staging, develop]
10
+
11
+ jobs:
12
+ extract-pr-number:
13
+ uses: ./.github/workflows/reusable-extract-pr-number.yml
14
+
15
+ cleanup-theme:
16
+ needs: extract-pr-number
17
+ uses: ./.github/workflows/reusable-cleanup-themes.yml
18
+ with:
19
+ pr_number: ${{ needs.extract-pr-number.outputs.pr_number }}
20
+ secrets: inherit
21
+
22
+ comment-on-pr:
23
+ needs: cleanup-theme
24
+ runs-on: ubuntu-latest
25
+ steps:
26
+ - name: Comment on PR about cleanup
27
+ uses: actions/github-script@v7
28
+ with:
29
+ script: |
30
+ const prNumber = context.payload.pull_request.number;
31
+ const deletedCount = parseInt('${{ needs.cleanup-theme.outputs.deleted_count }}') || 0;
32
+ const deletedThemesRaw = `${{ needs.cleanup-theme.outputs.deleted_themes }}`.trim();
33
+
34
+ const lines = [
35
+ '## 🧹 Theme Cleanup Complete',
36
+ '',
37
+ 'The preview theme cleanup has finished.',
38
+ '',
39
+ `**Branch:** ${context.payload.pull_request.head.ref}`,
40
+ `**PR Status:** ${context.payload.pull_request.state}`,
41
+ `**Deleted Themes:** ${deletedCount}`
42
+ ];
43
+
44
+ if (deletedCount > 0 && deletedThemesRaw) {
45
+ const deletedThemeLines = deletedThemesRaw
46
+ .split('\n')
47
+ .map(name => name.trim())
48
+ .filter(Boolean)
49
+ .map(name => `- ${name}`);
50
+
51
+ if (deletedThemeLines.length > 0) {
52
+ lines.push('', '### Deleted Theme Names', ...deletedThemeLines);
53
+ }
54
+ }
55
+
56
+ lines.push('', '---', '*Theme cleanup runs automatically when a PR is closed.*');
57
+
58
+ await github.rest.issues.createComment({
59
+ issue_number: prNumber,
60
+ owner: context.repo.owner,
61
+ repo: context.repo.repo,
62
+ body: lines.join('\n')
63
+ });
@@ -0,0 +1,120 @@
1
+ # climaybe — PR Update (Optional Preview Package)
2
+ # Creates a Shopify preview theme on PR update, renames it with PR number,
3
+ # and posts preview/customize links back to the PR.
4
+
5
+ name: PR Update
6
+
7
+ on:
8
+ pull_request:
9
+ types: [opened, synchronize, reopened]
10
+ branches: [main, staging, develop]
11
+
12
+ jobs:
13
+ extract-pr-number:
14
+ uses: ./.github/workflows/reusable-extract-pr-number.yml
15
+
16
+ validate-environment:
17
+ runs-on: ubuntu-latest
18
+ outputs:
19
+ store_alias: ${{ steps.resolve.outputs.alias }}
20
+ store_alias_secret: ${{ steps.resolve.outputs.alias_secret }}
21
+ steps:
22
+ - name: Checkout code
23
+ uses: actions/checkout@v4
24
+
25
+ - name: Resolve default store alias
26
+ id: resolve
27
+ run: |
28
+ ALIAS=$(node -e "
29
+ const fs = require('fs');
30
+ const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
31
+ const stores = pkg?.config?.stores || {};
32
+ const defaultStoreRaw = pkg?.config?.default_store;
33
+ const normalize = (v) => String(v || '')
34
+ .toLowerCase()
35
+ .replace(/^https?:\\/\\//, '')
36
+ .replace(/\\/.*$/, '');
37
+ const defaultStore = normalize(defaultStoreRaw);
38
+ let alias = '';
39
+ if (defaultStore) {
40
+ for (const [k, d] of Object.entries(stores)) {
41
+ if (normalize(d) === defaultStore) {
42
+ alias = k;
43
+ break;
44
+ }
45
+ }
46
+ }
47
+ if (!alias) {
48
+ alias = Object.keys(stores)[0] || '';
49
+ }
50
+ if (!alias && defaultStore) {
51
+ const subdomain = defaultStore.split('.')[0] || 'default';
52
+ alias = subdomain.replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-') || 'default';
53
+ }
54
+ if (!alias) {
55
+ alias = 'default';
56
+ }
57
+ process.stdout.write(alias);
58
+ ")
59
+
60
+ if [ -z "$ALIAS" ]; then
61
+ echo "Could not resolve default store alias from package.json config."
62
+ exit 1
63
+ fi
64
+
65
+ ALIAS_SECRET=$(echo "$ALIAS" | tr '[:lower:]-' '[:upper:]_')
66
+ echo "alias=$ALIAS" >> $GITHUB_OUTPUT
67
+ echo "alias_secret=$ALIAS_SECRET" >> $GITHUB_OUTPUT
68
+
69
+ - name: Validate Shopify credentials
70
+ env:
71
+ SHOPIFY_STORE_URL_SCOPED: ${{ secrets[format('SHOPIFY_STORE_URL_{0}', steps.resolve.outputs.alias_secret)] }}
72
+ SHOPIFY_CLI_THEME_TOKEN_SCOPED: ${{ secrets[format('SHOPIFY_CLI_THEME_TOKEN_{0}', steps.resolve.outputs.alias_secret)] }}
73
+ SHOPIFY_STORE_URL_DEFAULT: ${{ secrets.SHOPIFY_STORE_URL }}
74
+ SHOPIFY_CLI_THEME_TOKEN_DEFAULT: ${{ secrets.SHOPIFY_CLI_THEME_TOKEN }}
75
+ run: |
76
+ SHOPIFY_STORE_URL="${SHOPIFY_STORE_URL_SCOPED:-$SHOPIFY_STORE_URL_DEFAULT}"
77
+ SHOPIFY_CLI_THEME_TOKEN="${SHOPIFY_CLI_THEME_TOKEN_SCOPED:-$SHOPIFY_CLI_THEME_TOKEN_DEFAULT}"
78
+
79
+ if [ -z "$SHOPIFY_STORE_URL" ]; then
80
+ echo "No store URL secret found. Expected SHOPIFY_STORE_URL_${{ steps.resolve.outputs.alias_secret }} or SHOPIFY_STORE_URL."
81
+ exit 1
82
+ fi
83
+ if [ -z "$SHOPIFY_CLI_THEME_TOKEN" ]; then
84
+ echo "No theme token secret found. Expected SHOPIFY_CLI_THEME_TOKEN_${{ steps.resolve.outputs.alias_secret }} or SHOPIFY_CLI_THEME_TOKEN."
85
+ exit 1
86
+ fi
87
+ echo "Shopify credentials are configured for alias: ${{ steps.resolve.outputs.alias }}"
88
+
89
+ share-theme:
90
+ needs: [validate-environment, extract-pr-number]
91
+ uses: ./.github/workflows/reusable-share-theme.yml
92
+ with:
93
+ pr_number: ${{ needs.extract-pr-number.outputs.pr_number }}
94
+ store_alias: ${{ needs.validate-environment.outputs.store_alias }}
95
+ secrets:
96
+ SHOPIFY_STORE_URL: ${{ secrets[format('SHOPIFY_STORE_URL_{0}', needs.validate-environment.outputs.store_alias_secret)] || secrets.SHOPIFY_STORE_URL }}
97
+ SHOPIFY_CLI_THEME_TOKEN: ${{ secrets[format('SHOPIFY_CLI_THEME_TOKEN_{0}', needs.validate-environment.outputs.store_alias_secret)] || secrets.SHOPIFY_CLI_THEME_TOKEN }}
98
+
99
+ rename-theme:
100
+ needs: [share-theme, extract-pr-number, validate-environment]
101
+ uses: ./.github/workflows/reusable-rename-theme.yml
102
+ with:
103
+ theme_id: ${{ needs.share-theme.outputs.theme_id }}
104
+ theme_name: ${{ needs.share-theme.outputs.theme_name }}
105
+ pr_number: ${{ needs.extract-pr-number.outputs.pr_number }}
106
+ store_alias: ${{ needs.validate-environment.outputs.store_alias }}
107
+ secrets:
108
+ SHOPIFY_STORE_URL: ${{ secrets[format('SHOPIFY_STORE_URL_{0}', needs.validate-environment.outputs.store_alias_secret)] || secrets.SHOPIFY_STORE_URL }}
109
+ SHOPIFY_CLI_THEME_TOKEN: ${{ secrets[format('SHOPIFY_CLI_THEME_TOKEN_{0}', needs.validate-environment.outputs.store_alias_secret)] || secrets.SHOPIFY_CLI_THEME_TOKEN }}
110
+
111
+ comment-on-pr:
112
+ needs: [share-theme, rename-theme, extract-pr-number, validate-environment]
113
+ uses: ./.github/workflows/reusable-comment-on-pr.yml
114
+ with:
115
+ theme_id: ${{ needs.share-theme.outputs.theme_id }}
116
+ share_output: ${{ needs.share-theme.outputs.share_output }}
117
+ pr_number: ${{ needs.extract-pr-number.outputs.pr_number_unpadded }}
118
+ store_alias: ${{ needs.validate-environment.outputs.store_alias }}
119
+ secrets:
120
+ SHOPIFY_STORE_URL: ${{ secrets[format('SHOPIFY_STORE_URL_{0}', needs.validate-environment.outputs.store_alias_secret)] || secrets.SHOPIFY_STORE_URL }}
@@ -0,0 +1,71 @@
1
+ name: Cleanup Themes
2
+
3
+ on:
4
+ workflow_call:
5
+ inputs:
6
+ pr_number:
7
+ required: true
8
+ type: string
9
+ description: "PR number to clean up themes for"
10
+ outputs:
11
+ deleted_count:
12
+ description: "Number of deleted themes"
13
+ value: ${{ jobs.cleanup.outputs.deleted_count }}
14
+ deleted_themes:
15
+ description: "Deleted theme names, one per line"
16
+ value: ${{ jobs.cleanup.outputs.deleted_themes }}
17
+
18
+ jobs:
19
+ cleanup:
20
+ runs-on: ubuntu-latest
21
+ outputs:
22
+ deleted_count: ${{ steps.cleanup.outputs.deleted_count }}
23
+ deleted_themes: ${{ steps.cleanup.outputs.deleted_themes }}
24
+ steps:
25
+ - name: Install Shopify CLI
26
+ run: npm install -g @shopify/cli @shopify/theme
27
+
28
+ - name: Cleanup PR preview themes
29
+ id: cleanup
30
+ env:
31
+ SHOPIFY_STORE_URL: ${{ secrets.SHOPIFY_STORE_URL }}
32
+ SHOPIFY_CLI_THEME_TOKEN: ${{ secrets.SHOPIFY_CLI_THEME_TOKEN }}
33
+ PR_NUMBER: ${{ inputs.pr_number }}
34
+ run: |
35
+ if [ -z "$SHOPIFY_STORE_URL" ] || [ -z "$SHOPIFY_CLI_THEME_TOKEN" ]; then
36
+ echo "Missing Shopify secrets."
37
+ exit 1
38
+ fi
39
+
40
+ THEME_LIST=$(shopify theme list \
41
+ --store "$SHOPIFY_STORE_URL" \
42
+ --password "$SHOPIFY_CLI_THEME_TOKEN" \
43
+ --json 2>/dev/null || echo "[]")
44
+
45
+ DELETED_COUNT=0
46
+ DELETED_THEMES=""
47
+
48
+ while IFS=$'\t' read -r THEME_ID THEME_NAME; do
49
+ [ -z "$THEME_ID" ] && continue
50
+ [ -z "$THEME_NAME" ] && continue
51
+
52
+ if printf "%s" "$THEME_NAME" | grep -q "PR${PR_NUMBER}"; then
53
+ if shopify theme delete \
54
+ --store "$SHOPIFY_STORE_URL" \
55
+ --password "$SHOPIFY_CLI_THEME_TOKEN" \
56
+ --force \
57
+ --theme "$THEME_ID" 2>/dev/null; then
58
+ DELETED_COUNT=$((DELETED_COUNT + 1))
59
+ DELETED_THEMES="${DELETED_THEMES}${THEME_NAME}\n"
60
+ else
61
+ echo "Failed to delete theme: $THEME_NAME ($THEME_ID)"
62
+ fi
63
+ fi
64
+ done < <(echo "$THEME_LIST" | jq -r '.[] | [.id, .name] | @tsv')
65
+
66
+ echo "deleted_count=$DELETED_COUNT" >> $GITHUB_OUTPUT
67
+ {
68
+ echo "deleted_themes<<DELETED_EOF"
69
+ printf "%b" "$DELETED_THEMES"
70
+ echo "DELETED_EOF"
71
+ } >> $GITHUB_OUTPUT
@@ -0,0 +1,76 @@
1
+ name: Comment on PR
2
+
3
+ on:
4
+ workflow_call:
5
+ inputs:
6
+ theme_id:
7
+ required: false
8
+ type: string
9
+ share_output:
10
+ required: false
11
+ type: string
12
+ pr_number:
13
+ required: true
14
+ type: string
15
+ store_alias:
16
+ required: false
17
+ type: string
18
+ store_alias_secret:
19
+ required: false
20
+ type: string
21
+ secrets:
22
+ SHOPIFY_STORE_URL:
23
+ required: false
24
+
25
+ jobs:
26
+ comment:
27
+ runs-on: ubuntu-latest
28
+ steps:
29
+ - name: Post preview comment
30
+ uses: actions/github-script@v7
31
+ env:
32
+ SHOPIFY_STORE_URL: ${{ secrets.SHOPIFY_STORE_URL }}
33
+ THEME_ID: ${{ inputs.theme_id }}
34
+ SHARE_OUTPUT: ${{ inputs.share_output }}
35
+ PR_NUMBER: ${{ inputs.pr_number }}
36
+ with:
37
+ script: |
38
+ const issueNumber = parseInt(process.env.PR_NUMBER);
39
+ const storeDomain = (process.env.SHOPIFY_STORE_URL || '')
40
+ .replace(/^https?:\/\//, '')
41
+ .replace(/\/$/, '');
42
+ const themeId = process.env.THEME_ID || '';
43
+ const shareOutput = process.env.SHARE_OUTPUT || '';
44
+
45
+ let previewUrl = '';
46
+ let customizeUrl = '';
47
+ if (storeDomain && themeId) {
48
+ previewUrl = `https://${storeDomain}?preview_theme_id=${themeId}`;
49
+ customizeUrl = `https://${storeDomain}/admin/themes/${themeId}/editor`;
50
+ }
51
+
52
+ const parts = [
53
+ '## 🎨 Theme Preview Generated',
54
+ '',
55
+ `**Store:** ${process.env.SHOPIFY_STORE_URL || 'N/A'}`,
56
+ `**Branch:** ${context.payload.pull_request?.head?.ref || context.ref.replace('refs/heads/', '')}`,
57
+ `**Commit:** ${(context.payload.pull_request?.head?.sha || context.sha || '').substring(0, 7)}`
58
+ ];
59
+
60
+ if (customizeUrl) {
61
+ parts.push('', `**Customize URL:** ${customizeUrl}`);
62
+ }
63
+ if (previewUrl) {
64
+ parts.push('', `**Preview URL:** ${previewUrl}`);
65
+ }
66
+ if (shareOutput.trim()) {
67
+ parts.push('', '### Share Output', '```', shareOutput, '```');
68
+ }
69
+ parts.push('', '---', '*This preview will be available for 7 days.*');
70
+
71
+ await github.rest.issues.createComment({
72
+ issue_number: issueNumber,
73
+ owner: context.repo.owner,
74
+ repo: context.repo.repo,
75
+ body: parts.join('\n')
76
+ });
@@ -0,0 +1,35 @@
1
+ name: Extract PR Number
2
+
3
+ on:
4
+ workflow_call:
5
+ outputs:
6
+ pr_number:
7
+ description: "The PR number (padded: 01, 02, etc.)"
8
+ value: ${{ jobs.extract.outputs.pr_number }}
9
+ pr_number_unpadded:
10
+ description: "The PR number (unpadded: 1, 2, etc.)"
11
+ value: ${{ jobs.extract.outputs.pr_number_unpadded }}
12
+
13
+ jobs:
14
+ extract:
15
+ runs-on: ubuntu-latest
16
+ outputs:
17
+ pr_number: ${{ steps.extract.outputs.pr_number }}
18
+ pr_number_unpadded: ${{ steps.extract.outputs.pr_number_unpadded }}
19
+ steps:
20
+ - name: Extract PR number
21
+ id: extract
22
+ run: |
23
+ PR_NUMBER="${{ github.event.pull_request.number }}"
24
+ if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" = "" ]; then
25
+ PR_NUMBER="${{ github.event.number }}"
26
+ fi
27
+
28
+ if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" = "" ]; then
29
+ echo "Could not extract PR number from event."
30
+ exit 1
31
+ fi
32
+
33
+ PADDED_PR_NUMBER=$(printf "%02d" "$PR_NUMBER")
34
+ echo "pr_number=$PADDED_PR_NUMBER" >> $GITHUB_OUTPUT
35
+ echo "pr_number_unpadded=$PR_NUMBER" >> $GITHUB_OUTPUT