climaybe 1.5.2 → 1.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
@@ -2,6 +2,13 @@
2
2
 
3
3
  Shopify CI/CD CLI — scaffolds GitHub Actions workflows, branch strategy, and store config for single-store and multi-store theme repositories.
4
4
 
5
+ **Commit linting and AI-assisted commits are available as optional setup steps:**
6
+
7
+ - **Conventional commit linting:** During `climaybe init`, you can choose to automatically install and configure [commitlint](https://commitlint.js.org/) and [Husky](https://typicode.github.io/husky) to enforce [Conventional Commits](https://www.conventionalcommits.org/) in your theme repository.
8
+ - **Cursor AI commit skill:** You can also opt-in to installing the [Cursor AI commit skill](https://cursor.so/) (`.cursor/skills/commit/SKILL.md`) for AI-assisted, conventional commit message support in your project.
9
+
10
+ Both options streamline commit message quality and team workflows but are fully optional during setup.
11
+
5
12
  ## Install
6
13
 
7
14
  Install in your theme repo (project-only, no global install):
@@ -34,10 +41,13 @@ Interactive setup that configures your repo for CI/CD.
34
41
  3. Asks if you want to add more stores
35
42
  4. Asks whether to enable optional **preview + cleanup** workflows
36
43
  5. Asks whether to enable optional **build + Lighthouse** workflows
37
- 6. Based on store count, sets up **single-store** or **multi-store** mode
38
- 7. Writes `package.json` config
39
- 8. Scaffolds GitHub Actions workflows
40
- 9. Creates git branches and store directories (multi-store)
44
+ 6. Asks whether to enable **commitlint + Husky** (enforce [conventional commits](https://www.conventionalcommits.org/) on `git commit`)
45
+ 7. Asks whether to add **Cursor commit skill** to the project (`.cursor/skills/commit/SKILL.md`) for AI-assisted conventional commits
46
+ 8. Based on store count, sets up **single-store** or **multi-store** mode
47
+ 9. Writes `package.json` config
48
+ 10. Scaffolds GitHub Actions workflows
49
+ 11. Creates git branches and store directories (multi-store)
50
+ 12. Optionally installs commitlint, Husky, and the Cursor skill
41
51
 
42
52
  ### `climaybe add-store`
43
53
 
@@ -91,6 +101,22 @@ npx climaybe update-workflows
91
101
 
92
102
  Useful after updating the CLI to get the latest workflow improvements.
93
103
 
104
+ ### `climaybe setup-commitlint`
105
+
106
+ Set up **only** commitlint + Husky (conventional commits enforced on `git commit`). Use this if you skipped it at init or want to add it later.
107
+
108
+ ```bash
109
+ npx climaybe setup-commitlint
110
+ ```
111
+
112
+ ### `climaybe add-cursor-skill`
113
+
114
+ Add **only** the Cursor commit skill to this project (`.cursor/skills/commit/SKILL.md`). Use this if you skipped it at init or want to add it later.
115
+
116
+ ```bash
117
+ npx climaybe add-cursor-skill
118
+ ```
119
+
94
120
  ## Configuration
95
121
 
96
122
  The CLI writes config into the `config` field of your `package.json`:
@@ -102,6 +128,8 @@ The CLI writes config into the `config` field of your `package.json`:
102
128
  "default_store": "voldt-staging.myshopify.com",
103
129
  "preview_workflows": true,
104
130
  "build_workflows": true,
131
+ "commitlint": true,
132
+ "cursor_skills": true,
105
133
  "stores": {
106
134
  "voldt-staging": "voldt-staging.myshopify.com",
107
135
  "voldt-norway": "voldt-norway.myshopify.com"
@@ -149,19 +177,19 @@ Direct pushes to `staging-<store>` or `live-<store>` are automatically synced ba
149
177
 
150
178
  | Workflow | Trigger | What it does |
151
179
  |----------|---------|-------------|
152
- | `release-pr-check.yml` | PR from `staging` to `main` | Generates changelog, creates pre-release patch tag, posts PR comment |
153
- | `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) |
154
- | `nightly-hotfix.yml` | Cron 02:00 US Eastern | Tags untagged hotfix commits with patch version |
180
+ | `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 |
181
+ | `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 |
182
+ | `nightly-hotfix.yml` | Cron 02:00 US Eastern | Collects commits since latest tag (incl. hotfix backports), AI changelog, patch bump and tag |
155
183
 
156
184
  ### Multi-store (additional)
157
185
 
158
186
  | Workflow | Trigger | What it does |
159
187
  |----------|---------|-------------|
160
- | `main-to-staging-stores.yml` (main-to-staging-&lt;store&gt;) | Push to `main` | Opens PRs from main to each `staging-<alias>` branch |
161
- | `stores-to-root.yml` | Push to `staging-*` | Copies `stores/<alias>/` JSONs to repo root |
188
+ | `main-to-staging-stores.yml` (main-to-staging-&lt;store&gt;) | Push to `main` | Merges main into each `staging-<alias>`; root JSONs ignored (restored from stores/). Skips on hotfix/version/sync commits |
189
+ | `stores-to-root.yml` | Push to `staging-*` | From main merge: stores→root. From elsewhere (e.g. Shopify): root→stores |
162
190
  | `pr-to-live.yml` | After stores-to-root | Opens PR from `staging-<alias>` to `live-<alias>` |
163
- | `root-to-stores.yml` | Push to `live-*` (hotfix) | Syncs root JSONs back to `stores/<alias>/` |
164
- | `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 |
191
+ | `root-to-stores.yml` | Push to `live-*` | From main merge: stores→root. From elsewhere: root→stores (same as stores-to-root on staging-*) |
192
+ | `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) |
165
193
 
166
194
  ### Optional preview + cleanup package
167
195
 
@@ -189,13 +217,11 @@ Enabled via `climaybe init` prompt (`Enable build + Lighthouse workflows?`).
189
217
 
190
218
  ## Versioning
191
219
 
192
- - **Version format**: Always three-part (e.g. `v3.2.0`). No two-part tags.
193
- - **Release merge** (`staging` `main`): Minor bump (e.g. `v3.1.12` `v3.2.0`)
194
- - **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`)
195
- - **Other hotfixes** (direct commit to `main`): Patch bump via nightly workflow or manual run
196
- - **PR title convention**: `Release v3.2` or `Release v3.2.0` — the workflow normalizes to three-part
197
-
198
- All version bumps update `config/settings_schema.json` automatically.
220
+ - **Version format**: Always three-part (e.g. `v3.2.0`). No version in code or PR title; the system infers from tags.
221
+ - **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.
222
+ - **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).
223
+ - **Non-staging to main** (hotfix backports, direct commits): **Patch** bump only, via **nightly workflow** at 02:00 US Eastern (not at commit time).
224
+ - All version bumps update `config/settings_schema.json` automatically.
199
225
 
200
226
  **Full specification:** For detailed versioning rules, local dev flow, hotfix behavior, and alignment with the external CI/CD doc, see **[CI/CD Reference](docs/CI_CD_REFERENCE.md)**.
201
227
 
package/bin/version.txt CHANGED
@@ -1 +1 @@
1
- 1.5.2
1
+ 1.6.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "climaybe",
3
- "version": "1.5.2",
3
+ "version": "1.6.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": {
@@ -0,0 +1,17 @@
1
+ import pc from 'picocolors';
2
+ import { writeConfig } from '../lib/config.js';
3
+ import { scaffoldCursorCommitSkill } from '../lib/commit-tooling.js';
4
+
5
+ /**
6
+ * Add only the Cursor commit skill to this project (.cursor/skills/commit/SKILL.md).
7
+ * Can be run standalone or after init without having chosen Cursor skills at init.
8
+ */
9
+ export async function addCursorSkillCommand() {
10
+ console.log(pc.bold('\n climaybe — Add Cursor commit skill\n'));
11
+
12
+ writeConfig({ cursor_skills: true });
13
+
14
+ scaffoldCursorCommitSkill();
15
+ console.log(pc.green(' Cursor commit skill added to .cursor/skills/commit/SKILL.md'));
16
+ console.log(pc.dim(' Use "commit" or "group and commit" in Cursor to get conventional-commit assistance.\n'));
17
+ }
@@ -4,6 +4,8 @@ import {
4
4
  promptStoreLoop,
5
5
  promptPreviewWorkflows,
6
6
  promptBuildWorkflows,
7
+ promptCommitlint,
8
+ promptCursorSkills,
7
9
  promptConfigureCISecrets,
8
10
  promptUpdateExistingSecrets,
9
11
  promptSecretValue,
@@ -13,6 +15,7 @@ import { readConfig, writeConfig } from '../lib/config.js';
13
15
  import { ensureGitRepo, ensureInitialCommit, ensureStagingBranch, createStoreBranches, getSuggestedTagForRelease } from '../lib/git.js';
14
16
  import { scaffoldWorkflows } from '../lib/workflows.js';
15
17
  import { createStoreDirectories } from '../lib/store-sync.js';
18
+ import { scaffoldCommitlint, scaffoldCursorCommitSkill } from '../lib/commit-tooling.js';
16
19
  import {
17
20
  isGhAvailable,
18
21
  hasGitHubRemote,
@@ -38,6 +41,8 @@ async function runInitFlow() {
38
41
  const mode = stores.length > 1 ? 'multi' : 'single';
39
42
  const enablePreviewWorkflows = await promptPreviewWorkflows();
40
43
  const enableBuildWorkflows = await promptBuildWorkflows();
44
+ const enableCommitlint = await promptCommitlint();
45
+ const enableCursorSkills = await promptCursorSkills();
41
46
 
42
47
  console.log(pc.dim(`\n Mode: ${mode}-store (${stores.length} store(s))`));
43
48
 
@@ -47,6 +52,8 @@ async function runInitFlow() {
47
52
  default_store: stores[0].domain,
48
53
  preview_workflows: enablePreviewWorkflows,
49
54
  build_workflows: enableBuildWorkflows,
55
+ commitlint: enableCommitlint,
56
+ cursor_skills: enableCursorSkills,
50
57
  stores: {},
51
58
  };
52
59
 
@@ -80,6 +87,20 @@ async function runInitFlow() {
80
87
  includeBuild: enableBuildWorkflows,
81
88
  });
82
89
 
90
+ // 7. Optional commitlint + Husky and Cursor commit skill
91
+ if (enableCommitlint) {
92
+ console.log(pc.dim(' Setting up commitlint + Husky...'));
93
+ if (scaffoldCommitlint()) {
94
+ console.log(pc.green(' commitlint + Husky installed (conventional commits enforced on git commit).'));
95
+ } else {
96
+ console.log(pc.yellow(' commitlint setup failed or skipped (run npm install manually).'));
97
+ }
98
+ }
99
+ if (enableCursorSkills) {
100
+ scaffoldCursorCommitSkill();
101
+ console.log(pc.green(' Cursor commit skill added to .cursor/skills/commit/SKILL.md'));
102
+ }
103
+
83
104
  // Done
84
105
  console.log(pc.bold(pc.green('\n Setup complete!\n')));
85
106
 
@@ -95,6 +116,8 @@ async function runInitFlow() {
95
116
  }
96
117
  console.log(pc.dim(` Preview workflows: ${enablePreviewWorkflows ? 'enabled' : 'disabled'}`));
97
118
  console.log(pc.dim(` Build workflows: ${enableBuildWorkflows ? 'enabled' : 'disabled'}`));
119
+ console.log(pc.dim(` commitlint + Husky: ${enableCommitlint ? 'enabled' : 'disabled'}`));
120
+ console.log(pc.dim(` Cursor commit skill: ${enableCursorSkills ? 'added' : 'skipped'}`));
98
121
 
99
122
  const suggestedTag = getSuggestedTagForRelease();
100
123
  const tagLabel = suggestedTag === 'v1.0.0' ? 'Tag your first release' : 'Tag your next release';
@@ -0,0 +1,21 @@
1
+ import pc from 'picocolors';
2
+ import { writeConfig } from '../lib/config.js';
3
+ import { scaffoldCommitlint } from '../lib/commit-tooling.js';
4
+
5
+ /**
6
+ * Set up commitlint + Husky only (conventional commits on git commit).
7
+ * Can be run standalone or after init without having chosen commitlint at init.
8
+ */
9
+ export async function setupCommitlintCommand() {
10
+ console.log(pc.bold('\n climaybe — Setup commitlint + Husky\n'));
11
+
12
+ writeConfig({ commitlint: true });
13
+
14
+ console.log(pc.dim(' Installing commitlint + Husky (conventional commits enforced on git commit)...'));
15
+ const skipInstall = process.env.CLIMAYBE_SKIP_INSTALL === '1';
16
+ if (scaffoldCommitlint(process.cwd(), skipInstall ? { skipInstall: true } : {})) {
17
+ console.log(pc.green('\n commitlint + Husky are set up. Use conventional commits (e.g. feat: add X, fix: resolve Y).\n'));
18
+ } else {
19
+ console.log(pc.yellow('\n Installation failed or skipped. Run npm install in this repo and try again.\n'));
20
+ }
21
+ }
package/src/index.js CHANGED
@@ -5,6 +5,8 @@ import { switchCommand } from './commands/switch.js';
5
5
  import { syncCommand } from './commands/sync.js';
6
6
  import { updateWorkflowsCommand } from './commands/update-workflows.js';
7
7
  import { ensureBranchesCommand } from './commands/ensure-branches.js';
8
+ import { setupCommitlintCommand } from './commands/setup-commitlint.js';
9
+ import { addCursorSkillCommand } from './commands/add-cursor-skill.js';
8
10
 
9
11
  /**
10
12
  * Create the CLI program (for testing and for run).
@@ -57,6 +59,16 @@ export function createProgram(version = '0.0.0', packageDir = '') {
57
59
  .description('Create missing staging and per-store branches from current HEAD (then push)')
58
60
  .action(ensureBranchesCommand);
59
61
 
62
+ program
63
+ .command('setup-commitlint')
64
+ .description('Set up only commitlint + Husky (conventional commits on git commit)')
65
+ .action(setupCommitlintCommand);
66
+
67
+ program
68
+ .command('add-cursor-skill')
69
+ .description('Add only the Cursor commit skill to this project (.cursor/skills/commit)')
70
+ .action(addCursorSkillCommand);
71
+
60
72
  return program;
61
73
  }
62
74
 
@@ -0,0 +1,110 @@
1
+ import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { execSync } from 'node:child_process';
4
+ import { readPkg, writePkg } from './config.js';
5
+
6
+ const COMMITLINT_DEPS = {
7
+ '@commitlint/cli': '^20.4.4',
8
+ '@commitlint/config-conventional': '^20.4.4',
9
+ husky: '^9.1.7',
10
+ };
11
+
12
+ const COMMITLINT_CONFIG = `/** @type {import('@commitlint/types').UserConfig} */
13
+ module.exports = {
14
+ extends: ['@commitlint/config-conventional'],
15
+ rules: {
16
+ 'type-enum': [
17
+ 2,
18
+ 'always',
19
+ ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'build', 'ci', 'chore'],
20
+ ],
21
+ 'header-max-length': [2, 'always', 100],
22
+ 'body-max-line-length': [2, 'always', 200],
23
+ },
24
+ };
25
+ `;
26
+
27
+ const HUSKY_COMMIT_MSG = `npx --no-install commitlint --edit "$1"
28
+ `;
29
+
30
+ const CURSOR_COMMIT_SKILL = `---
31
+ name: commit
32
+ description: Groups working-tree changes into logical commits and commits them using conventional commit rules (type, optional scope, subject; commitlint). Use when the user asks to commit, group commits, stage and commit, or write conventional commits.
33
+ ---
34
+
35
+ # Commit (group + conventional)
36
+
37
+ Group changes by purpose, then commit each group with a valid conventional message so commitlint and semantic-release stay happy.
38
+
39
+ ## Workflow
40
+
41
+ 1. **Inspect** — Get full picture of changes (git status, git diff).
42
+ 2. **Group** — Partition by type: feat, fix, docs, style, refactor, perf, test, build, ci, chore.
43
+ 3. **Commit each group** — type(scope): subject, imperative, lowercase, no period, ≤100 chars.
44
+ 4. **Validate** — commit-msg hook will reject invalid messages.
45
+
46
+ ## Message rules (commitlint)
47
+
48
+ - **Types:** feat, fix, docs, style, refactor, perf, test, build, ci, chore.
49
+ - **Header:** type(scope): subject — subject max 100 chars. Body lines max 200.
50
+ - **Imperative, present tense:** "add feature" not "added feature".
51
+
52
+ ## Examples
53
+
54
+ feat(init): add store alias prompt
55
+ fix(sync): prevent overwrite of unchanged files
56
+ docs: add conventional commit section to CONTRIBUTING
57
+ `;
58
+
59
+ /**
60
+ * Scaffold commitlint config, Husky commit-msg hook, and add deps to package.json.
61
+ * Runs npm install so husky prepare runs and hooks are installed (unless options.skipInstall).
62
+ * @param {string} [cwd] - Working directory (default process.cwd())
63
+ * @param {{ skipInstall?: boolean }} [options] - skipInstall: true to skip npm install (e.g. in tests)
64
+ * @returns {boolean} - true if scaffolded, false if skipped/failed
65
+ */
66
+ export function scaffoldCommitlint(cwd = process.cwd(), options = {}) {
67
+ const pkg = readPkg(cwd);
68
+ if (!pkg) return false;
69
+
70
+ const updated = { ...pkg };
71
+ updated.scripts = { ...pkg.scripts };
72
+ if (!updated.scripts.prepare) {
73
+ updated.scripts.prepare = 'husky';
74
+ } else if (!updated.scripts.prepare.includes('husky')) {
75
+ updated.scripts.prepare = `husky && ${updated.scripts.prepare}`;
76
+ }
77
+ updated.devDependencies = { ...pkg.devDependencies, ...COMMITLINT_DEPS };
78
+ writePkg(updated, cwd);
79
+
80
+ writeFileSync(join(cwd, 'commitlint.config.js'), COMMITLINT_CONFIG, 'utf-8');
81
+
82
+ const huskyDir = join(cwd, '.husky');
83
+ if (!existsSync(huskyDir)) {
84
+ mkdirSync(huskyDir, { recursive: true });
85
+ }
86
+ writeFileSync(join(huskyDir, 'commit-msg'), HUSKY_COMMIT_MSG, 'utf-8');
87
+
88
+ if (options.skipInstall) return true;
89
+ try {
90
+ execSync('npm install', { cwd, stdio: 'inherit' });
91
+ } catch {
92
+ return false;
93
+ }
94
+ return true;
95
+ }
96
+
97
+ /**
98
+ * Scaffold Cursor commit skill into .cursor/skills/commit/SKILL.md.
99
+ * @param {string} [cwd] - Working directory (default process.cwd())
100
+ * @returns {boolean} - true if written
101
+ */
102
+ export function scaffoldCursorCommitSkill(cwd = process.cwd()) {
103
+ const skillDir = join(cwd, '.cursor', 'skills', 'commit');
104
+ if (!existsSync(skillDir)) {
105
+ mkdirSync(skillDir, { recursive: true });
106
+ }
107
+ const skillPath = join(skillDir, 'SKILL.md');
108
+ writeFileSync(skillPath, CURSOR_COMMIT_SKILL, 'utf-8');
109
+ return true;
110
+ }
package/src/lib/config.js CHANGED
@@ -97,6 +97,22 @@ export function isBuildWorkflowsEnabled(cwd = process.cwd()) {
97
97
  return config?.build_workflows === true;
98
98
  }
99
99
 
100
+ /**
101
+ * Whether commitlint + Husky was enabled at init.
102
+ */
103
+ export function isCommitlintEnabled(cwd = process.cwd()) {
104
+ const config = readConfig(cwd);
105
+ return config?.commitlint === true;
106
+ }
107
+
108
+ /**
109
+ * Whether Cursor commit skill was added at init.
110
+ */
111
+ export function isCursorSkillsEnabled(cwd = process.cwd()) {
112
+ const config = readConfig(cwd);
113
+ return config?.cursor_skills === true;
114
+ }
115
+
100
116
  /**
101
117
  * Add a store entry to the config.
102
118
  * Returns the updated config.
@@ -148,6 +148,34 @@ export async function promptBuildWorkflows() {
148
148
  return !!enableBuildWorkflows;
149
149
  }
150
150
 
151
+ /**
152
+ * Ask whether to set up commitlint + Husky (conventional commits enforced on git commit).
153
+ */
154
+ export async function promptCommitlint() {
155
+ const { enableCommitlint } = await prompts({
156
+ type: 'confirm',
157
+ name: 'enableCommitlint',
158
+ message: 'Enable commitlint + Husky? (enforce conventional commits on git commit)',
159
+ initial: true,
160
+ });
161
+
162
+ return !!enableCommitlint;
163
+ }
164
+
165
+ /**
166
+ * Ask whether to add Cursor commit skill to the project (.cursor/skills/commit).
167
+ */
168
+ export async function promptCursorSkills() {
169
+ const { enableCursorSkills } = await prompts({
170
+ type: 'confirm',
171
+ name: 'enableCursorSkills',
172
+ message: 'Add Cursor commit skill to this project? (AI-assisted conventional commits)',
173
+ initial: true,
174
+ });
175
+
176
+ return !!enableCursorSkills;
177
+ }
178
+
151
179
  /**
152
180
  * Prompt for a single new store (used by add-store command).
153
181
  * Takes existing aliases to prevent duplicates.
@@ -10,43 +10,43 @@ jobs:
10
10
  build:
11
11
  uses: ./.github/workflows/reusable-build.yml
12
12
 
13
- lighthouse-ci:
13
+ lighthouse-gate:
14
14
  runs-on: ubuntu-latest
15
15
  needs: [build]
16
+ outputs:
17
+ run_lighthouse: ${{ steps.check.outputs.run_lighthouse }}
16
18
  env:
17
19
  SHOPIFY_STORE_URL: ${{ secrets.SHOPIFY_STORE_URL }}
18
20
  SHOP_ACCESS_TOKEN: ${{ secrets.SHOP_ACCESS_TOKEN }}
19
21
  LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
20
22
  steps:
21
- - name: Check conditions and log skip reason
23
+ - name: Check if Lighthouse CI should run
24
+ id: check
22
25
  run: |
23
26
  SKIP_REASONS=()
24
-
25
27
  if [ "${{ github.ref }}" != "refs/heads/main" ] && [ "${{ github.ref }}" != "refs/heads/staging" ]; then
26
28
  SKIP_REASONS+=("Not on main or staging branch (current: ${{ github.ref }})")
27
29
  fi
28
-
29
30
  if [ -z "$SHOPIFY_STORE_URL" ] || [ -z "$SHOP_ACCESS_TOKEN" ] || [ -z "$LHCI_GITHUB_APP_TOKEN" ]; then
30
31
  SKIP_REASONS+=("Missing required secrets: SHOPIFY_STORE_URL, SHOP_ACCESS_TOKEN, or LHCI_GITHUB_APP_TOKEN")
31
32
  fi
32
-
33
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
34
+ echo "run_lighthouse=false" >> $GITHUB_OUTPUT
35
+ echo "Lighthouse CI skipped:"
36
+ for reason in "${SKIP_REASONS[@]}"; do echo " - $reason"; done
37
+ else
38
+ echo "run_lighthouse=true" >> $GITHUB_OUTPUT
40
39
  fi
41
40
 
42
- echo "All conditions met, proceeding with Lighthouse CI"
43
-
41
+ lighthouse-ci:
42
+ runs-on: ubuntu-latest
43
+ needs: [build, lighthouse-gate]
44
+ if: needs.lighthouse-gate.outputs.run_lighthouse == 'true'
45
+ steps:
44
46
  - name: Checkout code
45
- if: env.SHOPIFY_STORE_URL != '' && env.SHOP_ACCESS_TOKEN != '' && env.LHCI_GITHUB_APP_TOKEN != '' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging')
46
47
  uses: actions/checkout@v4
47
48
 
48
49
  - name: Lighthouse
49
- if: env.SHOPIFY_STORE_URL != '' && env.SHOP_ACCESS_TOKEN != '' && env.LHCI_GITHUB_APP_TOKEN != '' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging')
50
50
  uses: shopify/lighthouse-ci-action@v1
51
51
  with:
52
52
  store: ${{ secrets.SHOPIFY_STORE_URL }}
@@ -1,7 +1,8 @@
1
1
  # climaybe — Main to Staging <store> (Multi-store)
2
- # When a PR is merged into main (from staging), this workflow
3
- # opens and auto-merges PRs from main to each staging-<alias> branch.
4
- # Skips [hotfix-backport] and version-bump commits so multistore-hotfix-to-main syncs are not re-pushed to stores.
2
+ # When main is pushed (e.g. after staging→main merge), syncs main to each staging-<alias>.
3
+ # Root JSON files (config/settings_data.json, templates/*.json, sections/*.json) are ignored:
4
+ # merge brings main in but we immediately restore root from stores/<alias>/ so store-specific data is kept.
5
+ # Skips [hotfix-backport], version-bump, and store-sync commits so hotfix syncs do not re-trigger release to stores.
5
6
 
6
7
  name: Main to Staging Stores
7
8
 
@@ -27,12 +28,12 @@ jobs:
27
28
  COMMIT_MSG="${{ github.event.head_commit.message }}"
28
29
 
29
30
  if echo "$COMMIT_MSG" | grep -q "\[hotfix-backport\]"; then
30
- echo "Skipping — hotfix backport commit"
31
+ echo "Skipping — hotfix backport commit (do not re-release to stores)"
31
32
  echo "should_run=false" >> $GITHUB_OUTPUT
32
33
  elif echo "$COMMIT_MSG" | grep -q "chore(release): bump version"; then
33
34
  echo "Skipping — version bump commit"
34
35
  echo "should_run=false" >> $GITHUB_OUTPUT
35
- elif echo "$COMMIT_MSG" | grep -q "\[skip-store-sync\]"; then
36
+ elif echo "$COMMIT_MSG" | grep -qE "\[skip-store-sync\]|\[stores-to-root\]|\[root-to-stores\]"; then
36
37
  echo "Skipping — store sync commit"
37
38
  echo "should_run=false" >> $GITHUB_OUTPUT
38
39
  else
@@ -60,7 +61,7 @@ jobs:
60
61
  echo "stores=$STORES" >> $GITHUB_OUTPUT
61
62
  echo "Store aliases: $STORES"
62
63
 
63
- # Create PRs to each staging-<alias> branch
64
+ # Merge main into each staging-<alias>; root JSONs ignored (restored from stores/<alias>/)
64
65
  sync:
65
66
  needs: [gate, config]
66
67
  if: needs.gate.outputs.should_run == 'true'
@@ -80,80 +81,50 @@ jobs:
80
81
  with:
81
82
  fetch-depth: 0
82
83
 
83
- - name: Sync main to staging-${{ matrix.store }}
84
+ - name: Sync main to staging-${{ matrix.store }} (root JSONs ignored)
84
85
  run: |
85
86
  BRANCH="staging-${{ matrix.store }}"
87
+ ALIAS="${{ matrix.store }}"
88
+ STORE_DIR="stores/${ALIAS}"
86
89
 
87
- # Check if branch exists
88
90
  if ! git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then
89
91
  echo "Branch $BRANCH does not exist on remote, skipping."
90
92
  exit 0
91
93
  fi
92
94
 
93
- # Check for existing open PR
94
- EXISTING_PR=$(gh pr list --base "$BRANCH" --head main --state open --json number --jq '.[0].number' 2>/dev/null || echo "")
95
+ git config user.name "github-actions[bot]"
96
+ git config user.email "github-actions[bot]@users.noreply.github.com"
95
97
 
96
- if [ -n "$EXISTING_PR" ]; then
97
- echo "PR #$EXISTING_PR already exists for main → $BRANCH"
98
- # Update the existing PR by force
99
- gh pr close "$EXISTING_PR" --delete-branch=false 2>/dev/null || true
98
+ git fetch origin "$BRANCH"
99
+ git checkout "$BRANCH"
100
+ git merge origin/main --no-ff -m "Sync main → $BRANCH" || true
101
+ if [ $? -ne 0 ]; then
102
+ echo "Merge had conflicts; aborting. Manual resolution may be needed."
103
+ git merge --abort 2>/dev/null || true
104
+ exit 1
100
105
  fi
101
106
 
102
- # Create PR
103
- PR_URL=$(gh pr create \
104
- --base "$BRANCH" \
105
- --head main \
106
- --title "Sync main $BRANCH" \
107
- --body "Automated sync from main to $BRANCH after staging merge.
108
-
109
- *Generated by climaybe*" 2>/dev/null || echo "")
110
-
111
- if [ -n "$PR_URL" ]; then
112
- echo "Created PR: $PR_URL"
113
-
114
- # Auto-merge
115
- PR_NUM=$(echo "$PR_URL" | grep -oP '\d+$')
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)"
107
+ # Ignore root JSONs: restore from stores/<alias>/ so main's versions are not kept
108
+ if [ -d "$STORE_DIR" ]; then
109
+ for DIR in config templates sections; do
110
+ SRC="${STORE_DIR}/${DIR}"
111
+ if [ -d "$SRC" ]; then
112
+ find "$SRC" -name "*.json" | while read -r FILE; do
113
+ REL_PATH="${FILE#$STORE_DIR/}"
114
+ if [ "$REL_PATH" = "config/settings_schema.json" ]; then
115
+ continue
148
116
  fi
149
- break
150
- fi
151
-
152
- sleep 5
153
- done
154
- else
155
- echo "Auto-merge failed manual review may be needed."
117
+ mkdir -p "$(dirname "$REL_PATH")"
118
+ cp "$FILE" "$REL_PATH"
119
+ done
120
+ fi
121
+ done
122
+ git add config templates sections 2>/dev/null || true
123
+ if ! git diff --cached --quiet; then
124
+ git commit -m "chore: keep store root JSONs from stores/$ALIAS/ [stores-to-root]"
156
125
  fi
157
- else
158
- echo "No changes to sync for $BRANCH"
159
126
  fi
127
+
128
+ git push origin "$BRANCH" && echo "Pushed $BRANCH" || { echo "Push failed."; exit 1; }
129
+
130
+ gh workflow run "PR to Live" --ref "$BRANCH" 2>/dev/null || echo "Failed to trigger PR to Live for $BRANCH"