climaybe 1.3.0 → 1.3.3

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
@@ -210,14 +210,14 @@ Add the following secrets to your GitHub repository (or use **GitLab CI/CD varia
210
210
  |--------|----------|-------------|
211
211
  | `GEMINI_API_KEY` | Yes | Google Gemini API key for changelog generation |
212
212
  | `SHOPIFY_STORE_URL` | Set from config | Store URL is set automatically from the store domain(s) you add during init (no prompt). |
213
- | `SHOPIFY_CLI_THEME_TOKEN` | Yes* | Theme access token for preview workflows (required when preview is enabled). |
213
+ | `SHOPIFY_THEME_ACCESS_TOKEN` | Yes* | Theme access token for preview workflows (required when preview is enabled). |
214
214
  | `SHOP_ACCESS_TOKEN` | Optional* | Required only when optional build workflows are enabled (Lighthouse) |
215
215
  | `LHCI_GITHUB_APP_TOKEN` | Optional* | Required only when optional build workflows are enabled (Lighthouse) |
216
216
  | `SHOP_PASSWORD` | Optional | Used by Lighthouse action when your store requires password auth |
217
217
 
218
218
  **Store URL:** During `climaybe init` (or `add-store`), store URL secret(s) are set from your configured store domain(s); you are only prompted for the theme token.
219
219
 
220
- **Multi-store:** Per-store secrets `SHOPIFY_STORE_URL_<ALIAS>` and `SHOPIFY_CLI_THEME_TOKEN_<ALIAS>` — the URL is set from config; you must provide the theme token per store. `<ALIAS>` is uppercase with hyphens as underscores (e.g. `voldt-norway` → `SHOPIFY_STORE_URL_VOLDT_NORWAY`).
220
+ **Multi-store:** Per-store secrets `SHOPIFY_STORE_URL_<ALIAS>` and `SHOPIFY_THEME_ACCESS_TOKEN_<ALIAS>` — the URL is set from config; you must provide the theme token per store. `<ALIAS>` is uppercase with hyphens as underscores (e.g. `voldt-norway` → `SHOPIFY_STORE_URL_VOLDT_NORWAY`).
221
221
 
222
222
  ## Directory Structure (Multi-store)
223
223
 
package/bin/cli.js CHANGED
@@ -1,5 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { createRequire } from 'node:module';
4
+ import { dirname, join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
3
6
  import { run } from '../src/index.js';
4
7
 
5
- run(process.argv);
8
+ // Resolve version from package.json next to this bin (works with npm link / global install)
9
+ const require = createRequire(import.meta.url);
10
+ const binDir = dirname(fileURLToPath(import.meta.url));
11
+ const pkg = require(join(binDir, '..', 'package.json'));
12
+
13
+ run(process.argv, pkg.version);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "climaybe",
3
- "version": "1.3.0",
3
+ "version": "1.3.3",
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": {
@@ -1,5 +1,5 @@
1
1
  import pc from 'picocolors';
2
- import { promptNewStore, promptConfigureCISecrets, promptUpdateExistingSecrets, promptSecretValue } from '../lib/prompts.js';
2
+ import { promptNewStore, promptConfigureCISecrets, promptUpdateExistingSecrets, promptSecretValue, promptTestThemeToken } from '../lib/prompts.js';
3
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';
@@ -13,6 +13,7 @@ import {
13
13
  listGitLabVariables,
14
14
  getStoreUrlSecretForNewStore,
15
15
  getSecretsToPromptForNewStore,
16
+ validateThemeAccessToken,
16
17
  setSecret,
17
18
  setGitLabVariable,
18
19
  } from '../lib/github-secrets.js';
@@ -109,15 +110,29 @@ export async function addStoreCommand() {
109
110
  for (let i = 0; i < secretsToPrompt.length; i++) {
110
111
  const secret = secretsToPrompt[i];
111
112
  const value = await promptSecretValue(secret, i, total);
112
- if (value) {
113
- try {
114
- await setter.set(secret.name, value);
115
- console.log(pc.green(` Set ${secret.name}.`));
116
- setCount++;
117
- } catch (err) {
118
- console.log(pc.red(` Failed to set ${secret.name}: ${err.message}`));
113
+ if (!value) continue;
114
+
115
+ const isThemeToken = secret.name === 'SHOPIFY_THEME_ACCESS_TOKEN' || secret.name.startsWith('SHOPIFY_THEME_ACCESS_TOKEN_');
116
+ if (isThemeToken && store.domain) {
117
+ const doTest = await promptTestThemeToken();
118
+ if (doTest) {
119
+ const result = await validateThemeAccessToken(store.domain, value);
120
+ if (!result.ok) {
121
+ console.log(pc.red(` Token test failed: ${result.error}`));
122
+ console.log(pc.dim(' Secret not set. You can add it later in repo Settings → Secrets.'));
123
+ continue;
124
+ }
125
+ console.log(pc.green(' Token validated against store.'));
119
126
  }
120
127
  }
128
+
129
+ try {
130
+ await setter.set(secret.name, value);
131
+ console.log(pc.green(` Set ${secret.name}.`));
132
+ setCount++;
133
+ } catch (err) {
134
+ console.log(pc.red(` Failed to set ${secret.name}: ${err.message}`));
135
+ }
121
136
  }
122
137
  if (setCount > 0) {
123
138
  console.log(pc.green(`\n Done. ${setCount} secret(s) set for store "${store.alias}".\n`));
@@ -7,9 +7,10 @@ import {
7
7
  promptConfigureCISecrets,
8
8
  promptUpdateExistingSecrets,
9
9
  promptSecretValue,
10
+ promptTestThemeToken,
10
11
  } from '../lib/prompts.js';
11
12
  import { readConfig, writeConfig } from '../lib/config.js';
12
- import { ensureGitRepo, ensureInitialCommit, ensureStagingBranch, createStoreBranches } from '../lib/git.js';
13
+ import { ensureGitRepo, ensureInitialCommit, ensureStagingBranch, createStoreBranches, getSuggestedTagForRelease } from '../lib/git.js';
13
14
  import { scaffoldWorkflows } from '../lib/workflows.js';
14
15
  import { createStoreDirectories } from '../lib/store-sync.js';
15
16
  import {
@@ -21,6 +22,8 @@ import {
21
22
  listGitLabVariables,
22
23
  getStoreUrlSecretsFromConfig,
23
24
  getSecretsToPrompt,
25
+ getStoreUrlForThemeTokenSecret,
26
+ validateThemeAccessToken,
24
27
  setSecret,
25
28
  setGitLabVariable,
26
29
  } from '../lib/github-secrets.js';
@@ -93,10 +96,12 @@ async function runInitFlow() {
93
96
  console.log(pc.dim(` Preview workflows: ${enablePreviewWorkflows ? 'enabled' : 'disabled'}`));
94
97
  console.log(pc.dim(` Build workflows: ${enableBuildWorkflows ? 'enabled' : 'disabled'}`));
95
98
 
99
+ const suggestedTag = getSuggestedTagForRelease();
100
+ const tagLabel = suggestedTag === 'v1.0.0' ? 'Tag your first release' : 'Tag your next release';
96
101
  console.log(pc.dim('\n Next steps:'));
97
102
  console.log(pc.dim(' 1. Add GEMINI_API_KEY to your CI secrets (or configure below)'));
98
103
  console.log(pc.dim(' 2. Push to GitHub/GitLab and start using the branching workflow'));
99
- console.log(pc.dim(' 3. Tag your first release: git tag v1.0.0\n'));
104
+ console.log(pc.dim(` 3. ${tagLabel}: git tag ${suggestedTag}\n`));
100
105
 
101
106
  const ciHost = await promptConfigureCISecrets();
102
107
  if (ciHost === 'skip') return;
@@ -167,15 +172,32 @@ async function runInitFlow() {
167
172
  for (let i = 0; i < secretsToPrompt.length; i++) {
168
173
  const secret = secretsToPrompt[i];
169
174
  const value = await promptSecretValue(secret, i, total);
170
- if (value) {
171
- try {
172
- await setter.set(secret.name, value);
173
- console.log(pc.green(` Set ${secret.name}.`));
174
- setCount++;
175
- } catch (err) {
176
- console.log(pc.red(` Failed to set ${secret.name}: ${err.message}`));
175
+ if (!value) continue;
176
+
177
+ const isThemeToken =
178
+ secret.name === 'SHOPIFY_THEME_ACCESS_TOKEN' || secret.name.startsWith('SHOPIFY_THEME_ACCESS_TOKEN_');
179
+ const storeUrl = isThemeToken ? getStoreUrlForThemeTokenSecret(secret.name, stores) : null;
180
+
181
+ if (storeUrl) {
182
+ const doTest = await promptTestThemeToken();
183
+ if (doTest) {
184
+ const result = await validateThemeAccessToken(storeUrl, value);
185
+ if (!result.ok) {
186
+ console.log(pc.red(` Token test failed: ${result.error}`));
187
+ console.log(pc.dim(' Secret not set. You can add it later in repo Settings → Secrets.'));
188
+ continue;
189
+ }
190
+ console.log(pc.green(' Token validated against store.'));
177
191
  }
178
192
  }
193
+
194
+ try {
195
+ await setter.set(secret.name, value);
196
+ console.log(pc.green(` Set ${secret.name}.`));
197
+ setCount++;
198
+ } catch (err) {
199
+ console.log(pc.red(` Failed to set ${secret.name}: ${err.message}`));
200
+ }
179
201
  }
180
202
  if (setCount > 0) {
181
203
  console.log(pc.green(`\n Done. ${setCount} secret(s) set for this repository.\n`));
@@ -186,13 +208,13 @@ export async function initCommand() {
186
208
  console.log(pc.bold('\n climaybe — Shopify CI/CD Setup\n'));
187
209
 
188
210
  const existing = readConfig();
189
- const alreadyInited = existing?.stores && Object.keys(existing.stores).length > 0;
211
+ const hasConfig = existing != null && typeof existing === 'object';
190
212
 
191
- if (alreadyInited) {
213
+ if (hasConfig) {
192
214
  const { reinit } = await prompts({
193
215
  type: 'confirm',
194
216
  name: 'reinit',
195
- message: 'This repo already has a climaybe config. Reinitialize? This will remove your current stores and workflow settings.',
217
+ message: 'This repo already has a climaybe config. Clear everything and reinitialize from scratch?',
196
218
  initial: false,
197
219
  });
198
220
  if (!reinit) {
package/src/index.js CHANGED
@@ -7,14 +7,15 @@ import { updateWorkflowsCommand } from './commands/update-workflows.js';
7
7
 
8
8
  /**
9
9
  * Create the CLI program (for testing and for run).
10
+ * @param {string} [version] - Version string (from bin/cli.js when run as CLI; from package.json in tests).
10
11
  */
11
- export function createProgram() {
12
+ export function createProgram(version = '0.0.0') {
12
13
  const program = new Command();
13
14
 
14
15
  program
15
16
  .name('climaybe')
16
17
  .description('Shopify CI/CD CLI — scaffolds workflows, branch strategy, and store config')
17
- .version('1.0.0');
18
+ .version(version);
18
19
 
19
20
  program
20
21
  .command('init')
@@ -51,6 +52,6 @@ export function createProgram() {
51
52
  return program;
52
53
  }
53
54
 
54
- export function run(argv) {
55
- createProgram().parse(argv);
55
+ export function run(argv, version) {
56
+ createProgram(version).parse(argv);
56
57
  }
package/src/lib/config.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { readFileSync, writeFileSync, existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
+ import { getLatestTagVersion } from './git.js';
3
4
 
4
5
  const PKG = 'package.json';
5
6
 
@@ -45,9 +46,16 @@ export function readConfig(cwd = process.cwd()) {
45
46
  export function writeConfig(config, cwd = process.cwd()) {
46
47
  let pkg = readPkg(cwd);
47
48
  if (!pkg) {
49
+ let version = '1.0.0';
50
+ try {
51
+ const fromTags = getLatestTagVersion(cwd);
52
+ if (fromTags) version = fromTags;
53
+ } catch {
54
+ // not a git repo or no tags
55
+ }
48
56
  pkg = {
49
57
  name: 'shopify-theme',
50
- version: '1.0.0',
58
+ version,
51
59
  private: true,
52
60
  config: {},
53
61
  };
package/src/lib/git.js CHANGED
@@ -106,3 +106,31 @@ export function ensureGitRepo(cwd = process.cwd()) {
106
106
  console.log(pc.green(' Initialized git repository.'));
107
107
  }
108
108
  }
109
+
110
+ /**
111
+ * Get the latest tag version (e.g. "1.2.3") from v* tags, or null if none.
112
+ * Sorts by version so v2.0.0 > v1.9.9.
113
+ */
114
+ export function getLatestTagVersion(cwd = process.cwd()) {
115
+ try {
116
+ const out = exec('git tag -l "v*" --sort=-v:refname', cwd);
117
+ const first = out.split(/\n/)[0]?.trim();
118
+ if (!first || !first.startsWith('v')) return null;
119
+ const match = first.replace(/^v/, '').match(/^(\d+)\.(\d+)\.(\d+)(?:-|$)/);
120
+ return match ? `${match[1]}.${match[2]}.${match[3]}` : null;
121
+ } catch {
122
+ return null;
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Suggested tag for next release: v1.0.0 if no tags, else next patch (e.g. v1.2.3 → v1.2.4).
128
+ */
129
+ export function getSuggestedTagForRelease(cwd = process.cwd()) {
130
+ const latest = getLatestTagVersion(cwd);
131
+ if (!latest) return 'v1.0.0';
132
+ const parts = latest.split('.').map(Number);
133
+ if (parts.length < 3) return 'v1.0.0';
134
+ parts[2] += 1;
135
+ return `v${parts[0]}.${parts[1]}.${parts[2]}`;
136
+ }
@@ -1,5 +1,4 @@
1
- import { spawn } from 'node:child_process';
2
- import { execSync } from 'node:child_process';
1
+ import { spawn, spawnSync, execSync } from 'node:child_process';
3
2
  import pc from 'picocolors';
4
3
 
5
4
  /**
@@ -24,12 +23,12 @@ export const SECRET_DEFINITIONS = [
24
23
  'Your theme’s store URL in Shopify Admin → Settings → Domains, or use the .myshopify.com URL.',
25
24
  },
26
25
  {
27
- name: 'SHOPIFY_CLI_THEME_TOKEN',
26
+ name: 'SHOPIFY_THEME_ACCESS_TOKEN',
28
27
  required: true,
29
28
  condition: 'preview',
30
- description: 'Theme access token so CI can push preview themes to your store',
29
+ description: 'Theme access token so CI can push preview themes (password from Shopify Theme Access app)',
31
30
  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.',
31
+ 'Install the Theme Access app from Shopify: it gives you a password that password is your theme access token.',
33
32
  },
34
33
  {
35
34
  name: 'SHOP_ACCESS_TOKEN',
@@ -80,12 +79,31 @@ export function hasGitHubRemote(cwd = process.cwd()) {
80
79
  }
81
80
  }
82
81
 
82
+ /**
83
+ * Get GitHub repo as "owner/repo" from origin remote. Returns null if not a GitHub URL or parse fails.
84
+ * Used to pass -R to gh when the repo has multiple remotes.
85
+ */
86
+ export function getGitHubRepoSpec(cwd = process.cwd()) {
87
+ try {
88
+ const url = execSync('git remote get-url origin', { cwd, encoding: 'utf-8', stdio: 'pipe' }).trim();
89
+ // https://github.com/owner/repo[.git] or git@github.com:owner/repo[.git]
90
+ const m = url.match(/github\.com[/:]([^/]+)\/([^/]+?)(?:\.git)?$/i);
91
+ return m ? `${m[1]}/${m[2]}` : null;
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+
83
97
  /**
84
98
  * List repository secret names (GitHub). Returns [] on error or if none.
85
99
  */
86
100
  export function listGitHubSecrets(cwd = process.cwd()) {
87
101
  try {
88
- const out = execSync('gh secret list --json name', { cwd, encoding: 'utf-8', stdio: 'pipe' }).trim();
102
+ const repo = getGitHubRepoSpec(cwd);
103
+ const args = ['secret', 'list', '--json', 'name'];
104
+ if (repo) args.push('-R', repo);
105
+ const result = spawnSync('gh', args, { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
106
+ const out = (result.stdout || '').trim();
89
107
  const data = JSON.parse(out || '[]');
90
108
  return Array.isArray(data) ? data.map((s) => s.name).filter(Boolean) : [];
91
109
  } catch {
@@ -109,11 +127,16 @@ export function listGitLabVariables(cwd = process.cwd()) {
109
127
 
110
128
  /**
111
129
  * Set a repository secret via gh CLI. Value is passed via stdin to avoid argv exposure.
130
+ * Uses -R owner/repo from origin when available so gh works with multiple remotes.
112
131
  */
113
- export function setSecret(name, value) {
132
+ export function setSecret(name, value, cwd = process.cwd()) {
114
133
  return new Promise((resolve, reject) => {
115
- const child = spawn('gh', ['secret', 'set', name], {
134
+ const repo = getGitHubRepoSpec(cwd);
135
+ const args = ['secret', 'set', name];
136
+ if (repo) args.push('-R', repo);
137
+ const child = spawn('gh', args, {
116
138
  stdio: ['pipe', 'inherit', 'inherit'],
139
+ cwd,
117
140
  });
118
141
  child.on('error', reject);
119
142
  child.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`gh secret set exited with ${code}`))));
@@ -174,6 +197,44 @@ export function aliasToSecretSuffix(alias) {
174
197
  return String(alias).replace(/-/g, '_').toUpperCase();
175
198
  }
176
199
 
200
+ /**
201
+ * Get store domain for a theme token secret name (single or SHOPIFY_THEME_ACCESS_TOKEN_<ALIAS>).
202
+ * Returns store domain or null if not found.
203
+ */
204
+ export function getStoreUrlForThemeTokenSecret(secretName, stores = []) {
205
+ if (!stores.length) return null;
206
+ if (secretName === 'SHOPIFY_THEME_ACCESS_TOKEN') return stores[0]?.domain ?? null;
207
+ if (!secretName.startsWith('SHOPIFY_THEME_ACCESS_TOKEN_')) return null;
208
+ const suffix = secretName.replace('SHOPIFY_THEME_ACCESS_TOKEN_', '');
209
+ const store = stores.find((s) => aliasToSecretSuffix(s.alias) === suffix);
210
+ return store?.domain ?? null;
211
+ }
212
+
213
+ /**
214
+ * Test theme access token against the store (read-only list). Never logs or persists the token.
215
+ * Returns { ok: true } or { ok: false, error: string }. Safe to call; token only sent to Shopify CLI.
216
+ */
217
+ export function validateThemeAccessToken(storeUrl, token) {
218
+ if (!storeUrl || !token) return { ok: false, error: 'Missing store URL or token' };
219
+ return new Promise((resolve) => {
220
+ const child = spawn('shopify', ['theme', 'list', '--store', storeUrl, '--password', token], {
221
+ stdio: ['ignore', 'pipe', 'pipe'],
222
+ env: { ...process.env },
223
+ });
224
+ let stderr = '';
225
+ child.stderr?.on('data', (chunk) => {
226
+ stderr += String(chunk);
227
+ });
228
+ child.on('error', (err) => {
229
+ resolve({ ok: false, error: err.code === 'ENOENT' ? 'Shopify CLI not installed (npm install -g @shopify/cli @shopify/theme)' : err.message });
230
+ });
231
+ child.on('close', (code) => {
232
+ if (code === 0) resolve({ ok: true });
233
+ else resolve({ ok: false, error: stderr.trim() || `Exit code ${code}` });
234
+ });
235
+ });
236
+ }
237
+
177
238
  /**
178
239
  * Store URL secrets are set from config (store domains added during init), not prompted.
179
240
  * Returns [{ name, value }] for SHOPIFY_STORE_URL and/or SHOPIFY_STORE_URL_<ALIAS>.
@@ -202,7 +263,7 @@ export function getStoreUrlSecretsFromConfig({ enablePreviewWorkflows, enableBui
202
263
 
203
264
  /**
204
265
  * 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.
266
+ * Theme token(s) are required when preview is enabled. In multi-store, all Shopify tokens are per-store.
206
267
  */
207
268
  export function getSecretsToPrompt({ enablePreviewWorkflows, enableBuildWorkflows, mode = 'single', stores = [] }) {
208
269
  const isMulti = mode === 'multi' && stores.length > 1;
@@ -216,23 +277,46 @@ export function getSecretsToPrompt({ enablePreviewWorkflows, enableBuildWorkflow
216
277
  return false;
217
278
  });
218
279
 
219
- const dropPreviewGeneric =
220
- isMulti && enablePreviewWorkflows
221
- ? (s) => s.name !== 'SHOPIFY_CLI_THEME_TOKEN'
280
+ // Multi-store: drop generic Shopify tokens; we prompt per-store below
281
+ const dropForMulti =
282
+ isMulti
283
+ ? (s) =>
284
+ s.name !== 'SHOPIFY_THEME_ACCESS_TOKEN' &&
285
+ s.name !== 'SHOP_ACCESS_TOKEN' &&
286
+ s.name !== 'SHOP_PASSWORD'
222
287
  : () => true;
223
288
 
224
- let list = base.filter(dropPreviewGeneric);
289
+ let list = base.filter(dropForMulti);
225
290
 
226
- if (isMulti && enablePreviewWorkflows) {
291
+ // Multi-store: prompt per store (theme token, then access token + password if build) so user configures one store at a time
292
+ if (isMulti) {
227
293
  for (const store of stores) {
228
294
  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
- });
295
+ if (enablePreviewWorkflows) {
296
+ list.push({
297
+ name: `SHOPIFY_THEME_ACCESS_TOKEN_${suffix}`,
298
+ required: true,
299
+ description: `Store ${store.alias}: Theme access token (password from Theme Access app)`,
300
+ whereToGet: 'Theme Access app in Shopify the password it gives is the token for this store.',
301
+ });
302
+ }
303
+ if (enableBuildWorkflows) {
304
+ list.push(
305
+ {
306
+ name: `SHOP_ACCESS_TOKEN_${suffix}`,
307
+ required: false,
308
+ description: `Store ${store.alias}: API access token for Lighthouse`,
309
+ whereToGet:
310
+ 'Shopify Admin → Develop apps → your app → API credentials (Admin/storefront access).',
311
+ },
312
+ {
313
+ name: `SHOP_PASSWORD_${suffix}`,
314
+ required: false,
315
+ description: `Store ${store.alias}: Storefront password if protected (optional)`,
316
+ whereToGet: 'Storefront password in Shopify Admin for this store.',
317
+ }
318
+ );
319
+ }
236
320
  }
237
321
  }
238
322
 
@@ -247,17 +331,28 @@ export function getStoreUrlSecretForNewStore(store) {
247
331
  }
248
332
 
249
333
  /**
250
- * Per-store secret to prompt for when adding a store (theme token only; URL is set from store.domain).
334
+ * Per-store secrets to prompt for when adding a store (URL is set from store.domain).
251
335
  */
252
336
  export function getSecretsToPromptForNewStore(store) {
253
337
  const suffix = aliasToSecretSuffix(store.alias);
254
338
  return [
255
339
  {
256
- name: `SHOPIFY_CLI_THEME_TOKEN_${suffix}`,
340
+ name: `SHOPIFY_THEME_ACCESS_TOKEN_${suffix}`,
257
341
  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.',
342
+ description: `Store ${store.alias}: Theme access token (password from Theme Access app)`,
343
+ whereToGet: 'Theme Access app in Shopify — the password it gives is the token for this store.',
344
+ },
345
+ {
346
+ name: `SHOP_ACCESS_TOKEN_${suffix}`,
347
+ required: false,
348
+ description: `Store ${store.alias}: API access token for Lighthouse (if using build workflows)`,
349
+ whereToGet: 'Shopify Admin → Develop apps → your app → API credentials.',
350
+ },
351
+ {
352
+ name: `SHOP_PASSWORD_${suffix}`,
353
+ required: false,
354
+ description: `Store ${store.alias}: Storefront password if protected (optional)`,
355
+ whereToGet: 'Storefront password in Shopify Admin for this store.',
261
356
  },
262
357
  ];
263
358
  }
@@ -225,3 +225,16 @@ export async function promptSecretValue(secret, index, total) {
225
225
  if (!trimmed && !secret.required) return null;
226
226
  return trimmed || null;
227
227
  }
228
+
229
+ /**
230
+ * Ask whether to test the theme token against the store now. Returns true to test, false to skip.
231
+ */
232
+ export async function promptTestThemeToken() {
233
+ const { test } = await prompts({
234
+ type: 'confirm',
235
+ name: 'test',
236
+ message: 'Test this token against the store now?',
237
+ initial: true,
238
+ });
239
+ return !!test;
240
+ }
@@ -22,7 +22,7 @@ jobs:
22
22
  pull-requests: write
23
23
  env:
24
24
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
25
- SHOPIFY_CLI_THEME_TOKEN: ${{ secrets.SHOPIFY_CLI_THEME_TOKEN }}
25
+ SHOPIFY_THEME_ACCESS_TOKEN: ${{ secrets.SHOPIFY_THEME_ACCESS_TOKEN }}
26
26
  steps:
27
27
  - uses: actions/checkout@v4
28
28
 
@@ -72,8 +72,8 @@ jobs:
72
72
 
73
73
  - name: Create PR to live branch
74
74
  env:
75
- SHOPIFY_CLI_THEME_TOKEN_SCOPED: ${{ secrets[format('SHOPIFY_CLI_THEME_TOKEN_{0}', steps.alias.outputs.alias_secret)] }}
76
- SHOPIFY_CLI_THEME_TOKEN_DEFAULT: ${{ secrets.SHOPIFY_CLI_THEME_TOKEN }}
75
+ SHOPIFY_THEME_ACCESS_TOKEN_SCOPED: ${{ secrets[format('SHOPIFY_THEME_ACCESS_TOKEN_{0}', steps.alias.outputs.alias_secret)] }}
76
+ SHOPIFY_THEME_ACCESS_TOKEN_DEFAULT: ${{ secrets.SHOPIFY_THEME_ACCESS_TOKEN }}
77
77
  run: |
78
78
  STAGING="${{ steps.alias.outputs.staging_branch }}"
79
79
  LIVE="${{ steps.alias.outputs.live_branch }}"
@@ -81,7 +81,7 @@ jobs:
81
81
  DOMAIN="${{ steps.store.outputs.domain }}"
82
82
  STAGING_THEME_ID=""
83
83
  REPO_NAME="${GITHUB_REPOSITORY#*/}"
84
- SHOPIFY_TOKEN="${SHOPIFY_CLI_THEME_TOKEN_SCOPED:-$SHOPIFY_CLI_THEME_TOKEN_DEFAULT}"
84
+ SHOPIFY_TOKEN="${SHOPIFY_THEME_ACCESS_TOKEN_SCOPED:-$SHOPIFY_THEME_ACCESS_TOKEN_DEFAULT}"
85
85
 
86
86
  # Check if live branch exists
87
87
  if ! git ls-remote --heads origin "$LIVE" | grep -q "$LIVE"; then
@@ -69,19 +69,19 @@ jobs:
69
69
  - name: Validate Shopify credentials
70
70
  env:
71
71
  SHOPIFY_STORE_URL_SCOPED: ${{ secrets[format('SHOPIFY_STORE_URL_{0}', steps.resolve.outputs.alias_secret)] }}
72
- SHOPIFY_CLI_THEME_TOKEN_SCOPED: ${{ secrets[format('SHOPIFY_CLI_THEME_TOKEN_{0}', steps.resolve.outputs.alias_secret)] }}
72
+ SHOPIFY_THEME_ACCESS_TOKEN_SCOPED: ${{ secrets[format('SHOPIFY_THEME_ACCESS_TOKEN_{0}', steps.resolve.outputs.alias_secret)] }}
73
73
  SHOPIFY_STORE_URL_DEFAULT: ${{ secrets.SHOPIFY_STORE_URL }}
74
- SHOPIFY_CLI_THEME_TOKEN_DEFAULT: ${{ secrets.SHOPIFY_CLI_THEME_TOKEN }}
74
+ SHOPIFY_THEME_ACCESS_TOKEN_DEFAULT: ${{ secrets.SHOPIFY_THEME_ACCESS_TOKEN }}
75
75
  run: |
76
76
  SHOPIFY_STORE_URL="${SHOPIFY_STORE_URL_SCOPED:-$SHOPIFY_STORE_URL_DEFAULT}"
77
- SHOPIFY_CLI_THEME_TOKEN="${SHOPIFY_CLI_THEME_TOKEN_SCOPED:-$SHOPIFY_CLI_THEME_TOKEN_DEFAULT}"
77
+ SHOPIFY_THEME_ACCESS_TOKEN="${SHOPIFY_THEME_ACCESS_TOKEN_SCOPED:-$SHOPIFY_THEME_ACCESS_TOKEN_DEFAULT}"
78
78
 
79
79
  if [ -z "$SHOPIFY_STORE_URL" ]; then
80
80
  echo "No store URL secret found. Expected SHOPIFY_STORE_URL_${{ steps.resolve.outputs.alias_secret }} or SHOPIFY_STORE_URL."
81
81
  exit 1
82
82
  fi
83
- if [ -z "$SHOPIFY_CLI_THEME_TOKEN" ]; then
84
- echo "No theme token secret found. Expected SHOPIFY_CLI_THEME_TOKEN_${{ steps.resolve.outputs.alias_secret }} or SHOPIFY_CLI_THEME_TOKEN."
83
+ if [ -z "$SHOPIFY_THEME_ACCESS_TOKEN" ]; then
84
+ echo "No theme token secret found. Expected SHOPIFY_THEME_ACCESS_TOKEN_${{ steps.resolve.outputs.alias_secret }} or SHOPIFY_THEME_ACCESS_TOKEN."
85
85
  exit 1
86
86
  fi
87
87
  echo "Shopify credentials are configured for alias: ${{ steps.resolve.outputs.alias }}"
@@ -94,7 +94,7 @@ jobs:
94
94
  store_alias: ${{ needs.validate-environment.outputs.store_alias }}
95
95
  secrets:
96
96
  SHOPIFY_STORE_URL: ${{ secrets[format('SHOPIFY_STORE_URL_{0}', needs.validate-environment.outputs.store_alias_secret)] || secrets.SHOPIFY_STORE_URL }}
97
- SHOPIFY_CLI_THEME_TOKEN: ${{ secrets[format('SHOPIFY_CLI_THEME_TOKEN_{0}', needs.validate-environment.outputs.store_alias_secret)] || secrets.SHOPIFY_CLI_THEME_TOKEN }}
97
+ SHOPIFY_THEME_ACCESS_TOKEN: ${{ secrets[format('SHOPIFY_THEME_ACCESS_TOKEN_{0}', needs.validate-environment.outputs.store_alias_secret)] || secrets.SHOPIFY_THEME_ACCESS_TOKEN }}
98
98
 
99
99
  rename-theme:
100
100
  needs: [share-theme, extract-pr-number, validate-environment]
@@ -106,7 +106,7 @@ jobs:
106
106
  store_alias: ${{ needs.validate-environment.outputs.store_alias }}
107
107
  secrets:
108
108
  SHOPIFY_STORE_URL: ${{ secrets[format('SHOPIFY_STORE_URL_{0}', needs.validate-environment.outputs.store_alias_secret)] || secrets.SHOPIFY_STORE_URL }}
109
- SHOPIFY_CLI_THEME_TOKEN: ${{ secrets[format('SHOPIFY_CLI_THEME_TOKEN_{0}', needs.validate-environment.outputs.store_alias_secret)] || secrets.SHOPIFY_CLI_THEME_TOKEN }}
109
+ SHOPIFY_THEME_ACCESS_TOKEN: ${{ secrets[format('SHOPIFY_THEME_ACCESS_TOKEN_{0}', needs.validate-environment.outputs.store_alias_secret)] || secrets.SHOPIFY_THEME_ACCESS_TOKEN }}
110
110
 
111
111
  comment-on-pr:
112
112
  needs: [share-theme, rename-theme, extract-pr-number, validate-environment]
@@ -29,17 +29,17 @@ jobs:
29
29
  id: cleanup
30
30
  env:
31
31
  SHOPIFY_STORE_URL: ${{ secrets.SHOPIFY_STORE_URL }}
32
- SHOPIFY_CLI_THEME_TOKEN: ${{ secrets.SHOPIFY_CLI_THEME_TOKEN }}
32
+ SHOPIFY_THEME_ACCESS_TOKEN: ${{ secrets.SHOPIFY_THEME_ACCESS_TOKEN }}
33
33
  PR_NUMBER: ${{ inputs.pr_number }}
34
34
  run: |
35
- if [ -z "$SHOPIFY_STORE_URL" ] || [ -z "$SHOPIFY_CLI_THEME_TOKEN" ]; then
35
+ if [ -z "$SHOPIFY_STORE_URL" ] || [ -z "$SHOPIFY_THEME_ACCESS_TOKEN" ]; then
36
36
  echo "Missing Shopify secrets."
37
37
  exit 1
38
38
  fi
39
39
 
40
40
  THEME_LIST=$(shopify theme list \
41
41
  --store "$SHOPIFY_STORE_URL" \
42
- --password "$SHOPIFY_CLI_THEME_TOKEN" \
42
+ --password "$SHOPIFY_THEME_ACCESS_TOKEN" \
43
43
  --json 2>/dev/null || echo "[]")
44
44
 
45
45
  DELETED_COUNT=0
@@ -52,7 +52,7 @@ jobs:
52
52
  if printf "%s" "$THEME_NAME" | grep -q "PR${PR_NUMBER}"; then
53
53
  if shopify theme delete \
54
54
  --store "$SHOPIFY_STORE_URL" \
55
- --password "$SHOPIFY_CLI_THEME_TOKEN" \
55
+ --password "$SHOPIFY_THEME_ACCESS_TOKEN" \
56
56
  --force \
57
57
  --theme "$THEME_ID" 2>/dev/null; then
58
58
  DELETED_COUNT=$((DELETED_COUNT + 1))
@@ -26,7 +26,7 @@ on:
26
26
  secrets:
27
27
  SHOPIFY_STORE_URL:
28
28
  required: false
29
- SHOPIFY_CLI_THEME_TOKEN:
29
+ SHOPIFY_THEME_ACCESS_TOKEN:
30
30
  required: false
31
31
 
32
32
  jobs:
@@ -39,7 +39,7 @@ jobs:
39
39
  - name: Rename theme
40
40
  env:
41
41
  SHOPIFY_STORE_URL: ${{ secrets.SHOPIFY_STORE_URL }}
42
- SHOPIFY_CLI_THEME_TOKEN: ${{ secrets.SHOPIFY_CLI_THEME_TOKEN }}
42
+ SHOPIFY_THEME_ACCESS_TOKEN: ${{ secrets.SHOPIFY_THEME_ACCESS_TOKEN }}
43
43
  THEME_ID: ${{ inputs.theme_id }}
44
44
  THEME_NAME: ${{ inputs.theme_name }}
45
45
  PR_NUMBER: ${{ inputs.pr_number }}
@@ -48,7 +48,7 @@ jobs:
48
48
  echo "Missing theme_id/theme_name."
49
49
  exit 1
50
50
  fi
51
- if [ -z "$SHOPIFY_STORE_URL" ] || [ -z "$SHOPIFY_CLI_THEME_TOKEN" ]; then
51
+ if [ -z "$SHOPIFY_STORE_URL" ] || [ -z "$SHOPIFY_THEME_ACCESS_TOKEN" ]; then
52
52
  echo "Missing Shopify store URL/token for rename."
53
53
  exit 1
54
54
  fi
@@ -58,7 +58,7 @@ jobs:
58
58
 
59
59
  if shopify theme rename \
60
60
  --store "$SHOPIFY_STORE_URL" \
61
- --password "$SHOPIFY_CLI_THEME_TOKEN" \
61
+ --password "$SHOPIFY_THEME_ACCESS_TOKEN" \
62
62
  --theme "$THEME_ID" \
63
63
  --name "$NEW_THEME_NAME" 2>&1; then
64
64
  echo "Rename succeeded with password auth."
@@ -28,7 +28,7 @@ on:
28
28
  secrets:
29
29
  SHOPIFY_STORE_URL:
30
30
  required: false
31
- SHOPIFY_CLI_THEME_TOKEN:
31
+ SHOPIFY_THEME_ACCESS_TOKEN:
32
32
  required: false
33
33
 
34
34
  jobs:
@@ -56,16 +56,16 @@ jobs:
56
56
  id: share
57
57
  env:
58
58
  SHOPIFY_STORE_URL: ${{ secrets.SHOPIFY_STORE_URL }}
59
- SHOPIFY_CLI_THEME_TOKEN: ${{ secrets.SHOPIFY_CLI_THEME_TOKEN }}
59
+ SHOPIFY_THEME_ACCESS_TOKEN: ${{ secrets.SHOPIFY_THEME_ACCESS_TOKEN }}
60
60
  run: |
61
- if [ -z "$SHOPIFY_STORE_URL" ] || [ -z "$SHOPIFY_CLI_THEME_TOKEN" ]; then
61
+ if [ -z "$SHOPIFY_STORE_URL" ] || [ -z "$SHOPIFY_THEME_ACCESS_TOKEN" ]; then
62
62
  echo "Missing Shopify secrets."
63
63
  exit 1
64
64
  fi
65
65
 
66
66
  OUTPUT=$(shopify theme share \
67
67
  --store "$SHOPIFY_STORE_URL" \
68
- --password "$SHOPIFY_CLI_THEME_TOKEN" 2>&1)
68
+ --password "$SHOPIFY_THEME_ACCESS_TOKEN" 2>&1)
69
69
  STATUS=$?
70
70
 
71
71
  echo "$OUTPUT"