climaybe 1.1.0 → 1.3.1
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 +16 -15
- package/package.json +39 -2
- package/src/commands/add-store.js +72 -1
- package/src/commands/init.js +140 -15
- package/src/index.js +23 -4
- package/src/lib/config.js +1 -0
- package/src/lib/git.js +21 -3
- package/src/lib/github-secrets.js +286 -0
- package/src/lib/prompts.js +61 -1
- package/src/workflows/build/build-pipeline.yml +6 -6
- package/src/workflows/preview/pr-update.yml +9 -1
package/README.md
CHANGED
|
@@ -182,6 +182,8 @@ Enabled via `climaybe init` prompt (`Enable build + Lighthouse workflows?`).
|
|
|
182
182
|
|
|
183
183
|
All version bumps update `config/settings_schema.json` automatically.
|
|
184
184
|
|
|
185
|
+
**Full specification:** For detailed versioning rules, local dev flow, hotfix behavior, and alignment with the external CI/CD doc, see **[CI/CD Reference](docs/CI_CD_REFERENCE.md)**.
|
|
186
|
+
|
|
185
187
|
## File Sync Rules (Multi-store)
|
|
186
188
|
|
|
187
189
|
**Synced between root and `stores/<alias>/`:**
|
|
@@ -202,30 +204,20 @@ All version bumps update `config/settings_schema.json` automatically.
|
|
|
202
204
|
|
|
203
205
|
## GitHub Secrets
|
|
204
206
|
|
|
205
|
-
Add the following
|
|
207
|
+
Add the following secrets to your GitHub repository (or use **GitLab CI/CD variables** if you use GitLab). You can configure them during `climaybe init` via the GitHub or GitLab CLI.
|
|
206
208
|
|
|
207
209
|
| Secret | Required | Description |
|
|
208
210
|
|--------|----------|-------------|
|
|
209
211
|
| `GEMINI_API_KEY` | Yes | Google Gemini API key for changelog generation |
|
|
210
|
-
| `SHOPIFY_STORE_URL` |
|
|
211
|
-
| `SHOPIFY_CLI_THEME_TOKEN` |
|
|
212
|
-
| `SHOP_STORE` | Optional* | Required only when optional build workflows are enabled (Lighthouse) |
|
|
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
214
|
| `SHOP_ACCESS_TOKEN` | Optional* | Required only when optional build workflows are enabled (Lighthouse) |
|
|
214
215
|
| `LHCI_GITHUB_APP_TOKEN` | Optional* | Required only when optional build workflows are enabled (Lighthouse) |
|
|
215
216
|
| `SHOP_PASSWORD` | Optional | Used by Lighthouse action when your store requires password auth |
|
|
216
217
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
- `SHOPIFY_STORE_URL_<ALIAS>`
|
|
220
|
-
- `SHOPIFY_CLI_THEME_TOKEN_<ALIAS>`
|
|
221
|
-
|
|
222
|
-
`<ALIAS>` must be uppercase with hyphens converted to underscores.
|
|
223
|
-
Example: alias `voldt-norway` → `SHOPIFY_STORE_URL_VOLDT_NORWAY`.
|
|
224
|
-
|
|
225
|
-
Preview workflows also support the same scoped secret pattern and will use:
|
|
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.
|
|
226
219
|
|
|
227
|
-
|
|
228
|
-
2. fallback to `SHOPIFY_*` (default)
|
|
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`).
|
|
229
221
|
|
|
230
222
|
## Directory Structure (Multi-store)
|
|
231
223
|
|
|
@@ -250,6 +242,15 @@ Preview workflows also support the same scoped secret pattern and will use:
|
|
|
250
242
|
└── .github/workflows/
|
|
251
243
|
```
|
|
252
244
|
|
|
245
|
+
## Releases and versioning
|
|
246
|
+
|
|
247
|
+
- **Branch:** Single default branch `main`. Feature branches open as PRs into `main`.
|
|
248
|
+
- **Versioning:** [SemVer](https://semver.org/). Versions are **bumped automatically** when PRs are merged to `main` using [conventional commits](https://www.conventionalcommits.org/): `fix:` → patch, `feat:` → minor, `BREAKING CHANGE` or `feat!:` → major.
|
|
249
|
+
- **Flow:** Merge to `main` → [Release version](.github/workflows/release-version.yml) runs semantic-release (bumps `package.json`, pushes tag) → tag push triggers [Release](.github/workflows/release.yml) (tests + publish to npm). Requires `NPM_TOKEN` secret for npm publish.
|
|
250
|
+
- **CI:** Every PR and push to `main` runs tests on Node 20 and 22 ([CI workflow](.github/workflows/ci.yml)).
|
|
251
|
+
|
|
252
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for branch, PR, and conventional-commit details.
|
|
253
|
+
|
|
253
254
|
## License
|
|
254
255
|
|
|
255
256
|
MIT — Electric Maybe
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "climaybe",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.3.1",
|
|
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": {
|
|
@@ -29,9 +29,46 @@
|
|
|
29
29
|
},
|
|
30
30
|
"bugs": "https://github.com/electricmaybe/climaybe/issues",
|
|
31
31
|
"homepage": "https://github.com/electricmaybe/climaybe#readme",
|
|
32
|
+
"scripts": {
|
|
33
|
+
"test": "node scripts/run-tests.js 2>&1 | tee test.log",
|
|
34
|
+
"prepare": "husky"
|
|
35
|
+
},
|
|
36
|
+
"release": {
|
|
37
|
+
"branches": [
|
|
38
|
+
"main"
|
|
39
|
+
],
|
|
40
|
+
"plugins": [
|
|
41
|
+
"@semantic-release/commit-analyzer",
|
|
42
|
+
"@semantic-release/release-notes-generator",
|
|
43
|
+
[
|
|
44
|
+
"@semantic-release/npm",
|
|
45
|
+
{
|
|
46
|
+
"npmPublish": false
|
|
47
|
+
}
|
|
48
|
+
],
|
|
49
|
+
[
|
|
50
|
+
"@semantic-release/git",
|
|
51
|
+
{
|
|
52
|
+
"assets": [
|
|
53
|
+
"package.json",
|
|
54
|
+
"package-lock.json"
|
|
55
|
+
],
|
|
56
|
+
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
]
|
|
60
|
+
},
|
|
32
61
|
"dependencies": {
|
|
33
|
-
"commander": "^
|
|
62
|
+
"commander": "^14.0.3",
|
|
34
63
|
"picocolors": "^1.1.1",
|
|
35
64
|
"prompts": "^2.4.2"
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@commitlint/cli": "^20.4.4",
|
|
68
|
+
"@commitlint/config-conventional": "^20.4.4",
|
|
69
|
+
"@semantic-release/git": "^10.0.1",
|
|
70
|
+
"@semantic-release/npm": "^13.1.5",
|
|
71
|
+
"husky": "^9.1.7",
|
|
72
|
+
"semantic-release": "^25.0.3"
|
|
36
73
|
}
|
|
37
74
|
}
|
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import pc from 'picocolors';
|
|
2
|
-
import { promptNewStore } from '../lib/prompts.js';
|
|
2
|
+
import { promptNewStore, promptConfigureCISecrets, promptUpdateExistingSecrets, promptSecretValue } 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';
|
|
6
6
|
import { createStoreDirectories } from '../lib/store-sync.js';
|
|
7
|
+
import {
|
|
8
|
+
isGhAvailable,
|
|
9
|
+
hasGitHubRemote,
|
|
10
|
+
isGlabAvailable,
|
|
11
|
+
hasGitLabRemote,
|
|
12
|
+
listGitHubSecrets,
|
|
13
|
+
listGitLabVariables,
|
|
14
|
+
getStoreUrlSecretForNewStore,
|
|
15
|
+
getSecretsToPromptForNewStore,
|
|
16
|
+
setSecret,
|
|
17
|
+
setGitLabVariable,
|
|
18
|
+
} from '../lib/github-secrets.js';
|
|
7
19
|
|
|
8
20
|
export async function addStoreCommand() {
|
|
9
21
|
console.log(pc.bold('\n climaybe — Add Store\n'));
|
|
@@ -54,4 +66,63 @@ export async function addStoreCommand() {
|
|
|
54
66
|
console.log(pc.bold(pc.green('\n Store added successfully!\n')));
|
|
55
67
|
console.log(pc.dim(` New branches: staging-${store.alias}, live-${store.alias}`));
|
|
56
68
|
console.log(pc.dim(` Store dir: stores/${store.alias}/\n`));
|
|
69
|
+
|
|
70
|
+
// If preview workflows are on, offer to set this store's CI secrets (multi-store uses per-store secrets)
|
|
71
|
+
if (includePreview) {
|
|
72
|
+
const ciHost = await promptConfigureCISecrets();
|
|
73
|
+
if (ciHost !== 'skip') {
|
|
74
|
+
const setter =
|
|
75
|
+
ciHost === 'github'
|
|
76
|
+
? { check: isGhAvailable, checkRemote: hasGitHubRemote, set: setSecret, name: 'GitHub' }
|
|
77
|
+
: { check: isGlabAvailable, checkRemote: hasGitLabRemote, set: setGitLabVariable, name: 'GitLab' };
|
|
78
|
+
|
|
79
|
+
if (!setter.check()) {
|
|
80
|
+
console.log(pc.yellow(` ${setter.name} CLI is not installed or not logged in. Add secrets manually in repo Settings.`));
|
|
81
|
+
} else if (!setter.checkRemote()) {
|
|
82
|
+
console.log(pc.yellow(' No ' + setter.name + ' remote (origin). Add secrets manually after pushing.'));
|
|
83
|
+
} else {
|
|
84
|
+
let setCount = 0;
|
|
85
|
+
const { name: urlName, value: urlValue } = getStoreUrlSecretForNewStore(store);
|
|
86
|
+
try {
|
|
87
|
+
await setter.set(urlName, urlValue);
|
|
88
|
+
console.log(pc.green(` Set ${urlName} (from store config).`));
|
|
89
|
+
setCount++;
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.log(pc.red(` Failed to set ${urlName}: ${err.message}`));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const secretsToPrompt = getSecretsToPromptForNewStore(store);
|
|
95
|
+
const existingNames = ciHost === 'github' ? listGitHubSecrets() : listGitLabVariables();
|
|
96
|
+
const namesWeWillPrompt = new Set(secretsToPrompt.map((s) => s.name));
|
|
97
|
+
const alreadySet = existingNames.filter((n) => namesWeWillPrompt.has(n));
|
|
98
|
+
if (alreadySet.length > 0) {
|
|
99
|
+
const doUpdate = await promptUpdateExistingSecrets(alreadySet);
|
|
100
|
+
if (!doUpdate) {
|
|
101
|
+
if (setCount > 0) {
|
|
102
|
+
console.log(pc.green(`\n Done. ${setCount} secret(s) set for store "${store.alias}".\n`));
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const total = secretsToPrompt.length;
|
|
108
|
+
console.log(pc.cyan(`\n Configure ${total} secret(s) for store "${store.alias}" (theme token required).\n`));
|
|
109
|
+
for (let i = 0; i < secretsToPrompt.length; i++) {
|
|
110
|
+
const secret = secretsToPrompt[i];
|
|
111
|
+
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}`));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (setCount > 0) {
|
|
123
|
+
console.log(pc.green(`\n Done. ${setCount} secret(s) set for store "${store.alias}".\n`));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
57
128
|
}
|
package/src/commands/init.js
CHANGED
|
@@ -1,22 +1,35 @@
|
|
|
1
|
+
import prompts from 'prompts';
|
|
1
2
|
import pc from 'picocolors';
|
|
2
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
promptStoreLoop,
|
|
5
|
+
promptPreviewWorkflows,
|
|
6
|
+
promptBuildWorkflows,
|
|
7
|
+
promptConfigureCISecrets,
|
|
8
|
+
promptUpdateExistingSecrets,
|
|
9
|
+
promptSecretValue,
|
|
10
|
+
} from '../lib/prompts.js';
|
|
3
11
|
import { readConfig, writeConfig } from '../lib/config.js';
|
|
4
12
|
import { ensureGitRepo, ensureInitialCommit, ensureStagingBranch, createStoreBranches } from '../lib/git.js';
|
|
5
13
|
import { scaffoldWorkflows } from '../lib/workflows.js';
|
|
6
14
|
import { createStoreDirectories } from '../lib/store-sync.js';
|
|
15
|
+
import {
|
|
16
|
+
isGhAvailable,
|
|
17
|
+
hasGitHubRemote,
|
|
18
|
+
isGlabAvailable,
|
|
19
|
+
hasGitLabRemote,
|
|
20
|
+
listGitHubSecrets,
|
|
21
|
+
listGitLabVariables,
|
|
22
|
+
getStoreUrlSecretsFromConfig,
|
|
23
|
+
getSecretsToPrompt,
|
|
24
|
+
setSecret,
|
|
25
|
+
setGitLabVariable,
|
|
26
|
+
} from '../lib/github-secrets.js';
|
|
7
27
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (existing?.stores && Object.keys(existing.stores).length > 0) {
|
|
14
|
-
console.log(pc.yellow(' This repo already has a climaybe config.'));
|
|
15
|
-
console.log(pc.dim(' Use "climaybe add-store" to add more stores.'));
|
|
16
|
-
console.log(pc.dim(' Use "climaybe update-workflows" to refresh workflows.\n'));
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Run the full init flow: prompts, config write, git, branches, workflows.
|
|
30
|
+
* Used by both init (when not already inited or user confirms reinit) and reinit.
|
|
31
|
+
*/
|
|
32
|
+
async function runInitFlow() {
|
|
20
33
|
// 1. Collect stores from user
|
|
21
34
|
const stores = await promptStoreLoop();
|
|
22
35
|
const mode = stores.length > 1 ? 'multi' : 'single';
|
|
@@ -81,7 +94,119 @@ export async function initCommand() {
|
|
|
81
94
|
console.log(pc.dim(` Build workflows: ${enableBuildWorkflows ? 'enabled' : 'disabled'}`));
|
|
82
95
|
|
|
83
96
|
console.log(pc.dim('\n Next steps:'));
|
|
84
|
-
console.log(pc.dim(' 1. Add GEMINI_API_KEY to your
|
|
85
|
-
console.log(pc.dim(' 2. Push to GitHub and start using the branching workflow'));
|
|
97
|
+
console.log(pc.dim(' 1. Add GEMINI_API_KEY to your CI secrets (or configure below)'));
|
|
98
|
+
console.log(pc.dim(' 2. Push to GitHub/GitLab and start using the branching workflow'));
|
|
86
99
|
console.log(pc.dim(' 3. Tag your first release: git tag v1.0.0\n'));
|
|
100
|
+
|
|
101
|
+
const ciHost = await promptConfigureCISecrets();
|
|
102
|
+
if (ciHost === 'skip') return;
|
|
103
|
+
|
|
104
|
+
const secretsToPrompt = getSecretsToPrompt({
|
|
105
|
+
enablePreviewWorkflows,
|
|
106
|
+
enableBuildWorkflows,
|
|
107
|
+
mode,
|
|
108
|
+
stores,
|
|
109
|
+
});
|
|
110
|
+
const total = secretsToPrompt.length;
|
|
111
|
+
const setter =
|
|
112
|
+
ciHost === 'github'
|
|
113
|
+
? { check: isGhAvailable, checkRemote: hasGitHubRemote, set: setSecret, name: 'GitHub' }
|
|
114
|
+
: { check: isGlabAvailable, checkRemote: hasGitLabRemote, set: setGitLabVariable, name: 'GitLab' };
|
|
115
|
+
|
|
116
|
+
if (!setter.check()) {
|
|
117
|
+
const installUrl = ciHost === 'github' ? 'https://cli.github.com/' : 'https://gitlab.com/gitlab-org/cli';
|
|
118
|
+
console.log(pc.yellow(` ${setter.name} CLI is not installed or not logged in.`));
|
|
119
|
+
console.log(pc.dim(` Install: ${installUrl} — then run ${ciHost === 'github' ? 'gh' : 'glab'} auth login`));
|
|
120
|
+
console.log(
|
|
121
|
+
pc.dim(
|
|
122
|
+
ciHost === 'github'
|
|
123
|
+
? ' You can add secrets later in the repo: Settings → Secrets and variables → Actions.\n'
|
|
124
|
+
: ' You can add variables later in the repo: Settings → CI/CD → Variables.\n'
|
|
125
|
+
)
|
|
126
|
+
);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (!setter.checkRemote()) {
|
|
130
|
+
console.log(pc.yellow(' This repo has no ' + setter.name + ' remote (origin).'));
|
|
131
|
+
console.log(pc.dim(' Add a remote and push first, then add secrets/variables in the repo Settings.\n'));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const existingNames =
|
|
136
|
+
ciHost === 'github' ? listGitHubSecrets() : listGitLabVariables();
|
|
137
|
+
const namesWeWillPrompt = new Set(secretsToPrompt.map((s) => s.name));
|
|
138
|
+
const alreadySet = existingNames.filter((n) => namesWeWillPrompt.has(n));
|
|
139
|
+
if (alreadySet.length > 0) {
|
|
140
|
+
const doUpdate = await promptUpdateExistingSecrets(alreadySet);
|
|
141
|
+
if (!doUpdate) {
|
|
142
|
+
console.log(pc.dim('\n Skipping. Existing secrets left unchanged.\n'));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let setCount = 0;
|
|
148
|
+
|
|
149
|
+
// Set store URL(s) from config (domains already added during init) — no prompt
|
|
150
|
+
const storeUrlSecrets = getStoreUrlSecretsFromConfig({
|
|
151
|
+
enablePreviewWorkflows,
|
|
152
|
+
enableBuildWorkflows,
|
|
153
|
+
mode,
|
|
154
|
+
stores,
|
|
155
|
+
});
|
|
156
|
+
for (const { name, value } of storeUrlSecrets) {
|
|
157
|
+
try {
|
|
158
|
+
await setter.set(name, value);
|
|
159
|
+
console.log(pc.green(` Set ${name} (from store config).`));
|
|
160
|
+
setCount++;
|
|
161
|
+
} catch (err) {
|
|
162
|
+
console.log(pc.red(` Failed to set ${name}: ${err.message}`));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.log(pc.cyan(`\n Configure ${total} ${setter.name} secret(s)/variable(s). Leave optional ones blank to skip.\n`));
|
|
167
|
+
for (let i = 0; i < secretsToPrompt.length; i++) {
|
|
168
|
+
const secret = secretsToPrompt[i];
|
|
169
|
+
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}`));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (setCount > 0) {
|
|
181
|
+
console.log(pc.green(`\n Done. ${setCount} secret(s) set for this repository.\n`));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function initCommand() {
|
|
186
|
+
console.log(pc.bold('\n climaybe — Shopify CI/CD Setup\n'));
|
|
187
|
+
|
|
188
|
+
const existing = readConfig();
|
|
189
|
+
const hasConfig = existing != null && typeof existing === 'object';
|
|
190
|
+
|
|
191
|
+
if (hasConfig) {
|
|
192
|
+
const { reinit } = await prompts({
|
|
193
|
+
type: 'confirm',
|
|
194
|
+
name: 'reinit',
|
|
195
|
+
message: 'This repo already has a climaybe config. Clear everything and reinitialize from scratch?',
|
|
196
|
+
initial: false,
|
|
197
|
+
});
|
|
198
|
+
if (!reinit) {
|
|
199
|
+
console.log(pc.dim(' Use "climaybe add-store" to add more stores.'));
|
|
200
|
+
console.log(pc.dim(' Use "climaybe update-workflows" to refresh workflows.'));
|
|
201
|
+
console.log(pc.dim(' Use "climaybe reinit" to reinitialize from scratch.\n'));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
await runInitFlow();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export async function reinitCommand() {
|
|
210
|
+
console.log(pc.bold('\n climaybe — Reinitialize CI/CD Setup\n'));
|
|
211
|
+
await runInitFlow();
|
|
87
212
|
}
|
package/src/index.js
CHANGED
|
@@ -1,23 +1,38 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
1
4
|
import { Command } from 'commander';
|
|
2
|
-
import { initCommand } from './commands/init.js';
|
|
5
|
+
import { initCommand, reinitCommand } from './commands/init.js';
|
|
3
6
|
import { addStoreCommand } from './commands/add-store.js';
|
|
4
7
|
import { switchCommand } from './commands/switch.js';
|
|
5
8
|
import { syncCommand } from './commands/sync.js';
|
|
6
9
|
import { updateWorkflowsCommand } from './commands/update-workflows.js';
|
|
7
10
|
|
|
8
|
-
|
|
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
|
+
/**
|
|
16
|
+
* Create the CLI program (for testing and for run).
|
|
17
|
+
*/
|
|
18
|
+
export function createProgram() {
|
|
9
19
|
const program = new Command();
|
|
10
20
|
|
|
11
21
|
program
|
|
12
22
|
.name('climaybe')
|
|
13
23
|
.description('Shopify CI/CD CLI — scaffolds workflows, branch strategy, and store config')
|
|
14
|
-
.version(
|
|
24
|
+
.version(version);
|
|
15
25
|
|
|
16
26
|
program
|
|
17
27
|
.command('init')
|
|
18
28
|
.description('Initialize CI/CD setup for a Shopify theme repo')
|
|
19
29
|
.action(initCommand);
|
|
20
30
|
|
|
31
|
+
program
|
|
32
|
+
.command('reinit')
|
|
33
|
+
.description('Reinitialize CI/CD setup (removes existing config and re-scaffolds workflows)')
|
|
34
|
+
.action(reinitCommand);
|
|
35
|
+
|
|
21
36
|
program
|
|
22
37
|
.command('add-store')
|
|
23
38
|
.description('Add a new store to an existing multi-store config')
|
|
@@ -40,5 +55,9 @@ export function run(argv) {
|
|
|
40
55
|
.description('Refresh GitHub Actions workflows from latest bundled templates')
|
|
41
56
|
.action(updateWorkflowsCommand);
|
|
42
57
|
|
|
43
|
-
program
|
|
58
|
+
return program;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function run(argv) {
|
|
62
|
+
createProgram().parse(argv);
|
|
44
63
|
}
|
package/src/lib/config.js
CHANGED
package/src/lib/git.js
CHANGED
|
@@ -4,6 +4,11 @@ import pc from 'picocolors';
|
|
|
4
4
|
const exec = (cmd, cwd = process.cwd()) =>
|
|
5
5
|
execSync(cmd, { cwd, encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
6
6
|
|
|
7
|
+
function isExpectedGitError(err, patterns) {
|
|
8
|
+
const text = [err.message, err.stderr].filter(Boolean).join(' ');
|
|
9
|
+
return patterns.some((p) => text.includes(p));
|
|
10
|
+
}
|
|
11
|
+
|
|
7
12
|
/**
|
|
8
13
|
* Check if current directory is a git repo.
|
|
9
14
|
*/
|
|
@@ -11,7 +16,10 @@ export function isGitRepo(cwd = process.cwd()) {
|
|
|
11
16
|
try {
|
|
12
17
|
exec('git rev-parse --is-inside-work-tree', cwd);
|
|
13
18
|
return true;
|
|
14
|
-
} catch {
|
|
19
|
+
} catch (err) {
|
|
20
|
+
if (!isExpectedGitError(err, ['not a git repository'])) {
|
|
21
|
+
console.error(err);
|
|
22
|
+
}
|
|
15
23
|
return false;
|
|
16
24
|
}
|
|
17
25
|
}
|
|
@@ -30,7 +38,10 @@ export function branchExists(name, cwd = process.cwd()) {
|
|
|
30
38
|
try {
|
|
31
39
|
exec(`git rev-parse --verify ${name}`, cwd);
|
|
32
40
|
return true;
|
|
33
|
-
} catch {
|
|
41
|
+
} catch (err) {
|
|
42
|
+
if (!isExpectedGitError(err, ['Needed a single revision'])) {
|
|
43
|
+
console.error(err);
|
|
44
|
+
}
|
|
34
45
|
return false;
|
|
35
46
|
}
|
|
36
47
|
}
|
|
@@ -71,7 +82,14 @@ export function ensureStagingBranch(cwd = process.cwd()) {
|
|
|
71
82
|
export function ensureInitialCommit(cwd = process.cwd()) {
|
|
72
83
|
try {
|
|
73
84
|
exec('git rev-parse HEAD', cwd);
|
|
74
|
-
} catch {
|
|
85
|
+
} catch (err) {
|
|
86
|
+
const expectedNoCommit = isExpectedGitError(err, [
|
|
87
|
+
'unknown revision',
|
|
88
|
+
'path not in the working tree',
|
|
89
|
+
]);
|
|
90
|
+
if (!expectedNoCommit) {
|
|
91
|
+
console.error(err);
|
|
92
|
+
}
|
|
75
93
|
// No commits yet — create an initial one
|
|
76
94
|
exec('git add -A', cwd);
|
|
77
95
|
exec('git commit -m "chore: initial commit" --allow-empty', cwd);
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { spawn, spawnSync, execSync } from 'node:child_process';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Secret/variable definitions for CI (GitHub Actions or GitLab CI).
|
|
6
|
+
* condition: 'always' = required for core workflows; 'preview' | 'build' = only when that feature is enabled.
|
|
7
|
+
*/
|
|
8
|
+
export const SECRET_DEFINITIONS = [
|
|
9
|
+
{
|
|
10
|
+
name: 'GEMINI_API_KEY',
|
|
11
|
+
required: true,
|
|
12
|
+
condition: 'always',
|
|
13
|
+
description: 'Google Gemini API key for AI-generated changelogs on release',
|
|
14
|
+
whereToGet:
|
|
15
|
+
'Google AI Studio: https://aistudio.google.com/apikey — create an API key, then paste it here.',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: 'SHOPIFY_STORE_URL',
|
|
19
|
+
required: false,
|
|
20
|
+
condition: 'preview_or_build',
|
|
21
|
+
description: 'Shopify store URL (e.g. your-store.myshopify.com) — preview themes and/or Lighthouse',
|
|
22
|
+
whereToGet:
|
|
23
|
+
'Your theme’s store URL in Shopify Admin → Settings → Domains, or use the .myshopify.com URL.',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'SHOPIFY_CLI_THEME_TOKEN',
|
|
27
|
+
required: true,
|
|
28
|
+
condition: 'preview',
|
|
29
|
+
description: 'Theme access token so CI can push preview themes to your store',
|
|
30
|
+
whereToGet:
|
|
31
|
+
'Shopify Partners: your app → Theme library access → Create theme access token. Or: Shopify Admin → Apps → Develop apps → your app → API credentials → Theme access.',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'SHOP_ACCESS_TOKEN',
|
|
35
|
+
required: false,
|
|
36
|
+
condition: 'build',
|
|
37
|
+
description: 'Store API access token for Lighthouse runs',
|
|
38
|
+
whereToGet:
|
|
39
|
+
'Shopify Admin → Settings → Apps and sales channels → Develop apps → your app → API credentials (Admin API or custom app with storefront/build access).',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'LHCI_GITHUB_APP_TOKEN',
|
|
43
|
+
required: false,
|
|
44
|
+
condition: 'build',
|
|
45
|
+
description: 'Lighthouse CI GitHub App token for posting results as PR comments',
|
|
46
|
+
whereToGet:
|
|
47
|
+
'Lighthouse CI GitHub App: https://github.com/apps/lighthouse-ci — install and create a token, or use a Personal Access Token with repo scope.',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'SHOP_PASSWORD',
|
|
51
|
+
required: false,
|
|
52
|
+
condition: 'build',
|
|
53
|
+
description: 'Store password if your storefront is password-protected (optional)',
|
|
54
|
+
whereToGet: 'The password visitors enter to access your store (Storefront password in Shopify Admin).',
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check if GitHub CLI is installed and authenticated.
|
|
60
|
+
*/
|
|
61
|
+
export function isGhAvailable() {
|
|
62
|
+
try {
|
|
63
|
+
execSync('gh auth status', { encoding: 'utf-8', stdio: 'pipe' });
|
|
64
|
+
return true;
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if the current repo has a GitHub remote (origin pointing to github.com).
|
|
72
|
+
*/
|
|
73
|
+
export function hasGitHubRemote(cwd = process.cwd()) {
|
|
74
|
+
try {
|
|
75
|
+
const url = execSync('git remote get-url origin', { cwd, encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
76
|
+
return /github\.com/i.test(url);
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
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
|
+
|
|
97
|
+
/**
|
|
98
|
+
* List repository secret names (GitHub). Returns [] on error or if none.
|
|
99
|
+
*/
|
|
100
|
+
export function listGitHubSecrets(cwd = process.cwd()) {
|
|
101
|
+
try {
|
|
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();
|
|
107
|
+
const data = JSON.parse(out || '[]');
|
|
108
|
+
return Array.isArray(data) ? data.map((s) => s.name).filter(Boolean) : [];
|
|
109
|
+
} catch {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* List project CI/CD variable names (GitLab). Returns [] on error or if none.
|
|
116
|
+
*/
|
|
117
|
+
export function listGitLabVariables(cwd = process.cwd()) {
|
|
118
|
+
try {
|
|
119
|
+
const out = execSync('glab variable list -F json', { cwd, encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
120
|
+
const data = JSON.parse(out || '[]');
|
|
121
|
+
if (!Array.isArray(data)) return [];
|
|
122
|
+
return data.map((v) => v.key ?? v.name ?? v.Key).filter(Boolean);
|
|
123
|
+
} catch {
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
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.
|
|
131
|
+
*/
|
|
132
|
+
export function setSecret(name, value, cwd = process.cwd()) {
|
|
133
|
+
return new Promise((resolve, reject) => {
|
|
134
|
+
const repo = getGitHubRepoSpec(cwd);
|
|
135
|
+
const args = ['secret', 'set', name];
|
|
136
|
+
if (repo) args.push('-R', repo);
|
|
137
|
+
const child = spawn('gh', args, {
|
|
138
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
139
|
+
cwd,
|
|
140
|
+
});
|
|
141
|
+
child.on('error', reject);
|
|
142
|
+
child.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`gh secret set exited with ${code}`))));
|
|
143
|
+
child.stdin.write(value, (err) => {
|
|
144
|
+
if (err) reject(err);
|
|
145
|
+
else child.stdin.end();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Check if GitLab CLI is installed and authenticated.
|
|
152
|
+
*/
|
|
153
|
+
export function isGlabAvailable() {
|
|
154
|
+
try {
|
|
155
|
+
execSync('glab auth status', { encoding: 'utf-8', stdio: 'pipe' });
|
|
156
|
+
return true;
|
|
157
|
+
} catch {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Check if the current repo has a GitLab remote (origin pointing to gitlab.com or common self-hosted hosts).
|
|
164
|
+
*/
|
|
165
|
+
export function hasGitLabRemote(cwd = process.cwd()) {
|
|
166
|
+
try {
|
|
167
|
+
const url = execSync('git remote get-url origin', { cwd, encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
168
|
+
return /gitlab\.com|gitlab\./i.test(url);
|
|
169
|
+
} catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Set a project CI/CD variable via glab CLI. Value is passed via stdin to avoid argv exposure.
|
|
176
|
+
*/
|
|
177
|
+
export function setGitLabVariable(name, value) {
|
|
178
|
+
return new Promise((resolve, reject) => {
|
|
179
|
+
const child = spawn('glab', ['variable', 'set', name, '--masked'], {
|
|
180
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
181
|
+
});
|
|
182
|
+
child.on('error', reject);
|
|
183
|
+
child.on('close', (code) =>
|
|
184
|
+
code === 0 ? resolve() : reject(new Error(`glab variable set exited with ${code}`))
|
|
185
|
+
);
|
|
186
|
+
child.stdin.write(value, (err) => {
|
|
187
|
+
if (err) reject(err);
|
|
188
|
+
else child.stdin.end();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Convert store alias to secret suffix (e.g. voldt-norway → VOLDT_NORWAY).
|
|
195
|
+
*/
|
|
196
|
+
export function aliasToSecretSuffix(alias) {
|
|
197
|
+
return String(alias).replace(/-/g, '_').toUpperCase();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Store URL secrets are set from config (store domains added during init), not prompted.
|
|
202
|
+
* Returns [{ name, value }] for SHOPIFY_STORE_URL and/or SHOPIFY_STORE_URL_<ALIAS>.
|
|
203
|
+
*/
|
|
204
|
+
export function getStoreUrlSecretsFromConfig({ enablePreviewWorkflows, enableBuildWorkflows, mode = 'single', stores = [] }) {
|
|
205
|
+
if (stores.length === 0) return [];
|
|
206
|
+
const isMulti = mode === 'multi' && stores.length > 1;
|
|
207
|
+
const out = [];
|
|
208
|
+
|
|
209
|
+
if (isMulti && enablePreviewWorkflows) {
|
|
210
|
+
for (const store of stores) {
|
|
211
|
+
out.push({ name: `SHOPIFY_STORE_URL_${aliasToSecretSuffix(store.alias)}`, value: store.domain });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (!isMulti && (enablePreviewWorkflows || enableBuildWorkflows)) {
|
|
215
|
+
out.push({ name: 'SHOPIFY_STORE_URL', value: stores[0].domain });
|
|
216
|
+
}
|
|
217
|
+
if (isMulti && enableBuildWorkflows) {
|
|
218
|
+
const defaultDomain = stores[0]?.domain;
|
|
219
|
+
if (defaultDomain && !out.some((s) => s.name === 'SHOPIFY_STORE_URL')) {
|
|
220
|
+
out.push({ name: 'SHOPIFY_STORE_URL', value: defaultDomain });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* 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.
|
|
229
|
+
*/
|
|
230
|
+
export function getSecretsToPrompt({ enablePreviewWorkflows, enableBuildWorkflows, mode = 'single', stores = [] }) {
|
|
231
|
+
const isMulti = mode === 'multi' && stores.length > 1;
|
|
232
|
+
|
|
233
|
+
const base = SECRET_DEFINITIONS.filter((s) => {
|
|
234
|
+
if (s.name === 'SHOPIFY_STORE_URL') return false; // set from config
|
|
235
|
+
if (s.condition === 'always') return true;
|
|
236
|
+
if (s.condition === 'preview_or_build') return enablePreviewWorkflows || enableBuildWorkflows;
|
|
237
|
+
if (s.condition === 'preview' && enablePreviewWorkflows) return true;
|
|
238
|
+
if (s.condition === 'build' && enableBuildWorkflows) return true;
|
|
239
|
+
return false;
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const dropPreviewGeneric =
|
|
243
|
+
isMulti && enablePreviewWorkflows
|
|
244
|
+
? (s) => s.name !== 'SHOPIFY_CLI_THEME_TOKEN'
|
|
245
|
+
: () => true;
|
|
246
|
+
|
|
247
|
+
let list = base.filter(dropPreviewGeneric);
|
|
248
|
+
|
|
249
|
+
if (isMulti && enablePreviewWorkflows) {
|
|
250
|
+
for (const store of stores) {
|
|
251
|
+
const suffix = aliasToSecretSuffix(store.alias);
|
|
252
|
+
list.push({
|
|
253
|
+
name: `SHOPIFY_CLI_THEME_TOKEN_${suffix}`,
|
|
254
|
+
required: true,
|
|
255
|
+
description: `Store ${store.alias}: Theme access token for CI (staging/live use this for ${store.alias})`,
|
|
256
|
+
whereToGet:
|
|
257
|
+
'Shopify Partners or Admin → Apps → Develop apps → your app → Theme access for this store.',
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return list;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Store URL for a new store (set from store.domain, no prompt).
|
|
267
|
+
*/
|
|
268
|
+
export function getStoreUrlSecretForNewStore(store) {
|
|
269
|
+
return { name: `SHOPIFY_STORE_URL_${aliasToSecretSuffix(store.alias)}`, value: store.domain };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Per-store secret to prompt for when adding a store (theme token only; URL is set from store.domain).
|
|
274
|
+
*/
|
|
275
|
+
export function getSecretsToPromptForNewStore(store) {
|
|
276
|
+
const suffix = aliasToSecretSuffix(store.alias);
|
|
277
|
+
return [
|
|
278
|
+
{
|
|
279
|
+
name: `SHOPIFY_CLI_THEME_TOKEN_${suffix}`,
|
|
280
|
+
required: true,
|
|
281
|
+
description: `Store ${store.alias}: Theme access token for CI (staging/live use this for ${store.alias})`,
|
|
282
|
+
whereToGet:
|
|
283
|
+
'Shopify Partners or Admin → Apps → Develop apps → your app → Theme access for this store.',
|
|
284
|
+
},
|
|
285
|
+
];
|
|
286
|
+
}
|
package/src/lib/prompts.js
CHANGED
|
@@ -6,7 +6,7 @@ import pc from 'picocolors';
|
|
|
6
6
|
* "voldt-staging.myshopify.com" → "voldt-staging"
|
|
7
7
|
*/
|
|
8
8
|
export function extractAlias(domain) {
|
|
9
|
-
return domain.replace(/\.myshopify\.com$/i, '').trim();
|
|
9
|
+
return domain.trim().replace(/\.myshopify\.com$/i, '').trim();
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -165,3 +165,63 @@ export async function promptNewStore(existingAliases = []) {
|
|
|
165
165
|
|
|
166
166
|
return store;
|
|
167
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
|
+
}
|
|
@@ -14,7 +14,7 @@ jobs:
|
|
|
14
14
|
runs-on: ubuntu-latest
|
|
15
15
|
needs: [build]
|
|
16
16
|
env:
|
|
17
|
-
|
|
17
|
+
SHOPIFY_STORE_URL: ${{ secrets.SHOPIFY_STORE_URL }}
|
|
18
18
|
SHOP_ACCESS_TOKEN: ${{ secrets.SHOP_ACCESS_TOKEN }}
|
|
19
19
|
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
|
|
20
20
|
steps:
|
|
@@ -26,8 +26,8 @@ jobs:
|
|
|
26
26
|
SKIP_REASONS+=("Not on main or staging branch (current: ${{ github.ref }})")
|
|
27
27
|
fi
|
|
28
28
|
|
|
29
|
-
if [ -z "$
|
|
30
|
-
SKIP_REASONS+=("Missing required secrets:
|
|
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
31
|
fi
|
|
32
32
|
|
|
33
33
|
if [ ${#SKIP_REASONS[@]} -gt 0 ]; then
|
|
@@ -42,14 +42,14 @@ jobs:
|
|
|
42
42
|
echo "All conditions met, proceeding with Lighthouse CI"
|
|
43
43
|
|
|
44
44
|
- name: Checkout code
|
|
45
|
-
if: env.
|
|
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
46
|
uses: actions/checkout@v4
|
|
47
47
|
|
|
48
48
|
- name: Lighthouse
|
|
49
|
-
if: env.
|
|
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
50
|
uses: shopify/lighthouse-ci-action@v1
|
|
51
51
|
with:
|
|
52
|
-
store: ${{ secrets.
|
|
52
|
+
store: ${{ secrets.SHOPIFY_STORE_URL }}
|
|
53
53
|
access_token: ${{ secrets.SHOP_ACCESS_TOKEN }}
|
|
54
54
|
password: ${{ secrets.SHOP_PASSWORD }}
|
|
55
55
|
lhci_github_app_token: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
|
|
@@ -29,11 +29,12 @@ jobs:
|
|
|
29
29
|
const fs = require('fs');
|
|
30
30
|
const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
|
|
31
31
|
const stores = pkg?.config?.stores || {};
|
|
32
|
+
const defaultStoreRaw = pkg?.config?.default_store;
|
|
32
33
|
const normalize = (v) => String(v || '')
|
|
33
34
|
.toLowerCase()
|
|
34
35
|
.replace(/^https?:\\/\\//, '')
|
|
35
36
|
.replace(/\\/.*$/, '');
|
|
36
|
-
const defaultStore = normalize(
|
|
37
|
+
const defaultStore = normalize(defaultStoreRaw);
|
|
37
38
|
let alias = '';
|
|
38
39
|
if (defaultStore) {
|
|
39
40
|
for (const [k, d] of Object.entries(stores)) {
|
|
@@ -46,6 +47,13 @@ jobs:
|
|
|
46
47
|
if (!alias) {
|
|
47
48
|
alias = Object.keys(stores)[0] || '';
|
|
48
49
|
}
|
|
50
|
+
if (!alias && defaultStore) {
|
|
51
|
+
const subdomain = defaultStore.split('.')[0] || 'default';
|
|
52
|
+
alias = subdomain.replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-') || 'default';
|
|
53
|
+
}
|
|
54
|
+
if (!alias) {
|
|
55
|
+
alias = 'default';
|
|
56
|
+
}
|
|
49
57
|
process.stdout.write(alias);
|
|
50
58
|
")
|
|
51
59
|
|