climaybe 1.0.0 → 1.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Electric Maybe
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -26,10 +26,12 @@ Interactive setup that configures your repo for CI/CD.
26
26
  1. Prompts for your store URL (e.g., `voldt-staging.myshopify.com`)
27
27
  2. Extracts subdomain as alias, lets you override
28
28
  3. Asks if you want to add more stores
29
- 4. Based on store count, sets up **single-store** or **multi-store** mode
30
- 5. Writes `package.json` config
31
- 6. Scaffolds GitHub Actions workflows
32
- 7. Creates git branches and store directories (multi-store)
29
+ 4. Asks whether to enable optional **preview + cleanup** workflows
30
+ 5. Asks whether to enable optional **build + Lighthouse** workflows
31
+ 6. Based on store count, sets up **single-store** or **multi-store** mode
32
+ 7. Writes `package.json` config
33
+ 8. Scaffolds GitHub Actions workflows
34
+ 9. Creates git branches and store directories (multi-store)
33
35
 
34
36
  ### `climaybe add-store`
35
37
 
@@ -83,6 +85,8 @@ The CLI writes config into the `config` field of your `package.json`:
83
85
  "config": {
84
86
  "port": 9295,
85
87
  "default_store": "voldt-staging.myshopify.com",
88
+ "preview_workflows": true,
89
+ "build_workflows": true,
86
90
  "stores": {
87
91
  "voldt-staging": "voldt-staging.myshopify.com",
88
92
  "voldt-norway": "voldt-norway.myshopify.com"
@@ -115,6 +119,8 @@ staging → main → staging-<store> → live-<store>
115
119
  - `staging-<store>` — per-store staging with store-specific JSON data
116
120
  - `live-<store>` — per-store production
117
121
 
122
+ Direct pushes to `staging-<store>` or `live-<store>` are automatically synced back to `main` (no PR; multistore-hotfix-to-main merges the branch into main).
123
+
118
124
  ## Workflows
119
125
 
120
126
  ### Shared (both modes)
@@ -129,24 +135,50 @@ staging → main → staging-<store> → live-<store>
129
135
  | Workflow | Trigger | What it does |
130
136
  |----------|---------|-------------|
131
137
  | `release-pr-check.yml` | PR from `staging` to `main` | Generates changelog, creates pre-release patch tag, posts PR comment |
132
- | `post-merge-tag.yml` | Push to `main` (merged PR) | Detects release version from PR title, bumps minor version |
138
+ | `post-merge-tag.yml` | Push to `main` (merged PR) | Staging→main: minor bump (e.g. v3.2.0). Hotfix sync: patch bump (e.g. v3.2.1) |
133
139
  | `nightly-hotfix.yml` | Cron 02:00 US Eastern | Tags untagged hotfix commits with patch version |
134
140
 
135
141
  ### Multi-store (additional)
136
142
 
137
143
  | Workflow | Trigger | What it does |
138
144
  |----------|---------|-------------|
139
- | `main-to-staging-stores.yml` | Push to `main` | Opens PRs to each `staging-<alias>` branch |
145
+ | `main-to-staging-stores.yml` (main-to-staging-&lt;store&gt;) | Push to `main` | Opens PRs from main to each `staging-<alias>` branch |
140
146
  | `stores-to-root.yml` | Push to `staging-*` | Copies `stores/<alias>/` JSONs to repo root |
141
147
  | `pr-to-live.yml` | After stores-to-root | Opens PR from `staging-<alias>` to `live-<alias>` |
142
148
  | `root-to-stores.yml` | Push to `live-*` (hotfix) | Syncs root JSONs back to `stores/<alias>/` |
143
- | `hotfix-backport.yml` | After root-to-stores | Creates backport PR to `main` |
149
+ | `multistore-hotfix-to-main.yml` | Push to `staging-*` or `live-*` (and after root-to-stores) | Automatically merges store branch into main (no PR); everything is synced back to main |
150
+
151
+ ### Optional preview + cleanup package
152
+
153
+ Enabled via `climaybe init` prompt (`Enable preview + cleanup workflows?`).
154
+
155
+ | Workflow | Trigger | What it does |
156
+ |----------|---------|-------------|
157
+ | `pr-update.yml` | PR opened/synchronize/reopened | Shares draft theme, renames with `-PR<number>`, comments preview + customize URLs |
158
+ | `pr-close.yml` | PR closed | Deletes matching preview themes and comments deleted count + names |
159
+ | `reusable-share-theme.yml` | workflow_call | Shares Shopify draft theme and returns `theme_id` |
160
+ | `reusable-rename-theme.yml` | workflow_call | Renames shared theme to include `PR<number>` (fails job on rename failure) |
161
+ | `reusable-comment-on-pr.yml` | workflow_call | Posts preview comment including Customize URL |
162
+ | `reusable-cleanup-themes.yml` | workflow_call | Deletes preview themes by PR number and exposes cleanup outputs |
163
+ | `reusable-extract-pr-number.yml` | workflow_call | Extracts padded/unpadded PR number outputs for naming and API-safe usage |
164
+
165
+ ### Optional build + Lighthouse package
166
+
167
+ Enabled via `climaybe init` prompt (`Enable build + Lighthouse workflows?`).
168
+
169
+ | Workflow | Trigger | What it does |
170
+ |----------|---------|-------------|
171
+ | `build-pipeline.yml` | Push to `main/staging/develop` | Runs reusable build and Lighthouse checks (when required secrets exist) |
172
+ | `reusable-build.yml` | workflow_call | Runs Node build + Tailwind compile, then commits compiled assets when changed |
173
+ | `create-release.yml` | Push tag `v*` | Builds release archive and creates GitHub Release using `release-notes.md` |
144
174
 
145
175
  ## Versioning
146
176
 
147
- - **Release merge** (`staging` `main`): Minor bump (e.g., `v3.1.x` `v3.2.0`)
148
- - **Hotfix** (direct commit to `main` or `live-*`): Patch bump (e.g., `v3.2.0` → `v3.2.1`)
149
- - **PR title convention**: `Release v3.2` the workflow extracts the version from this
177
+ - **Version format**: Always three-part (e.g. `v3.2.0`). No two-part tags.
178
+ - **Release merge** (`staging` `main`): Minor bump (e.g. `v3.1.12` → `v3.2.0`)
179
+ - **Hotfix sync** (`staging-<store>` or `live-<store>` main via multistore-hotfix-to-main): Patch bump runs immediately (e.g. `v3.2.0` → `v3.2.1`)
180
+ - **Other hotfixes** (direct commit to `main`): Patch bump via nightly workflow or manual run
181
+ - **PR title convention**: `Release v3.2` or `Release v3.2.0` — the workflow normalizes to three-part
150
182
 
151
183
  All version bumps update `config/settings_schema.json` automatically.
152
184
 
@@ -163,7 +195,7 @@ All version bumps update `config/settings_schema.json` automatically.
163
195
 
164
196
  ## Recursive Trigger Prevention
165
197
 
166
- - Hotfix backport commits contain `[hotfix-backport]` in the message
198
+ - Hotfix sync merge commits (multistore-hotfix-to-main) contain `[hotfix-backport]` in the message
167
199
  - Store sync commits contain `[stores-to-root]` or `[root-to-stores]`
168
200
  - Version bump commits contain `chore(release): bump version`
169
201
  - All workflows check for these flags and skip accordingly
@@ -175,6 +207,25 @@ Add the following secret to your GitHub repository:
175
207
  | Secret | Required | Description |
176
208
  |--------|----------|-------------|
177
209
  | `GEMINI_API_KEY` | Yes | Google Gemini API key for changelog generation |
210
+ | `SHOPIFY_STORE_URL` | Optional* | Required only when optional preview workflows are enabled |
211
+ | `SHOPIFY_CLI_THEME_TOKEN` | Optional* | Required only when optional preview workflows are enabled |
212
+ | `SHOP_STORE` | Optional* | Required only when optional build workflows are enabled (Lighthouse) |
213
+ | `SHOP_ACCESS_TOKEN` | Optional* | Required only when optional build workflows are enabled (Lighthouse) |
214
+ | `LHCI_GITHUB_APP_TOKEN` | Optional* | Required only when optional build workflows are enabled (Lighthouse) |
215
+ | `SHOP_PASSWORD` | Optional | Used by Lighthouse action when your store requires password auth |
216
+
217
+ For multi-store deploy PR links, you can optionally define store-scoped secrets:
218
+
219
+ - `SHOPIFY_STORE_URL_<ALIAS>`
220
+ - `SHOPIFY_CLI_THEME_TOKEN_<ALIAS>`
221
+
222
+ `<ALIAS>` must be uppercase with hyphens converted to underscores.
223
+ Example: alias `voldt-norway` → `SHOPIFY_STORE_URL_VOLDT_NORWAY`.
224
+
225
+ Preview workflows also support the same scoped secret pattern and will use:
226
+
227
+ 1. `SHOPIFY_*_<ALIAS>`
228
+ 2. fallback to `SHOPIFY_*` (default)
178
229
 
179
230
  ## Directory Structure (Multi-store)
180
231
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "climaybe",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Shopify CI/CD CLI — scaffolds workflows, branch strategy, and store config for single-store and multi-store theme repos",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,6 +23,12 @@
23
23
  ],
24
24
  "author": "Electric Maybe",
25
25
  "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/electricmaybe/climaybe.git"
29
+ },
30
+ "bugs": "https://github.com/electricmaybe/climaybe/issues",
31
+ "homepage": "https://github.com/electricmaybe/climaybe#readme",
26
32
  "dependencies": {
27
33
  "commander": "^13.1.0",
28
34
  "picocolors": "^1.1.1",
@@ -1,6 +1,6 @@
1
1
  import pc from 'picocolors';
2
2
  import { promptNewStore } from '../lib/prompts.js';
3
- import { readConfig, addStoreToConfig, getStoreAliases, getMode } from '../lib/config.js';
3
+ import { readConfig, addStoreToConfig, getStoreAliases, getMode, isPreviewWorkflowsEnabled, isBuildWorkflowsEnabled } from '../lib/config.js';
4
4
  import { createStoreBranches } from '../lib/git.js';
5
5
  import { scaffoldWorkflows } from '../lib/workflows.js';
6
6
  import { createStoreDirectories } from '../lib/store-sync.js';
@@ -17,6 +17,8 @@ export async function addStoreCommand() {
17
17
 
18
18
  const existingAliases = getStoreAliases();
19
19
  const previousMode = getMode();
20
+ const includePreview = isPreviewWorkflowsEnabled();
21
+ const includeBuild = isBuildWorkflowsEnabled();
20
22
 
21
23
  // Prompt for new store
22
24
  const store = await promptNewStore(existingAliases);
@@ -42,11 +44,11 @@ export async function addStoreCommand() {
42
44
  createStoreDirectories(originalAlias);
43
45
 
44
46
  // Re-scaffold workflows for multi mode
45
- scaffoldWorkflows('multi');
47
+ scaffoldWorkflows('multi', { includePreview, includeBuild });
46
48
  console.log(pc.green(' Migration complete — workflows updated to multi-store mode.'));
47
49
  } else if (newMode === 'multi') {
48
50
  // Already multi, just make sure workflows are current
49
- scaffoldWorkflows('multi');
51
+ scaffoldWorkflows('multi', { includePreview, includeBuild });
50
52
  }
51
53
 
52
54
  console.log(pc.bold(pc.green('\n Store added successfully!\n')));
@@ -1,5 +1,5 @@
1
1
  import pc from 'picocolors';
2
- import { promptStoreLoop } from '../lib/prompts.js';
2
+ import { promptStoreLoop, promptPreviewWorkflows, promptBuildWorkflows } from '../lib/prompts.js';
3
3
  import { readConfig, writeConfig } from '../lib/config.js';
4
4
  import { ensureGitRepo, ensureInitialCommit, ensureStagingBranch, createStoreBranches } from '../lib/git.js';
5
5
  import { scaffoldWorkflows } from '../lib/workflows.js';
@@ -20,6 +20,8 @@ export async function initCommand() {
20
20
  // 1. Collect stores from user
21
21
  const stores = await promptStoreLoop();
22
22
  const mode = stores.length > 1 ? 'multi' : 'single';
23
+ const enablePreviewWorkflows = await promptPreviewWorkflows();
24
+ const enableBuildWorkflows = await promptBuildWorkflows();
23
25
 
24
26
  console.log(pc.dim(`\n Mode: ${mode}-store (${stores.length} store(s))`));
25
27
 
@@ -27,6 +29,8 @@ export async function initCommand() {
27
29
  const config = {
28
30
  port: 9295,
29
31
  default_store: stores[0].domain,
32
+ preview_workflows: enablePreviewWorkflows,
33
+ build_workflows: enableBuildWorkflows,
30
34
  stores: {},
31
35
  };
32
36
 
@@ -55,7 +59,10 @@ export async function initCommand() {
55
59
  }
56
60
 
57
61
  // 6. Scaffold workflows
58
- scaffoldWorkflows(mode);
62
+ scaffoldWorkflows(mode, {
63
+ includePreview: enablePreviewWorkflows,
64
+ includeBuild: enableBuildWorkflows,
65
+ });
59
66
 
60
67
  // Done
61
68
  console.log(pc.bold(pc.green('\n Setup complete!\n')));
@@ -70,6 +77,8 @@ export async function initCommand() {
70
77
  }
71
78
  console.log(pc.dim(' Workflow: staging → main → staging-<store> → live-<store>'));
72
79
  }
80
+ console.log(pc.dim(` Preview workflows: ${enablePreviewWorkflows ? 'enabled' : 'disabled'}`));
81
+ console.log(pc.dim(` Build workflows: ${enableBuildWorkflows ? 'enabled' : 'disabled'}`));
73
82
 
74
83
  console.log(pc.dim('\n Next steps:'));
75
84
  console.log(pc.dim(' 1. Add GEMINI_API_KEY to your GitHub repo secrets'));
@@ -1,5 +1,5 @@
1
1
  import pc from 'picocolors';
2
- import { getMode, readConfig } from '../lib/config.js';
2
+ import { getMode, isBuildWorkflowsEnabled, isPreviewWorkflowsEnabled, readConfig } from '../lib/config.js';
3
3
  import { scaffoldWorkflows } from '../lib/workflows.js';
4
4
 
5
5
  export async function updateWorkflowsCommand() {
@@ -12,7 +12,9 @@ export async function updateWorkflowsCommand() {
12
12
  }
13
13
 
14
14
  const mode = getMode();
15
- scaffoldWorkflows(mode);
15
+ const includePreview = isPreviewWorkflowsEnabled();
16
+ const includeBuild = isBuildWorkflowsEnabled();
17
+ scaffoldWorkflows(mode, { includePreview, includeBuild });
16
18
 
17
19
  console.log(pc.bold(pc.green('\n Workflows updated!\n')));
18
20
  }
package/src/lib/config.js CHANGED
@@ -72,6 +72,22 @@ export function getMode(cwd = process.cwd()) {
72
72
  return aliases.length > 1 ? 'multi' : 'single';
73
73
  }
74
74
 
75
+ /**
76
+ * Whether optional preview/cleanup workflows are enabled.
77
+ */
78
+ export function isPreviewWorkflowsEnabled(cwd = process.cwd()) {
79
+ const config = readConfig(cwd);
80
+ return config?.preview_workflows === true;
81
+ }
82
+
83
+ /**
84
+ * Whether optional build/Lighthouse workflows are enabled.
85
+ */
86
+ export function isBuildWorkflowsEnabled(cwd = process.cwd()) {
87
+ const config = readConfig(cwd);
88
+ return config?.build_workflows === true;
89
+ }
90
+
75
91
  /**
76
92
  * Add a store entry to the config.
77
93
  * Returns the updated config.
@@ -14,9 +14,24 @@ export function extractAlias(domain) {
14
14
  * Appends ".myshopify.com" if not present.
15
15
  */
16
16
  export function normalizeDomain(input) {
17
- const trimmed = input.trim().toLowerCase();
18
- if (trimmed.endsWith('.myshopify.com')) return trimmed;
19
- return `${trimmed}.myshopify.com`;
17
+ const cleaned = input
18
+ .trim()
19
+ .toLowerCase()
20
+ .replace(/^https?:\/\//, '')
21
+ .replace(/\/.*$/, '')
22
+ .replace(/\s+/g, '');
23
+
24
+ if (!cleaned) return '';
25
+ if (cleaned.endsWith('.myshopify.com')) return cleaned;
26
+ return `${cleaned}.myshopify.com`;
27
+ }
28
+
29
+ /**
30
+ * Validate normalized Shopify store domain format.
31
+ * Expected: "<subdomain>.myshopify.com"
32
+ */
33
+ export function isValidShopifyDomain(domain) {
34
+ return /^[a-z0-9][a-z0-9-]*\.myshopify\.com$/.test(domain);
20
35
  }
21
36
 
22
37
  /**
@@ -29,13 +44,20 @@ export async function promptStore(defaultDomain = '') {
29
44
  name: 'domain',
30
45
  message: 'Store URL',
31
46
  initial: defaultDomain,
32
- validate: (v) =>
33
- v.trim().length > 0 ? true : 'Store URL is required',
47
+ validate: (v) => {
48
+ if (v.trim().length === 0) return 'Store URL is required';
49
+ const normalized = normalizeDomain(v);
50
+ if (!normalized || !isValidShopifyDomain(normalized)) {
51
+ return 'Enter a valid Shopify domain (e.g. voldt-staging.myshopify.com)';
52
+ }
53
+ return true;
54
+ },
34
55
  });
35
56
 
36
57
  if (!domain) return null;
37
58
 
38
59
  const normalized = normalizeDomain(domain);
60
+ if (!isValidShopifyDomain(normalized)) return null;
39
61
  const suggestedAlias = extractAlias(normalized);
40
62
 
41
63
  const { alias } = await prompts({
@@ -98,6 +120,34 @@ export async function promptStoreLoop() {
98
120
  return stores;
99
121
  }
100
122
 
123
+ /**
124
+ * Ask whether preview + cleanup workflows should be scaffolded.
125
+ */
126
+ export async function promptPreviewWorkflows() {
127
+ const { enablePreviewWorkflows } = await prompts({
128
+ type: 'confirm',
129
+ name: 'enablePreviewWorkflows',
130
+ message: 'Enable preview + cleanup workflows?',
131
+ initial: false,
132
+ });
133
+
134
+ return !!enablePreviewWorkflows;
135
+ }
136
+
137
+ /**
138
+ * Ask whether build workflows should be scaffolded.
139
+ */
140
+ export async function promptBuildWorkflows() {
141
+ const { enableBuildWorkflows } = await prompts({
142
+ type: 'confirm',
143
+ name: 'enableBuildWorkflows',
144
+ message: 'Enable build + Lighthouse workflows?',
145
+ initial: false,
146
+ });
147
+
148
+ return !!enableBuildWorkflows;
149
+ }
150
+
101
151
  /**
102
152
  * Prompt for a single new store (used by add-store command).
103
153
  * Takes existing aliases to prevent duplicates.
@@ -36,7 +36,7 @@ function copyWorkflow(srcDir, fileName, destDir) {
36
36
  * but for simplicity we track known filenames instead.
37
37
  */
38
38
  function getKnownWorkflowFiles() {
39
- const dirs = ['shared', 'single', 'multi'];
39
+ const dirs = ['shared', 'single', 'multi', 'preview', 'build'];
40
40
  const files = new Set();
41
41
  for (const dir of dirs) {
42
42
  const dirPath = join(TEMPLATES_DIR, dir);
@@ -47,6 +47,9 @@ function getKnownWorkflowFiles() {
47
47
  return files;
48
48
  }
49
49
 
50
+ /** Deprecated workflow filenames to remove when scaffolding (renamed or replaced). */
51
+ const DEPRECATED_WORKFLOW_FILES = ['hotfix-backport.yml'];
52
+
50
53
  /**
51
54
  * Remove previously scaffolded climaybe workflows from target.
52
55
  */
@@ -56,7 +59,7 @@ function cleanWorkflows(cwd = process.cwd()) {
56
59
 
57
60
  const known = getKnownWorkflowFiles();
58
61
  for (const file of readdirSync(dest)) {
59
- if (known.has(file)) {
62
+ if (known.has(file) || DEPRECATED_WORKFLOW_FILES.includes(file)) {
60
63
  rmSync(join(dest, file));
61
64
  }
62
65
  }
@@ -67,7 +70,8 @@ function cleanWorkflows(cwd = process.cwd()) {
67
70
  * - Always copies shared/ workflows.
68
71
  * - Copies single/ or multi/ (+ single/) based on mode.
69
72
  */
70
- export function scaffoldWorkflows(mode = 'single', cwd = process.cwd()) {
73
+ export function scaffoldWorkflows(mode = 'single', options = {}, cwd = process.cwd()) {
74
+ const { includePreview = false, includeBuild = false } = options;
71
75
  const dest = ghWorkflowsDir(cwd);
72
76
  mkdirSync(dest, { recursive: true });
73
77
 
@@ -93,7 +97,23 @@ export function scaffoldWorkflows(mode = 'single', cwd = process.cwd()) {
93
97
  }
94
98
  }
95
99
 
100
+ if (includePreview) {
101
+ const previewDir = join(TEMPLATES_DIR, 'preview');
102
+ for (const f of listYmls(previewDir)) {
103
+ copyWorkflow(previewDir, f, dest);
104
+ }
105
+ }
106
+
107
+ if (includeBuild) {
108
+ const buildDir = join(TEMPLATES_DIR, 'build');
109
+ for (const f of listYmls(buildDir)) {
110
+ copyWorkflow(buildDir, f, dest);
111
+ }
112
+ }
113
+
96
114
  const total = readdirSync(dest).filter((f) => f.endsWith('.yml')).length;
97
115
  console.log(pc.green(` Scaffolded ${total} workflow(s) → .github/workflows/`));
98
116
  console.log(pc.dim(` Mode: ${mode}-store`));
117
+ console.log(pc.dim(` Preview workflows: ${includePreview ? 'enabled' : 'disabled'}`));
118
+ console.log(pc.dim(` Build workflows: ${includeBuild ? 'enabled' : 'disabled'}`));
99
119
  }
@@ -0,0 +1,57 @@
1
+ # climaybe — Build Pipeline (Optional Build Package)
2
+
3
+ name: Build Pipeline
4
+
5
+ on:
6
+ push:
7
+ branches: [main, staging, develop]
8
+
9
+ jobs:
10
+ build:
11
+ uses: ./.github/workflows/reusable-build.yml
12
+
13
+ lighthouse-ci:
14
+ runs-on: ubuntu-latest
15
+ needs: [build]
16
+ env:
17
+ SHOP_STORE: ${{ secrets.SHOP_STORE }}
18
+ SHOP_ACCESS_TOKEN: ${{ secrets.SHOP_ACCESS_TOKEN }}
19
+ LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
20
+ steps:
21
+ - name: Check conditions and log skip reason
22
+ run: |
23
+ SKIP_REASONS=()
24
+
25
+ if [ "${{ github.ref }}" != "refs/heads/main" ] && [ "${{ github.ref }}" != "refs/heads/staging" ]; then
26
+ SKIP_REASONS+=("Not on main or staging branch (current: ${{ github.ref }})")
27
+ fi
28
+
29
+ if [ -z "$SHOP_STORE" ] || [ -z "$SHOP_ACCESS_TOKEN" ] || [ -z "$LHCI_GITHUB_APP_TOKEN" ]; then
30
+ SKIP_REASONS+=("Missing required secrets: SHOP_STORE, SHOP_ACCESS_TOKEN, or LHCI_GITHUB_APP_TOKEN")
31
+ fi
32
+
33
+ if [ ${#SKIP_REASONS[@]} -gt 0 ]; then
34
+ echo "Lighthouse CI skipped"
35
+ echo "Reasons:"
36
+ for reason in "${SKIP_REASONS[@]}"; do
37
+ echo " - $reason"
38
+ done
39
+ exit 0
40
+ fi
41
+
42
+ echo "All conditions met, proceeding with Lighthouse CI"
43
+
44
+ - name: Checkout code
45
+ if: env.SHOP_STORE != '' && env.SHOP_ACCESS_TOKEN != '' && env.LHCI_GITHUB_APP_TOKEN != '' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging')
46
+ uses: actions/checkout@v4
47
+
48
+ - name: Lighthouse
49
+ if: env.SHOP_STORE != '' && env.SHOP_ACCESS_TOKEN != '' && env.LHCI_GITHUB_APP_TOKEN != '' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging')
50
+ uses: shopify/lighthouse-ci-action@v1
51
+ with:
52
+ store: ${{ secrets.SHOP_STORE }}
53
+ access_token: ${{ secrets.SHOP_ACCESS_TOKEN }}
54
+ password: ${{ secrets.SHOP_PASSWORD }}
55
+ lhci_github_app_token: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
56
+ lhci_min_score_performance: 0.9
57
+ lhci_min_score_accessibility: 0.9
@@ -0,0 +1,52 @@
1
+ name: Create Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ build:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+
14
+ - name: Create release archive
15
+ run: |
16
+ mkdir release-temp
17
+ mkdir -p .sys/release-notes
18
+
19
+ TAG_NAME=${GITHUB_REF#refs/tags/}
20
+ cp release-notes.md ".sys/release-notes/release-note-${TAG_NAME}.md"
21
+
22
+ for dir in assets layout locales sections snippets config; do
23
+ mkdir -p "release-temp/$dir"
24
+ find "$dir" -type f ! -name "*.md" -exec cp --parents {} release-temp/ \;
25
+ done
26
+
27
+ mkdir -p release-temp/templates
28
+ find templates -type f ! -name "*.md" ! -name "page.test-*" -exec cp --parents {} release-temp/ \;
29
+
30
+ find release-temp -type f -name "*.liquid" -exec sed -i '
31
+ /{% comment %}/{
32
+ N
33
+ /{% comment %}\s*\nINTERNAL/{
34
+ :a
35
+ N
36
+ /{% endcomment %}/!ba
37
+ d
38
+ }
39
+ }' {} \;
40
+
41
+ cd release-temp
42
+ zip -r ../release.zip ./*
43
+ cd ..
44
+ rm -rf release-temp
45
+
46
+ - name: Create Release
47
+ uses: softprops/action-gh-release@v1
48
+ with:
49
+ files: release.zip
50
+ body_path: release-notes.md
51
+ env:
52
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,52 @@
1
+ name: Build
2
+
3
+ on:
4
+ workflow_call:
5
+ outputs:
6
+ build-success:
7
+ description: "Whether build was successful"
8
+ value: ${{ jobs.build.outputs.success }}
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ permissions:
14
+ contents: write
15
+ outputs:
16
+ success: ${{ steps.build.outputs.success }}
17
+ steps:
18
+ - name: Checkout code
19
+ uses: actions/checkout@v4
20
+
21
+ - name: Set up Node.js
22
+ uses: actions/setup-node@v4
23
+ with:
24
+ node-version: "24"
25
+ cache: "npm"
26
+
27
+ - name: Install dependencies
28
+ run: npm ci
29
+
30
+ - name: Build scripts
31
+ run: node build-scripts.js
32
+
33
+ - name: Run Tailwind build
34
+ id: build
35
+ run: |
36
+ NODE_ENV=production npx @tailwindcss/cli -i _styles/main.css -o assets/style.css --minify
37
+ echo "success=true" >> $GITHUB_OUTPUT
38
+
39
+ - name: Commit and push changes
40
+ run: |
41
+ git config --local user.email "action@github.com"
42
+ git config --local user.name "GitHub Action"
43
+ BRANCH_NAME=${{ github.head_ref || github.ref_name }}
44
+ git fetch origin "$BRANCH_NAME" || echo "Branch does not exist yet"
45
+ git checkout -B "$BRANCH_NAME" "origin/$BRANCH_NAME" 2>/dev/null || git checkout -B "$BRANCH_NAME"
46
+ git add -f assets/index.js assets/style.css
47
+ if git diff --staged --quiet; then
48
+ echo "No changes to commit"
49
+ else
50
+ git commit -m "Update compiled assets (JavaScript and CSS)"
51
+ git push origin "$BRANCH_NAME"
52
+ fi
@@ -1,7 +1,7 @@
1
- # climaybe — Main to Staging Stores (Multi-store)
1
+ # climaybe — Main to Staging <store> (Multi-store)
2
2
  # When a PR is merged into main (from staging), this workflow
3
3
  # opens and auto-merges PRs from main to each staging-<alias> branch.
4
- # Skips hotfix backport commits to prevent recursive triggers.
4
+ # Skips [hotfix-backport] and version-bump commits so multistore-hotfix-to-main syncs are not re-pushed to stores.
5
5
 
6
6
  name: Main to Staging Stores
7
7
 
@@ -72,6 +72,7 @@ jobs:
72
72
  permissions:
73
73
  contents: write
74
74
  pull-requests: write
75
+ actions: write
75
76
  env:
76
77
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
77
78
  steps:
@@ -112,7 +113,47 @@ jobs:
112
113
 
113
114
  # Auto-merge
114
115
  PR_NUM=$(echo "$PR_URL" | grep -oP '\d+$')
115
- gh pr merge "$PR_NUM" --merge --admin 2>/dev/null || echo "Auto-merge failed — manual review may be needed."
116
+ if gh pr merge "$PR_NUM" --merge --admin 2>/dev/null; then
117
+ echo "Auto-merge succeeded for $BRANCH"
118
+
119
+ # Explicitly trigger Stores to Root to avoid missing downstream runs
120
+ # when merges are performed by GITHUB_TOKEN-based automation.
121
+ gh workflow run "Stores to Root" --ref "$BRANCH" 2>/dev/null || \
122
+ echo "Failed to trigger Stores to Root for $BRANCH"
123
+
124
+ # Wait for the latest Stores to Root run on this branch to complete,
125
+ # then trigger PR to Live to keep ordering deterministic.
126
+ for i in $(seq 1 30); do
127
+ RUN_STATUS=$(gh run list \
128
+ --workflow "Stores to Root" \
129
+ --branch "$BRANCH" \
130
+ --limit 1 \
131
+ --json status,conclusion \
132
+ --jq '.[0].status + "|" + (.[0].conclusion // "")' 2>/dev/null || echo "")
133
+
134
+ if [ -z "$RUN_STATUS" ]; then
135
+ sleep 5
136
+ continue
137
+ fi
138
+
139
+ STATUS="${RUN_STATUS%%|*}"
140
+ CONCLUSION="${RUN_STATUS##*|}"
141
+
142
+ if [ "$STATUS" = "completed" ]; then
143
+ if [ "$CONCLUSION" = "success" ]; then
144
+ gh workflow run "PR to Live" --ref "$BRANCH" 2>/dev/null || \
145
+ echo "Failed to trigger PR to Live for $BRANCH"
146
+ else
147
+ echo "Stores to Root did not succeed for $BRANCH (conclusion: $CONCLUSION)"
148
+ fi
149
+ break
150
+ fi
151
+
152
+ sleep 5
153
+ done
154
+ else
155
+ echo "Auto-merge failed — manual review may be needed."
156
+ fi
116
157
  else
117
158
  echo "No changes to sync for $BRANCH"
118
159
  fi