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 +21 -0
- package/README.md +62 -11
- package/package.json +7 -1
- package/src/commands/add-store.js +5 -3
- package/src/commands/init.js +11 -2
- package/src/commands/update-workflows.js +4 -2
- package/src/lib/config.js +16 -0
- package/src/lib/prompts.js +55 -5
- package/src/lib/workflows.js +23 -3
- package/src/workflows/build/build-pipeline.yml +57 -0
- package/src/workflows/build/create-release.yml +52 -0
- package/src/workflows/build/reusable-build.yml +52 -0
- package/src/workflows/multi/main-to-staging-stores.yml +44 -3
- package/src/workflows/multi/multistore-hotfix-to-main.yml +84 -0
- package/src/workflows/multi/pr-to-live.yml +82 -8
- package/src/workflows/multi/stores-to-root.yml +10 -3
- package/src/workflows/preview/pr-close.yml +63 -0
- package/src/workflows/preview/pr-update.yml +112 -0
- package/src/workflows/preview/reusable-cleanup-themes.yml +71 -0
- package/src/workflows/preview/reusable-comment-on-pr.yml +76 -0
- package/src/workflows/preview/reusable-extract-pr-number.yml +35 -0
- package/src/workflows/preview/reusable-rename-theme.yml +73 -0
- package/src/workflows/preview/reusable-share-theme.yml +94 -0
- package/src/workflows/shared/ai-changelog.yml +82 -24
- package/src/workflows/shared/version-bump.yml +22 -7
- package/src/workflows/single/nightly-hotfix.yml +38 -2
- package/src/workflows/single/post-merge-tag.yml +68 -15
- package/src/workflows/single/release-pr-check.yml +16 -6
- package/src/workflows/multi/hotfix-backport.yml +0 -100
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.
|
|
30
|
-
5.
|
|
31
|
-
6.
|
|
32
|
-
7.
|
|
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) |
|
|
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-<store>) | 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-
|
|
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
|
-
- **
|
|
148
|
-
- **
|
|
149
|
-
- **
|
|
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
|
|
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.
|
|
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')));
|
package/src/commands/init.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
package/src/lib/prompts.js
CHANGED
|
@@ -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
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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.
|
package/src/lib/workflows.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|