climaybe 3.0.9 → 3.1.1
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 +60 -3
- package/bin/version.txt +1 -1
- package/package.json +2 -2
- package/src/commands/build-schemas.js +74 -0
- package/src/cursor/rules/00-rule-index.mdc +1 -1
- package/src/cursor/rules/commit-rules.mdc +74 -74
- package/src/cursor/rules/schemas.mdc +124 -104
- package/src/cursor/rules/sections.mdc +3 -1
- package/src/cursor/skills/changelog-release/SKILL.md +7 -7
- package/src/cursor/skills/commit-in-groups/SKILL.md +6 -6
- package/src/index.js +8 -0
- package/src/lib/build-scripts.js +60 -7
- package/src/lib/dev-runtime.js +67 -3
- package/src/lib/schema-builder.js +242 -0
- package/src/lib/theme-dev-kit.js +3 -0
- package/src/lib/update-notifier.js +19 -5
- package/src/workflows/build/build-pipeline.yml +18 -2
- package/src/workflows/build/reusable-build.yml +60 -19
- package/src/workflows/multi/main-to-staging-stores.yml +4 -1
- package/src/workflows/preview/pr-update.yml +28 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const SCHEMA_DIR = '_schemas';
|
|
6
|
+
const LIQUID_DIRS = ['sections', 'blocks'];
|
|
7
|
+
|
|
8
|
+
// Matches the inline-comment marker: {% # schema 'name' %}
|
|
9
|
+
const MARKER_REGEX =
|
|
10
|
+
/\{%-?\s*#\s*schema\s+['"]([^'"]+)['"]\s*-?%\}/;
|
|
11
|
+
|
|
12
|
+
// Matches the marker, then an optional inline-override marker, then an
|
|
13
|
+
// optional existing generated {% schema %}...{% endschema %} block, all the
|
|
14
|
+
// way to end-of-file.
|
|
15
|
+
const MARKER_WITH_OUTPUT_REGEX =
|
|
16
|
+
/(\{%-?\s*#\s*schema\s+['"]([^'"]+)['"]\s*-?%\})([\s\S]*?)(\{%-?\s*schema\s*-?%\}[\s\S]*?\{%-?\s*endschema\s*-?%\})?\s*$/;
|
|
17
|
+
|
|
18
|
+
// Matches an inline-comment override: {% # { "name": "Custom" } %}
|
|
19
|
+
const INLINE_OVERRIDE_REGEX =
|
|
20
|
+
/\{%-?\s*#\s*(\{[\s\S]*?\})\s*-?%\}/;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolve a schema source file (.js or .json) from the schemas directory.
|
|
24
|
+
*/
|
|
25
|
+
function resolveSchemaFile(schemasDir, name) {
|
|
26
|
+
const jsPath = join(schemasDir, `${name}.js`);
|
|
27
|
+
if (existsSync(jsPath)) return jsPath;
|
|
28
|
+
const jsonPath = join(schemasDir, `${name}.json`);
|
|
29
|
+
if (existsSync(jsonPath)) return jsonPath;
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Load a schema module. CommonJS `.js` files are loaded via `createRequire`;
|
|
35
|
+
* `.json` files are parsed directly. Cache is busted on every load so
|
|
36
|
+
* rebuilds pick up changes without restarting the process.
|
|
37
|
+
*/
|
|
38
|
+
function loadSchemaModule(schemasDir, absolutePath) {
|
|
39
|
+
if (absolutePath.endsWith('.json')) {
|
|
40
|
+
return JSON.parse(readFileSync(absolutePath, 'utf-8'));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const localRequire = createRequire(join(schemasDir, '_entry.js'));
|
|
44
|
+
const resolved = localRequire.resolve(absolutePath);
|
|
45
|
+
delete localRequire.cache[resolved];
|
|
46
|
+
return localRequire(resolved);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parse optional inline JSON.
|
|
51
|
+
*/
|
|
52
|
+
function parseInlineContent(raw) {
|
|
53
|
+
const trimmed = (raw || '').trim();
|
|
54
|
+
if (!trimmed) return {};
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(trimmed);
|
|
57
|
+
} catch {
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Evaluate the schema export.
|
|
64
|
+
*
|
|
65
|
+
* - **Function exports** receive `(filename, inlineContent)` and must return
|
|
66
|
+
* the schema object.
|
|
67
|
+
* - **Object exports** are shallow-merged with any inline content (inline wins).
|
|
68
|
+
*/
|
|
69
|
+
function evaluateSchema(schemaExport, sectionFilename, inlineContent) {
|
|
70
|
+
const raw = schemaExport?.default ?? schemaExport;
|
|
71
|
+
if (typeof raw === 'function') {
|
|
72
|
+
return raw(sectionFilename, inlineContent);
|
|
73
|
+
}
|
|
74
|
+
const hasInline = inlineContent && typeof inlineContent === 'object' && Object.keys(inlineContent).length > 0;
|
|
75
|
+
if (hasInline) {
|
|
76
|
+
return { ...raw, ...inlineContent };
|
|
77
|
+
}
|
|
78
|
+
return raw;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Process a single directory of liquid files (sections/ or blocks/).
|
|
83
|
+
*/
|
|
84
|
+
function processLiquidDir({ dirPath, dirName, schemasDir, dryRun, processed, skipped, errors }) {
|
|
85
|
+
if (!existsSync(dirPath)) return;
|
|
86
|
+
|
|
87
|
+
const files = readdirSync(dirPath, { withFileTypes: true })
|
|
88
|
+
.filter((d) => d.isFile() && d.name.endsWith('.liquid'))
|
|
89
|
+
.map((d) => d.name)
|
|
90
|
+
.sort();
|
|
91
|
+
|
|
92
|
+
for (const fileName of files) {
|
|
93
|
+
const filePath = join(dirPath, fileName);
|
|
94
|
+
const displayName = `${dirName}/${fileName}`;
|
|
95
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
96
|
+
|
|
97
|
+
if (!MARKER_REGEX.test(content)) {
|
|
98
|
+
skipped.push(displayName);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!existsSync(schemasDir)) {
|
|
103
|
+
errors.push({
|
|
104
|
+
section: displayName,
|
|
105
|
+
schema: '(unknown)',
|
|
106
|
+
error: '_schemas/ directory not found',
|
|
107
|
+
});
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const fullMatch = content.match(MARKER_WITH_OUTPUT_REGEX);
|
|
112
|
+
if (!fullMatch) {
|
|
113
|
+
skipped.push(displayName);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const marker = fullMatch[1];
|
|
118
|
+
const schemaName = fullMatch[2];
|
|
119
|
+
const betweenContent = fullMatch[3] || '';
|
|
120
|
+
|
|
121
|
+
const schemaFile = resolveSchemaFile(schemasDir, schemaName);
|
|
122
|
+
if (!schemaFile) {
|
|
123
|
+
errors.push({
|
|
124
|
+
section: displayName,
|
|
125
|
+
schema: schemaName,
|
|
126
|
+
error: `Schema file not found: _schemas/${schemaName}.js or .json`,
|
|
127
|
+
});
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const schemaExport = loadSchemaModule(schemasDir, schemaFile);
|
|
133
|
+
|
|
134
|
+
let inlineContent = {};
|
|
135
|
+
const inlineMatch = betweenContent.match(INLINE_OVERRIDE_REGEX);
|
|
136
|
+
if (inlineMatch) {
|
|
137
|
+
inlineContent = parseInlineContent(inlineMatch[1]);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const schema = evaluateSchema(schemaExport, fileName, inlineContent);
|
|
141
|
+
const json = JSON.stringify(schema, null, 2);
|
|
142
|
+
const generatedBlock = `{% schema %}\n${json}\n{% endschema %}`;
|
|
143
|
+
|
|
144
|
+
const markerIndex = content.indexOf(marker);
|
|
145
|
+
const beforeMarker = content.substring(0, markerIndex);
|
|
146
|
+
|
|
147
|
+
let inlineOverrideBlock = '';
|
|
148
|
+
if (inlineMatch) {
|
|
149
|
+
inlineOverrideBlock = '\n' + inlineMatch[0];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const newContent = beforeMarker + marker + inlineOverrideBlock + '\n' + generatedBlock + '\n';
|
|
153
|
+
|
|
154
|
+
if (!dryRun) {
|
|
155
|
+
writeFileSync(filePath, newContent, 'utf-8');
|
|
156
|
+
}
|
|
157
|
+
processed.push({ section: displayName, schemaName });
|
|
158
|
+
} catch (err) {
|
|
159
|
+
errors.push({
|
|
160
|
+
section: displayName,
|
|
161
|
+
schema: schemaName,
|
|
162
|
+
error: err.message,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Build section and block schemas for a Shopify theme project.
|
|
170
|
+
*
|
|
171
|
+
* Scans `sections/*.liquid` and `blocks/*.liquid` for an inline-comment marker:
|
|
172
|
+
*
|
|
173
|
+
* {% # schema 'hero-banner' %}
|
|
174
|
+
*
|
|
175
|
+
* When found, resolves `_schemas/hero-banner.js` (or `.json`), evaluates it,
|
|
176
|
+
* and writes (or replaces) the generated `{% schema %}...{% endschema %}`
|
|
177
|
+
* block directly below the marker. The marker is never removed, so rebuilds
|
|
178
|
+
* always work — even after Shopify theme editor edits.
|
|
179
|
+
*
|
|
180
|
+
* @param {object} options
|
|
181
|
+
* @param {string} [options.cwd] Theme project root (default `process.cwd()`).
|
|
182
|
+
* @param {boolean} [options.dryRun] When true, compute schemas without writing files.
|
|
183
|
+
* @returns {{ processed: Array<{section: string, schemaName: string}>, skipped: string[], errors: Array<{section: string, schema: string, error: string}> }}
|
|
184
|
+
*/
|
|
185
|
+
export function buildSchemas({ cwd = process.cwd(), dryRun = false } = {}) {
|
|
186
|
+
const schemasDir = join(cwd, SCHEMA_DIR);
|
|
187
|
+
|
|
188
|
+
const processed = [];
|
|
189
|
+
const skipped = [];
|
|
190
|
+
const errors = [];
|
|
191
|
+
|
|
192
|
+
for (const dirName of LIQUID_DIRS) {
|
|
193
|
+
processLiquidDir({
|
|
194
|
+
dirPath: join(cwd, dirName),
|
|
195
|
+
dirName,
|
|
196
|
+
schemasDir,
|
|
197
|
+
dryRun,
|
|
198
|
+
processed,
|
|
199
|
+
skipped,
|
|
200
|
+
errors,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return { processed, skipped, errors };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* List available schema source files in `_schemas/`.
|
|
209
|
+
*/
|
|
210
|
+
export function listSchemaFiles(cwd = process.cwd()) {
|
|
211
|
+
const schemasDir = join(cwd, SCHEMA_DIR);
|
|
212
|
+
if (!existsSync(schemasDir)) return [];
|
|
213
|
+
return readdirSync(schemasDir, { withFileTypes: true })
|
|
214
|
+
.filter((d) => d.isFile() && (d.name.endsWith('.js') || d.name.endsWith('.json')))
|
|
215
|
+
.map((d) => d.name)
|
|
216
|
+
.sort();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* List liquid files in sections/ and blocks/ that contain the inline-comment schema marker.
|
|
221
|
+
*/
|
|
222
|
+
export function listSectionsWithSchemaRefs(cwd = process.cwd()) {
|
|
223
|
+
const results = [];
|
|
224
|
+
|
|
225
|
+
for (const dirName of LIQUID_DIRS) {
|
|
226
|
+
const dirPath = join(cwd, dirName);
|
|
227
|
+
if (!existsSync(dirPath)) continue;
|
|
228
|
+
|
|
229
|
+
const files = readdirSync(dirPath, { withFileTypes: true })
|
|
230
|
+
.filter((d) => d.isFile() && d.name.endsWith('.liquid'));
|
|
231
|
+
|
|
232
|
+
for (const file of files) {
|
|
233
|
+
const content = readFileSync(join(dirPath, file.name), 'utf-8');
|
|
234
|
+
const match = content.match(MARKER_REGEX);
|
|
235
|
+
if (match) {
|
|
236
|
+
results.push({ section: `${dirName}/${file.name}`, schemas: [match[1]] });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return results;
|
|
242
|
+
}
|
package/src/lib/theme-dev-kit.js
CHANGED
|
@@ -45,7 +45,19 @@ function canPromptForUpdate() {
|
|
|
45
45
|
return Boolean(input.isTTY && output.isTTY && process.env.CI !== 'true');
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
function isNodeModulesInstall(packageDir, packageName) {
|
|
49
|
+
if (!packageDir || !packageName) return false;
|
|
50
|
+
const normalized = resolve(packageDir);
|
|
51
|
+
const nm = `${join('node_modules', packageName)}`;
|
|
52
|
+
return normalized.includes(`${join('node_modules', '')}`) && normalized.endsWith(nm);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function resolveInstallScope({ packageName, packageDir, cwd = process.cwd() } = {}) {
|
|
56
|
+
// Prefer using the *running* CLI install path to decide how to update.
|
|
57
|
+
// This prevents "update loops" where we update a project dependency while the
|
|
58
|
+
// user is actually running a global install (or vice-versa).
|
|
59
|
+
if (isNodeModulesInstall(packageDir, packageName)) return 'local';
|
|
60
|
+
|
|
49
61
|
try {
|
|
50
62
|
const globalRoot = execSync('npm root -g', { encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
51
63
|
if (packageDir && resolve(packageDir).startsWith(resolve(globalRoot))) return 'global';
|
|
@@ -53,7 +65,8 @@ export function resolveInstallScope({ packageDir, cwd = process.cwd() } = {}) {
|
|
|
53
65
|
// ignore and fallback to local checks
|
|
54
66
|
}
|
|
55
67
|
|
|
56
|
-
|
|
68
|
+
// If we don't have an install path, fall back to "am I in a project?"
|
|
69
|
+
if (!packageDir && existsSync(join(cwd, 'package.json'))) return 'local';
|
|
57
70
|
return 'global';
|
|
58
71
|
}
|
|
59
72
|
|
|
@@ -73,13 +86,14 @@ export function getLocalInstallFlag({ packageName, cwd = process.cwd() } = {}) {
|
|
|
73
86
|
}
|
|
74
87
|
|
|
75
88
|
function runUpdate(packageName, { packageDir, cwd = process.cwd() } = {}) {
|
|
76
|
-
const scope = resolveInstallScope({ packageDir, cwd });
|
|
89
|
+
const scope = resolveInstallScope({ packageName, packageDir, cwd });
|
|
77
90
|
if (scope === 'global') {
|
|
78
91
|
execSync(`npm install -g ${packageName}@latest`, { stdio: 'inherit' });
|
|
79
92
|
return 'global';
|
|
80
93
|
}
|
|
81
|
-
const
|
|
82
|
-
|
|
94
|
+
const installCwd = cwd || process.cwd();
|
|
95
|
+
const flag = getLocalInstallFlag({ packageName, cwd: installCwd });
|
|
96
|
+
execSync(`npm install ${packageName}@latest ${flag}`, { cwd: installCwd, stdio: 'inherit' });
|
|
83
97
|
return 'local';
|
|
84
98
|
}
|
|
85
99
|
|
|
@@ -5,6 +5,17 @@ name: Build Pipeline
|
|
|
5
5
|
on:
|
|
6
6
|
push:
|
|
7
7
|
branches: ['**']
|
|
8
|
+
paths-ignore:
|
|
9
|
+
- '**/*.md'
|
|
10
|
+
- 'docs/**'
|
|
11
|
+
- '.github/**'
|
|
12
|
+
- '.cursor/**'
|
|
13
|
+
- '.claude/**'
|
|
14
|
+
- '.eser/**'
|
|
15
|
+
- 'README*'
|
|
16
|
+
- 'LICENSE*'
|
|
17
|
+
- 'CHANGELOG*'
|
|
18
|
+
- 'CONTRIBUTING*'
|
|
8
19
|
|
|
9
20
|
jobs:
|
|
10
21
|
build:
|
|
@@ -15,11 +26,13 @@ jobs:
|
|
|
15
26
|
!contains(github.event.head_commit.message, '[hotfix-backport]')
|
|
16
27
|
&& !contains(github.event.head_commit.message, '[stores-to-root]')
|
|
17
28
|
&& !contains(github.event.head_commit.message, '[root-to-stores]')
|
|
29
|
+
&& !(github.actor == 'github-actions[bot]' && contains(github.event.head_commit.message, 'chore(assets): update compiled javascript and css'))
|
|
18
30
|
uses: ./.github/workflows/reusable-build.yml
|
|
19
31
|
|
|
20
32
|
lighthouse-gate:
|
|
21
33
|
runs-on: ubuntu-latest
|
|
22
34
|
needs: [build]
|
|
35
|
+
if: needs.build.outputs.build_ran == 'true'
|
|
23
36
|
outputs:
|
|
24
37
|
run_lighthouse: ${{ steps.check.outputs.run_lighthouse }}
|
|
25
38
|
env:
|
|
@@ -31,8 +44,11 @@ jobs:
|
|
|
31
44
|
id: check
|
|
32
45
|
run: |
|
|
33
46
|
SKIP_REASONS=()
|
|
34
|
-
|
|
35
|
-
|
|
47
|
+
BR="${{ github.ref_name }}"
|
|
48
|
+
if [[ "$BR" == "staging" ]]; then
|
|
49
|
+
:
|
|
50
|
+
else
|
|
51
|
+
SKIP_REASONS+=("Lighthouse runs only on branch staging (current branch: $BR)")
|
|
36
52
|
fi
|
|
37
53
|
if [ -z "$SHOPIFY_STORE_URL" ] || [ -z "$SHOP_ACCESS_TOKEN" ] || [ -z "$LHCI_GITHUB_APP_TOKEN" ]; then
|
|
38
54
|
SKIP_REASONS+=("Missing required secrets: SHOPIFY_STORE_URL, SHOP_ACCESS_TOKEN, or LHCI_GITHUB_APP_TOKEN")
|
|
@@ -6,6 +6,9 @@ on:
|
|
|
6
6
|
build-success:
|
|
7
7
|
description: "Whether build was successful"
|
|
8
8
|
value: ${{ jobs.build.outputs.success }}
|
|
9
|
+
build_ran:
|
|
10
|
+
description: "True when npm ci and at least one build step ran (path filters matched)"
|
|
11
|
+
value: ${{ jobs.build.outputs.build_ran }}
|
|
9
12
|
|
|
10
13
|
jobs:
|
|
11
14
|
build:
|
|
@@ -14,22 +17,25 @@ jobs:
|
|
|
14
17
|
contents: write
|
|
15
18
|
outputs:
|
|
16
19
|
success: ${{ steps.build.outputs.success }}
|
|
20
|
+
build_ran: ${{ steps.run.outputs.build_ran }}
|
|
17
21
|
steps:
|
|
18
22
|
- name: Checkout code
|
|
19
23
|
uses: actions/checkout@v4
|
|
20
24
|
|
|
21
|
-
- name:
|
|
22
|
-
uses:
|
|
25
|
+
- name: Detect changed paths
|
|
26
|
+
uses: dorny/paths-filter@v3
|
|
27
|
+
id: changes
|
|
23
28
|
with:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
filters: |
|
|
30
|
+
scripts:
|
|
31
|
+
- '_scripts/**/*.js'
|
|
32
|
+
- 'assets/**/*.js'
|
|
33
|
+
tailwind:
|
|
34
|
+
- '**/*.liquid'
|
|
35
|
+
- '**/*.css'
|
|
36
|
+
- '_scripts/**/*.js'
|
|
37
|
+
- 'tailwind.config.*'
|
|
38
|
+
- 'postcss.config.*'
|
|
33
39
|
|
|
34
40
|
- name: Detect build entrypoints
|
|
35
41
|
id: detect
|
|
@@ -51,8 +57,42 @@ jobs:
|
|
|
51
57
|
echo "scripts_minify=false" >> $GITHUB_OUTPUT
|
|
52
58
|
fi
|
|
53
59
|
|
|
60
|
+
- name: Decide which build steps to run
|
|
61
|
+
id: run
|
|
62
|
+
run: |
|
|
63
|
+
SCRIPTS="${{ steps.changes.outputs.scripts }}"
|
|
64
|
+
TW="${{ steps.changes.outputs.tailwind }}"
|
|
65
|
+
HAS_SCR="${{ steps.detect.outputs.has_scripts }}"
|
|
66
|
+
HAS_STY="${{ steps.detect.outputs.has_styles }}"
|
|
67
|
+
run_scripts=false
|
|
68
|
+
run_tailwind=false
|
|
69
|
+
if [ "$SCRIPTS" = "true" ] && [ "$HAS_SCR" = "true" ]; then run_scripts=true; fi
|
|
70
|
+
if [ "$TW" = "true" ] && [ "$HAS_STY" = "true" ]; then run_tailwind=true; fi
|
|
71
|
+
if [ "$run_scripts" = "true" ] || [ "$run_tailwind" = "true" ]; then
|
|
72
|
+
echo "build_ran=true" >> $GITHUB_OUTPUT
|
|
73
|
+
else
|
|
74
|
+
echo "build_ran=false" >> $GITHUB_OUTPUT
|
|
75
|
+
fi
|
|
76
|
+
echo "run_scripts=$run_scripts" >> $GITHUB_OUTPUT
|
|
77
|
+
echo "run_tailwind=$run_tailwind" >> $GITHUB_OUTPUT
|
|
78
|
+
|
|
79
|
+
- name: Set up Node.js
|
|
80
|
+
if: steps.run.outputs.build_ran == 'true'
|
|
81
|
+
uses: actions/setup-node@v4
|
|
82
|
+
with:
|
|
83
|
+
node-version: "24"
|
|
84
|
+
|
|
85
|
+
- name: Install dependencies from lockfile
|
|
86
|
+
if: steps.run.outputs.build_ran == 'true'
|
|
87
|
+
run: |
|
|
88
|
+
if [ ! -f "package-lock.json" ]; then
|
|
89
|
+
echo "package-lock.json is required for deterministic build workflow installs."
|
|
90
|
+
exit 1
|
|
91
|
+
fi
|
|
92
|
+
npm ci
|
|
93
|
+
|
|
54
94
|
- name: Build scripts
|
|
55
|
-
if: steps.
|
|
95
|
+
if: steps.run.outputs.build_ran == 'true' && steps.run.outputs.run_scripts == 'true'
|
|
56
96
|
run: |
|
|
57
97
|
if [ "${{ steps.detect.outputs.scripts_minify }}" = "true" ]; then
|
|
58
98
|
npx --no-install climaybe build-scripts --minify
|
|
@@ -61,16 +101,12 @@ jobs:
|
|
|
61
101
|
fi
|
|
62
102
|
|
|
63
103
|
- name: Run Tailwind build
|
|
64
|
-
|
|
104
|
+
if: steps.run.outputs.build_ran == 'true' && steps.run.outputs.run_tailwind == 'true'
|
|
65
105
|
run: |
|
|
66
|
-
|
|
67
|
-
NODE_ENV=production npx --no-install climaybe build
|
|
68
|
-
else
|
|
69
|
-
echo "No _styles/main.css found; skipping Tailwind build."
|
|
70
|
-
fi
|
|
71
|
-
echo "success=true" >> $GITHUB_OUTPUT
|
|
106
|
+
NODE_ENV=production npx --no-install climaybe build
|
|
72
107
|
|
|
73
108
|
- name: Commit and push changes
|
|
109
|
+
if: steps.run.outputs.build_ran == 'true'
|
|
74
110
|
run: |
|
|
75
111
|
git config --local user.email "action@github.com"
|
|
76
112
|
git config --local user.name "GitHub Action"
|
|
@@ -89,3 +125,8 @@ jobs:
|
|
|
89
125
|
git commit -m "chore(assets): update compiled javascript and css"
|
|
90
126
|
git push origin "$BRANCH_NAME"
|
|
91
127
|
fi
|
|
128
|
+
|
|
129
|
+
- name: Finalize success
|
|
130
|
+
id: build
|
|
131
|
+
if: success()
|
|
132
|
+
run: echo "success=true" >> $GITHUB_OUTPUT
|
|
@@ -141,7 +141,10 @@ jobs:
|
|
|
141
141
|
exit 0
|
|
142
142
|
fi
|
|
143
143
|
|
|
144
|
-
|
|
144
|
+
# Keep generated assets branch-local to avoid chronic merge conflicts on staging branches.
|
|
145
|
+
# This does NOT prevent main changes from landing; it only resolves conflicts in favor
|
|
146
|
+
# of the current staging-<alias> branch when the same file was modified on both sides.
|
|
147
|
+
if ! git merge -X ours origin/main --no-ff -m "Sync main → $BRANCH"; then
|
|
145
148
|
echo "Merge had conflicts; aborting. Manual resolution may be needed."
|
|
146
149
|
git merge --abort 2>/dev/null || true
|
|
147
150
|
exit 1
|
|
@@ -10,10 +10,38 @@ on:
|
|
|
10
10
|
branches: [main, staging, develop, 'staging-*', 'live-*']
|
|
11
11
|
|
|
12
12
|
jobs:
|
|
13
|
+
filter:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
outputs:
|
|
16
|
+
theme: ${{ steps.paths.outputs.theme }}
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- uses: dorny/paths-filter@v3
|
|
20
|
+
id: paths
|
|
21
|
+
with:
|
|
22
|
+
filters: |
|
|
23
|
+
theme:
|
|
24
|
+
- 'assets/**'
|
|
25
|
+
- 'blocks/**'
|
|
26
|
+
- 'config/**'
|
|
27
|
+
- 'layout/**'
|
|
28
|
+
- 'locales/**'
|
|
29
|
+
- 'sections/**'
|
|
30
|
+
- 'snippets/**'
|
|
31
|
+
- 'templates/**'
|
|
32
|
+
- '_scripts/**'
|
|
33
|
+
- '_styles/**'
|
|
34
|
+
- 'shopify.theme.toml'
|
|
35
|
+
- 'stores/**'
|
|
36
|
+
|
|
13
37
|
extract-pr-number:
|
|
38
|
+
needs: [filter]
|
|
39
|
+
if: needs.filter.outputs.theme == 'true'
|
|
14
40
|
uses: ./.github/workflows/reusable-extract-pr-number.yml
|
|
15
41
|
|
|
16
42
|
validate-environment:
|
|
43
|
+
needs: [filter]
|
|
44
|
+
if: needs.filter.outputs.theme == 'true'
|
|
17
45
|
runs-on: ubuntu-latest
|
|
18
46
|
outputs:
|
|
19
47
|
store_alias: ${{ steps.resolve.outputs.alias }}
|