climaybe 3.2.0 → 3.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/README.md +14 -8
- package/bin/version.txt +1 -1
- package/package.json +1 -1
- package/src/index.js +6 -2
- package/src/lib/config.js +17 -0
- package/src/lib/dev-runtime.js +27 -6
- package/src/lib/prompts.js +22 -0
- package/src/lib/serve-multi-store.js +81 -0
- package/src/workflows/preview/cleanup-orphan-preview-themes.yml +59 -0
- package/src/workflows/preview/pr-close.yml +103 -62
- package/src/workflows/preview/pr-update.yml +87 -76
- package/src/workflows/preview/reusable-cleanup-themes.yml +91 -6
- package/src/workflows/preview/reusable-comment-on-pr.yml +83 -19
- package/src/workflows/preview/reusable-publish-pr-preview-store.yml +162 -0
package/README.md
CHANGED
|
@@ -94,7 +94,11 @@ Switch your local dev environment to a specific store (multi-store only).
|
|
|
94
94
|
npx climaybe switch voldt-norway
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
-
Copies `stores/<alias>/` JSON files to the repo root so you can preview that store locally, and sets `default_store` in `climaybe.config.json` so `climaybe serve` / `shopify theme dev` targets that store. If your current git branch is `staging-<alias>` or `live-<alias>`, `climaybe serve`
|
|
97
|
+
Copies `stores/<alias>/` JSON files to the repo root so you can preview that store locally, and sets `default_store` in `climaybe.config.json` so `climaybe serve` / `shopify theme dev` targets that store. If your current git branch is `staging-<alias>` or `live-<alias>`, `climaybe serve` still passes that branch’s store to Shopify for `--store` (even if `default_store` differs), matching CI preview behavior.
|
|
98
|
+
|
|
99
|
+
### `climaybe serve` / `climaybe theme serve`
|
|
100
|
+
|
|
101
|
+
Runs Tailwind/scripts/schema watchers and `shopify theme dev` (see the theme dev kit section for flags). In **multi-store** mode, the first step asks **which store to serve** (interactive terminal); the default choice is the store matching `default_store` (same as after `climaybe switch`). If you pick a **different** store, the CLI saves the current root `config/`, `templates/`, and `sections/` JSON files into `stores/<previous-alias>/` (same paths as `climaybe sync`), then copies the selected store’s JSONs to the root and updates `default_store`, so work in progress is not lost. In **CI** or when **stdin is not a TTY**, there is no prompt: it uses `default_store` unless you set **`CLIMAYBE_SERVE_STORE=<alias>`** to a configured alias.
|
|
98
102
|
|
|
99
103
|
### `climaybe sync [alias]` / `climaybe theme sync`
|
|
100
104
|
|
|
@@ -227,12 +231,14 @@ Enabled via `climaybe init` prompt (`Enable preview + cleanup workflows?`; defau
|
|
|
227
231
|
|
|
228
232
|
| Workflow | Trigger | What it does |
|
|
229
233
|
|----------|---------|-------------|
|
|
230
|
-
| `pr-update.yml` | PR opened/synchronize/reopened (base: main, staging, develop, staging-*, live-*) | Shares draft theme, renames with `-PR<number>`, comments preview + customize URLs
|
|
231
|
-
| `pr-close.yml` | PR closed (same branch set) | Deletes
|
|
232
|
-
| `
|
|
233
|
-
| `reusable-
|
|
234
|
-
| `reusable-
|
|
235
|
-
| `reusable-
|
|
234
|
+
| `pr-update.yml` | PR opened/synchronize/reopened (base: main, staging, develop, staging-*, live-*) | Shares draft theme, renames with `-PR<number>`, comments preview + customize URLs. **Multi-store + base not** `staging-<alias>` **or** `live-<alias>`**:** publishes to **every** configured store (matrix) and posts **all** links in one PR comment. For `staging-<alias>` / `live-<alias>` bases, only that store is used. **Path filter:** theme paths only (`assets/`, `blocks/`, `config/`, `layout/`, `locales/`, `sections/`, `snippets/`, `templates/`, `_scripts/`, `_styles/`, `shopify.theme.toml`, `stores/**`). |
|
|
235
|
+
| `pr-close.yml` | PR closed (same branch set) | Deletes this PR’s preview themes using the **same store matrix rule** as `pr-update`; PR comment shows **total** deleted count across stores. |
|
|
236
|
+
| `cleanup-orphan-preview-themes.yml` | Weekly (Mon 06:00 UTC) + `workflow_dispatch` | Per store: deletes themes ending with `-PR<n>` when PR `#n` is **not** open (merged/closed without cleanup). Uses `gh pr list` (limit 1000 open PRs). |
|
|
237
|
+
| `reusable-publish-pr-preview-store.yml` | workflow_call | Share + rename + upload comment fragment for **one** store (matrix leg in `pr-update`). |
|
|
238
|
+
| `reusable-share-theme.yml` | workflow_call | Shares Shopify draft theme and returns `theme_id` (still available if you call it elsewhere) |
|
|
239
|
+
| `reusable-rename-theme.yml` | workflow_call | Renames shared theme to include `PR<number>` (still available for custom flows) |
|
|
240
|
+
| `reusable-comment-on-pr.yml` | workflow_call | Posts preview comment; aggregates `preview-fragment-*` artifacts when `use_preview_fragments` is true |
|
|
241
|
+
| `reusable-cleanup-themes.yml` | workflow_call | `cleanup_mode: by_pr` deletes names ending with `-PR{padded}`; `orphan_pr` deletes `-PR<n>` when PR `n` is not open. Optional `result_artifact_prefix` for matrix fan-in on `pr-close` |
|
|
236
242
|
| `reusable-extract-pr-number.yml` | workflow_call | Extracts padded/unpadded PR number outputs for naming and API-safe usage |
|
|
237
243
|
|
|
238
244
|
### Optional build + Lighthouse package
|
|
@@ -374,7 +380,7 @@ Add the following secrets to your GitHub repository (or use **GitLab CI/CD varia
|
|
|
374
380
|
|
|
375
381
|
**Store URL:** During `climaybe init` (or `add-store`), store URL secret(s) are set from your configured store domain(s); theme tokens are optional prompts.
|
|
376
382
|
|
|
377
|
-
**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`). Preview
|
|
383
|
+
**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`). **Preview:** for PRs targeting **staging-<alias>** or **live-<alias>** only that store is used. For **main**, **staging**, **develop**, or other bases with **multiple** stores in `climaybe.config.json`, **pr-update** publishes the preview theme to **every** store (each must have secrets); **pr-close** cleans that PR’s preview themes on **all** stores. For a **single** store in config, behavior matches the default-store case. Optional **cleanup-orphan-preview-themes** (weekly + manual) removes stale `-PR<n>` themes when PR `n` is no longer open.
|
|
378
384
|
|
|
379
385
|
## Directory Structure (Multi-store)
|
|
380
386
|
|
package/bin/version.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.3.0
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -64,8 +64,12 @@ function registerThemeCommands(cmd) {
|
|
|
64
64
|
.command('serve')
|
|
65
65
|
.description('Run local theme dev (Shopify + assets; Theme Check off by default)')
|
|
66
66
|
.option('--theme-check', 'Enable Theme Check watcher')
|
|
67
|
-
.action((opts) =>
|
|
68
|
-
|
|
67
|
+
.action(async (opts) => {
|
|
68
|
+
await serveAll({ includeThemeCheck: opts.themeCheck === true });
|
|
69
|
+
});
|
|
70
|
+
cmd.command('serve:shopify').description('Run Shopify theme dev server').action(async () => {
|
|
71
|
+
await serveShopify();
|
|
72
|
+
});
|
|
69
73
|
cmd
|
|
70
74
|
.command('serve:assets')
|
|
71
75
|
.description('Run assets watch (Tailwind + scripts; Theme Check off by default)')
|
package/src/lib/config.js
CHANGED
|
@@ -180,6 +180,23 @@ export function getStoreAliases(cwd = process.cwd()) {
|
|
|
180
180
|
return Object.keys(config.stores);
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Resolve the store alias for `default_store` (the domain last set by `climaybe switch` or serve).
|
|
185
|
+
* @param {string} [cwd]
|
|
186
|
+
* @returns {string | null}
|
|
187
|
+
*/
|
|
188
|
+
export function getAliasForDefaultStore(cwd = process.cwd()) {
|
|
189
|
+
const config = readConfig(cwd);
|
|
190
|
+
const domain = config?.default_store;
|
|
191
|
+
const stores = config?.stores;
|
|
192
|
+
if (!domain || !stores || typeof stores !== 'object') return null;
|
|
193
|
+
const normalized = String(domain).trim();
|
|
194
|
+
for (const [alias, storeDomain] of Object.entries(stores)) {
|
|
195
|
+
if (String(storeDomain).trim() === normalized) return alias;
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
183
200
|
/**
|
|
184
201
|
* Determine the current mode: 'single' or 'multi'.
|
|
185
202
|
*/
|
package/src/lib/dev-runtime.js
CHANGED
|
@@ -4,6 +4,7 @@ import { watchTree } from './watch.js';
|
|
|
4
4
|
import { isAbsolute, join, relative } from 'node:path';
|
|
5
5
|
import pc from 'picocolors';
|
|
6
6
|
import { readConfig, getStoreDomainFromBranch } from './config.js';
|
|
7
|
+
import { prepareMultiStoreForServe } from './serve-multi-store.js';
|
|
7
8
|
import { buildScripts } from './build-scripts.js';
|
|
8
9
|
import { buildSchemas } from './schema-builder.js';
|
|
9
10
|
import { runShopify } from './shopify-cli.js';
|
|
@@ -209,7 +210,15 @@ function runThemeCheckFiltered({ cwd = process.cwd() } = {}) {
|
|
|
209
210
|
return child;
|
|
210
211
|
}
|
|
211
212
|
|
|
212
|
-
|
|
213
|
+
/**
|
|
214
|
+
* @param {{ cwd?: string; skipMultiStorePrep?: boolean }} [opts]
|
|
215
|
+
* @returns {Promise<import('node:child_process').ChildProcess | null>} Null when multi-store prep failed or was cancelled.
|
|
216
|
+
*/
|
|
217
|
+
export async function serveShopify({ cwd = process.cwd(), skipMultiStorePrep = false } = {}) {
|
|
218
|
+
if (!skipMultiStorePrep) {
|
|
219
|
+
const ok = await prepareMultiStoreForServe(cwd);
|
|
220
|
+
if (!ok) return null;
|
|
221
|
+
}
|
|
213
222
|
const config = readConfig(cwd) || {};
|
|
214
223
|
const branchStore = getStoreDomainFromBranch(cwd);
|
|
215
224
|
const store = branchStore || config.default_store || config.store || '';
|
|
@@ -365,16 +374,28 @@ export function serveAssets({ cwd = process.cwd(), includeThemeCheck = false } =
|
|
|
365
374
|
return { tailwind, devMcp, scriptsWatch, schemasWatch, themeCheckWatch, cleanup };
|
|
366
375
|
}
|
|
367
376
|
|
|
368
|
-
export function serveAll({ cwd = process.cwd(), includeThemeCheck = false } = {}) {
|
|
377
|
+
export async function serveAll({ cwd = process.cwd(), includeThemeCheck = false } = {}) {
|
|
378
|
+
const prepOk = await prepareMultiStoreForServe(cwd);
|
|
379
|
+
if (!prepOk) {
|
|
380
|
+
const noop = () => {};
|
|
381
|
+
return { cleanup: noop };
|
|
382
|
+
}
|
|
383
|
+
|
|
369
384
|
// Start assets first, then bring up Shopify after a short delay.
|
|
370
385
|
const assets = serveAssets({ cwd, includeThemeCheck });
|
|
371
386
|
let shopify = null;
|
|
372
387
|
const shopifyStartDelayMs = 2500;
|
|
373
388
|
const shopifyTimer = setTimeout(() => {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
389
|
+
void (async () => {
|
|
390
|
+
shopify = await serveShopify({ cwd, skipMultiStorePrep: true });
|
|
391
|
+
if (shopify) {
|
|
392
|
+
shopify.on('exit', () => {
|
|
393
|
+
cleanup();
|
|
394
|
+
});
|
|
395
|
+
} else {
|
|
396
|
+
cleanup();
|
|
397
|
+
}
|
|
398
|
+
})();
|
|
378
399
|
}, shopifyStartDelayMs);
|
|
379
400
|
console.log(pc.dim(` Waiting ${shopifyStartDelayMs}ms before starting Shopify...`));
|
|
380
401
|
|
package/src/lib/prompts.js
CHANGED
|
@@ -229,6 +229,28 @@ export async function promptProjectName(cwd = process.cwd()) {
|
|
|
229
229
|
return String(projectName || '').trim();
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Ask which store to use for local theme dev (multi-store `climaybe serve`).
|
|
234
|
+
* @param {{ aliases: string[]; stores: Record<string, string>; suggestedAlias: string }} opts
|
|
235
|
+
* @returns {Promise<string | null>} Selected alias, or null if cancelled.
|
|
236
|
+
*/
|
|
237
|
+
export async function promptServeStore({ aliases, stores, suggestedAlias }) {
|
|
238
|
+
const sorted = [...aliases].sort();
|
|
239
|
+
const choices = sorted.map((alias) => ({
|
|
240
|
+
title: `${alias} (${stores[alias]})`,
|
|
241
|
+
value: alias,
|
|
242
|
+
}));
|
|
243
|
+
const initialIndex = sorted.indexOf(suggestedAlias);
|
|
244
|
+
const { alias } = await prompts({
|
|
245
|
+
type: 'select',
|
|
246
|
+
name: 'alias',
|
|
247
|
+
message: 'Which store should we serve?',
|
|
248
|
+
choices,
|
|
249
|
+
initial: initialIndex >= 0 ? initialIndex : 0,
|
|
250
|
+
});
|
|
251
|
+
return alias ?? null;
|
|
252
|
+
}
|
|
253
|
+
|
|
232
254
|
/**
|
|
233
255
|
* Prompt for a single new store (used by add-store command).
|
|
234
256
|
* Takes existing aliases to prevent duplicates.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { readConfig, getMode, getProjectType, getStoreAliases, writeConfig, getAliasForDefaultStore } from './config.js';
|
|
3
|
+
import { rootToStores, storesToRoot } from './store-sync.js';
|
|
4
|
+
import { promptServeStore } from './prompts.js';
|
|
5
|
+
|
|
6
|
+
function readExplicitServeAlias(cwd) {
|
|
7
|
+
const raw = process.env.CLIMAYBE_SERVE_STORE?.trim();
|
|
8
|
+
if (!raw) return { ok: true, alias: null };
|
|
9
|
+
const aliases = getStoreAliases(cwd);
|
|
10
|
+
if (aliases.includes(raw)) return { ok: true, alias: raw };
|
|
11
|
+
console.log(pc.red(` CLIMAYBE_SERVE_STORE="${raw}" is not a known store alias.`));
|
|
12
|
+
console.log(pc.dim(` Available: ${aliases.join(', ')}\n`));
|
|
13
|
+
return { ok: false, alias: null };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isNonInteractiveServe() {
|
|
17
|
+
if (process.env.CLIMAYBE_SERVE_STORE?.trim()) return true;
|
|
18
|
+
if (process.env.CI === 'true') return true;
|
|
19
|
+
return process.stdin.isTTY !== true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* In multi-store theme repos, ensure root JSONs and `default_store` match the store
|
|
24
|
+
* the user wants to serve. If the choice differs from the previous switch target,
|
|
25
|
+
* saves current root → `stores/<previous>/` then loads `stores/<selected>/` → root.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} [cwd]
|
|
28
|
+
* @returns {Promise<boolean>} False if the user cancelled or `CLIMAYBE_SERVE_STORE` is invalid.
|
|
29
|
+
*/
|
|
30
|
+
export async function prepareMultiStoreForServe(cwd = process.cwd()) {
|
|
31
|
+
if (getProjectType(cwd) === 'app') return true;
|
|
32
|
+
if (getMode(cwd) !== 'multi') return true;
|
|
33
|
+
|
|
34
|
+
const config = readConfig(cwd);
|
|
35
|
+
if (!config?.stores || typeof config.stores !== 'object') return true;
|
|
36
|
+
|
|
37
|
+
const aliases = getStoreAliases(cwd);
|
|
38
|
+
if (aliases.length < 2) return true;
|
|
39
|
+
|
|
40
|
+
const stores = config.stores;
|
|
41
|
+
const suggestedAlias = getAliasForDefaultStore(cwd) ?? aliases[0];
|
|
42
|
+
|
|
43
|
+
const explicit = readExplicitServeAlias(cwd);
|
|
44
|
+
if (!explicit.ok) return false;
|
|
45
|
+
|
|
46
|
+
let selected = explicit.alias;
|
|
47
|
+
if (!selected) {
|
|
48
|
+
if (isNonInteractiveServe()) {
|
|
49
|
+
selected = suggestedAlias;
|
|
50
|
+
} else {
|
|
51
|
+
console.log(pc.bold('\n climaybe — serve (store)\n'));
|
|
52
|
+
selected = await promptServeStore({ aliases, stores, suggestedAlias });
|
|
53
|
+
if (!selected) {
|
|
54
|
+
console.log(pc.dim(' Cancelled.\n'));
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const previousAlias = getAliasForDefaultStore(cwd);
|
|
61
|
+
|
|
62
|
+
if (selected === previousAlias) return true;
|
|
63
|
+
|
|
64
|
+
if (previousAlias) {
|
|
65
|
+
rootToStores(previousAlias, cwd);
|
|
66
|
+
} else {
|
|
67
|
+
console.log(
|
|
68
|
+
pc.yellow(
|
|
69
|
+
' Warning: default_store did not match any store alias; current root JSONs are not auto-saved to a store folder.'
|
|
70
|
+
)
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const ok = storesToRoot(selected, cwd);
|
|
75
|
+
if (!ok) return false;
|
|
76
|
+
|
|
77
|
+
const domain = stores[selected];
|
|
78
|
+
if (domain) writeConfig({ default_store: domain }, cwd);
|
|
79
|
+
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# climaybe — Cleanup orphan preview themes (optional)
|
|
2
|
+
# Weekly (and manual): on each configured store, delete themes whose name ends with -PR<n>
|
|
3
|
+
# when PR #n is not open in this repo (merged/closed without theme cleanup).
|
|
4
|
+
|
|
5
|
+
name: Cleanup orphan preview themes
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
schedule:
|
|
9
|
+
- cron: '0 6 * * 1'
|
|
10
|
+
workflow_dispatch:
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
pull-requests: read
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
configure:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
outputs:
|
|
20
|
+
preview_targets_json: ${{ steps.cfg.outputs.preview_targets_json }}
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
|
|
24
|
+
- name: Build store matrix from climaybe.config.json
|
|
25
|
+
id: cfg
|
|
26
|
+
run: |
|
|
27
|
+
if [ ! -f climaybe.config.json ]; then
|
|
28
|
+
{
|
|
29
|
+
echo 'preview_targets_json<<PT_JSON'
|
|
30
|
+
echo '[]'
|
|
31
|
+
echo 'PT_JSON'
|
|
32
|
+
} >> "$GITHUB_OUTPUT"
|
|
33
|
+
exit 0
|
|
34
|
+
fi
|
|
35
|
+
node <<'NODE' >>"$GITHUB_OUTPUT"
|
|
36
|
+
const fs = require('fs');
|
|
37
|
+
const cfg = JSON.parse(fs.readFileSync('climaybe.config.json', 'utf8'));
|
|
38
|
+
const stores = cfg?.stores || {};
|
|
39
|
+
const keys = Object.keys(stores);
|
|
40
|
+
const aliasToSecret = (a) => String(a).toUpperCase().replace(/-/g, '_');
|
|
41
|
+
const targets = keys.map((alias) => ({ alias, alias_secret: aliasToSecret(alias) }));
|
|
42
|
+
console.log('preview_targets_json<<PT_JSON');
|
|
43
|
+
console.log(JSON.stringify(targets));
|
|
44
|
+
console.log('PT_JSON');
|
|
45
|
+
NODE
|
|
46
|
+
|
|
47
|
+
cleanup-orphans:
|
|
48
|
+
needs: [configure]
|
|
49
|
+
if: needs.configure.outputs.preview_targets_json != '[]'
|
|
50
|
+
strategy:
|
|
51
|
+
fail-fast: false
|
|
52
|
+
matrix:
|
|
53
|
+
include: ${{ fromJson(needs.configure.outputs.preview_targets_json) }}
|
|
54
|
+
uses: ./.github/workflows/reusable-cleanup-themes.yml
|
|
55
|
+
with:
|
|
56
|
+
cleanup_mode: orphan_pr
|
|
57
|
+
pr_number: ''
|
|
58
|
+
store_alias_secret: ${{ matrix.alias_secret }}
|
|
59
|
+
secrets: inherit
|
|
@@ -8,6 +8,10 @@ on:
|
|
|
8
8
|
types: [closed]
|
|
9
9
|
branches: [main, staging, develop, 'staging-*', 'live-*']
|
|
10
10
|
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
pull-requests: write
|
|
14
|
+
|
|
11
15
|
jobs:
|
|
12
16
|
extract-pr-number:
|
|
13
17
|
uses: ./.github/workflows/reusable-extract-pr-number.yml
|
|
@@ -15,73 +19,123 @@ jobs:
|
|
|
15
19
|
resolve-store:
|
|
16
20
|
runs-on: ubuntu-latest
|
|
17
21
|
outputs:
|
|
18
|
-
store_alias: ${{ steps.resolve.outputs.
|
|
19
|
-
store_alias_secret: ${{ steps.resolve.outputs.
|
|
22
|
+
store_alias: ${{ steps.resolve.outputs.store_alias }}
|
|
23
|
+
store_alias_secret: ${{ steps.resolve.outputs.store_alias_secret }}
|
|
24
|
+
preview_targets_json: ${{ steps.resolve.outputs.preview_targets_json }}
|
|
20
25
|
steps:
|
|
21
26
|
- name: Checkout code
|
|
22
27
|
uses: actions/checkout@v4
|
|
23
28
|
|
|
24
|
-
- name: Resolve store
|
|
29
|
+
- name: Resolve preview store targets from base branch
|
|
25
30
|
id: resolve
|
|
26
31
|
env:
|
|
27
32
|
BASE_REF: ${{ github.event.pull_request.base.ref }}
|
|
28
33
|
run: |
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
34
|
+
node <<'NODE' >>"$GITHUB_OUTPUT"
|
|
35
|
+
const fs = require('fs');
|
|
36
|
+
const cfg = JSON.parse(fs.readFileSync('climaybe.config.json', 'utf8'));
|
|
37
|
+
const stores = cfg?.stores || {};
|
|
38
|
+
const keys = Object.keys(stores);
|
|
39
|
+
const baseRef = process.env.BASE_REF || '';
|
|
40
|
+
const isMulti = keys.length > 1;
|
|
41
|
+
const isStagingLive =
|
|
42
|
+
baseRef && (/^staging-/.test(baseRef) || /^live-/.test(baseRef));
|
|
43
|
+
|
|
44
|
+
const aliasToSecret = (a) => String(a).toUpperCase().replace(/-/g, '_');
|
|
45
|
+
const normalizeDomain = (v) =>
|
|
46
|
+
String(v || '')
|
|
47
|
+
.toLowerCase()
|
|
48
|
+
.replace(/^https?:\/\//, '')
|
|
49
|
+
.replace(/\/.*$/, '');
|
|
50
|
+
|
|
51
|
+
let targets = [];
|
|
52
|
+
if (isMulti && !isStagingLive) {
|
|
53
|
+
targets = keys.map((alias) => ({ alias, alias_secret: aliasToSecret(alias) }));
|
|
54
|
+
} else if (isStagingLive) {
|
|
55
|
+
let a = baseRef.replace(/^staging-/, '').replace(/^live-/, '');
|
|
56
|
+
targets = [{ alias: a, alias_secret: aliasToSecret(a) }];
|
|
57
|
+
} else {
|
|
58
|
+
const defaultStoreNorm = normalizeDomain(cfg?.default_store);
|
|
59
|
+
let alias = '';
|
|
60
|
+
if (defaultStoreNorm) {
|
|
61
|
+
for (const [k, d] of Object.entries(stores)) {
|
|
62
|
+
if (normalizeDomain(d) === defaultStoreNorm) {
|
|
63
|
+
alias = k;
|
|
64
|
+
break;
|
|
50
65
|
}
|
|
51
66
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
process.stdout.write(alias);
|
|
63
|
-
")
|
|
64
|
-
fi
|
|
67
|
+
}
|
|
68
|
+
if (!alias) alias = keys[0] || '';
|
|
69
|
+
if (!alias && cfg?.default_store) {
|
|
70
|
+
const sub = normalizeDomain(cfg.default_store).split('.')[0] || 'default';
|
|
71
|
+
alias = sub.replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-') || 'default';
|
|
72
|
+
}
|
|
73
|
+
if (!alias) alias = 'default';
|
|
74
|
+
targets = [{ alias, alias_secret: aliasToSecret(alias) }];
|
|
75
|
+
}
|
|
65
76
|
|
|
66
|
-
if
|
|
67
|
-
|
|
68
|
-
exit
|
|
69
|
-
|
|
77
|
+
if (targets.length === 0 || targets.some((t) => !t.alias)) {
|
|
78
|
+
console.error('Could not resolve store targets from climaybe.config.json');
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
70
81
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
console.log('preview_targets_json<<PT_JSON');
|
|
83
|
+
console.log(JSON.stringify(targets));
|
|
84
|
+
console.log('PT_JSON');
|
|
85
|
+
console.log(`store_alias=${targets[0].alias}`);
|
|
86
|
+
console.log(`store_alias_secret=${targets[0].alias_secret}`);
|
|
87
|
+
NODE
|
|
74
88
|
|
|
75
89
|
cleanup-theme:
|
|
76
90
|
needs: [extract-pr-number, resolve-store]
|
|
91
|
+
strategy:
|
|
92
|
+
fail-fast: false
|
|
93
|
+
matrix:
|
|
94
|
+
include: ${{ fromJson(needs.resolve-store.outputs.preview_targets_json) }}
|
|
77
95
|
uses: ./.github/workflows/reusable-cleanup-themes.yml
|
|
78
96
|
with:
|
|
97
|
+
cleanup_mode: by_pr
|
|
79
98
|
pr_number: ${{ needs.extract-pr-number.outputs.pr_number }}
|
|
80
|
-
store_alias_secret: ${{
|
|
99
|
+
store_alias_secret: ${{ matrix.alias_secret }}
|
|
100
|
+
result_artifact_prefix: pr-close-cleanup
|
|
81
101
|
secrets: inherit
|
|
82
102
|
|
|
103
|
+
cleanup-totals:
|
|
104
|
+
needs: [cleanup-theme]
|
|
105
|
+
if: always()
|
|
106
|
+
runs-on: ubuntu-latest
|
|
107
|
+
permissions:
|
|
108
|
+
actions: read
|
|
109
|
+
outputs:
|
|
110
|
+
deleted_count: ${{ steps.sum.outputs.deleted_count }}
|
|
111
|
+
steps:
|
|
112
|
+
- name: Download cleanup result fragments
|
|
113
|
+
continue-on-error: true
|
|
114
|
+
uses: actions/download-artifact@v4
|
|
115
|
+
with:
|
|
116
|
+
pattern: pr-close-cleanup-*
|
|
117
|
+
merge-multiple: true
|
|
118
|
+
path: cleanup-counts
|
|
119
|
+
|
|
120
|
+
- name: Sum deleted themes across stores
|
|
121
|
+
id: sum
|
|
122
|
+
run: |
|
|
123
|
+
total=0
|
|
124
|
+
if [ -d cleanup-counts ]; then
|
|
125
|
+
while IFS= read -r -d '' f; do
|
|
126
|
+
[ -z "$f" ] && continue
|
|
127
|
+
[ ! -f "$f" ] && continue
|
|
128
|
+
n="$(tr -d ' \n\r' < "$f" 2>/dev/null || echo 0)"
|
|
129
|
+
n="${n:-0}"
|
|
130
|
+
total=$((total + n))
|
|
131
|
+
done < <(find cleanup-counts -name 'deleted-count.txt' -type f -print0 2>/dev/null || true)
|
|
132
|
+
fi
|
|
133
|
+
echo "deleted_count=$total" >> "$GITHUB_OUTPUT"
|
|
134
|
+
echo "Total deleted across stores: $total"
|
|
135
|
+
|
|
83
136
|
comment-on-pr:
|
|
84
|
-
needs: [cleanup-
|
|
137
|
+
needs: [cleanup-totals, resolve-store]
|
|
138
|
+
if: always()
|
|
85
139
|
runs-on: ubuntu-latest
|
|
86
140
|
steps:
|
|
87
141
|
- name: Comment on PR about cleanup
|
|
@@ -90,8 +144,7 @@ jobs:
|
|
|
90
144
|
script: |
|
|
91
145
|
const prNumber = context.payload.pull_request.number;
|
|
92
146
|
const storeAlias = '${{ needs.resolve-store.outputs.store_alias }}' || '';
|
|
93
|
-
const deletedCount = parseInt('${{ needs.cleanup-
|
|
94
|
-
const deletedThemesRaw = `${{ needs.cleanup-theme.outputs.deleted_themes }}`.trim();
|
|
147
|
+
const deletedCount = parseInt('${{ needs.cleanup-totals.outputs.deleted_count }}') || 0;
|
|
95
148
|
|
|
96
149
|
const lines = [
|
|
97
150
|
'## 🧹 Theme Cleanup Complete',
|
|
@@ -100,22 +153,10 @@ jobs:
|
|
|
100
153
|
'',
|
|
101
154
|
`**Branch:** ${context.payload.pull_request.head.ref}`,
|
|
102
155
|
`**PR Status:** ${context.payload.pull_request.state}`,
|
|
103
|
-
storeAlias ? `**
|
|
104
|
-
`**Deleted
|
|
156
|
+
storeAlias ? `**Primary store (config):** ${storeAlias}` : null,
|
|
157
|
+
`**Deleted themes (all preview stores):** ${deletedCount}`
|
|
105
158
|
].filter(Boolean);
|
|
106
159
|
|
|
107
|
-
if (deletedCount > 0 && deletedThemesRaw) {
|
|
108
|
-
const deletedThemeLines = deletedThemesRaw
|
|
109
|
-
.split('\n')
|
|
110
|
-
.map(name => name.trim())
|
|
111
|
-
.filter(Boolean)
|
|
112
|
-
.map(name => `- ${name}`);
|
|
113
|
-
|
|
114
|
-
if (deletedThemeLines.length > 0) {
|
|
115
|
-
lines.push('', '### Deleted Theme Names', ...deletedThemeLines);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
160
|
lines.push('', '---', '*Theme cleanup runs automatically when a PR is closed.*');
|
|
120
161
|
|
|
121
162
|
await github.rest.issues.createComment({
|
|
@@ -44,119 +44,130 @@ jobs:
|
|
|
44
44
|
if: needs.filter.outputs.theme == 'true'
|
|
45
45
|
runs-on: ubuntu-latest
|
|
46
46
|
outputs:
|
|
47
|
-
store_alias: ${{ steps.resolve.outputs.
|
|
48
|
-
store_alias_secret: ${{ steps.resolve.outputs.
|
|
47
|
+
store_alias: ${{ steps.resolve.outputs.store_alias }}
|
|
48
|
+
store_alias_secret: ${{ steps.resolve.outputs.store_alias_secret }}
|
|
49
|
+
preview_targets_json: ${{ steps.resolve.outputs.preview_targets_json }}
|
|
49
50
|
steps:
|
|
50
51
|
- name: Checkout code
|
|
51
52
|
uses: actions/checkout@v4
|
|
52
53
|
|
|
53
|
-
- name: Resolve store
|
|
54
|
+
- name: Resolve preview store targets from base branch
|
|
54
55
|
id: resolve
|
|
55
56
|
env:
|
|
56
57
|
BASE_REF: ${{ github.event.pull_request.base.ref }}
|
|
57
58
|
run: |
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
59
|
+
node <<'NODE' >>"$GITHUB_OUTPUT"
|
|
60
|
+
const fs = require('fs');
|
|
61
|
+
const cfg = JSON.parse(fs.readFileSync('climaybe.config.json', 'utf8'));
|
|
62
|
+
const stores = cfg?.stores || {};
|
|
63
|
+
const keys = Object.keys(stores);
|
|
64
|
+
const baseRef = process.env.BASE_REF || '';
|
|
65
|
+
const isMulti = keys.length > 1;
|
|
66
|
+
const isStagingLive =
|
|
67
|
+
baseRef && (/^staging-/.test(baseRef) || /^live-/.test(baseRef));
|
|
68
|
+
|
|
69
|
+
const aliasToSecret = (a) => String(a).toUpperCase().replace(/-/g, '_');
|
|
70
|
+
const normalizeDomain = (v) =>
|
|
71
|
+
String(v || '')
|
|
72
|
+
.toLowerCase()
|
|
73
|
+
.replace(/^https?:\/\//, '')
|
|
74
|
+
.replace(/\/.*$/, '');
|
|
75
|
+
|
|
76
|
+
let targets = [];
|
|
77
|
+
if (isMulti && !isStagingLive) {
|
|
78
|
+
targets = keys.map((alias) => ({ alias, alias_secret: aliasToSecret(alias) }));
|
|
79
|
+
} else if (isStagingLive) {
|
|
80
|
+
let a = baseRef.replace(/^staging-/, '').replace(/^live-/, '');
|
|
81
|
+
targets = [{ alias: a, alias_secret: aliasToSecret(a) }];
|
|
82
|
+
} else {
|
|
83
|
+
const defaultStoreNorm = normalizeDomain(cfg?.default_store);
|
|
84
|
+
let alias = '';
|
|
85
|
+
if (defaultStoreNorm) {
|
|
86
|
+
for (const [k, d] of Object.entries(stores)) {
|
|
87
|
+
if (normalizeDomain(d) === defaultStoreNorm) {
|
|
88
|
+
alias = k;
|
|
89
|
+
break;
|
|
80
90
|
}
|
|
81
91
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
process.stdout.write(alias);
|
|
93
|
-
")
|
|
94
|
-
fi
|
|
92
|
+
}
|
|
93
|
+
if (!alias) alias = keys[0] || '';
|
|
94
|
+
if (!alias && cfg?.default_store) {
|
|
95
|
+
const sub = normalizeDomain(cfg.default_store).split('.')[0] || 'default';
|
|
96
|
+
alias = sub.replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-') || 'default';
|
|
97
|
+
}
|
|
98
|
+
if (!alias) alias = 'default';
|
|
99
|
+
targets = [{ alias, alias_secret: aliasToSecret(alias) }];
|
|
100
|
+
}
|
|
95
101
|
|
|
96
|
-
if
|
|
97
|
-
|
|
98
|
-
exit
|
|
99
|
-
|
|
102
|
+
if (targets.length === 0 || targets.some((t) => !t.alias)) {
|
|
103
|
+
console.error('Could not resolve store targets from climaybe.config.json');
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
100
106
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
107
|
+
console.log('preview_targets_json<<PT_JSON');
|
|
108
|
+
console.log(JSON.stringify(targets));
|
|
109
|
+
console.log('PT_JSON');
|
|
110
|
+
console.log(`store_alias=${targets[0].alias}`);
|
|
111
|
+
console.log(`store_alias_secret=${targets[0].alias_secret}`);
|
|
112
|
+
NODE
|
|
104
113
|
|
|
105
|
-
|
|
114
|
+
validate-secrets-per-store:
|
|
115
|
+
needs: [validate-environment]
|
|
116
|
+
strategy:
|
|
117
|
+
fail-fast: false
|
|
118
|
+
matrix:
|
|
119
|
+
include: ${{ fromJson(needs.validate-environment.outputs.preview_targets_json) }}
|
|
120
|
+
runs-on: ubuntu-latest
|
|
121
|
+
steps:
|
|
122
|
+
- name: Require Shopify URL + theme token for this store
|
|
106
123
|
env:
|
|
107
|
-
SHOPIFY_STORE_URL_SCOPED: ${{ secrets[format('SHOPIFY_STORE_URL_{0}',
|
|
108
|
-
SHOPIFY_THEME_ACCESS_TOKEN_SCOPED: ${{ secrets[format('SHOPIFY_THEME_ACCESS_TOKEN_{0}',
|
|
124
|
+
SHOPIFY_STORE_URL_SCOPED: ${{ secrets[format('SHOPIFY_STORE_URL_{0}', matrix.alias_secret)] }}
|
|
125
|
+
SHOPIFY_THEME_ACCESS_TOKEN_SCOPED: ${{ secrets[format('SHOPIFY_THEME_ACCESS_TOKEN_{0}', matrix.alias_secret)] }}
|
|
109
126
|
SHOPIFY_STORE_URL_DEFAULT: ${{ secrets.SHOPIFY_STORE_URL }}
|
|
110
127
|
SHOPIFY_THEME_ACCESS_TOKEN_DEFAULT: ${{ secrets.SHOPIFY_THEME_ACCESS_TOKEN }}
|
|
111
128
|
run: |
|
|
112
129
|
SHOPIFY_STORE_URL="${SHOPIFY_STORE_URL_SCOPED:-$SHOPIFY_STORE_URL_DEFAULT}"
|
|
113
130
|
SHOPIFY_THEME_ACCESS_TOKEN="${SHOPIFY_THEME_ACCESS_TOKEN_SCOPED:-$SHOPIFY_THEME_ACCESS_TOKEN_DEFAULT}"
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
echo "No store URL secret found. Expected SHOPIFY_STORE_URL_${{ steps.resolve.outputs.alias_secret }} or SHOPIFY_STORE_URL."
|
|
131
|
+
if [ -z "$SHOPIFY_STORE_URL" ] || [ -z "$SHOPIFY_THEME_ACCESS_TOKEN" ]; then
|
|
132
|
+
echo "Missing SHOPIFY_* for ${{ matrix.alias }} (secret suffix ${{ matrix.alias_secret }})."
|
|
117
133
|
exit 1
|
|
118
134
|
fi
|
|
119
|
-
|
|
120
|
-
echo "No theme token secret found. Expected SHOPIFY_THEME_ACCESS_TOKEN_${{ steps.resolve.outputs.alias_secret }} or SHOPIFY_THEME_ACCESS_TOKEN."
|
|
121
|
-
exit 1
|
|
122
|
-
fi
|
|
123
|
-
echo "Shopify credentials are configured for alias: ${{ steps.resolve.outputs.alias }}"
|
|
135
|
+
echo "Shopify credentials OK for ${{ matrix.alias }}"
|
|
124
136
|
|
|
125
137
|
cleanup-themes:
|
|
126
|
-
needs: [extract-pr-number, validate-
|
|
138
|
+
needs: [extract-pr-number, validate-secrets-per-store]
|
|
139
|
+
strategy:
|
|
140
|
+
fail-fast: false
|
|
141
|
+
matrix:
|
|
142
|
+
include: ${{ fromJson(needs.validate-environment.outputs.preview_targets_json) }}
|
|
127
143
|
uses: ./.github/workflows/reusable-cleanup-themes.yml
|
|
128
144
|
with:
|
|
145
|
+
cleanup_mode: by_pr
|
|
129
146
|
pr_number: ${{ needs.extract-pr-number.outputs.pr_number }}
|
|
130
|
-
store_alias_secret: ${{
|
|
147
|
+
store_alias_secret: ${{ matrix.alias_secret }}
|
|
131
148
|
secrets: inherit
|
|
132
149
|
|
|
133
|
-
|
|
150
|
+
publish-preview-store:
|
|
134
151
|
needs: [validate-environment, extract-pr-number, cleanup-themes]
|
|
135
|
-
|
|
152
|
+
strategy:
|
|
153
|
+
fail-fast: false
|
|
154
|
+
matrix:
|
|
155
|
+
include: ${{ fromJson(needs.validate-environment.outputs.preview_targets_json) }}
|
|
156
|
+
uses: ./.github/workflows/reusable-publish-pr-preview-store.yml
|
|
136
157
|
with:
|
|
137
158
|
pr_number: ${{ needs.extract-pr-number.outputs.pr_number }}
|
|
138
|
-
store_alias: ${{
|
|
139
|
-
store_alias_secret: ${{
|
|
140
|
-
secrets: inherit
|
|
141
|
-
|
|
142
|
-
rename-theme:
|
|
143
|
-
needs: [share-theme, extract-pr-number, validate-environment]
|
|
144
|
-
uses: ./.github/workflows/reusable-rename-theme.yml
|
|
145
|
-
with:
|
|
146
|
-
theme_id: ${{ needs.share-theme.outputs.theme_id }}
|
|
147
|
-
theme_name: ${{ needs.share-theme.outputs.theme_name }}
|
|
148
|
-
pr_number: ${{ needs.extract-pr-number.outputs.pr_number }}
|
|
149
|
-
store_alias: ${{ needs.validate-environment.outputs.store_alias }}
|
|
150
|
-
store_alias_secret: ${{ needs.validate-environment.outputs.store_alias_secret }}
|
|
159
|
+
store_alias: ${{ matrix.alias }}
|
|
160
|
+
store_alias_secret: ${{ matrix.alias_secret }}
|
|
151
161
|
secrets: inherit
|
|
152
162
|
|
|
153
163
|
comment-on-pr:
|
|
154
|
-
needs: [
|
|
164
|
+
needs: [publish-preview-store, extract-pr-number, validate-environment]
|
|
155
165
|
uses: ./.github/workflows/reusable-comment-on-pr.yml
|
|
156
166
|
with:
|
|
157
|
-
theme_id: ${{ needs.
|
|
158
|
-
share_output: ${{ needs.
|
|
167
|
+
theme_id: ${{ needs.publish-preview-store.outputs.theme_id }}
|
|
168
|
+
share_output: ${{ needs.publish-preview-store.outputs.share_output }}
|
|
159
169
|
pr_number: ${{ needs.extract-pr-number.outputs.pr_number_unpadded }}
|
|
160
170
|
store_alias: ${{ needs.validate-environment.outputs.store_alias }}
|
|
161
171
|
store_alias_secret: ${{ needs.validate-environment.outputs.store_alias_secret }}
|
|
172
|
+
use_preview_fragments: 'true'
|
|
162
173
|
secrets: inherit
|
|
@@ -3,14 +3,25 @@ name: Cleanup Themes
|
|
|
3
3
|
on:
|
|
4
4
|
workflow_call:
|
|
5
5
|
inputs:
|
|
6
|
+
cleanup_mode:
|
|
7
|
+
required: false
|
|
8
|
+
type: string
|
|
9
|
+
default: 'by_pr'
|
|
10
|
+
description: "by_pr: delete themes whose name ends with -PR{pr_number} (padded). orphan_pr: delete themes ending with -PR<n> when PR n is not open in this repo."
|
|
6
11
|
pr_number:
|
|
7
|
-
required:
|
|
12
|
+
required: false
|
|
8
13
|
type: string
|
|
9
|
-
|
|
14
|
+
default: ''
|
|
15
|
+
description: "Padded PR number (e.g. 09) when cleanup_mode is by_pr; ignored for orphan_pr."
|
|
10
16
|
store_alias_secret:
|
|
11
17
|
required: false
|
|
12
18
|
type: string
|
|
13
19
|
description: "Upper snake-case alias for scoped secret (e.g. VOLDT_STAGING). If set, uses SHOPIFY_*_<this>; else uses SHOPIFY_STORE_URL / SHOPIFY_THEME_ACCESS_TOKEN."
|
|
20
|
+
result_artifact_prefix:
|
|
21
|
+
required: false
|
|
22
|
+
type: string
|
|
23
|
+
default: ''
|
|
24
|
+
description: "When non-empty, uploads a tiny artifact named {prefix}-{store_alias_secret} with deleted_count for matrix fan-in."
|
|
14
25
|
outputs:
|
|
15
26
|
deleted_count:
|
|
16
27
|
description: "Number of deleted themes"
|
|
@@ -34,6 +45,10 @@ on:
|
|
|
34
45
|
jobs:
|
|
35
46
|
cleanup:
|
|
36
47
|
runs-on: ubuntu-latest
|
|
48
|
+
permissions:
|
|
49
|
+
contents: read
|
|
50
|
+
pull-requests: read
|
|
51
|
+
actions: write
|
|
37
52
|
outputs:
|
|
38
53
|
deleted_count: ${{ steps.cleanup.outputs.deleted_count }}
|
|
39
54
|
deleted_themes: ${{ steps.cleanup.outputs.deleted_themes }}
|
|
@@ -51,7 +66,10 @@ jobs:
|
|
|
51
66
|
SHOPIFY_STORE_URL: ${{ inputs.store_alias_secret && secrets[format('SHOPIFY_STORE_URL_{0}', inputs.store_alias_secret)] || secrets.SHOPIFY_STORE_URL }}
|
|
52
67
|
SHOPIFY_THEME_ACCESS_TOKEN: ${{ inputs.store_alias_secret && secrets[format('SHOPIFY_THEME_ACCESS_TOKEN_{0}', inputs.store_alias_secret)] || secrets.SHOPIFY_THEME_ACCESS_TOKEN }}
|
|
53
68
|
PR_NUMBER: ${{ inputs.pr_number }}
|
|
69
|
+
CLEANUP_MODE: ${{ inputs.cleanup_mode }}
|
|
54
70
|
STORE_ALIAS_SECRET: ${{ inputs.store_alias_secret }}
|
|
71
|
+
RESULT_ARTIFACT_PREFIX: ${{ inputs.result_artifact_prefix }}
|
|
72
|
+
GH_TOKEN: ${{ github.token }}
|
|
55
73
|
run: |
|
|
56
74
|
set -euo pipefail
|
|
57
75
|
|
|
@@ -62,11 +80,13 @@ jobs:
|
|
|
62
80
|
{
|
|
63
81
|
echo "### 🧹 Cleanup preview themes"
|
|
64
82
|
echo ""
|
|
65
|
-
echo "- **
|
|
83
|
+
echo "- **Mode**: \`${CLEANUP_MODE}\`"
|
|
84
|
+
echo "- **PR (by_pr mode)**: \`${PR_NUMBER:-<n/a>}\`"
|
|
66
85
|
echo "- **Store alias secret**: \`${STORE_ALIAS_SECRET:-<default>}\`"
|
|
67
86
|
echo "- **Store (host)**: \`${STORE_HOST:-<missing>}\`"
|
|
68
87
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
69
88
|
|
|
89
|
+
echo "CLEANUP_MODE=${CLEANUP_MODE}"
|
|
70
90
|
echo "PR=${PR_NUMBER}"
|
|
71
91
|
echo "Store alias secret=${STORE_ALIAS_SECRET:-<default>}"
|
|
72
92
|
echo "Store host=${STORE_HOST:-<missing>}"
|
|
@@ -93,6 +113,11 @@ jobs:
|
|
|
93
113
|
exit 0
|
|
94
114
|
fi
|
|
95
115
|
|
|
116
|
+
if [ "$CLEANUP_MODE" = "by_pr" ] && [ -z "${PR_NUMBER:-}" ]; then
|
|
117
|
+
echo "cleanup_mode is by_pr but pr_number is empty."
|
|
118
|
+
exit 1
|
|
119
|
+
fi
|
|
120
|
+
|
|
96
121
|
echo "store_hint=${STORE_HOST:-}" >> "$GITHUB_OUTPUT"
|
|
97
122
|
echo "skipped_reason=" >> "$GITHUB_OUTPUT"
|
|
98
123
|
|
|
@@ -121,7 +146,6 @@ jobs:
|
|
|
121
146
|
exit 1
|
|
122
147
|
fi
|
|
123
148
|
|
|
124
|
-
# Defensive: if CLI returned empty, treat as an empty list (instead of jq failure).
|
|
125
149
|
if [ ! -s "$THEME_LIST_JSON" ]; then
|
|
126
150
|
echo "[]" > "$THEME_LIST_JSON"
|
|
127
151
|
fi
|
|
@@ -134,11 +158,59 @@ jobs:
|
|
|
134
158
|
DELETED_COUNT=0
|
|
135
159
|
DELETED_THEMES=""
|
|
136
160
|
|
|
161
|
+
OPEN_PRS_FILE="$(mktemp)"
|
|
162
|
+
if [ "$CLEANUP_MODE" = "orphan_pr" ]; then
|
|
163
|
+
echo "Fetching open PR numbers (limit 1000)…"
|
|
164
|
+
if ! gh pr list --repo "${GITHUB_REPOSITORY}" --state open --limit 1000 --json number -q '.[].number' > "$OPEN_PRS_FILE" 2>/dev/null; then
|
|
165
|
+
echo "gh pr list failed; cannot determine open PRs safely. Skipping orphan cleanup."
|
|
166
|
+
{
|
|
167
|
+
echo ""
|
|
168
|
+
echo "#### Result"
|
|
169
|
+
echo "- **Skipped**: \`gh pr list\` failed (check \`pull-requests: read\` permission)"
|
|
170
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
171
|
+
echo "skipped_reason=gh_pr_list_failed" >> "$GITHUB_OUTPUT"
|
|
172
|
+
echo "matched_count=0" >> "$GITHUB_OUTPUT"
|
|
173
|
+
echo "matched_themes<<MATCHED_EOF" >> "$GITHUB_OUTPUT"
|
|
174
|
+
echo "MATCHED_EOF" >> "$GITHUB_OUTPUT"
|
|
175
|
+
echo "deleted_count=0" >> "$GITHUB_OUTPUT"
|
|
176
|
+
echo "deleted_themes<<DELETED_EOF" >> "$GITHUB_OUTPUT"
|
|
177
|
+
echo "DELETED_EOF" >> "$GITHUB_OUTPUT"
|
|
178
|
+
rm -f "$THEME_LIST_ERR" "$THEME_LIST_JSON" "$OPEN_PRS_FILE"
|
|
179
|
+
exit 0
|
|
180
|
+
fi
|
|
181
|
+
echo "Open PR count (lines): $(wc -l < "$OPEN_PRS_FILE" | tr -d ' ')"
|
|
182
|
+
fi
|
|
183
|
+
|
|
184
|
+
should_delete_theme() {
|
|
185
|
+
local id="$1"
|
|
186
|
+
local name="$2"
|
|
187
|
+
local suffix=""
|
|
188
|
+
suffix="$(printf '%s' "$name" | sed -n 's/.*-PR\([0-9][0-9]*\)$/\1/p')"
|
|
189
|
+
if [ -z "$suffix" ]; then
|
|
190
|
+
return 1
|
|
191
|
+
fi
|
|
192
|
+
if [ "$CLEANUP_MODE" = "by_pr" ]; then
|
|
193
|
+
case "$name" in
|
|
194
|
+
*"-PR${PR_NUMBER}") return 0 ;;
|
|
195
|
+
*) return 1 ;;
|
|
196
|
+
esac
|
|
197
|
+
fi
|
|
198
|
+
if [ "$CLEANUP_MODE" = "orphan_pr" ]; then
|
|
199
|
+
local num=""
|
|
200
|
+
num=$((10#$suffix))
|
|
201
|
+
if grep -qx "$num" "$OPEN_PRS_FILE" 2>/dev/null; then
|
|
202
|
+
return 1
|
|
203
|
+
fi
|
|
204
|
+
return 0
|
|
205
|
+
fi
|
|
206
|
+
return 1
|
|
207
|
+
}
|
|
208
|
+
|
|
137
209
|
while IFS=$'\t' read -r THEME_ID THEME_NAME; do
|
|
138
210
|
[ -z "$THEME_ID" ] && continue
|
|
139
211
|
[ -z "$THEME_NAME" ] && continue
|
|
140
212
|
|
|
141
|
-
if
|
|
213
|
+
if should_delete_theme "$THEME_ID" "$THEME_NAME"; then
|
|
142
214
|
MATCHED_COUNT=$((MATCHED_COUNT + 1))
|
|
143
215
|
MATCHED_THEMES="${MATCHED_THEMES}${THEME_NAME}\n"
|
|
144
216
|
echo "Matched: $THEME_NAME ($THEME_ID)"
|
|
@@ -164,7 +236,7 @@ jobs:
|
|
|
164
236
|
echo ""
|
|
165
237
|
echo "#### Result"
|
|
166
238
|
echo "- **Total themes on store**: ${ALL_COUNT}"
|
|
167
|
-
echo "- **Matched
|
|
239
|
+
echo "- **Matched**: ${MATCHED_COUNT}"
|
|
168
240
|
echo "- **Deleted**: ${DELETED_COUNT}"
|
|
169
241
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
170
242
|
|
|
@@ -197,3 +269,16 @@ jobs:
|
|
|
197
269
|
printf "%b" "$DELETED_THEMES"
|
|
198
270
|
echo "DELETED_EOF"
|
|
199
271
|
} >> "$GITHUB_OUTPUT"
|
|
272
|
+
|
|
273
|
+
rm -f "$THEME_LIST_ERR" "$THEME_LIST_JSON" "$OPEN_PRS_FILE"
|
|
274
|
+
|
|
275
|
+
- name: Upload deleted count for matrix fan-in
|
|
276
|
+
if: inputs.result_artifact_prefix != ''
|
|
277
|
+
run: printf '%s' '${{ steps.cleanup.outputs.deleted_count }}' > deleted-count.txt
|
|
278
|
+
- name: Save cleanup result artifact
|
|
279
|
+
if: inputs.result_artifact_prefix != ''
|
|
280
|
+
uses: actions/upload-artifact@v4
|
|
281
|
+
with:
|
|
282
|
+
name: ${{ inputs.result_artifact_prefix }}-${{ inputs.store_alias_secret }}
|
|
283
|
+
path: deleted-count.txt
|
|
284
|
+
retention-days: 2
|
|
@@ -19,11 +19,29 @@ on:
|
|
|
19
19
|
required: false
|
|
20
20
|
type: string
|
|
21
21
|
description: "Upper snake-case alias for scoped secret (e.g. VOLDT_STAGING). If set, uses SHOPIFY_STORE_URL_<this>; else uses SHOPIFY_STORE_URL."
|
|
22
|
+
use_preview_fragments:
|
|
23
|
+
required: false
|
|
24
|
+
type: string
|
|
25
|
+
default: 'false'
|
|
26
|
+
description: "When 'true', download preview-fragment-* artifacts (from publish matrix) and post one comment with all preview URLs."
|
|
22
27
|
|
|
23
28
|
jobs:
|
|
24
29
|
comment:
|
|
25
30
|
runs-on: ubuntu-latest
|
|
31
|
+
permissions:
|
|
32
|
+
contents: read
|
|
33
|
+
actions: read
|
|
34
|
+
pull-requests: write
|
|
26
35
|
steps:
|
|
36
|
+
- name: Download preview fragments (multi-store / matrix publish)
|
|
37
|
+
if: inputs.use_preview_fragments == 'true'
|
|
38
|
+
continue-on-error: true
|
|
39
|
+
uses: actions/download-artifact@v4
|
|
40
|
+
with:
|
|
41
|
+
pattern: preview-fragment-*
|
|
42
|
+
merge-multiple: true
|
|
43
|
+
path: preview-fragments
|
|
44
|
+
|
|
27
45
|
- name: Post preview comment
|
|
28
46
|
uses: actions/github-script@v7
|
|
29
47
|
env:
|
|
@@ -31,39 +49,85 @@ jobs:
|
|
|
31
49
|
THEME_ID: ${{ inputs.theme_id }}
|
|
32
50
|
SHARE_OUTPUT: ${{ inputs.share_output }}
|
|
33
51
|
PR_NUMBER: ${{ inputs.pr_number }}
|
|
52
|
+
USE_PREVIEW_FRAGMENTS: ${{ inputs.use_preview_fragments }}
|
|
34
53
|
with:
|
|
35
54
|
script: |
|
|
55
|
+
const fs = require('fs');
|
|
56
|
+
const path = require('path');
|
|
36
57
|
const issueNumber = parseInt(process.env.PR_NUMBER);
|
|
37
|
-
const
|
|
38
|
-
.replace(/^https?:\/\//, '')
|
|
39
|
-
.replace(/\/$/, '');
|
|
40
|
-
const themeId = process.env.THEME_ID || '';
|
|
41
|
-
const shareOutput = process.env.SHARE_OUTPUT || '';
|
|
58
|
+
const useFragments = process.env.USE_PREVIEW_FRAGMENTS === 'true';
|
|
42
59
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
60
|
+
const walkJsonFiles = (dir) => {
|
|
61
|
+
const out = [];
|
|
62
|
+
if (!fs.existsSync(dir)) return out;
|
|
63
|
+
const stack = [dir];
|
|
64
|
+
while (stack.length) {
|
|
65
|
+
const d = stack.pop();
|
|
66
|
+
for (const ent of fs.readdirSync(d, { withFileTypes: true })) {
|
|
67
|
+
const p = path.join(d, ent.name);
|
|
68
|
+
if (ent.isDirectory()) stack.push(p);
|
|
69
|
+
else if (ent.name === 'fragment.json' || ent.name.endsWith('.json')) {
|
|
70
|
+
try {
|
|
71
|
+
out.push(JSON.parse(fs.readFileSync(p, 'utf8')));
|
|
72
|
+
} catch {
|
|
73
|
+
// ignore
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
let fragments = [];
|
|
82
|
+
if (useFragments) {
|
|
83
|
+
fragments = walkJsonFiles('preview-fragments');
|
|
48
84
|
}
|
|
49
85
|
|
|
50
86
|
const parts = [
|
|
51
87
|
'## 🎨 Theme Preview Generated',
|
|
52
88
|
'',
|
|
53
|
-
`**Store:** ${process.env.SHOPIFY_STORE_URL || 'N/A'}`,
|
|
54
89
|
`**Branch:** ${context.payload.pull_request?.head?.ref || context.ref.replace('refs/heads/', '')}`,
|
|
55
90
|
`**Commit:** ${(context.payload.pull_request?.head?.sha || context.sha || '').substring(0, 7)}`
|
|
56
91
|
];
|
|
57
92
|
|
|
58
|
-
if (
|
|
59
|
-
parts.push('',
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
93
|
+
if (fragments.length > 0) {
|
|
94
|
+
parts.push('', '### Preview links (per store)');
|
|
95
|
+
const byAlias = [...fragments].sort((a, b) => String(a.alias).localeCompare(String(b.alias)));
|
|
96
|
+
for (const f of byAlias) {
|
|
97
|
+
const host = (f.store_host || '').replace(/\/$/, '');
|
|
98
|
+
const tid = f.theme_id || '';
|
|
99
|
+
const alias = f.alias || 'store';
|
|
100
|
+
if (host && tid) {
|
|
101
|
+
const previewUrl = `https://${host}?preview_theme_id=${tid}`;
|
|
102
|
+
const customizeUrl = `https://${host}/admin/themes/${tid}/editor`;
|
|
103
|
+
parts.push('', `**${alias}**`, `- Customize: ${customizeUrl}`, `- Preview: ${previewUrl}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} else if (useFragments) {
|
|
107
|
+
parts.push(
|
|
108
|
+
'',
|
|
109
|
+
'*(Could not load per-store preview fragments from workflow artifacts; see the publish job logs.)*'
|
|
110
|
+
);
|
|
111
|
+
} else {
|
|
112
|
+
const storeDomain = (process.env.SHOPIFY_STORE_URL || '')
|
|
113
|
+
.replace(/^https?:\/\//, '')
|
|
114
|
+
.replace(/\/$/, '');
|
|
115
|
+
const themeId = process.env.THEME_ID || '';
|
|
116
|
+
const shareOutput = process.env.SHARE_OUTPUT || '';
|
|
117
|
+
parts.push('', `**Store:** ${process.env.SHOPIFY_STORE_URL || 'N/A'}`);
|
|
118
|
+
let previewUrl = '';
|
|
119
|
+
let customizeUrl = '';
|
|
120
|
+
if (storeDomain && themeId) {
|
|
121
|
+
previewUrl = `https://${storeDomain}?preview_theme_id=${themeId}`;
|
|
122
|
+
customizeUrl = `https://${storeDomain}/admin/themes/${themeId}/editor`;
|
|
123
|
+
}
|
|
124
|
+
if (customizeUrl) parts.push('', `**Customize URL:** ${customizeUrl}`);
|
|
125
|
+
if (previewUrl) parts.push('', `**Preview URL:** ${previewUrl}`);
|
|
126
|
+
if (shareOutput.trim()) {
|
|
127
|
+
parts.push('', '### Share Output', '```', shareOutput, '```');
|
|
128
|
+
}
|
|
66
129
|
}
|
|
130
|
+
|
|
67
131
|
parts.push('', '---', '*This preview will be available for 7 days.*');
|
|
68
132
|
|
|
69
133
|
await github.rest.issues.createComment({
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Share theme to one Shopify store, rename with -PR{padded}, upload JSON fragment for PR comment fan-in.
|
|
2
|
+
# Intended to be called from a matrix job (one invocation per store).
|
|
3
|
+
|
|
4
|
+
name: Publish PR preview (single store)
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
workflow_call:
|
|
8
|
+
inputs:
|
|
9
|
+
pr_number:
|
|
10
|
+
required: true
|
|
11
|
+
type: string
|
|
12
|
+
description: "Padded PR number (e.g. 09) for theme name suffix -PR09"
|
|
13
|
+
store_alias:
|
|
14
|
+
required: true
|
|
15
|
+
type: string
|
|
16
|
+
description: "Store alias (for artifact naming and comment JSON)"
|
|
17
|
+
store_alias_secret:
|
|
18
|
+
required: false
|
|
19
|
+
type: string
|
|
20
|
+
default: ''
|
|
21
|
+
description: "Upper snake-case secret suffix; empty uses SHOPIFY_STORE_URL / SHOPIFY_THEME_ACCESS_TOKEN"
|
|
22
|
+
outputs:
|
|
23
|
+
theme_id:
|
|
24
|
+
description: "Theme ID after share/rename"
|
|
25
|
+
value: ${{ jobs.publish.outputs.theme_id }}
|
|
26
|
+
theme_name:
|
|
27
|
+
description: "Final theme name after rename"
|
|
28
|
+
value: ${{ jobs.publish.outputs.theme_name }}
|
|
29
|
+
share_output:
|
|
30
|
+
description: "Raw share command output"
|
|
31
|
+
value: ${{ jobs.publish.outputs.share_output }}
|
|
32
|
+
|
|
33
|
+
jobs:
|
|
34
|
+
publish:
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
permissions:
|
|
37
|
+
contents: read
|
|
38
|
+
actions: write
|
|
39
|
+
outputs:
|
|
40
|
+
theme_id: ${{ steps.rename.outputs.theme_id }}
|
|
41
|
+
theme_name: ${{ steps.rename.outputs.theme_name }}
|
|
42
|
+
share_output: ${{ steps.share.outputs.share_output }}
|
|
43
|
+
steps:
|
|
44
|
+
- name: Checkout code
|
|
45
|
+
uses: actions/checkout@v4
|
|
46
|
+
|
|
47
|
+
- name: Validate theme root
|
|
48
|
+
run: |
|
|
49
|
+
if [ ! -f "layout/theme.liquid" ]; then
|
|
50
|
+
echo "layout/theme.liquid not found. Ensure workflow runs at theme repository root."
|
|
51
|
+
exit 1
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
- name: Install Shopify CLI
|
|
55
|
+
run: npm install -g @shopify/cli @shopify/theme
|
|
56
|
+
|
|
57
|
+
- name: Share theme
|
|
58
|
+
id: share
|
|
59
|
+
env:
|
|
60
|
+
SHOPIFY_STORE_URL: ${{ inputs.store_alias_secret && secrets[format('SHOPIFY_STORE_URL_{0}', inputs.store_alias_secret)] || secrets.SHOPIFY_STORE_URL }}
|
|
61
|
+
SHOPIFY_THEME_ACCESS_TOKEN: ${{ inputs.store_alias_secret && secrets[format('SHOPIFY_THEME_ACCESS_TOKEN_{0}', inputs.store_alias_secret)] || secrets.SHOPIFY_THEME_ACCESS_TOKEN }}
|
|
62
|
+
run: |
|
|
63
|
+
if [ -z "$SHOPIFY_STORE_URL" ] || [ -z "$SHOPIFY_THEME_ACCESS_TOKEN" ]; then
|
|
64
|
+
echo "Missing Shopify secrets."
|
|
65
|
+
exit 1
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
OUTPUT=$(shopify theme share \
|
|
69
|
+
--store "$SHOPIFY_STORE_URL" \
|
|
70
|
+
--password "$SHOPIFY_THEME_ACCESS_TOKEN" 2>&1)
|
|
71
|
+
STATUS=$?
|
|
72
|
+
|
|
73
|
+
echo "$OUTPUT"
|
|
74
|
+
if [ $STATUS -ne 0 ]; then
|
|
75
|
+
echo "Theme share failed."
|
|
76
|
+
exit $STATUS
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
THEME_NAME=$(echo "$OUTPUT" | sed -n "s/.*The theme '\([^']*\)'.*/\1/p" | head -1)
|
|
80
|
+
THEME_ID=$(echo "$OUTPUT" | sed -n 's/.*#\([0-9]*\).*/\1/p' | head -1)
|
|
81
|
+
|
|
82
|
+
if [ -z "$THEME_ID" ]; then
|
|
83
|
+
echo "Could not parse theme id from share output."
|
|
84
|
+
exit 1
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
echo "theme_id=$THEME_ID" >> "$GITHUB_OUTPUT"
|
|
88
|
+
if [ -n "$THEME_NAME" ]; then
|
|
89
|
+
echo "theme_name=$THEME_NAME" >> "$GITHUB_OUTPUT"
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
{
|
|
93
|
+
echo "share_output<<SHARE_EOF"
|
|
94
|
+
echo "$OUTPUT"
|
|
95
|
+
echo "SHARE_EOF"
|
|
96
|
+
} >> "$GITHUB_OUTPUT"
|
|
97
|
+
|
|
98
|
+
- name: Rename theme with PR suffix
|
|
99
|
+
id: rename
|
|
100
|
+
env:
|
|
101
|
+
SHOPIFY_STORE_URL: ${{ inputs.store_alias_secret && secrets[format('SHOPIFY_STORE_URL_{0}', inputs.store_alias_secret)] || secrets.SHOPIFY_STORE_URL }}
|
|
102
|
+
SHOPIFY_THEME_ACCESS_TOKEN: ${{ inputs.store_alias_secret && secrets[format('SHOPIFY_THEME_ACCESS_TOKEN_{0}', inputs.store_alias_secret)] || secrets.SHOPIFY_THEME_ACCESS_TOKEN }}
|
|
103
|
+
THEME_ID: ${{ steps.share.outputs.theme_id }}
|
|
104
|
+
THEME_NAME: ${{ steps.share.outputs.theme_name }}
|
|
105
|
+
PR_NUMBER: ${{ inputs.pr_number }}
|
|
106
|
+
run: |
|
|
107
|
+
if [ -z "$THEME_ID" ] || [ -z "$THEME_NAME" ]; then
|
|
108
|
+
echo "Missing theme_id/theme_name from share step."
|
|
109
|
+
exit 1
|
|
110
|
+
fi
|
|
111
|
+
if [ -z "$SHOPIFY_STORE_URL" ] || [ -z "$SHOPIFY_THEME_ACCESS_TOKEN" ]; then
|
|
112
|
+
echo "Missing Shopify store URL/token for rename."
|
|
113
|
+
exit 1
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
NEW_THEME_NAME="${THEME_NAME}-PR${PR_NUMBER}"
|
|
117
|
+
echo "Renaming theme $THEME_ID to '$NEW_THEME_NAME'..."
|
|
118
|
+
|
|
119
|
+
if shopify theme rename \
|
|
120
|
+
--store "$SHOPIFY_STORE_URL" \
|
|
121
|
+
--password "$SHOPIFY_THEME_ACCESS_TOKEN" \
|
|
122
|
+
--theme "$THEME_ID" \
|
|
123
|
+
--name "$NEW_THEME_NAME" 2>&1; then
|
|
124
|
+
echo "Rename succeeded with password auth."
|
|
125
|
+
elif shopify theme rename \
|
|
126
|
+
--store "$SHOPIFY_STORE_URL" \
|
|
127
|
+
--theme "$THEME_ID" \
|
|
128
|
+
--name "$NEW_THEME_NAME" 2>&1; then
|
|
129
|
+
echo "Rename succeeded with authenticated session."
|
|
130
|
+
else
|
|
131
|
+
echo "Failed to rename theme."
|
|
132
|
+
exit 1
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
echo "theme_id=$THEME_ID" >> "$GITHUB_OUTPUT"
|
|
136
|
+
echo "theme_name=$NEW_THEME_NAME" >> "$GITHUB_OUTPUT"
|
|
137
|
+
|
|
138
|
+
- name: Write preview fragment for PR comment
|
|
139
|
+
env:
|
|
140
|
+
STORE_ALIAS: ${{ inputs.store_alias }}
|
|
141
|
+
THEME_ID: ${{ steps.rename.outputs.theme_id }}
|
|
142
|
+
SHOPIFY_STORE_URL: ${{ inputs.store_alias_secret && secrets[format('SHOPIFY_STORE_URL_{0}', inputs.store_alias_secret)] || secrets.SHOPIFY_STORE_URL }}
|
|
143
|
+
run: |
|
|
144
|
+
node <<'NODE'
|
|
145
|
+
const fs = require('fs');
|
|
146
|
+
const url = (process.env.SHOPIFY_STORE_URL || '')
|
|
147
|
+
.replace(/^https?:\/\//, '')
|
|
148
|
+
.replace(/\/.*$/, '');
|
|
149
|
+
const payload = {
|
|
150
|
+
alias: process.env.STORE_ALIAS || '',
|
|
151
|
+
theme_id: process.env.THEME_ID || '',
|
|
152
|
+
store_host: url,
|
|
153
|
+
};
|
|
154
|
+
fs.writeFileSync('fragment.json', JSON.stringify(payload, null, 0));
|
|
155
|
+
NODE
|
|
156
|
+
|
|
157
|
+
- name: Upload preview fragment
|
|
158
|
+
uses: actions/upload-artifact@v4
|
|
159
|
+
with:
|
|
160
|
+
name: preview-fragment-${{ inputs.store_alias }}
|
|
161
|
+
path: fragment.json
|
|
162
|
+
retention-days: 2
|