climaybe 2.4.0 → 2.4.2

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 CHANGED
@@ -205,17 +205,17 @@ Direct pushes to `staging-<store>` or `live-<store>` are automatically synced ba
205
205
  |----------|---------|-------------|
206
206
  | `release-pr-check.yml` | PR from `staging` to `main` | Finds latest tag on main, AI changelog to PR head, creates pre-release patch tag (e.g. v3.1.13) to lock state; posts changelog comment |
207
207
  | `post-merge-tag.yml` | Push to `main` (merged PR) | Staging→main only: minor bump from latest tag (e.g. v3.1.13 → v3.2.0). No version in PR title |
208
- | `nightly-hotfix.yml` | Cron 02:00 US Eastern | Collects commits since latest tag (incl. hotfix backports), AI changelog, patch bump and tag |
208
+ | `nightly-hotfix.yml` | Cron 02:00 US Eastern | Collects commits since latest tag (incl. hotfix backports), ignores no-op/empty-tree commits, generates AI changelog, patch bump and tag |
209
209
 
210
210
  ### Multi-store (additional)
211
211
 
212
212
  | Workflow | Trigger | What it does |
213
213
  |----------|---------|-------------|
214
- | `main-to-staging-stores.yml` (main-to-staging-&lt;store&gt;) | Push to `main` | Merges main into each `staging-<alias>`; root JSONs ignored. For hotfix-backport: if source is `staging-<alias>`, that same staging branch is skipped; if source is `live-<alias>`, `staging-<alias>` is also synced. Skips only on pure store-sync. |
214
+ | `main-to-staging-stores.yml` (main-to-staging-&lt;store&gt;) | Push to `main` | Merges main into each `staging-<alias>`; root JSONs ignored. Skips no-op sync when branch tree already matches main. For hotfix-backport: if source is `staging-<alias>`, that same staging branch is skipped; if source is `live-<alias>`, `staging-<alias>` is also synced. Skips only on pure store-sync. |
215
215
  | `stores-to-root.yml` | Push to `staging-*` | From main merge: stores→root. From elsewhere (e.g. Shopify): root→stores |
216
216
  | `pr-to-live.yml` | After stores-to-root | Opens PR from `staging-<alias>` to `live-<alias>` |
217
217
  | `root-to-stores.yml` | Push to `live-*` | From main merge: stores→root. From elsewhere: root→stores (same as stores-to-root on staging-*) |
218
- | `multistore-hotfix-to-main.yml` | Push to `staging-*` or `live-*` (and after root-to-stores) | Merges store branch into main (no PR). Skips when push is a merge from main (avoids loop) |
218
+ | `multistore-hotfix-to-main.yml` | Push to `staging-*` or `live-*` (and after root-to-stores) | Merges store branch into main (no PR). Skips when push is a merge from main (avoids loop) and skips no-op backports when source and main trees are identical |
219
219
 
220
220
  ### Optional preview + cleanup package
221
221
 
@@ -268,7 +268,7 @@ You can install/update this later with:
268
268
  - **Version format**: Always three-part (e.g. `v3.2.0`). No version in code or PR title; the system infers from tags.
269
269
  - **No tags yet?** The system uses `theme_version` from `config/settings_schema.json` (`theme_info`), creates that tag on main (e.g. `v1.0.0`), and continues from there.
270
270
  - **Staging → main**: On PR, a pre-release patch tag (e.g. v3.1.13) locks the current minor line; on merge, **minor** bump (e.g. v3.1.13 → v3.2.0).
271
- - **Non-staging to main** (hotfix backports, direct commits): **Patch** bump only, via **nightly workflow** at 02:00 US Eastern (not at commit time).
271
+ - **Non-staging to main** (hotfix backports, direct commits): **Patch** bump only, via **nightly workflow** at 02:00 US Eastern (not at commit time). No-op/empty-tree commits are ignored.
272
272
  - **Version bump runs only on main** (post-merge-tag and nightly-hotfix). Main-to-staging-stores merges main into each `staging-<alias>` on every push (version bumps and hotfixes). For hotfix-backport, only a `staging-<alias> -> main` source skips syncing back to the same staging branch; a `live-<alias> -> main` source still syncs into `staging-<alias>`.
273
273
  - Version bumps update `config/settings_schema.json` and, when present, `package.json` `version`.
274
274
  - **Safety**: The version-bump workflow fails if the new tag would not be **strictly higher** than the latest merged release tag (semver), so the release line cannot step backward.
