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 +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 +34 -12
- package/src/index.js +5 -4
- package/src/lib/config.js +9 -1
- package/src/lib/git.js +28 -0
- package/src/lib/github-secrets.js +121 -26
- 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`));
|
|
@@ -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
|
|
211
|
+
const hasConfig = existing != null && typeof existing === 'object';
|
|
190
212
|
|
|
191
|
-
if (
|
|
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.
|
|
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(
|
|
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
|
|
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: '
|
|
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
|
|
29
|
+
description: 'Theme access token so CI can push preview themes (password from Shopify Theme Access app)',
|
|
31
30
|
whereToGet:
|
|
32
|
-
'
|
|
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
|
|
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
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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(
|
|
289
|
+
let list = base.filter(dropForMulti);
|
|
225
290
|
|
|
226
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
'
|
|
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
|
|
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: `
|
|
340
|
+
name: `SHOPIFY_THEME_ACCESS_TOKEN_${suffix}`,
|
|
257
341
|
required: true,
|
|
258
|
-
description: `Store ${store.alias}: Theme access token
|
|
259
|
-
whereToGet:
|
|
260
|
-
|
|
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
|
}
|
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"
|