climaybe 3.5.0 → 3.6.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 CHANGED
@@ -169,6 +169,7 @@ The CLI writes config into `climaybe.config.json`:
169
169
  "default_store": "voldt-staging.myshopify.com",
170
170
  "preview_workflows": true,
171
171
  "build_workflows": true,
172
+ "profile_workflows": true,
172
173
  "lighthouse_workflows": true,
173
174
  "commitlint": true,
174
175
  "cursor_skills": true,
@@ -263,6 +264,19 @@ Optional package, enabled via the `climaybe init` prompt (`Enable preview + clea
263
264
  | `reusable-cleanup-themes.yml` | workflow_call | `cleanup_mode: by_pr` deletes names ending with `-PR{padded}`; `orphan_pr` deletes `-PR<n>` when PR `n` is not open. Optional `result_artifact_prefix` for matrix fan-in on `pr-close` |
264
265
  | `reusable-extract-pr-number.yml` | workflow_call | Extracts padded/unpadded PR number outputs for naming and API-safe usage |
265
266
 
267
+ ### Optional Liquid performance profiling package
268
+
269
+ Enabled via `climaybe init` prompt (`Enable Liquid performance profiling workflows?`; default: yes).
270
+
271
+ Runs on every push to `main` (skipping store-sync and hotfix-backport commits). Creates a shared preview theme, measures TTFB for four core templates (index, collection, product, cart) over 4 iterations each, discards the first (warm-up) run, and reports the average of the last 3 in the GitHub Actions job summary.
272
+
273
+ Requires `SHOPIFY_STORE_URL` and `SHOPIFY_THEME_ACCESS_TOKEN` secrets. When secrets are missing, the workflow skips gracefully.
274
+
275
+ | Workflow | Trigger | What it does |
276
+ |----------|---------|-------------|
277
+ | `liquid-performance.yml` | Push to `main` | Gate check for secrets, then calls reusable profiler |
278
+ | `reusable-liquid-profile.yml` | workflow_call | Shares theme, measures TTFB per template × 4 iterations, reports avg of last 3, cleans up theme |
279
+
266
280
  ### Build and Lighthouse workflows
267
281
 
268
282
  Optional package. `climaybe init` asks two separate questions: **build workflows** (bundle `_scripts` JS + compile Tailwind in CI; default: yes) and, if build is on, **Lighthouse CI** (performance + a11y budget on the `staging` branch; default: yes, stored as `lighthouse_workflows`). Lighthouse runs inside the build pipeline, so it requires build workflows.
package/bin/version.txt CHANGED
@@ -1 +1 @@
1
- 3.5.0
1
+ 3.6.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "climaybe",
3
- "version": "3.5.0",
3
+ "version": "3.6.0",
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": {
@@ -5,6 +5,7 @@ import {
5
5
  promptStoreLoop,
6
6
  promptPreviewWorkflows,
7
7
  promptBuildWorkflows,
8
+ promptProfileWorkflows,
8
9
  promptLighthouseWorkflows,
9
10
  promptDevKit,
10
11
  promptVSCodeDevTasks,
@@ -99,6 +100,9 @@ async function runInitFlow() {
99
100
  enableLighthouseWorkflows = await promptLighthouseWorkflows();
100
101
  }
101
102
 
103
+ hint('CI that measures Liquid TTFB for core templates on every push to main.');
104
+ const enableProfileWorkflows = await promptProfileWorkflows();
105
+
102
106
  hint('Local config + ignore defaults for theme work (Theme Check, Prettier, Lighthouse, gitignore).', 'theme-dev-kit');
103
107
  const enableDevKit = await promptDevKit();
104
108
  let enableVSCodeTasks = false;
@@ -157,6 +161,7 @@ async function runInitFlow() {
157
161
  default_store: stores[0].domain,
158
162
  preview_workflows: enablePreviewWorkflows,
159
163
  build_workflows: enableBuildWorkflows,
164
+ profile_workflows: enableProfileWorkflows,
160
165
  lighthouse_workflows: enableBuildWorkflows ? enableLighthouseWorkflows : undefined,
161
166
  build_entrypoints_ready: enableBuildWorkflows ? missingBuildFiles?.length === 0 : undefined,
162
167
  dev_kit: enableDevKit,
@@ -213,6 +218,7 @@ async function runInitFlow() {
213
218
  scaffoldWorkflows(mode, {
214
219
  includePreview: enablePreviewWorkflows,
215
220
  includeBuild: enableBuildWorkflows,
221
+ includeProfile: enableProfileWorkflows,
216
222
  });
217
223
 
218
224
  // 7. Optional commitlint + Husky and the AI ruleset (rules, skills, agents + editor bridges)
@@ -259,6 +265,7 @@ async function runInitFlow() {
259
265
  }
260
266
  console.log(pc.dim(` Preview workflows: ${enablePreviewWorkflows ? 'enabled' : 'disabled'}`));
261
267
  console.log(pc.dim(` Build workflows: ${enableBuildWorkflows ? 'enabled' : 'disabled'}`));
268
+ console.log(pc.dim(` Profile workflows: ${enableProfileWorkflows ? 'enabled' : 'disabled'}`));
262
269
  if (enableBuildWorkflows) {
263
270
  console.log(pc.dim(` Lighthouse CI: ${enableLighthouseWorkflows ? 'enabled' : 'disabled'}`));
264
271
  }
@@ -6,6 +6,7 @@ import {
6
6
  isCursorSkillsEnabled,
7
7
  getAiEditors,
8
8
  isPreviewWorkflowsEnabled,
9
+ isProfileWorkflowsEnabled,
9
10
  readConfig,
10
11
  } from '../lib/config.js';
11
12
  import { scaffoldWorkflows } from '../lib/workflows.js';
@@ -28,6 +29,7 @@ export async function updateCommand() {
28
29
  const mode = getMode();
29
30
  const includePreview = isPreviewWorkflowsEnabled();
30
31
  const includeBuild = isBuildWorkflowsEnabled();
32
+ const includeProfile = isProfileWorkflowsEnabled();
31
33
 
32
34
  // Keep theme project files in sync (root files, package.json, .gitignore, VS Code tasks).
33
35
  scaffoldThemeDevKit({
@@ -42,7 +44,7 @@ export async function updateCommand() {
42
44
  scaffoldAiConfig(process.cwd(), { editors: getAiEditors() });
43
45
  }
44
46
 
45
- scaffoldWorkflows(mode, { includePreview, includeBuild });
47
+ scaffoldWorkflows(mode, { includePreview, includeBuild, includeProfile });
46
48
 
47
49
  console.log(pc.bold(pc.green('\n Project files updated!\n')));
48
50
  }
package/src/lib/config.js CHANGED
@@ -260,6 +260,14 @@ export function isCommitlintEnabled(cwd = process.cwd()) {
260
260
  return config?.commitlint === true;
261
261
  }
262
262
 
263
+ /**
264
+ * Whether optional Liquid performance profile workflows are enabled.
265
+ */
266
+ export function isProfileWorkflowsEnabled(cwd = process.cwd()) {
267
+ const config = readConfig(cwd);
268
+ return config?.profile_workflows === true;
269
+ }
270
+
263
271
  /**
264
272
  * Whether the bundled AI ruleset (rules, skills, subagents) was installed (init or add-cursor).
265
273
  */
@@ -149,6 +149,20 @@ export async function promptBuildWorkflows() {
149
149
  return !!enableBuildWorkflows;
150
150
  }
151
151
 
152
+ /**
153
+ * Ask whether Liquid performance profile workflows should be scaffolded.
154
+ */
155
+ export async function promptProfileWorkflows() {
156
+ const { enableProfileWorkflows } = await prompts({
157
+ type: 'confirm',
158
+ name: 'enableProfileWorkflows',
159
+ message: 'Enable Liquid performance profiling workflows? (TTFB measurement on main)',
160
+ initial: true,
161
+ });
162
+
163
+ return !!enableProfileWorkflows;
164
+ }
165
+
152
166
  /**
153
167
  * Ask whether Lighthouse CI should run as part of the build pipeline.
154
168
  * Only meaningful when build workflows are enabled (Lighthouse runs after the build).
@@ -37,7 +37,7 @@ function copyWorkflow(srcDir, fileName, destDir) {
37
37
  * but for simplicity we track known filenames instead.
38
38
  */
39
39
  function getKnownWorkflowFiles() {
40
- const dirs = ['shared', 'single', 'multi', 'preview', 'build'];
40
+ const dirs = ['shared', 'single', 'multi', 'preview', 'build', 'profile'];
41
41
  const files = new Set();
42
42
  for (const dir of dirs) {
43
43
  const dirPath = join(TEMPLATES_DIR, dir);
@@ -72,7 +72,7 @@ function cleanWorkflows(cwd = process.cwd()) {
72
72
  * - Copies single/ or multi/ (+ single/) based on mode.
73
73
  */
74
74
  export function scaffoldWorkflows(mode = 'single', options = {}, cwd = process.cwd()) {
75
- const { includePreview = false, includeBuild = false } = options;
75
+ const { includePreview = false, includeBuild = false, includeProfile = false } = options;
76
76
  const dest = ghWorkflowsDir(cwd);
77
77
  mkdirSync(dest, { recursive: true });
78
78
 
@@ -113,9 +113,17 @@ export function scaffoldWorkflows(mode = 'single', options = {}, cwd = process.c
113
113
  ensureBuildWorkflowDefaults(cwd);
114
114
  }
115
115
 
116
+ if (includeProfile) {
117
+ const profileDir = join(TEMPLATES_DIR, 'profile');
118
+ for (const f of listYmls(profileDir)) {
119
+ copyWorkflow(profileDir, f, dest);
120
+ }
121
+ }
122
+
116
123
  const total = readdirSync(dest).filter((f) => f.endsWith('.yml')).length;
117
124
  console.log(pc.green(` Scaffolded ${total} workflow(s) → .github/workflows/`));
118
125
  console.log(pc.dim(` Mode: ${mode}-store`));
119
126
  console.log(pc.dim(` Preview workflows: ${includePreview ? 'enabled' : 'disabled'}`));
120
127
  console.log(pc.dim(` Build workflows: ${includeBuild ? 'enabled' : 'disabled'}`));
128
+ console.log(pc.dim(` Profile workflows: ${includeProfile ? 'enabled' : 'disabled'}`));
121
129
  }
@@ -0,0 +1,47 @@
1
+ # climaybe — Liquid Performance (Optional Profile Package)
2
+ # Profiles Liquid rendering performance (TTFB) for core templates on main.
3
+ # Pushes a shared theme, measures TTFB for index/collection/product/cart,
4
+ # runs 4 iterations per template, reports the average of the last 3 runs.
5
+
6
+ name: Liquid Performance
7
+
8
+ on:
9
+ push:
10
+ branches: [main]
11
+
12
+ jobs:
13
+ profile-gate:
14
+ runs-on: ubuntu-latest
15
+ if: >
16
+ !contains(github.event.head_commit.message, '[hotfix-backport]')
17
+ && !contains(github.event.head_commit.message, '[stores-to-root]')
18
+ && !contains(github.event.head_commit.message, '[root-to-stores]')
19
+ outputs:
20
+ run_profile: ${{ steps.check.outputs.run_profile }}
21
+ env:
22
+ SHOPIFY_STORE_URL: ${{ secrets.SHOPIFY_STORE_URL }}
23
+ SHOPIFY_THEME_ACCESS_TOKEN: ${{ secrets.SHOPIFY_THEME_ACCESS_TOKEN }}
24
+ steps:
25
+ - name: Check if profiling should run
26
+ id: check
27
+ run: |
28
+ SKIP_REASONS=()
29
+ if [ -z "$SHOPIFY_STORE_URL" ]; then
30
+ SKIP_REASONS+=("Missing secret: SHOPIFY_STORE_URL")
31
+ fi
32
+ if [ -z "$SHOPIFY_THEME_ACCESS_TOKEN" ]; then
33
+ SKIP_REASONS+=("Missing secret: SHOPIFY_THEME_ACCESS_TOKEN")
34
+ fi
35
+ if [ ${#SKIP_REASONS[@]} -gt 0 ]; then
36
+ echo "run_profile=false" >> $GITHUB_OUTPUT
37
+ echo "Liquid profiling skipped:"
38
+ for reason in "${SKIP_REASONS[@]}"; do echo " - $reason"; done
39
+ else
40
+ echo "run_profile=true" >> $GITHUB_OUTPUT
41
+ fi
42
+
43
+ liquid-profile:
44
+ needs: [profile-gate]
45
+ if: needs.profile-gate.outputs.run_profile == 'true'
46
+ uses: ./.github/workflows/reusable-liquid-profile.yml
47
+ secrets: inherit
@@ -0,0 +1,199 @@
1
+ name: Reusable Liquid Profile
2
+
3
+ on:
4
+ workflow_call:
5
+ inputs:
6
+ iterations:
7
+ required: false
8
+ type: number
9
+ default: 4
10
+ description: "Total profiling iterations per template (first is discarded as warm-up)"
11
+ store_alias_secret:
12
+ required: false
13
+ type: string
14
+ description: "Upper snake-case alias for scoped secret (e.g. VOLDT_STAGING). If set, uses SHOPIFY_*_<this>; else uses SHOPIFY_STORE_URL / SHOPIFY_THEME_ACCESS_TOKEN."
15
+
16
+ jobs:
17
+ profile:
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - name: Checkout code
21
+ uses: actions/checkout@v4
22
+
23
+ - name: Validate theme root
24
+ run: |
25
+ if [ ! -f "layout/theme.liquid" ]; then
26
+ echo "layout/theme.liquid not found. Ensure workflow runs at theme repository root."
27
+ exit 1
28
+ fi
29
+
30
+ - name: Install Shopify CLI
31
+ run: npm install -g @shopify/cli @shopify/theme
32
+
33
+ - name: Share preview theme
34
+ id: share
35
+ env:
36
+ SHOPIFY_STORE_URL: ${{ inputs.store_alias_secret && secrets[format('SHOPIFY_STORE_URL_{0}', inputs.store_alias_secret)] || secrets.SHOPIFY_STORE_URL }}
37
+ SHOPIFY_THEME_ACCESS_TOKEN: ${{ inputs.store_alias_secret && secrets[format('SHOPIFY_THEME_ACCESS_TOKEN_{0}', inputs.store_alias_secret)] || secrets.SHOPIFY_THEME_ACCESS_TOKEN }}
38
+ run: |
39
+ if [ -z "$SHOPIFY_STORE_URL" ] || [ -z "$SHOPIFY_THEME_ACCESS_TOKEN" ]; then
40
+ echo "Missing Shopify secrets."
41
+ exit 1
42
+ fi
43
+
44
+ OUTPUT=$(shopify theme share \
45
+ --store "$SHOPIFY_STORE_URL" \
46
+ --password "$SHOPIFY_THEME_ACCESS_TOKEN" 2>&1)
47
+ STATUS=$?
48
+
49
+ echo "$OUTPUT"
50
+ if [ $STATUS -ne 0 ]; then
51
+ echo "Theme share failed."
52
+ exit $STATUS
53
+ fi
54
+
55
+ THEME_ID=$(echo "$OUTPUT" | sed -n 's/.*#\([0-9]*\).*/\1/p' | head -1)
56
+ if [ -z "$THEME_ID" ]; then
57
+ echo "Could not parse theme id from share output."
58
+ exit 1
59
+ fi
60
+
61
+ echo "theme_id=$THEME_ID" >> $GITHUB_OUTPUT
62
+ echo "Shared theme ID: $THEME_ID"
63
+
64
+ - name: Resolve profiling URLs
65
+ id: urls
66
+ run: |
67
+ URLS_JSON='[
68
+ {"template": "index", "path": "/"},
69
+ {"template": "collection", "path": "/collections"},
70
+ {"template": "product", "path": "/products"},
71
+ {"template": "cart", "path": "/cart"}
72
+ ]'
73
+ echo "urls=$URLS_JSON" >> $GITHUB_OUTPUT
74
+
75
+ - name: Profile Liquid performance
76
+ id: profile
77
+ env:
78
+ SHOPIFY_STORE_URL: ${{ inputs.store_alias_secret && secrets[format('SHOPIFY_STORE_URL_{0}', inputs.store_alias_secret)] || secrets.SHOPIFY_STORE_URL }}
79
+ SHOPIFY_CLI_THEME_TOKEN: ${{ inputs.store_alias_secret && secrets[format('SHOPIFY_THEME_ACCESS_TOKEN_{0}', inputs.store_alias_secret)] || secrets.SHOPIFY_THEME_ACCESS_TOKEN }}
80
+ THEME_ID: ${{ steps.share.outputs.theme_id }}
81
+ ITERATIONS: ${{ inputs.iterations }}
82
+ run: |
83
+ STORE=$(echo "$SHOPIFY_STORE_URL" | sed 's|^https\?://||; s|/.*||')
84
+ REPORT_FILE="liquid-profile-report.md"
85
+ RESULTS_JSON="[]"
86
+
87
+ TEMPLATES=("index" "collection" "product" "cart")
88
+ PATHS=("/" "/collections" "/products" "/cart")
89
+
90
+ echo "## Liquid Performance Report" > "$REPORT_FILE"
91
+ echo "" >> "$REPORT_FILE"
92
+ echo "| Template | Avg TTFB (last 3) | Run 1 (warm-up) | Run 2 | Run 3 | Run 4 |" >> "$REPORT_FILE"
93
+ echo "|----------|-------------------|-----------------|-------|-------|-------|" >> "$REPORT_FILE"
94
+
95
+ HAS_ERROR=false
96
+
97
+ for i in "${!TEMPLATES[@]}"; do
98
+ TEMPLATE="${TEMPLATES[$i]}"
99
+ URL_PATH="${PATHS[$i]}"
100
+ echo ""
101
+ echo "=== Profiling $TEMPLATE ($URL_PATH) ==="
102
+
103
+ TIMES=()
104
+ ALL_TIMES_DISPLAY=()
105
+
106
+ for ((iter=1; iter<=ITERATIONS; iter++)); do
107
+ echo " Iteration $iter/$ITERATIONS for $TEMPLATE..."
108
+
109
+ START_MS=$(date +%s%N)
110
+ HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
111
+ --connect-timeout 30 \
112
+ --max-time 60 \
113
+ "https://${STORE}${URL_PATH}?preview_theme_id=${THEME_ID}" \
114
+ -H "Accept: text/html") || true
115
+ END_MS=$(date +%s%N)
116
+
117
+ TTFB_MS=$(( (END_MS - START_MS) / 1000000 ))
118
+ echo " HTTP $HTTP_CODE — TTFB: ${TTFB_MS}ms"
119
+
120
+ TIMES+=("$TTFB_MS")
121
+ ALL_TIMES_DISPLAY+=("${TTFB_MS}ms")
122
+
123
+ if [ "$HTTP_CODE" != "200" ] && [ "$HTTP_CODE" != "301" ] && [ "$HTTP_CODE" != "302" ]; then
124
+ echo " Warning: unexpected HTTP status $HTTP_CODE"
125
+ fi
126
+
127
+ sleep 2
128
+ done
129
+
130
+ WARMUP="${ALL_TIMES_DISPLAY[0]}"
131
+
132
+ if [ ${#TIMES[@]} -ge 4 ]; then
133
+ SUM=0
134
+ COUNT=0
135
+ for ((j=1; j<${#TIMES[@]}; j++)); do
136
+ SUM=$((SUM + TIMES[j]))
137
+ COUNT=$((COUNT + 1))
138
+ done
139
+ AVG=$((SUM / COUNT))
140
+ else
141
+ SUM=0
142
+ for t in "${TIMES[@]}"; do
143
+ SUM=$((SUM + t))
144
+ done
145
+ AVG=$((SUM / ${#TIMES[@]}))
146
+ fi
147
+
148
+ RUN2="${ALL_TIMES_DISPLAY[1]:-n/a}"
149
+ RUN3="${ALL_TIMES_DISPLAY[2]:-n/a}"
150
+ RUN4="${ALL_TIMES_DISPLAY[3]:-n/a}"
151
+
152
+ echo "| \`$TEMPLATE\` | **${AVG}ms** | $WARMUP | $RUN2 | $RUN3 | $RUN4 |" >> "$REPORT_FILE"
153
+
154
+ RESULTS_JSON=$(echo "$RESULTS_JSON" | jq --arg t "$TEMPLATE" --argjson avg "$AVG" \
155
+ --argjson times "$(printf '%s\n' "${TIMES[@]}" | jq -s '.')" \
156
+ '. + [{"template": $t, "avg_ms": $avg, "times_ms": $times}]')
157
+
158
+ echo " → $TEMPLATE average (last 3): ${AVG}ms"
159
+ done
160
+
161
+ echo "" >> "$REPORT_FILE"
162
+ echo "_${ITERATIONS} iterations per template; average excludes the first (warm-up) run._" >> "$REPORT_FILE"
163
+ echo "_Theme ID: ${THEME_ID} | Store: ${STORE}_" >> "$REPORT_FILE"
164
+
165
+ echo ""
166
+ echo "=== Report ==="
167
+ cat "$REPORT_FILE"
168
+
169
+ {
170
+ echo "report<<REPORT_EOF"
171
+ cat "$REPORT_FILE"
172
+ echo "REPORT_EOF"
173
+ } >> $GITHUB_OUTPUT
174
+
175
+ echo "results_json=$(echo "$RESULTS_JSON" | jq -c '.')" >> $GITHUB_OUTPUT
176
+
177
+ - name: Post results as job summary
178
+ env:
179
+ REPORT: ${{ steps.profile.outputs.report }}
180
+ run: |
181
+ echo "$REPORT" >> $GITHUB_STEP_SUMMARY
182
+
183
+ - name: Cleanup preview theme
184
+ if: always()
185
+ env:
186
+ SHOPIFY_STORE_URL: ${{ inputs.store_alias_secret && secrets[format('SHOPIFY_STORE_URL_{0}', inputs.store_alias_secret)] || secrets.SHOPIFY_STORE_URL }}
187
+ SHOPIFY_THEME_ACCESS_TOKEN: ${{ inputs.store_alias_secret && secrets[format('SHOPIFY_THEME_ACCESS_TOKEN_{0}', inputs.store_alias_secret)] || secrets.SHOPIFY_THEME_ACCESS_TOKEN }}
188
+ THEME_ID: ${{ steps.share.outputs.theme_id }}
189
+ run: |
190
+ if [ -z "$THEME_ID" ]; then
191
+ echo "No theme ID to clean up."
192
+ exit 0
193
+ fi
194
+
195
+ echo "Deleting preview theme $THEME_ID..."
196
+ shopify theme delete --theme "$THEME_ID" \
197
+ --store "$SHOPIFY_STORE_URL" \
198
+ --password "$SHOPIFY_THEME_ACCESS_TOKEN" \
199
+ --force 2>&1 || echo "Theme cleanup failed (non-fatal)."