package/bin/version.txt CHANGED
@@ -1 +1 @@
1
- 2.4.0
1
+ 2.4.2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "climaybe",
3
- "version": "2.4.0",
3
+ "version": "2.4.2",
4
4
  "description": "Shopify CLI by Electric Maybe for theme CI/CD workflows, branch orchestration, app setup, and dev tooling",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,11 +1,15 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const ROOT_DIR = process.cwd();
3
+ const CLIMAYBE_DIR = process.cwd();
4
+ const SCRIPTS_DIR = path.join(CLIMAYBE_DIR, '_scripts');
4
5
 
5
6
  function extractImports(content) {
6
7
  const imports = [];
7
- const fromImportRegex = /(^|\n)\s*import\s+[\s\S]*?\s+from\s+['"]([^'"]+)['"]\s*;?/g;
8
- const sideEffectImportRegex = /(^|\n)\s*import\s+['"]([^'"]+)['"]\s*;?/g;
8
+ // Supports compact imports (import{a}from"./x"), multiline forms,
9
+ // and import attributes (with { type: "json" }).
10
+ const fromImportRegex =
11
+ /(^|\n)\s*import(?:\s+type)?\s*[\s\S]*?\s*\bfrom\b\s*['"]([^'"]+)['"](?:\s+with\s*\{[\s\S]*?\})?\s*;?/g;
12
+ const sideEffectImportRegex = /(^|\n)\s*import\s*['"]([^'"]+)['"](?:\s+with\s*\{[\s\S]*?\})?\s*;?/g;
9
13
  let match;
10
14
 
11
15
  while ((match = fromImportRegex.exec(content)) !== null) {
@@ -18,6 +22,27 @@ function extractImports(content) {
18
22
  return imports;
19
23
  }
20
24
 
25
+ function stripModuleSyntax(content) {
26
+ // Remove import statements (including multiline/compact forms and import attributes).
27
+ let cleaned = content.replace(
28
+ /(^|\n)\s*import(?:\s+type)?\s*[\s\S]*?\s*\bfrom\b\s*['"][^'"]+['"](?:\s+with\s*\{[\s\S]*?\})?\s*;?/g,
29
+ '$1'
30
+ );
31
+ cleaned = cleaned.replace(/(^|\n)\s*import\s*['"][^'"]+['"](?:\s+with\s*\{[\s\S]*?\})?\s*;?/g, '$1');
32
+
33
+ // Fallback: ensure no standalone import declarations leak into bundle output.
34
+ cleaned = cleaned.replace(/^[ \t]*import\s*['"][^'"]+['"][ \t]*;?[ \t]*$/gm, '');
35
+ cleaned = cleaned.replace(
36
+ /^[ \t]*import(?:\s+type)?[ \t]*[^;\n\r]*\bfrom\b[ \t]*['"][^'"]+['"][ \t]*(?:with[ \t]*\{[^}\n\r]*\})?[ \t]*;?[ \t]*$/gm,
37
+ ''
38
+ );
39
+
40
+ cleaned = cleaned.replace(/^\s*export\s+default\s+/gm, '');
41
+ cleaned = cleaned.replace(/^\s*export\s+\{[^}]*\}\s*;?\s*$/gm, '');
42
+ cleaned = cleaned.replace(/^\s*export\s+(?=(const|let|var|function|class)\b)/gm, '');
43
+ return cleaned;
44
+ }
45
+
21
46
  function processScriptFile(filePath, processedFiles = new Set()) {
22
47
  if (processedFiles.has(filePath)) {
23
48
  return '';
@@ -25,7 +50,7 @@ function processScriptFile(filePath, processedFiles = new Set()) {
25
50
 
26
51
  processedFiles.add(filePath);
27
52
 
28
- const fullPath = path.join(ROOT_DIR, '_scripts', filePath);
53
+ const fullPath = path.join(SCRIPTS_DIR, filePath);
29
54
 
30
55
  if (!fs.existsSync(fullPath)) {
31
56
  console.warn(`Warning: File ${filePath} not found`);
@@ -40,12 +65,7 @@ function processScriptFile(filePath, processedFiles = new Set()) {
40
65
  importedContent += processScriptFile(importPath, processedFiles);
41
66
  }
42
67
 
43
- // Remove import statements (including multiline "import { ... } from '...'" forms).
44
- content = content.replace(/(^|\n)\s*import\s+[\s\S]*?\s+from\s+['"][^'"]+['"]\s*;?/g, '$1');
45
- content = content.replace(/(^|\n)\s*import\s+['"][^'"]+['"]\s*;?/g, '$1');
46
- content = content.replace(/^\s*export\s+default\s+/gm, '');
47
- content = content.replace(/^\s*export\s+\{[^}]*\}\s*;?\s*$/gm, '');
48
- content = content.replace(/^\s*export\s+(?=(const|let|var|function|class)\b)/gm, '');
68
+ content = stripModuleSyntax(content);
49
69
 
50
70
  if (process.env.NODE_ENV === 'production') {
51
71
  content = content.replace(/\/\*\*[\s\S]*?\*\//g, '');
@@ -61,12 +81,13 @@ function buildScripts() {
61
81
  try {
62
82
  if (global.gc) global.gc();
63
83
 
64
- const mainPath = path.join(ROOT_DIR, '_scripts', 'main.js');
84
+ const mainPath = path.join(SCRIPTS_DIR, 'main.js');
65
85
  fs.readFileSync(mainPath, 'utf8');
66
86
 
67
87
  const processedFiles = new Set();
68
- const finalContent = processScriptFile('main.js', processedFiles);
69
- const outputPath = path.join(ROOT_DIR, 'assets', 'index.js');
88
+ let finalContent = processScriptFile('main.js', processedFiles);
89
+ finalContent = stripModuleSyntax(finalContent);
90
+ const outputPath = path.join(CLIMAYBE_DIR, 'assets', 'index.js');
70
91
  fs.writeFileSync(outputPath, finalContent.trim() + '\n');
71
92
 
72
93
  const fileCount = processedFiles.size;
@@ -83,4 +104,4 @@ if (require.main === module) {
83
104
  buildScripts();
84
105
  }
85
106
 
86
- module.exports = { buildScripts };
107
+ module.exports = { buildScripts };
@@ -133,8 +133,14 @@ jobs:
133
133
 
134
134
  git fetch origin "$BRANCH"
135
135
  git checkout "$BRANCH"
136
- git merge origin/main --no-ff -m "Sync main $BRANCH" || true
137
- if [ $? -ne 0 ]; then
136
+ BRANCH_TREE=$(git rev-parse HEAD^{tree} 2>/dev/null || echo "")
137
+ MAIN_TREE=$(git rev-parse origin/main^{tree} 2>/dev/null || echo "")
138
+ if [ -n "$BRANCH_TREE" ] && [ "$BRANCH_TREE" = "$MAIN_TREE" ]; then
139
+ echo "$BRANCH is already in sync with main (no-op), skipping."
140
+ exit 0
141
+ fi
142
+
143
+ if ! git merge origin/main --no-ff -m "Sync main → $BRANCH"; then
138
144
  echo "Merge had conflicts; aborting. Manual resolution may be needed."
139
145
  git merge --abort 2>/dev/null || true
140
146
  exit 1
@@ -103,6 +103,15 @@ jobs:
103
103
  fi
104
104
 
105
105
  if [ -n "$COMMITS" ]; then
106
+ # Skip no-op history merges when source and main already have identical content.
107
+ MAIN_TREE=$(git rev-parse origin/main^{tree} 2>/dev/null || echo "")
108
+ SOURCE_TREE=$(git rev-parse origin/$SOURCE^{tree} 2>/dev/null || echo "")
109
+ if [ -n "$MAIN_TREE" ] && [ "$MAIN_TREE" = "$SOURCE_TREE" ]; then
110
+ echo "needs_backport=false" >> $GITHUB_OUTPUT
111
+ echo "No-op backport: origin/main and origin/$SOURCE trees are identical."
112
+ exit 0
113
+ fi
114
+
106
115
  echo "needs_backport=true" >> $GITHUB_OUTPUT
107
116
  echo "is_default_store=$([ -n "$DEFAULT_ALIAS" ] && [ "$SOURCE_ALIAS" = "$DEFAULT_ALIAS" ] && echo true || echo false)" >> $GITHUB_OUTPUT
108
117
  echo "Commits to sync to main:"
@@ -90,6 +90,12 @@ jobs:
90
90
  while IFS=$'\t' read -r SHA SUBJECT; do
91
91
  [ -z "$SHA" ] && continue
92
92
 
93
+ # Ignore metadata-only commits (e.g. no-op merges) so they do not trigger bumps.
94
+ if git diff-tree --quiet --no-commit-id -r "$SHA"; then
95
+ echo "Skipping no-op commit (empty tree diff): $SHA"
96
+ continue
97
+ fi
98
+
93
99
  HEAD_REF=$(gh api \
94
100
  repos/${{ github.repository }}/commits/$SHA/pulls \
95
101
  --jq '.[0].head.ref // ""' 2>/dev/null || true)