climaybe 3.2.0 → 3.4.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 +16 -8
- package/bin/version.txt +1 -1
- package/package.json +1 -1
- package/src/commands/build-scripts.js +7 -1
- package/src/cursor/rules/00-rule-index.mdc +1 -0
- package/src/cursor/rules/javascript-standards.mdc +3 -1
- package/src/cursor/rules/js-refactor-tasks.mdc +1 -1
- package/src/cursor/rules/project-overview.mdc +20 -2
- package/src/cursor/rules/tailwindcss-rules.mdc +2 -1
- package/src/index.js +6 -2
- package/src/lib/build-scripts.js +36 -3
- package/src/lib/config.js +17 -0
- package/src/lib/dev-runtime.js +39 -9
- 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
|
@@ -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
|