climaybe 1.0.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +64 -12
- package/package.json +45 -2
- package/src/commands/add-store.js +77 -4
- package/src/commands/init.js +150 -16
- package/src/commands/update-workflows.js +4 -2
- package/src/index.js +15 -3
- package/src/lib/config.js +17 -0
- package/src/lib/git.js +21 -3
- package/src/lib/github-secrets.js +263 -0
- package/src/lib/prompts.js +116 -6
- package/src/lib/workflows.js +23 -3
- package/src/workflows/build/build-pipeline.yml +57 -0
- package/src/workflows/build/create-release.yml +52 -0
- package/src/workflows/build/reusable-build.yml +52 -0
- package/src/workflows/multi/main-to-staging-stores.yml +44 -3
- package/src/workflows/multi/multistore-hotfix-to-main.yml +84 -0
- package/src/workflows/multi/pr-to-live.yml +82 -8
- package/src/workflows/multi/stores-to-root.yml +10 -3
- package/src/workflows/preview/pr-close.yml +63 -0
- package/src/workflows/preview/pr-update.yml +120 -0
- package/src/workflows/preview/reusable-cleanup-themes.yml +71 -0
- package/src/workflows/preview/reusable-comment-on-pr.yml +76 -0
- package/src/workflows/preview/reusable-extract-pr-number.yml +35 -0
- package/src/workflows/preview/reusable-rename-theme.yml +73 -0
- package/src/workflows/preview/reusable-share-theme.yml +94 -0
- package/src/workflows/shared/ai-changelog.yml +82 -24
- package/src/workflows/shared/version-bump.yml +22 -7
- package/src/workflows/single/nightly-hotfix.yml +38 -2
- package/src/workflows/single/post-merge-tag.yml +68 -15
- package/src/workflows/single/release-pr-check.yml +16 -6
- package/src/workflows/multi/hotfix-backport.yml +0 -100
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Electric Maybe
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -26,10 +26,12 @@ Interactive setup that configures your repo for CI/CD.
|
|
|
26
26
|
1. Prompts for your store URL (e.g., `voldt-staging.myshopify.com`)
|
|
27
27
|
2. Extracts subdomain as alias, lets you override
|
|
28
28
|
3. Asks if you want to add more stores
|
|
29
|
-
4.
|
|
30
|
-
5.
|
|
31
|
-
6.
|
|
32
|
-
7.
|
|
29
|
+
4. Asks whether to enable optional **preview + cleanup** workflows
|
|
30
|
+
5. Asks whether to enable optional **build + Lighthouse** workflows
|
|
31
|
+
6. Based on store count, sets up **single-store** or **multi-store** mode
|
|
32
|
+
7. Writes `package.json` config
|
|
33
|
+
8. Scaffolds GitHub Actions workflows
|
|
34
|
+
9. Creates git branches and store directories (multi-store)
|
|
33
35
|
|
|
34
36
|
### `climaybe add-store`
|
|
35
37
|
|
|
@@ -83,6 +85,8 @@ The CLI writes config into the `config` field of your `package.json`:
|
|
|
83
85
|
"config": {
|
|
84
86
|
"port": 9295,
|
|
85
87
|
"default_store": "voldt-staging.myshopify.com",
|
|
88
|
+
"preview_workflows": true,
|
|
89
|
+
"build_workflows": true,
|
|
86
90
|
"stores": {
|
|
87
91
|
"voldt-staging": "voldt-staging.myshopify.com",
|
|
88
92
|
"voldt-norway": "voldt-norway.myshopify.com"
|
|
@@ -115,6 +119,8 @@ staging → main → staging-<store> → live-<store>
|
|
|
115
119
|
- `staging-<store>` — per-store staging with store-specific JSON data
|
|
116
120
|
- `live-<store>` — per-store production
|
|
117
121
|
|
|
122
|
+
Direct pushes to `staging-<store>` or `live-<store>` are automatically synced back to `main` (no PR; multistore-hotfix-to-main merges the branch into main).
|
|
123
|
+
|
|
118
124
|
## Workflows
|
|
119
125
|
|
|
120
126
|
### Shared (both modes)
|
|
@@ -129,27 +135,55 @@ staging → main → staging-<store> → live-<store>
|
|
|
129
135
|
| Workflow | Trigger | What it does |
|
|
130
136
|
|----------|---------|-------------|
|
|
131
137
|
| `release-pr-check.yml` | PR from `staging` to `main` | Generates changelog, creates pre-release patch tag, posts PR comment |
|
|
132
|
-
| `post-merge-tag.yml` | Push to `main` (merged PR) |
|
|
138
|
+
| `post-merge-tag.yml` | Push to `main` (merged PR) | Staging→main: minor bump (e.g. v3.2.0). Hotfix sync: patch bump (e.g. v3.2.1) |
|
|
133
139
|
| `nightly-hotfix.yml` | Cron 02:00 US Eastern | Tags untagged hotfix commits with patch version |
|
|
134
140
|
|
|
135
141
|
### Multi-store (additional)
|
|
136
142
|
|
|
137
143
|
| Workflow | Trigger | What it does |
|
|
138
144
|
|----------|---------|-------------|
|
|
139
|
-
| `main-to-staging-stores.yml` | Push to `main` | Opens PRs to each `staging-<alias>` branch |
|
|
145
|
+
| `main-to-staging-stores.yml` (main-to-staging-<store>) | Push to `main` | Opens PRs from main to each `staging-<alias>` branch |
|
|
140
146
|
| `stores-to-root.yml` | Push to `staging-*` | Copies `stores/<alias>/` JSONs to repo root |
|
|
141
147
|
| `pr-to-live.yml` | After stores-to-root | Opens PR from `staging-<alias>` to `live-<alias>` |
|
|
142
148
|
| `root-to-stores.yml` | Push to `live-*` (hotfix) | Syncs root JSONs back to `stores/<alias>/` |
|
|
143
|
-
| `hotfix-
|
|
149
|
+
| `multistore-hotfix-to-main.yml` | Push to `staging-*` or `live-*` (and after root-to-stores) | Automatically merges store branch into main (no PR); everything is synced back to main |
|
|
150
|
+
|
|
151
|
+
### Optional preview + cleanup package
|
|
152
|
+
|
|
153
|
+
Enabled via `climaybe init` prompt (`Enable preview + cleanup workflows?`).
|
|
154
|
+
|
|
155
|
+
| Workflow | Trigger | What it does |
|
|
156
|
+
|----------|---------|-------------|
|
|
157
|
+
| `pr-update.yml` | PR opened/synchronize/reopened | Shares draft theme, renames with `-PR<number>`, comments preview + customize URLs |
|
|
158
|
+
| `pr-close.yml` | PR closed | Deletes matching preview themes and comments deleted count + names |
|
|
159
|
+
| `reusable-share-theme.yml` | workflow_call | Shares Shopify draft theme and returns `theme_id` |
|
|
160
|
+
| `reusable-rename-theme.yml` | workflow_call | Renames shared theme to include `PR<number>` (fails job on rename failure) |
|
|
161
|
+
| `reusable-comment-on-pr.yml` | workflow_call | Posts preview comment including Customize URL |
|
|
162
|
+
| `reusable-cleanup-themes.yml` | workflow_call | Deletes preview themes by PR number and exposes cleanup outputs |
|
|
163
|
+
| `reusable-extract-pr-number.yml` | workflow_call | Extracts padded/unpadded PR number outputs for naming and API-safe usage |
|
|
164
|
+
|
|
165
|
+
### Optional build + Lighthouse package
|
|
166
|
+
|
|
167
|
+
Enabled via `climaybe init` prompt (`Enable build + Lighthouse workflows?`).
|
|
168
|
+
|
|
169
|
+
| Workflow | Trigger | What it does |
|
|
170
|
+
|----------|---------|-------------|
|
|
171
|
+
| `build-pipeline.yml` | Push to `main/staging/develop` | Runs reusable build and Lighthouse checks (when required secrets exist) |
|
|
172
|
+
| `reusable-build.yml` | workflow_call | Runs Node build + Tailwind compile, then commits compiled assets when changed |
|
|
173
|
+
| `create-release.yml` | Push tag `v*` | Builds release archive and creates GitHub Release using `release-notes.md` |
|
|
144
174
|
|
|
145
175
|
## Versioning
|
|
146
176
|
|
|
147
|
-
- **
|
|
148
|
-
- **
|
|
149
|
-
- **
|
|
177
|
+
- **Version format**: Always three-part (e.g. `v3.2.0`). No two-part tags.
|
|
178
|
+
- **Release merge** (`staging` → `main`): Minor bump (e.g. `v3.1.12` → `v3.2.0`)
|
|
179
|
+
- **Hotfix sync** (`staging-<store>` or `live-<store>` → main via multistore-hotfix-to-main): Patch bump runs immediately (e.g. `v3.2.0` → `v3.2.1`)
|
|
180
|
+
- **Other hotfixes** (direct commit to `main`): Patch bump via nightly workflow or manual run
|
|
181
|
+
- **PR title convention**: `Release v3.2` or `Release v3.2.0` — the workflow normalizes to three-part
|
|
150
182
|
|
|
151
183
|
All version bumps update `config/settings_schema.json` automatically.
|
|
152
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
|
+
|
|
153
187
|
## File Sync Rules (Multi-store)
|
|
154
188
|
|
|
155
189
|
**Synced between root and `stores/<alias>/`:**
|
|
@@ -163,18 +197,27 @@ All version bumps update `config/settings_schema.json` automatically.
|
|
|
163
197
|
|
|
164
198
|
## Recursive Trigger Prevention
|
|
165
199
|
|
|
166
|
-
- Hotfix
|
|
200
|
+
- Hotfix sync merge commits (multistore-hotfix-to-main) contain `[hotfix-backport]` in the message
|
|
167
201
|
- Store sync commits contain `[stores-to-root]` or `[root-to-stores]`
|
|
168
202
|
- Version bump commits contain `chore(release): bump version`
|
|
169
203
|
- All workflows check for these flags and skip accordingly
|
|
170
204
|
|
|
171
205
|
## GitHub Secrets
|
|
172
206
|
|
|
173
|
-
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.
|
|
174
208
|
|
|
175
209
|
| Secret | Required | Description |
|
|
176
210
|
|--------|----------|-------------|
|
|
177
211
|
| `GEMINI_API_KEY` | Yes | Google Gemini API key for changelog generation |
|
|
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). |
|
|
214
|
+
| `SHOP_ACCESS_TOKEN` | Optional* | Required only when optional build workflows are enabled (Lighthouse) |
|
|
215
|
+
| `LHCI_GITHUB_APP_TOKEN` | Optional* | Required only when optional build workflows are enabled (Lighthouse) |
|
|
216
|
+
| `SHOP_PASSWORD` | Optional | Used by Lighthouse action when your store requires password auth |
|
|
217
|
+
|
|
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
|
+
|
|
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`).
|
|
178
221
|
|
|
179
222
|
## Directory Structure (Multi-store)
|
|
180
223
|
|
|
@@ -199,6 +242,15 @@ Add the following secret to your GitHub repository:
|
|
|
199
242
|
└── .github/workflows/
|
|
200
243
|
```
|
|
201
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
|
+
|
|
202
254
|
## License
|
|
203
255
|
|
|
204
256
|
MIT — Electric Maybe
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "climaybe",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
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": {
|
|
@@ -23,9 +23,52 @@
|
|
|
23
23
|
],
|
|
24
24
|
"author": "Electric Maybe",
|
|
25
25
|
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/electricmaybe/climaybe.git"
|
|
29
|
+
},
|
|
30
|
+
"bugs": "https://github.com/electricmaybe/climaybe/issues",
|
|
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
|
+
},
|
|
26
61
|
"dependencies": {
|
|
27
|
-
"commander": "^
|
|
62
|
+
"commander": "^14.0.3",
|
|
28
63
|
"picocolors": "^1.1.1",
|
|
29
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"
|
|
30
73
|
}
|
|
31
74
|
}
|
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import pc from 'picocolors';
|
|
2
|
-
import { promptNewStore } from '../lib/prompts.js';
|
|
3
|
-
import { readConfig, addStoreToConfig, getStoreAliases, getMode } from '../lib/config.js';
|
|
2
|
+
import { promptNewStore, promptConfigureCISecrets, promptUpdateExistingSecrets, promptSecretValue } from '../lib/prompts.js';
|
|
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'));
|
|
@@ -17,6 +29,8 @@ export async function addStoreCommand() {
|
|
|
17
29
|
|
|
18
30
|
const existingAliases = getStoreAliases();
|
|
19
31
|
const previousMode = getMode();
|
|
32
|
+
const includePreview = isPreviewWorkflowsEnabled();
|
|
33
|
+
const includeBuild = isBuildWorkflowsEnabled();
|
|
20
34
|
|
|
21
35
|
// Prompt for new store
|
|
22
36
|
const store = await promptNewStore(existingAliases);
|
|
@@ -42,14 +56,73 @@ export async function addStoreCommand() {
|
|
|
42
56
|
createStoreDirectories(originalAlias);
|
|
43
57
|
|
|
44
58
|
// Re-scaffold workflows for multi mode
|
|
45
|
-
scaffoldWorkflows('multi');
|
|
59
|
+
scaffoldWorkflows('multi', { includePreview, includeBuild });
|
|
46
60
|
console.log(pc.green(' Migration complete — workflows updated to multi-store mode.'));
|
|
47
61
|
} else if (newMode === 'multi') {
|
|
48
62
|
// Already multi, just make sure workflows are current
|
|
49
|
-
scaffoldWorkflows('multi');
|
|
63
|
+
scaffoldWorkflows('multi', { includePreview, includeBuild });
|
|
50
64
|
}
|
|
51
65
|
|
|
52
66
|
console.log(pc.bold(pc.green('\n Store added successfully!\n')));
|
|
53
67
|
console.log(pc.dim(` New branches: staging-${store.alias}, live-${store.alias}`));
|
|
54
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
|
+
}
|
|
55
128
|
}
|
package/src/commands/init.js
CHANGED
|
@@ -1,25 +1,40 @@
|
|
|
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';
|
|
36
|
+
const enablePreviewWorkflows = await promptPreviewWorkflows();
|
|
37
|
+
const enableBuildWorkflows = await promptBuildWorkflows();
|
|
23
38
|
|
|
24
39
|
console.log(pc.dim(`\n Mode: ${mode}-store (${stores.length} store(s))`));
|
|
25
40
|
|
|
@@ -27,6 +42,8 @@ export async function initCommand() {
|
|
|
27
42
|
const config = {
|
|
28
43
|
port: 9295,
|
|
29
44
|
default_store: stores[0].domain,
|
|
45
|
+
preview_workflows: enablePreviewWorkflows,
|
|
46
|
+
build_workflows: enableBuildWorkflows,
|
|
30
47
|
stores: {},
|
|
31
48
|
};
|
|
32
49
|
|
|
@@ -55,7 +72,10 @@ export async function initCommand() {
|
|
|
55
72
|
}
|
|
56
73
|
|
|
57
74
|
// 6. Scaffold workflows
|
|
58
|
-
scaffoldWorkflows(mode
|
|
75
|
+
scaffoldWorkflows(mode, {
|
|
76
|
+
includePreview: enablePreviewWorkflows,
|
|
77
|
+
includeBuild: enableBuildWorkflows,
|
|
78
|
+
});
|
|
59
79
|
|
|
60
80
|
// Done
|
|
61
81
|
console.log(pc.bold(pc.green('\n Setup complete!\n')));
|
|
@@ -70,9 +90,123 @@ export async function initCommand() {
|
|
|
70
90
|
}
|
|
71
91
|
console.log(pc.dim(' Workflow: staging → main → staging-<store> → live-<store>'));
|
|
72
92
|
}
|
|
93
|
+
console.log(pc.dim(` Preview workflows: ${enablePreviewWorkflows ? 'enabled' : 'disabled'}`));
|
|
94
|
+
console.log(pc.dim(` Build workflows: ${enableBuildWorkflows ? 'enabled' : 'disabled'}`));
|
|
73
95
|
|
|
74
96
|
console.log(pc.dim('\n Next steps:'));
|
|
75
|
-
console.log(pc.dim(' 1. Add GEMINI_API_KEY to your
|
|
76
|
-
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'));
|
|
77
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 alreadyInited = existing?.stores && Object.keys(existing.stores).length > 0;
|
|
190
|
+
|
|
191
|
+
if (alreadyInited) {
|
|
192
|
+
const { reinit } = await prompts({
|
|
193
|
+
type: 'confirm',
|
|
194
|
+
name: 'reinit',
|
|
195
|
+
message: 'This repo already has a climaybe config. Reinitialize? This will remove your current stores and workflow settings.',
|
|
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();
|
|
78
212
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import pc from 'picocolors';
|
|
2
|
-
import { getMode, readConfig } from '../lib/config.js';
|
|
2
|
+
import { getMode, isBuildWorkflowsEnabled, isPreviewWorkflowsEnabled, readConfig } from '../lib/config.js';
|
|
3
3
|
import { scaffoldWorkflows } from '../lib/workflows.js';
|
|
4
4
|
|
|
5
5
|
export async function updateWorkflowsCommand() {
|
|
@@ -12,7 +12,9 @@ export async function updateWorkflowsCommand() {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
const mode = getMode();
|
|
15
|
-
|
|
15
|
+
const includePreview = isPreviewWorkflowsEnabled();
|
|
16
|
+
const includeBuild = isBuildWorkflowsEnabled();
|
|
17
|
+
scaffoldWorkflows(mode, { includePreview, includeBuild });
|
|
16
18
|
|
|
17
19
|
console.log(pc.bold(pc.green('\n Workflows updated!\n')));
|
|
18
20
|
}
|
package/src/index.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { initCommand } from './commands/init.js';
|
|
2
|
+
import { initCommand, reinitCommand } from './commands/init.js';
|
|
3
3
|
import { addStoreCommand } from './commands/add-store.js';
|
|
4
4
|
import { switchCommand } from './commands/switch.js';
|
|
5
5
|
import { syncCommand } from './commands/sync.js';
|
|
6
6
|
import { updateWorkflowsCommand } from './commands/update-workflows.js';
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Create the CLI program (for testing and for run).
|
|
10
|
+
*/
|
|
11
|
+
export function createProgram() {
|
|
9
12
|
const program = new Command();
|
|
10
13
|
|
|
11
14
|
program
|
|
@@ -18,6 +21,11 @@ export function run(argv) {
|
|
|
18
21
|
.description('Initialize CI/CD setup for a Shopify theme repo')
|
|
19
22
|
.action(initCommand);
|
|
20
23
|
|
|
24
|
+
program
|
|
25
|
+
.command('reinit')
|
|
26
|
+
.description('Reinitialize CI/CD setup (removes existing config and re-scaffolds workflows)')
|
|
27
|
+
.action(reinitCommand);
|
|
28
|
+
|
|
21
29
|
program
|
|
22
30
|
.command('add-store')
|
|
23
31
|
.description('Add a new store to an existing multi-store config')
|
|
@@ -40,5 +48,9 @@ export function run(argv) {
|
|
|
40
48
|
.description('Refresh GitHub Actions workflows from latest bundled templates')
|
|
41
49
|
.action(updateWorkflowsCommand);
|
|
42
50
|
|
|
43
|
-
program
|
|
51
|
+
return program;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function run(argv) {
|
|
55
|
+
createProgram().parse(argv);
|
|
44
56
|
}
|
package/src/lib/config.js
CHANGED
|
@@ -14,6 +14,7 @@ function pkgPath(cwd = process.cwd()) {
|
|
|
14
14
|
/**
|
|
15
15
|
* Read the full package.json from a target repo.
|
|
16
16
|
* Returns null if it doesn't exist.
|
|
17
|
+
* @public (used by tests)
|
|
17
18
|
*/
|
|
18
19
|
export function readPkg(cwd = process.cwd()) {
|
|
19
20
|
const p = pkgPath(cwd);
|
|
@@ -72,6 +73,22 @@ export function getMode(cwd = process.cwd()) {
|
|
|
72
73
|
return aliases.length > 1 ? 'multi' : 'single';
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Whether optional preview/cleanup workflows are enabled.
|
|
78
|
+
*/
|
|
79
|
+
export function isPreviewWorkflowsEnabled(cwd = process.cwd()) {
|
|
80
|
+
const config = readConfig(cwd);
|
|
81
|
+
return config?.preview_workflows === true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Whether optional build/Lighthouse workflows are enabled.
|
|
86
|
+
*/
|
|
87
|
+
export function isBuildWorkflowsEnabled(cwd = process.cwd()) {
|
|
88
|
+
const config = readConfig(cwd);
|
|
89
|
+
return config?.build_workflows === true;
|
|
90
|
+
}
|
|
91
|
+
|
|
75
92
|
/**
|
|
76
93
|
* Add a store entry to the config.
|
|
77
94
|
* Returns the updated config.
|
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);
|