climaybe 1.0.0 → 1.3.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 +64 -12
- package/package.json +45 -2
- package/src/commands/add-store.js +77 -4
- package/src/commands/init.js +150 -16
- package/src/commands/update-workflows.js +4 -2
- package/src/index.js +15 -3
- package/src/lib/config.js +17 -0
- package/src/lib/git.js +21 -3
- package/src/lib/github-secrets.js +263 -0
- package/src/lib/prompts.js +116 -6
- 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 +120 -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
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Secret/variable definitions for CI (GitHub Actions or GitLab CI).
|
|
7
|
+
* condition: 'always' = required for core workflows; 'preview' | 'build' = only when that feature is enabled.
|
|
8
|
+
*/
|
|
9
|
+
export const SECRET_DEFINITIONS = [
|
|
10
|
+
{
|
|
11
|
+
name: 'GEMINI_API_KEY',
|
|
12
|
+
required: true,
|
|
13
|
+
condition: 'always',
|
|
14
|
+
description: 'Google Gemini API key for AI-generated changelogs on release',
|
|
15
|
+
whereToGet:
|
|
16
|
+
'Google AI Studio: https://aistudio.google.com/apikey — create an API key, then paste it here.',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: 'SHOPIFY_STORE_URL',
|
|
20
|
+
required: false,
|
|
21
|
+
condition: 'preview_or_build',
|
|
22
|
+
description: 'Shopify store URL (e.g. your-store.myshopify.com) — preview themes and/or Lighthouse',
|
|
23
|
+
whereToGet:
|
|
24
|
+
'Your theme’s store URL in Shopify Admin → Settings → Domains, or use the .myshopify.com URL.',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'SHOPIFY_CLI_THEME_TOKEN',
|
|
28
|
+
required: true,
|
|
29
|
+
condition: 'preview',
|
|
30
|
+
description: 'Theme access token so CI can push preview themes to your store',
|
|
31
|
+
whereToGet:
|
|
32
|
+
'Shopify Partners: your app → Theme library access → Create theme access token. Or: Shopify Admin → Apps → Develop apps → your app → API credentials → Theme access.',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'SHOP_ACCESS_TOKEN',
|
|
36
|
+
required: false,
|
|
37
|
+
condition: 'build',
|
|
38
|
+
description: 'Store API access token for Lighthouse runs',
|
|
39
|
+
whereToGet:
|
|
40
|
+
'Shopify Admin → Settings → Apps and sales channels → Develop apps → your app → API credentials (Admin API or custom app with storefront/build access).',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'LHCI_GITHUB_APP_TOKEN',
|
|
44
|
+
required: false,
|
|
45
|
+
condition: 'build',
|
|
46
|
+
description: 'Lighthouse CI GitHub App token for posting results as PR comments',
|
|
47
|
+
whereToGet:
|
|
48
|
+
'Lighthouse CI GitHub App: https://github.com/apps/lighthouse-ci — install and create a token, or use a Personal Access Token with repo scope.',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'SHOP_PASSWORD',
|
|
52
|
+
required: false,
|
|
53
|
+
condition: 'build',
|
|
54
|
+
description: 'Store password if your storefront is password-protected (optional)',
|
|
55
|
+
whereToGet: 'The password visitors enter to access your store (Storefront password in Shopify Admin).',
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check if GitHub CLI is installed and authenticated.
|
|
61
|
+
*/
|
|
62
|
+
export function isGhAvailable() {
|
|
63
|
+
try {
|
|
64
|
+
execSync('gh auth status', { encoding: 'utf-8', stdio: 'pipe' });
|
|
65
|
+
return true;
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check if the current repo has a GitHub remote (origin pointing to github.com).
|
|
73
|
+
*/
|
|
74
|
+
export function hasGitHubRemote(cwd = process.cwd()) {
|
|
75
|
+
try {
|
|
76
|
+
const url = execSync('git remote get-url origin', { cwd, encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
77
|
+
return /github\.com/i.test(url);
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* List repository secret names (GitHub). Returns [] on error or if none.
|
|
85
|
+
*/
|
|
86
|
+
export function listGitHubSecrets(cwd = process.cwd()) {
|
|
87
|
+
try {
|
|
88
|
+
const out = execSync('gh secret list --json name', { cwd, encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
89
|
+
const data = JSON.parse(out || '[]');
|
|
90
|
+
return Array.isArray(data) ? data.map((s) => s.name).filter(Boolean) : [];
|
|
91
|
+
} catch {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* List project CI/CD variable names (GitLab). Returns [] on error or if none.
|
|
98
|
+
*/
|
|
99
|
+
export function listGitLabVariables(cwd = process.cwd()) {
|
|
100
|
+
try {
|
|
101
|
+
const out = execSync('glab variable list -F json', { cwd, encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
102
|
+
const data = JSON.parse(out || '[]');
|
|
103
|
+
if (!Array.isArray(data)) return [];
|
|
104
|
+
return data.map((v) => v.key ?? v.name ?? v.Key).filter(Boolean);
|
|
105
|
+
} catch {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Set a repository secret via gh CLI. Value is passed via stdin to avoid argv exposure.
|
|
112
|
+
*/
|
|
113
|
+
export function setSecret(name, value) {
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
const child = spawn('gh', ['secret', 'set', name], {
|
|
116
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
117
|
+
});
|
|
118
|
+
child.on('error', reject);
|
|
119
|
+
child.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`gh secret set exited with ${code}`))));
|
|
120
|
+
child.stdin.write(value, (err) => {
|
|
121
|
+
if (err) reject(err);
|
|
122
|
+
else child.stdin.end();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Check if GitLab CLI is installed and authenticated.
|
|
129
|
+
*/
|
|
130
|
+
export function isGlabAvailable() {
|
|
131
|
+
try {
|
|
132
|
+
execSync('glab auth status', { encoding: 'utf-8', stdio: 'pipe' });
|
|
133
|
+
return true;
|
|
134
|
+
} catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Check if the current repo has a GitLab remote (origin pointing to gitlab.com or common self-hosted hosts).
|
|
141
|
+
*/
|
|
142
|
+
export function hasGitLabRemote(cwd = process.cwd()) {
|
|
143
|
+
try {
|
|
144
|
+
const url = execSync('git remote get-url origin', { cwd, encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
145
|
+
return /gitlab\.com|gitlab\./i.test(url);
|
|
146
|
+
} catch {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Set a project CI/CD variable via glab CLI. Value is passed via stdin to avoid argv exposure.
|
|
153
|
+
*/
|
|
154
|
+
export function setGitLabVariable(name, value) {
|
|
155
|
+
return new Promise((resolve, reject) => {
|
|
156
|
+
const child = spawn('glab', ['variable', 'set', name, '--masked'], {
|
|
157
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
158
|
+
});
|
|
159
|
+
child.on('error', reject);
|
|
160
|
+
child.on('close', (code) =>
|
|
161
|
+
code === 0 ? resolve() : reject(new Error(`glab variable set exited with ${code}`))
|
|
162
|
+
);
|
|
163
|
+
child.stdin.write(value, (err) => {
|
|
164
|
+
if (err) reject(err);
|
|
165
|
+
else child.stdin.end();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Convert store alias to secret suffix (e.g. voldt-norway → VOLDT_NORWAY).
|
|
172
|
+
*/
|
|
173
|
+
export function aliasToSecretSuffix(alias) {
|
|
174
|
+
return String(alias).replace(/-/g, '_').toUpperCase();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Store URL secrets are set from config (store domains added during init), not prompted.
|
|
179
|
+
* Returns [{ name, value }] for SHOPIFY_STORE_URL and/or SHOPIFY_STORE_URL_<ALIAS>.
|
|
180
|
+
*/
|
|
181
|
+
export function getStoreUrlSecretsFromConfig({ enablePreviewWorkflows, enableBuildWorkflows, mode = 'single', stores = [] }) {
|
|
182
|
+
if (stores.length === 0) return [];
|
|
183
|
+
const isMulti = mode === 'multi' && stores.length > 1;
|
|
184
|
+
const out = [];
|
|
185
|
+
|
|
186
|
+
if (isMulti && enablePreviewWorkflows) {
|
|
187
|
+
for (const store of stores) {
|
|
188
|
+
out.push({ name: `SHOPIFY_STORE_URL_${aliasToSecretSuffix(store.alias)}`, value: store.domain });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (!isMulti && (enablePreviewWorkflows || enableBuildWorkflows)) {
|
|
192
|
+
out.push({ name: 'SHOPIFY_STORE_URL', value: stores[0].domain });
|
|
193
|
+
}
|
|
194
|
+
if (isMulti && enableBuildWorkflows) {
|
|
195
|
+
const defaultDomain = stores[0]?.domain;
|
|
196
|
+
if (defaultDomain && !out.some((s) => s.name === 'SHOPIFY_STORE_URL')) {
|
|
197
|
+
out.push({ name: 'SHOPIFY_STORE_URL', value: defaultDomain });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get secrets we need to prompt for (excludes store URLs; those are set from config).
|
|
205
|
+
* Theme token(s) are required when preview is enabled.
|
|
206
|
+
*/
|
|
207
|
+
export function getSecretsToPrompt({ enablePreviewWorkflows, enableBuildWorkflows, mode = 'single', stores = [] }) {
|
|
208
|
+
const isMulti = mode === 'multi' && stores.length > 1;
|
|
209
|
+
|
|
210
|
+
const base = SECRET_DEFINITIONS.filter((s) => {
|
|
211
|
+
if (s.name === 'SHOPIFY_STORE_URL') return false; // set from config
|
|
212
|
+
if (s.condition === 'always') return true;
|
|
213
|
+
if (s.condition === 'preview_or_build') return enablePreviewWorkflows || enableBuildWorkflows;
|
|
214
|
+
if (s.condition === 'preview' && enablePreviewWorkflows) return true;
|
|
215
|
+
if (s.condition === 'build' && enableBuildWorkflows) return true;
|
|
216
|
+
return false;
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const dropPreviewGeneric =
|
|
220
|
+
isMulti && enablePreviewWorkflows
|
|
221
|
+
? (s) => s.name !== 'SHOPIFY_CLI_THEME_TOKEN'
|
|
222
|
+
: () => true;
|
|
223
|
+
|
|
224
|
+
let list = base.filter(dropPreviewGeneric);
|
|
225
|
+
|
|
226
|
+
if (isMulti && enablePreviewWorkflows) {
|
|
227
|
+
for (const store of stores) {
|
|
228
|
+
const suffix = aliasToSecretSuffix(store.alias);
|
|
229
|
+
list.push({
|
|
230
|
+
name: `SHOPIFY_CLI_THEME_TOKEN_${suffix}`,
|
|
231
|
+
required: true,
|
|
232
|
+
description: `Store ${store.alias}: Theme access token for CI (staging/live use this for ${store.alias})`,
|
|
233
|
+
whereToGet:
|
|
234
|
+
'Shopify Partners or Admin → Apps → Develop apps → your app → Theme access for this store.',
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return list;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Store URL for a new store (set from store.domain, no prompt).
|
|
244
|
+
*/
|
|
245
|
+
export function getStoreUrlSecretForNewStore(store) {
|
|
246
|
+
return { name: `SHOPIFY_STORE_URL_${aliasToSecretSuffix(store.alias)}`, value: store.domain };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Per-store secret to prompt for when adding a store (theme token only; URL is set from store.domain).
|
|
251
|
+
*/
|
|
252
|
+
export function getSecretsToPromptForNewStore(store) {
|
|
253
|
+
const suffix = aliasToSecretSuffix(store.alias);
|
|
254
|
+
return [
|
|
255
|
+
{
|
|
256
|
+
name: `SHOPIFY_CLI_THEME_TOKEN_${suffix}`,
|
|
257
|
+
required: true,
|
|
258
|
+
description: `Store ${store.alias}: Theme access token for CI (staging/live use this for ${store.alias})`,
|
|
259
|
+
whereToGet:
|
|
260
|
+
'Shopify Partners or Admin → Apps → Develop apps → your app → Theme access for this store.',
|
|
261
|
+
},
|
|
262
|
+
];
|
|
263
|
+
}
|
package/src/lib/prompts.js
CHANGED
|
@@ -6,7 +6,7 @@ import pc from 'picocolors';
|
|
|
6
6
|
* "voldt-staging.myshopify.com" → "voldt-staging"
|
|
7
7
|
*/
|
|
8
8
|
export function extractAlias(domain) {
|
|
9
|
-
return domain.replace(/\.myshopify\.com$/i, '').trim();
|
|
9
|
+
return domain.trim().replace(/\.myshopify\.com$/i, '').trim();
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -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.
|
|
@@ -115,3 +165,63 @@ export async function promptNewStore(existingAliases = []) {
|
|
|
115
165
|
|
|
116
166
|
return store;
|
|
117
167
|
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Ask which CI host to configure for secrets (after init). Returns 'github' | 'gitlab' | 'skip'.
|
|
171
|
+
*/
|
|
172
|
+
export async function promptConfigureCISecrets() {
|
|
173
|
+
const { host } = await prompts({
|
|
174
|
+
type: 'select',
|
|
175
|
+
name: 'host',
|
|
176
|
+
message: 'Configure CI secrets / variables now?',
|
|
177
|
+
choices: [
|
|
178
|
+
{ title: 'GitHub (gh CLI)', value: 'github' },
|
|
179
|
+
{ title: 'GitLab (glab CLI)', value: 'gitlab' },
|
|
180
|
+
{ title: 'Skip', value: 'skip' },
|
|
181
|
+
],
|
|
182
|
+
initial: 0,
|
|
183
|
+
});
|
|
184
|
+
return host ?? 'skip';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Ask whether to update existing CI secrets (when some are already set). Returns true to update, false to skip.
|
|
189
|
+
*/
|
|
190
|
+
export async function promptUpdateExistingSecrets(existingNames) {
|
|
191
|
+
const list = existingNames.length <= 5 ? existingNames.join(', ') : `${existingNames.slice(0, 3).join(', ')} and ${existingNames.length - 3} more`;
|
|
192
|
+
const { update } = await prompts({
|
|
193
|
+
type: 'confirm',
|
|
194
|
+
name: 'update',
|
|
195
|
+
message: `You already have ${existingNames.length} secret(s) set (${list}). Update them?`,
|
|
196
|
+
initial: false,
|
|
197
|
+
});
|
|
198
|
+
return !!update;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Prompt for a single secret value. Shows name, required/optional, description, and where to get it.
|
|
203
|
+
* Returns the value string or null if user skips (optional secrets only).
|
|
204
|
+
*/
|
|
205
|
+
export async function promptSecretValue(secret, index, total) {
|
|
206
|
+
const requiredLabel = secret.required ? pc.red('required') : pc.dim('optional');
|
|
207
|
+
const message = `[${index + 1}/${total}] ${secret.name} (${requiredLabel})`;
|
|
208
|
+
|
|
209
|
+
console.log(pc.cyan(`\n ${secret.name}`));
|
|
210
|
+
console.log(pc.dim(` ${secret.description}`));
|
|
211
|
+
console.log(pc.dim(` Where to get: ${secret.whereToGet}`));
|
|
212
|
+
|
|
213
|
+
const { value } = await prompts({
|
|
214
|
+
type: 'password',
|
|
215
|
+
name: 'value',
|
|
216
|
+
message,
|
|
217
|
+
validate: (v) => {
|
|
218
|
+
if (secret.required && !(v && v.trim())) return 'This secret is required for your workflows.';
|
|
219
|
+
return true;
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (value === undefined) return null;
|
|
224
|
+
const trimmed = typeof value === 'string' ? value.trim() : '';
|
|
225
|
+
if (!trimmed && !secret.required) return null;
|
|
226
|
+
return trimmed || null;
|
|
227
|
+
}
|
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
|
+
SHOPIFY_STORE_URL: ${{ secrets.SHOPIFY_STORE_URL }}
|
|
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 "$SHOPIFY_STORE_URL" ] || [ -z "$SHOP_ACCESS_TOKEN" ] || [ -z "$LHCI_GITHUB_APP_TOKEN" ]; then
|
|
30
|
+
SKIP_REASONS+=("Missing required secrets: SHOPIFY_STORE_URL, 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.SHOPIFY_STORE_URL != '' && 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.SHOPIFY_STORE_URL != '' && 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.SHOPIFY_STORE_URL }}
|
|
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
|