climaybe 3.1.4 → 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 +35 -8
- 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 +105 -61
- package/src/workflows/preview/pr-update.yml +87 -76
- package/src/workflows/preview/reusable-cleanup-themes.yml +220 -14
- package/src/workflows/preview/reusable-comment-on-pr.yml +83 -19
- package/src/workflows/preview/reusable-publish-pr-preview-store.yml +162 -0
|
@@ -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"
|
|
@@ -18,13 +29,33 @@ on:
|
|
|
18
29
|
deleted_themes:
|
|
19
30
|
description: "Deleted theme names, one per line"
|
|
20
31
|
value: ${{ jobs.cleanup.outputs.deleted_themes }}
|
|
32
|
+
matched_count:
|
|
33
|
+
description: "Number of themes matching this PR (before delete)"
|
|
34
|
+
value: ${{ jobs.cleanup.outputs.matched_count }}
|
|
35
|
+
matched_themes:
|
|
36
|
+
description: "Matched theme names, one per line (before delete)"
|
|
37
|
+
value: ${{ jobs.cleanup.outputs.matched_themes }}
|
|
38
|
+
skipped_reason:
|
|
39
|
+
description: "Why cleanup was skipped (empty if not skipped)"
|
|
40
|
+
value: ${{ jobs.cleanup.outputs.skipped_reason }}
|
|
41
|
+
store_hint:
|
|
42
|
+
description: "Redacted store host (best-effort; may be masked)"
|
|
43
|
+
value: ${{ jobs.cleanup.outputs.store_hint }}
|
|
21
44
|
|
|
22
45
|
jobs:
|
|
23
46
|
cleanup:
|
|
24
47
|
runs-on: ubuntu-latest
|
|
48
|
+
permissions:
|
|
49
|
+
contents: read
|
|
50
|
+
pull-requests: read
|
|
51
|
+
actions: write
|
|
25
52
|
outputs:
|
|
26
53
|
deleted_count: ${{ steps.cleanup.outputs.deleted_count }}
|
|
27
54
|
deleted_themes: ${{ steps.cleanup.outputs.deleted_themes }}
|
|
55
|
+
matched_count: ${{ steps.cleanup.outputs.matched_count }}
|
|
56
|
+
matched_themes: ${{ steps.cleanup.outputs.matched_themes }}
|
|
57
|
+
skipped_reason: ${{ steps.cleanup.outputs.skipped_reason }}
|
|
58
|
+
store_hint: ${{ steps.cleanup.outputs.store_hint }}
|
|
28
59
|
steps:
|
|
29
60
|
- name: Install Shopify CLI
|
|
30
61
|
run: npm install -g @shopify/cli @shopify/theme
|
|
@@ -35,44 +66,219 @@ jobs:
|
|
|
35
66
|
SHOPIFY_STORE_URL: ${{ inputs.store_alias_secret && secrets[format('SHOPIFY_STORE_URL_{0}', inputs.store_alias_secret)] || secrets.SHOPIFY_STORE_URL }}
|
|
36
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 }}
|
|
37
68
|
PR_NUMBER: ${{ inputs.pr_number }}
|
|
69
|
+
CLEANUP_MODE: ${{ inputs.cleanup_mode }}
|
|
70
|
+
STORE_ALIAS_SECRET: ${{ inputs.store_alias_secret }}
|
|
71
|
+
RESULT_ARTIFACT_PREFIX: ${{ inputs.result_artifact_prefix }}
|
|
72
|
+
GH_TOKEN: ${{ github.token }}
|
|
38
73
|
run: |
|
|
39
|
-
|
|
74
|
+
set -euo pipefail
|
|
75
|
+
|
|
76
|
+
STORE_HOST="${SHOPIFY_STORE_URL#https://}"
|
|
77
|
+
STORE_HOST="${STORE_HOST#http://}"
|
|
78
|
+
STORE_HOST="${STORE_HOST%%/*}"
|
|
79
|
+
|
|
80
|
+
{
|
|
81
|
+
echo "### 🧹 Cleanup preview themes"
|
|
82
|
+
echo ""
|
|
83
|
+
echo "- **Mode**: \`${CLEANUP_MODE}\`"
|
|
84
|
+
echo "- **PR (by_pr mode)**: \`${PR_NUMBER:-<n/a>}\`"
|
|
85
|
+
echo "- **Store alias secret**: \`${STORE_ALIAS_SECRET:-<default>}\`"
|
|
86
|
+
echo "- **Store (host)**: \`${STORE_HOST:-<missing>}\`"
|
|
87
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
88
|
+
|
|
89
|
+
echo "CLEANUP_MODE=${CLEANUP_MODE}"
|
|
90
|
+
echo "PR=${PR_NUMBER}"
|
|
91
|
+
echo "Store alias secret=${STORE_ALIAS_SECRET:-<default>}"
|
|
92
|
+
echo "Store host=${STORE_HOST:-<missing>}"
|
|
93
|
+
|
|
94
|
+
if [ -z "${SHOPIFY_STORE_URL:-}" ] || [ -z "${SHOPIFY_THEME_ACCESS_TOKEN:-}" ]; then
|
|
40
95
|
echo "Missing Shopify secrets; skipping theme cleanup (no themes deleted)."
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
96
|
+
|
|
97
|
+
{
|
|
98
|
+
echo ""
|
|
99
|
+
echo "#### Result"
|
|
100
|
+
echo "- **Skipped**: yes (missing Shopify secrets)"
|
|
101
|
+
echo "- **Matched themes**: 0"
|
|
102
|
+
echo "- **Deleted themes**: 0"
|
|
103
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
104
|
+
|
|
105
|
+
echo "skipped_reason=missing_secrets" >> "$GITHUB_OUTPUT"
|
|
106
|
+
echo "store_hint=${STORE_HOST:-}" >> "$GITHUB_OUTPUT"
|
|
107
|
+
echo "matched_count=0" >> "$GITHUB_OUTPUT"
|
|
108
|
+
echo "matched_themes<<MATCHED_EOF" >> "$GITHUB_OUTPUT"
|
|
109
|
+
echo "MATCHED_EOF" >> "$GITHUB_OUTPUT"
|
|
110
|
+
echo "deleted_count=0" >> "$GITHUB_OUTPUT"
|
|
111
|
+
echo "deleted_themes<<DELETED_EOF" >> "$GITHUB_OUTPUT"
|
|
112
|
+
echo "DELETED_EOF" >> "$GITHUB_OUTPUT"
|
|
44
113
|
exit 0
|
|
45
114
|
fi
|
|
46
115
|
|
|
47
|
-
|
|
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
|
+
|
|
121
|
+
echo "store_hint=${STORE_HOST:-}" >> "$GITHUB_OUTPUT"
|
|
122
|
+
echo "skipped_reason=" >> "$GITHUB_OUTPUT"
|
|
123
|
+
|
|
124
|
+
THEME_LIST_ERR="$(mktemp)"
|
|
125
|
+
THEME_LIST_JSON="$(mktemp)"
|
|
126
|
+
|
|
127
|
+
if ! shopify theme list \
|
|
48
128
|
--store "$SHOPIFY_STORE_URL" \
|
|
49
129
|
--password "$SHOPIFY_THEME_ACCESS_TOKEN" \
|
|
50
|
-
--json
|
|
130
|
+
--json > "$THEME_LIST_JSON" 2> "$THEME_LIST_ERR"; then
|
|
131
|
+
echo "shopify theme list failed."
|
|
132
|
+
echo "Error output (first 120 lines):"
|
|
133
|
+
sed -n '1,120p' "$THEME_LIST_ERR" || true
|
|
134
|
+
{
|
|
135
|
+
echo ""
|
|
136
|
+
echo "#### Result"
|
|
137
|
+
echo "- **Error**: \`shopify theme list\` failed"
|
|
138
|
+
echo ""
|
|
139
|
+
echo "<details><summary>CLI error output</summary>"
|
|
140
|
+
echo ""
|
|
141
|
+
echo "\`\`\`"
|
|
142
|
+
sed -n '1,240p' "$THEME_LIST_ERR" || true
|
|
143
|
+
echo "\`\`\`"
|
|
144
|
+
echo "</details>"
|
|
145
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
146
|
+
exit 1
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
if [ ! -s "$THEME_LIST_JSON" ]; then
|
|
150
|
+
echo "[]" > "$THEME_LIST_JSON"
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
ALL_COUNT="$(jq -r 'length' "$THEME_LIST_JSON" 2>/dev/null || echo "0")"
|
|
154
|
+
echo "Found ${ALL_COUNT} total theme(s) on store."
|
|
51
155
|
|
|
156
|
+
MATCHED_COUNT=0
|
|
157
|
+
MATCHED_THEMES=""
|
|
52
158
|
DELETED_COUNT=0
|
|
53
159
|
DELETED_THEMES=""
|
|
54
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
|
+
|
|
55
209
|
while IFS=$'\t' read -r THEME_ID THEME_NAME; do
|
|
56
210
|
[ -z "$THEME_ID" ] && continue
|
|
57
211
|
[ -z "$THEME_NAME" ] && continue
|
|
58
212
|
|
|
59
|
-
if
|
|
213
|
+
if should_delete_theme "$THEME_ID" "$THEME_NAME"; then
|
|
214
|
+
MATCHED_COUNT=$((MATCHED_COUNT + 1))
|
|
215
|
+
MATCHED_THEMES="${MATCHED_THEMES}${THEME_NAME}\n"
|
|
216
|
+
echo "Matched: $THEME_NAME ($THEME_ID)"
|
|
217
|
+
|
|
218
|
+
DELETE_ERR="$(mktemp)"
|
|
60
219
|
if shopify theme delete \
|
|
61
220
|
--store "$SHOPIFY_STORE_URL" \
|
|
62
221
|
--password "$SHOPIFY_THEME_ACCESS_TOKEN" \
|
|
63
222
|
--force \
|
|
64
|
-
--theme "$THEME_ID"
|
|
223
|
+
--theme "$THEME_ID" 1>/dev/null 2>"$DELETE_ERR"; then
|
|
65
224
|
DELETED_COUNT=$((DELETED_COUNT + 1))
|
|
66
225
|
DELETED_THEMES="${DELETED_THEMES}${THEME_NAME}\n"
|
|
226
|
+
echo "Deleted: $THEME_NAME ($THEME_ID)"
|
|
67
227
|
else
|
|
68
|
-
echo "Failed to delete
|
|
228
|
+
echo "Failed to delete: $THEME_NAME ($THEME_ID)"
|
|
229
|
+
echo "Delete error output (first 80 lines):"
|
|
230
|
+
sed -n '1,80p' "$DELETE_ERR" || true
|
|
69
231
|
fi
|
|
70
232
|
fi
|
|
71
|
-
done < <(
|
|
233
|
+
done < <(jq -r '.[] | [.id, .name] | @tsv' "$THEME_LIST_JSON")
|
|
72
234
|
|
|
73
|
-
|
|
235
|
+
{
|
|
236
|
+
echo ""
|
|
237
|
+
echo "#### Result"
|
|
238
|
+
echo "- **Total themes on store**: ${ALL_COUNT}"
|
|
239
|
+
echo "- **Matched**: ${MATCHED_COUNT}"
|
|
240
|
+
echo "- **Deleted**: ${DELETED_COUNT}"
|
|
241
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
242
|
+
|
|
243
|
+
if [ "$MATCHED_COUNT" -gt 0 ]; then
|
|
244
|
+
{
|
|
245
|
+
echo ""
|
|
246
|
+
echo "#### Matched theme names"
|
|
247
|
+
printf "%b" "$MATCHED_THEMES" | sed -e 's/^/- /'
|
|
248
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
249
|
+
fi
|
|
250
|
+
|
|
251
|
+
if [ "$DELETED_COUNT" -gt 0 ]; then
|
|
252
|
+
{
|
|
253
|
+
echo ""
|
|
254
|
+
echo "#### Deleted theme names"
|
|
255
|
+
printf "%b" "$DELETED_THEMES" | sed -e 's/^/- /'
|
|
256
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
257
|
+
fi
|
|
258
|
+
|
|
259
|
+
echo "matched_count=$MATCHED_COUNT" >> "$GITHUB_OUTPUT"
|
|
260
|
+
{
|
|
261
|
+
echo "matched_themes<<MATCHED_EOF"
|
|
262
|
+
printf "%b" "$MATCHED_THEMES"
|
|
263
|
+
echo "MATCHED_EOF"
|
|
264
|
+
} >> "$GITHUB_OUTPUT"
|
|
265
|
+
|
|
266
|
+
echo "deleted_count=$DELETED_COUNT" >> "$GITHUB_OUTPUT"
|
|
74
267
|
{
|
|
75
268
|
echo "deleted_themes<<DELETED_EOF"
|
|
76
269
|
printf "%b" "$DELETED_THEMES"
|
|
77
270
|
echo "DELETED_EOF"
|
|
78
|
-
} >> $GITHUB_OUTPUT
|
|
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({
|