climaybe 3.2.0 → 3.4.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.
@@ -19,11 +19,29 @@ on:
19
19
  required: false
20
20
  type: string
21
21
  description: "Upper snake-case alias for scoped secret (e.g. VOLDT_STAGING). If set, uses SHOPIFY_STORE_URL_<this>; else uses SHOPIFY_STORE_URL."
22
+ use_preview_fragments:
23
+ required: false
24
+ type: string
25
+ default: 'false'
26
+ description: "When 'true', download preview-fragment-* artifacts (from publish matrix) and post one comment with all preview URLs."
22
27
 
23
28
  jobs:
24
29
  comment:
25
30
  runs-on: ubuntu-latest
31
+ permissions:
32
+ contents: read
33
+ actions: read
34
+ pull-requests: write
26
35
  steps:
36
+ - name: Download preview fragments (multi-store / matrix publish)
37
+ if: inputs.use_preview_fragments == 'true'
38
+ continue-on-error: true
39
+ uses: actions/download-artifact@v4
40
+ with:
41
+ pattern: preview-fragment-*
42
+ merge-multiple: true
43
+ path: preview-fragments
44
+
27
45
  - name: Post preview comment
28
46
  uses: actions/github-script@v7
29
47
  env:
@@ -31,39 +49,85 @@ jobs:
31
49
  THEME_ID: ${{ inputs.theme_id }}
32
50
  SHARE_OUTPUT: ${{ inputs.share_output }}
33
51
  PR_NUMBER: ${{ inputs.pr_number }}
52
+ USE_PREVIEW_FRAGMENTS: ${{ inputs.use_preview_fragments }}
34
53
  with:
35
54
  script: |
55
+ const fs = require('fs');
56
+ const path = require('path');
36
57
  const issueNumber = parseInt(process.env.PR_NUMBER);
