climaybe 1.3.1 → 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 +2 -2
- package/bin/cli.js +9 -1
- package/package.json +1 -1
- package/src/commands/add-store.js +23 -8
- package/src/commands/init.js +31 -9
- package/src/index.js +4 -10
- package/src/lib/config.js +9 -1
- package/src/lib/git.js +28 -0
- package/src/lib/github-secrets.js +93 -21
- package/src/lib/prompts.js +13 -0
- package/src/workflows/multi/pr-to-live.yml +4 -4
- package/src/workflows/preview/pr-update.yml +7 -7
- package/src/workflows/preview/reusable-cleanup-themes.yml +4 -4
- package/src/workflows/preview/reusable-rename-theme.yml +4 -4
- package/src/workflows/preview/reusable-share-theme.yml +4 -4
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
|
-
| `
|
|
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 `
|
|
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
|
-
|
|
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,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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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`));
|
package/src/commands/init.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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`));
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { createRequire } from 'node:module';
|
|
2
|
-
import { dirname, join } from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
1
|
import { Command } from 'commander';
|
|
5
2
|
import { initCommand, reinitCommand } from './commands/init.js';
|
|
6
3
|
import { addStoreCommand } from './commands/add-store.js';
|
|
@@ -8,14 +5,11 @@ import { switchCommand } from './commands/switch.js';
|
|
|
8
5
|
import { syncCommand } from './commands/sync.js';
|
|
9
6
|
import { updateWorkflowsCommand } from './commands/update-workflows.js';
|
|
10
7
|
|
|
11
|
-
const require = createRequire(import.meta.url);
|
|
12
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
-
const { version } = require(join(__dirname, '..', 'package.json'));
|
|
14
|
-
|
|
15
8
|
/**
|
|
16
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).
|
|
17
11
|
*/
|
|
18
|
-
export function createProgram() {
|
|
12
|
+
export function createProgram(version = '0.0.0') {
|
|
19
13
|
const program = new Command();
|
|
20
14
|
|
|
21
15
|
program
|
|
@@ -58,6 +52,6 @@ export function createProgram() {
|
|
|
58
52
|
return program;
|
|
59
53
|
}
|
|
60
54
|
|
|
61
|
-
export function run(argv) {
|
|
62
|
-
createProgram().parse(argv);
|
|
55
|
+
export function run(argv, version) {
|
|
56
|
+
createProgram(version).parse(argv);
|
|
63
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
|
|
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
|
+
}
|
|
@@ -23,12 +23,12 @@ export const SECRET_DEFINITIONS = [
|
|
|
23
23
|
'Your theme’s store URL in Shopify Admin → Settings → Domains, or use the .myshopify.com URL.',
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
|
-
name: '
|
|
26
|
+
name: 'SHOPIFY_THEME_ACCESS_TOKEN',
|
|
27
27
|
required: true,
|
|
28
28
|
condition: 'preview',
|
|
29
|
-
description: 'Theme access token so CI can push preview themes
|
|
29
|
+
description: 'Theme access token so CI can push preview themes (password from Shopify Theme Access app)',
|
|
30
30
|
whereToGet:
|
|
31
|
-
'
|
|
31
|
+
'Install the Theme Access app from Shopify: it gives you a password — that password is your theme access token.',
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
name: 'SHOP_ACCESS_TOKEN',
|
|
@@ -197,6 +197,44 @@ export function aliasToSecretSuffix(alias) {
|
|
|
197
197
|
return String(alias).replace(/-/g, '_').toUpperCase();
|
|
198
198
|
}
|
|
199
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
|
+
|
|
200
238
|
/**
|
|
201
239
|
* Store URL secrets are set from config (store domains added during init), not prompted.
|
|
202
240
|
* Returns [{ name, value }] for SHOPIFY_STORE_URL and/or SHOPIFY_STORE_URL_<ALIAS>.
|
|
@@ -225,7 +263,7 @@ export function getStoreUrlSecretsFromConfig({ enablePreviewWorkflows, enableBui
|
|
|
225
263
|
|
|
226
264
|
/**
|
|
227
265
|
* Get secrets we need to prompt for (excludes store URLs; those are set from config).
|
|
228
|
-
* 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.
|
|
229
267
|
*/
|
|
230
268
|
export function getSecretsToPrompt({ enablePreviewWorkflows, enableBuildWorkflows, mode = 'single', stores = [] }) {
|
|
231
269
|
const isMulti = mode === 'multi' && stores.length > 1;
|
|
@@ -239,23 +277,46 @@ export function getSecretsToPrompt({ enablePreviewWorkflows, enableBuildWorkflow
|
|
|
239
277
|
return false;
|
|
240
278
|
});
|
|
241
279
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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'
|
|
245
287
|
: () => true;
|
|
246
288
|
|
|
247
|
-
let list = base.filter(
|
|
289
|
+
let list = base.filter(dropForMulti);
|
|
248
290
|
|
|
249
|
-
|
|
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) {
|
|
250
293
|
for (const store of stores) {
|
|
251
294
|
const suffix = aliasToSecretSuffix(store.alias);
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
'
|
|
258
|
-
|
|
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
|
+
}
|
|
259
320
|
}
|
|
260
321
|
}
|
|
261
322
|
|
|
@@ -270,17 +331,28 @@ export function getStoreUrlSecretForNewStore(store) {
|
|
|
270
331
|
}
|
|
271
332
|
|
|
272
333
|
/**
|
|
273
|
-
* Per-store
|
|
334
|
+
* Per-store secrets to prompt for when adding a store (URL is set from store.domain).
|
|
274
335
|
*/
|
|
275
336
|
export function getSecretsToPromptForNewStore(store) {
|
|
276
337
|
const suffix = aliasToSecretSuffix(store.alias);
|
|
277
338
|
return [
|
|
278
339
|
{
|
|
279
|
-
name: `
|
|
340
|
+
name: `SHOPIFY_THEME_ACCESS_TOKEN_${suffix}`,
|
|
280
341
|
required: true,
|
|
281
|
-
description: `Store ${store.alias}: Theme access token
|
|
282
|
-
whereToGet:
|
|
283
|
-
|
|
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.',
|
|
284
356
|
},
|
|
285
357
|
];
|
|
286
358
|
}
|
package/src/lib/prompts.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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="${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 "$
|
|
84
|
-
echo "No theme token secret found. Expected
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 "$
|
|
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 "$
|
|
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 "$
|
|
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
|
-
|
|
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
|
-
|
|
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 "$
|
|
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 "$
|
|
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
|
-
|
|
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
|
-
|
|
59
|
+
SHOPIFY_THEME_ACCESS_TOKEN: ${{ secrets.SHOPIFY_THEME_ACCESS_TOKEN }}
|
|
60
60
|
run: |
|
|
61
|
-
if [ -z "$SHOPIFY_STORE_URL" ] || [ -z "$
|
|
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 "$
|
|
68
|
+
--password "$SHOPIFY_THEME_ACCESS_TOKEN" 2>&1)
|
|
69
69
|
STATUS=$?
|
|
70
70
|
|
|
71
71
|
echo "$OUTPUT"
|