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.
Files changed (32) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +64 -12
  3. package/package.json +45 -2
  4. package/src/commands/add-store.js +77 -4
  5. package/src/commands/init.js +150 -16
  6. package/src/commands/update-workflows.js +4 -2
  7. package/src/index.js +15 -3
  8. package/src/lib/config.js +17 -0
  9. package/src/lib/git.js +21 -3
  10. package/src/lib/github-secrets.js +263 -0
  11. package/src/lib/prompts.js +116 -6
  12. package/src/lib/workflows.js +23 -3
  13. package/src/workflows/build/build-pipeline.yml +57 -0
  14. package/src/workflows/build/create-release.yml +52 -0
  15. package/src/workflows/build/reusable-build.yml +52 -0
  16. package/src/workflows/multi/main-to-staging-stores.yml +44 -3
  17. package/src/workflows/multi/multistore-hotfix-to-main.yml +84 -0
  18. package/src/workflows/multi/pr-to-live.yml +82 -8
  19. package/src/workflows/multi/stores-to-root.yml +10 -3
  20. package/src/workflows/preview/pr-close.yml +63 -0
  21. package/src/workflows/preview/pr-update.yml +120 -0
  22. package/src/workflows/preview/reusable-cleanup-themes.yml +71 -0
  23. package/src/workflows/preview/reusable-comment-on-pr.yml +76 -0
  24. package/src/workflows/preview/reusable-extract-pr-number.yml +35 -0
  25. package/src/workflows/preview/reusable-rename-theme.yml +73 -0
  26. package/src/workflows/preview/reusable-share-theme.yml +94 -0
  27. package/src/workflows/shared/ai-changelog.yml +82 -24
  28. package/src/workflows/shared/version-bump.yml +22 -7
  29. package/src/workflows/single/nightly-hotfix.yml +38 -2
  30. package/src/workflows/single/post-merge-tag.yml +68 -15
  31. package/src/workflows/single/release-pr-check.yml +16 -6
  32. 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
+ }
@@ -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 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.
@@ -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
+ }
@@ -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