37
- const storeDomain = (process.env.SHOPIFY_STORE_URL || '')
38
- .replace(/^https?:\/\//, '')
39
- .replace(/\/$/, '');
40
- const themeId = process.env.THEME_ID || '';
41
- const shareOutput = process.env.SHARE_OUTPUT || '';
58
+ const useFragments = process.env.USE_PREVIEW_FRAGMENTS === 'true';
42
59
 
43
- let previewUrl = '';
44
- let customizeUrl = '';
45
- if (storeDomain && themeId) {
46
- previewUrl = `https://${storeDomain}?preview_theme_id=${themeId}`;
47
- customizeUrl = `https://${storeDomain}/admin/themes/${themeId}/editor`;
60
+ const walkJsonFiles = (dir) => {
61
+ const out = [];
62
+ if (!fs.existsSync(dir)) return out;
63
+ const stack = [dir];
64
+ while (stack.length) {
65
+ const d = stack.pop();
66
+ for (const ent of fs.readdirSync(d, { withFileTypes: true })) {
67
+ const p = path.join(d, ent.name);
68
+ if (ent.isDirectory()) stack.push(p);
69
+ else if (ent.name === 'fragment.json' || ent.name.endsWith('.json')) {
70
+ try {
71
+ out.push(JSON.parse(fs.readFileSync(p, 'utf8')));
72
+ } catch {
73
+ // ignore
74
+ }
75
+ }
76
+ }
77
+ }
78
+ return out;
79
+ };
80
+
81
+ let fragments = [];
82
+ if (useFragments) {
83
+ fragments = walkJsonFiles('preview-fragments');
48
84
  }
49
85
 
50
86
  const parts = [
51
87
  '## 🎨 Theme Preview Generated',
52
88
  '',
53
- `**Store:** ${process.env.SHOPIFY_STORE_URL || 'N/A'}`,
54
89
  `**Branch:** ${context.payload.pull_request?.head?.ref || context.ref.replace('refs/heads/', '')}`,
55
90
  `**Commit:** ${(context.payload.pull_request?.head?.sha || context.sha || '').substring(0, 7)}`
56
91
  ];
57
92
 
58
- if (customizeUrl) {
59
- parts.push('', `**Customize URL:** ${customizeUrl}`);
60
- }
61
- if (previewUrl) {
62
- parts.push('', `**Preview URL:** ${previewUrl}`);
63
- }
64
- if (shareOutput.trim()) {
65
- parts.push('', '### Share Output', '```', shareOutput, '```');
93
+ if (fragments.length > 0) {
94
+ parts.push('', '### Preview links (per store)');
95
+ const byAlias = [...fragments].sort((a, b) => String(a.alias).localeCompare(String(b.alias)));
96
+ for (const f of byAlias) {
97
+ const host = (f.store_host || '').replace(/\/$/, '');
98
+ const tid = f.theme_id || '';
99
+ const alias = f.alias || 'store';
100
+ if (host && tid) {
101
+ const previewUrl = `https://${host}?preview_theme_id=${tid}`;
102
+ const customizeUrl = `https://${host}/admin/themes/${tid}/editor`;
103
+ parts.push('', `**${alias}**`, `- Customize: ${customizeUrl}`, `- Preview: ${previewUrl}`);
104
+ }
105
+ }
106
+ } else if (useFragments) {
107
+ parts.push(
108
+ '',
109
+ '*(Could not load per-store preview fragments from workflow artifacts; see the publish job logs.)*'
110
+ );
111
+ } else {
112
+ const storeDomain = (process.env.SHOPIFY_STORE_URL || '')
113
+ .replace(/^https?:\/\//, '')
114
+ .replace(/\/$/, '');
115
+ const themeId = process.env.THEME_ID || '';
116
+ const shareOutput = process.env.SHARE_OUTPUT || '';
117
+ parts.push('', `**Store:** ${process.env.SHOPIFY_STORE_URL || 'N/A'}`);
118
+ let previewUrl = '';
119
+ let customizeUrl = '';
120
+ if (storeDomain && themeId) {
121
+ previewUrl = `https://${storeDomain}?preview_theme_id=${themeId}`;
122
+ customizeUrl = `https://${storeDomain}/admin/themes/${themeId}/editor`;
123
+ }
124
+ if (customizeUrl) parts.push('', `**Customize URL:** ${customizeUrl}`);
125
+ if (previewUrl) parts.push('', `**Preview URL:** ${previewUrl}`);
126
+ if (shareOutput.trim()) {
127
+ parts.push('', '### Share Output', '```', shareOutput, '```');
128
+ }
66
129
  }
130
+
67
131
  parts.push('', '---', '*This preview will be available for 7 days.*');
68
132
 
69
133
  await github.rest.issues.createComment({
@@ -0,0 +1,162 @@
1
+ # Share theme to one Shopify store, rename with -PR{padded}, upload JSON fragment for PR comment fan-in.
2
+ # Intended to be called from a matrix job (one invocation per store).
3
+
4
+ name: Publish PR preview (single store)
5
+
6
+ on:
7
+ workflow_call:
8
+ inputs:
9
+ pr_number:
10
+ required: true
11
+ type: string
12
+ description: "Padded PR number (e.g. 09) for theme name suffix -PR09"
13
+ store_alias:
14
+ required: true
15
+ type: string
16
+ description: "Store alias (for artifact naming and comment JSON)"
17
+ store_alias_secret:
18
+ required: false
19
+ type: string
20
+ default: ''
21
+ description: "Upper snake-case secret suffix; empty uses SHOPIFY_STORE_URL / SHOPIFY_THEME_ACCESS_TOKEN"
22
+ outputs:
23
+ theme_id:
24
+ description: "Theme ID after share/rename"
25
+ value: ${{ jobs.publish.outputs.theme_id }}
26
+ theme_name:
27
+ description: "Final theme name after rename"
28
+ value: ${{ jobs.publish.outputs.theme_name }}
29
+ share_output:
30
+ description: "Raw share command output"
31
+ value: ${{ jobs.publish.outputs.share_output }}
32
+
33
+ jobs:
34
+ publish:
35
+ runs-on: ubuntu-latest
36
+ permissions:
37
+ contents: read
38
+ actions: write
39
+ outputs:
40
+ theme_id: ${{ steps.rename.outputs.theme_id }}
41
+ theme_name: ${{ steps.rename.outputs.theme_name }}
42
+ share_output: ${{ steps.share.outputs.share_output }}
43
+ steps:
44
+ - name: Checkout code
45
+ uses: actions/checkout@v4
46
+
47
+ - name: Validate theme root
48
+ run: |
49
+ if [ ! -f "layout/theme.liquid" ]; then
50
+ echo "layout/theme.liquid not found. Ensure workflow runs at theme repository root."
51
+ exit 1
52
+ fi
53
+
54
+ - name: Install Shopify CLI
55
+ run: npm install -g @shopify/cli @shopify/theme
56
+
57
+ - name: Share theme
58
+ id: share
59
+ env:
60
+ SHOPIFY_STORE_URL: ${{ inputs.store_alias_secret && secrets[format('SHOPIFY_STORE_URL_{0}', inputs.store_alias_secret)] || secrets.SHOPIFY_STORE_URL }}
61
+ SHOPIFY_THEME_ACCESS_TOKEN: ${{ inputs.store_alias_secret && secrets[format('SHOPIFY_THEME_ACCESS_TOKEN_{0}', inputs.store_alias_secret)] || secrets.SHOPIFY_THEME_ACCESS_TOKEN }}
62
+ run: |
63
+ if [ -z "$SHOPIFY_STORE_URL" ] || [ -z "$SHOPIFY_THEME_ACCESS_TOKEN" ]; then
64
+ echo "Missing Shopify secrets."
65
+ exit 1
66
+ fi
67
+
68
+ OUTPUT=$(shopify theme share \
69
+ --store "$SHOPIFY_STORE_URL" \
70
+ --password "$SHOPIFY_THEME_ACCESS_TOKEN" 2>&1)
71
+ STATUS=$?
72
+
73
+ echo "$OUTPUT"
74
+ if [ $STATUS -ne 0 ]; then
75
+ echo "Theme share failed."
76
+ exit $STATUS
77
+ fi
78
+
79
+ THEME_NAME=$(echo "$OUTPUT" | sed -n "s/.*The theme '\([^']*\)'.*/\1/p" | head -1)
80
+ THEME_ID=$(echo "$OUTPUT" | sed -n 's/.*#\([0-9]*\).*/\1/p' | head -1)
81
+
82
+ if [ -z "$THEME_ID" ]; then
83
+ echo "Could not parse theme id from share output."
84
+ exit 1
85
+ fi
86
+
87
+ echo "theme_id=$THEME_ID" >> "$GITHUB_OUTPUT"
88
+ if [ -n "$THEME_NAME" ]; then
89
+ echo "theme_name=$THEME_NAME" >> "$GITHUB_OUTPUT"
90
+ fi
91
+
92
+ {
93
+ echo "share_output<<SHARE_EOF"
94
+ echo "$OUTPUT"
95
+ echo "SHARE_EOF"
96
+ } >> "$GITHUB_OUTPUT"
97
+
98
+ - name: Rename theme with PR suffix
99
+ id: rename
100
+ env:
101
+ SHOPIFY_STORE_URL: ${{ inputs.store_alias_secret && secrets[format('SHOPIFY_STORE_URL_{0}', inputs.store_alias_secret)] || secrets.SHOPIFY_STORE_URL }}
102
+ SHOPIFY_THEME_ACCESS_TOKEN: ${{ inputs.store_alias_secret && secrets[format('SHOPIFY_THEME_ACCESS_TOKEN_{0}', inputs.store_alias_secret)] || secrets.SHOPIFY_THEME_ACCESS_TOKEN }}
103
+ THEME_ID: ${{ steps.share.outputs.theme_id }}
104
+ THEME_NAME: ${{ steps.share.outputs.theme_name }}
105
+ PR_NUMBER: ${{ inputs.pr_number }}
106
+ run: |
107
+ if [ -z "$THEME_ID" ] || [ -z "$THEME_NAME" ]; then
108
+ echo "Missing theme_id/theme_name from share step."
109
+ exit 1
110
+ fi
111
+ if [ -z "$SHOPIFY_STORE_URL" ] || [ -z "$SHOPIFY_THEME_ACCESS_TOKEN" ]; then
112
+ echo "Missing Shopify store URL/token for rename."
113
+ exit 1
114
+ fi
115
+
116
+ NEW_THEME_NAME="${THEME_NAME}-PR${PR_NUMBER}"
117
+ echo "Renaming theme $THEME_ID to '$NEW_THEME_NAME'..."
118
+
119
+ if shopify theme rename \
120
+ --store "$SHOPIFY_STORE_URL" \
121
+ --password "$SHOPIFY_THEME_ACCESS_TOKEN" \
122
+ --theme "$THEME_ID" \
123
+ --name "$NEW_THEME_NAME" 2>&1; then
124
+ echo "Rename succeeded with password auth."
125
+ elif shopify theme rename \
126
+ --store "$SHOPIFY_STORE_URL" \
127
+ --theme "$THEME_ID" \
128
+ --name "$NEW_THEME_NAME" 2>&1; then
129
+ echo "Rename succeeded with authenticated session."
130
+ else
131
+ echo "Failed to rename theme."
132
+ exit 1
133
+ fi
134
+
135
+ echo "theme_id=$THEME_ID" >> "$GITHUB_OUTPUT"
136
+ echo "theme_name=$NEW_THEME_NAME" >> "$GITHUB_OUTPUT"
137
+
138
+ - name: Write preview fragment for PR comment
139
+ env:
140
+ STORE_ALIAS: ${{ inputs.store_alias }}
141
+ THEME_ID: ${{ steps.rename.outputs.theme_id }}
142
+ SHOPIFY_STORE_URL: ${{ inputs.store_alias_secret && secrets[format('SHOPIFY_STORE_URL_{0}', inputs.store_alias_secret)] || secrets.SHOPIFY_STORE_URL }}
143
+ run: |
144
+ node <<'NODE'
145
+ const fs = require('fs');
146
+ const url = (process.env.SHOPIFY_STORE_URL || '')
147
+ .replace(/^https?:\/\//, '')
148
+ .replace(/\/.*$/, '');
149
+ const payload = {
150
+ alias: process.env.STORE_ALIAS || '',
151
+ theme_id: process.env.THEME_ID || '',
152
+ store_host: url,
153
+ };
154
+ fs.writeFileSync('fragment.json', JSON.stringify(payload, null, 0));
155
+ NODE
156
+
157
+ - name: Upload preview fragment
158
+ uses: actions/upload-artifact@v4
159
+ with:
160
+ name: preview-fragment-${{ inputs.store_alias }}
161
+ path: fragment.json
162
+ retention-days: 2