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.
- package/README.md +16 -8
- package/bin/version.txt +1 -1
- package/package.json +1 -1
- package/src/commands/build-scripts.js +7 -1
- package/src/cursor/rules/00-rule-index.mdc +1 -0
- package/src/cursor/rules/javascript-standards.mdc +3 -1
- package/src/cursor/rules/js-refactor-tasks.mdc +1 -1
- package/src/cursor/rules/project-overview.mdc +20 -2
- package/src/cursor/rules/tailwindcss-rules.mdc +2 -1
- package/src/index.js +6 -2
- package/src/lib/build-scripts.js +36 -3
- package/src/lib/config.js +17 -0
- package/src/lib/dev-runtime.js +39 -9
- package/src/lib/prompts.js +22 -0
- package/src/lib/serve-multi-store.js +81 -0
- package/src/workflows/preview/cleanup-orphan-preview-themes.yml +59 -0
- package/src/workflows/preview/pr-close.yml +103 -62
- package/src/workflows/preview/pr-update.yml +87 -76
- package/src/workflows/preview/reusable-cleanup-themes.yml +91 -6
- package/src/workflows/preview/reusable-comment-on-pr.yml +83 -19
- package/src/workflows/preview/reusable-publish-pr-preview-store.yml +162 -0
|
@@ -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
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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 (
|
|
59
|
-
parts.push('',
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|