climaybe 1.0.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +64 -12
- package/package.json +45 -2
- package/src/commands/add-store.js +77 -4
- package/src/commands/init.js +150 -16
- package/src/commands/update-workflows.js +4 -2
- package/src/index.js +15 -3
- package/src/lib/config.js +17 -0
- package/src/lib/git.js +21 -3
- package/src/lib/github-secrets.js +263 -0
- package/src/lib/prompts.js +116 -6
- package/src/lib/workflows.js +23 -3
- package/src/workflows/build/build-pipeline.yml +57 -0
- package/src/workflows/build/create-release.yml +52 -0
- package/src/workflows/build/reusable-build.yml +52 -0
- package/src/workflows/multi/main-to-staging-stores.yml +44 -3
- package/src/workflows/multi/multistore-hotfix-to-main.yml +84 -0
- package/src/workflows/multi/pr-to-live.yml +82 -8
- package/src/workflows/multi/stores-to-root.yml +10 -3
- package/src/workflows/preview/pr-close.yml +63 -0
- package/src/workflows/preview/pr-update.yml +120 -0
- package/src/workflows/preview/reusable-cleanup-themes.yml +71 -0
- package/src/workflows/preview/reusable-comment-on-pr.yml +76 -0
- package/src/workflows/preview/reusable-extract-pr-number.yml +35 -0
- package/src/workflows/preview/reusable-rename-theme.yml +73 -0
- package/src/workflows/preview/reusable-share-theme.yml +94 -0
- package/src/workflows/shared/ai-changelog.yml +82 -24
- package/src/workflows/shared/version-bump.yml +22 -7
- package/src/workflows/single/nightly-hotfix.yml +38 -2
- package/src/workflows/single/post-merge-tag.yml +68 -15
- package/src/workflows/single/release-pr-check.yml +16 -6
- package/src/workflows/multi/hotfix-backport.yml +0 -100
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# climaybe — Main to Staging
|
|
1
|
+
# climaybe — Main to Staging <store> (Multi-store)
|
|
2
2
|
# When a PR is merged into main (from staging), this workflow
|
|
3
3
|
# opens and auto-merges PRs from main to each staging-<alias> branch.
|
|
4
|
-
# Skips hotfix
|
|
4
|
+
# Skips [hotfix-backport] and version-bump commits so multistore-hotfix-to-main syncs are not re-pushed to stores.
|
|
5
5
|
|
|
6
6
|
name: Main to Staging Stores
|
|
7
7
|
|
|
@@ -72,6 +72,7 @@ jobs:
|
|
|
72
72
|
permissions:
|
|
73
73
|
contents: write
|
|
74
74
|
pull-requests: write
|
|
75
|
+
actions: write
|
|
75
76
|
env:
|
|
76
77
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
77
78
|
steps:
|
|
@@ -112,7 +113,47 @@ jobs:
|
|
|
112
113
|
|
|
113
114
|
# Auto-merge
|
|
114
115
|
PR_NUM=$(echo "$PR_URL" | grep -oP '\d+$')
|
|
115
|
-
gh pr merge "$PR_NUM" --merge --admin 2>/dev/null
|
|
116
|
+
if gh pr merge "$PR_NUM" --merge --admin 2>/dev/null; then
|
|
117
|
+
echo "Auto-merge succeeded for $BRANCH"
|
|
118
|
+
|
|
119
|
+
# Explicitly trigger Stores to Root to avoid missing downstream runs
|
|
120
|
+
# when merges are performed by GITHUB_TOKEN-based automation.
|
|
121
|
+
gh workflow run "Stores to Root" --ref "$BRANCH" 2>/dev/null || \
|
|
122
|
+
echo "Failed to trigger Stores to Root for $BRANCH"
|
|
123
|
+
|
|
124
|
+
# Wait for the latest Stores to Root run on this branch to complete,
|
|
125
|
+
# then trigger PR to Live to keep ordering deterministic.
|
|
126
|
+
for i in $(seq 1 30); do
|
|
127
|
+
RUN_STATUS=$(gh run list \
|
|
128
|
+
--workflow "Stores to Root" \
|
|
129
|
+
--branch "$BRANCH" \
|
|
130
|
+
--limit 1 \
|
|
131
|
+
--json status,conclusion \
|
|
132
|
+
--jq '.[0].status + "|" + (.[0].conclusion // "")' 2>/dev/null || echo "")
|
|
133
|
+
|
|
134
|
+
if [ -z "$RUN_STATUS" ]; then
|
|
135
|
+
sleep 5
|
|
136
|
+
continue
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
STATUS="${RUN_STATUS%%|*}"
|
|
140
|
+
CONCLUSION="${RUN_STATUS##*|}"
|
|
141
|
+
|
|
142
|
+
if [ "$STATUS" = "completed" ]; then
|
|
143
|
+
if [ "$CONCLUSION" = "success" ]; then
|
|
144
|
+
gh workflow run "PR to Live" --ref "$BRANCH" 2>/dev/null || \
|
|
145
|
+
echo "Failed to trigger PR to Live for $BRANCH"
|
|
146
|
+
else
|
|
147
|
+
echo "Stores to Root did not succeed for $BRANCH (conclusion: $CONCLUSION)"
|
|
148
|
+
fi
|
|
149
|
+
break
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
sleep 5
|
|
153
|
+
done
|
|
154
|
+
else
|
|
155
|
+
echo "Auto-merge failed — manual review may be needed."
|
|
156
|
+
fi
|
|
116
157
|
else
|
|
117
158
|
echo "No changes to sync for $BRANCH"
|
|
118
159
|
fi
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# climaybe — Multistore Hotfix to Main
|
|
2
|
+
# Automatically syncs direct hotfixes from staging-<store> or live-<store> back to main.
|
|
3
|
+
# Triggers: push to staging-* or live-*; also after Root to Stores completes (live-*).
|
|
4
|
+
# Merges the store branch into main with [hotfix-backport] so post-merge-tag runs patch bump.
|
|
5
|
+
# main-to-staging-stores skips [hotfix-backport] commits so hotfixes are not re-pushed to stores.
|
|
6
|
+
|
|
7
|
+
name: Multistore Hotfix to Main
|
|
8
|
+
|
|
9
|
+
on:
|
|
10
|
+
push:
|
|
11
|
+
branches:
|
|
12
|
+
- 'staging-*'
|
|
13
|
+
- 'live-*'
|
|
14
|
+
workflow_run:
|
|
15
|
+
workflows: ["Root to Stores"]
|
|
16
|
+
types: [completed]
|
|
17
|
+
branches:
|
|
18
|
+
- 'live-*'
|
|
19
|
+
|
|
20
|
+
concurrency:
|
|
21
|
+
group: multistore-hotfix-to-main-${{ github.event_name == 'push' && github.ref_name || github.event.workflow_run.head_branch }}
|
|
22
|
+
cancel-in-progress: false
|
|
23
|
+
|
|
24
|
+
jobs:
|
|
25
|
+
sync-to-main:
|
|
26
|
+
if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success'
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
permissions:
|
|
29
|
+
contents: write
|
|
30
|
+
env:
|
|
31
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
32
|
+
steps:
|
|
33
|
+
- name: Resolve source branch ref
|
|
34
|
+
id: ref
|
|
35
|
+
run: |
|
|
36
|
+
if [ "${{ github.event_name }}" = "workflow_run" ]; then
|
|
37
|
+
echo "branch=${{ github.event.workflow_run.head_branch }}" >> $GITHUB_OUTPUT
|
|
38
|
+
else
|
|
39
|
+
echo "branch=${{ github.ref_name }}" >> $GITHUB_OUTPUT
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
- uses: actions/checkout@v4
|
|
43
|
+
with:
|
|
44
|
+
fetch-depth: 0
|
|
45
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
46
|
+
|
|
47
|
+
- name: Check if backport is needed
|
|
48
|
+
id: check
|
|
49
|
+
run: |
|
|
50
|
+
SOURCE="${{ steps.ref.outputs.branch }}"
|
|
51
|
+
|
|
52
|
+
MERGE_BASE=$(git merge-base origin/main origin/$SOURCE 2>/dev/null || echo "")
|
|
53
|
+
|
|
54
|
+
if [ -z "$MERGE_BASE" ]; then
|
|
55
|
+
echo "needs_backport=false" >> $GITHUB_OUTPUT
|
|
56
|
+
exit 0
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
COMMITS=$(git log --oneline ${MERGE_BASE}..origin/$SOURCE -- . ':!stores/' 2>/dev/null | \
|
|
60
|
+
grep -v "\[stores-to-root\]" | grep -v "\[root-to-stores\]" | grep -v "chore(release)" || true)
|
|
61
|
+
|
|
62
|
+
if [ -n "$COMMITS" ]; then
|
|
63
|
+
echo "needs_backport=true" >> $GITHUB_OUTPUT
|
|
64
|
+
echo "Commits to sync to main:"
|
|
65
|
+
echo "$COMMITS"
|
|
66
|
+
else
|
|
67
|
+
echo "needs_backport=false" >> $GITHUB_OUTPUT
|
|
68
|
+
echo "No new commits to sync."
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
- name: Merge into main and push
|
|
72
|
+
if: steps.check.outputs.needs_backport == 'true'
|
|
73
|
+
run: |
|
|
74
|
+
SOURCE="${{ steps.ref.outputs.branch }}"
|
|
75
|
+
|
|
76
|
+
git config user.name "github-actions[bot]"
|
|
77
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
78
|
+
|
|
79
|
+
git fetch origin
|
|
80
|
+
git checkout main
|
|
81
|
+
git merge origin/$SOURCE --no-ff -m "Merge $SOURCE into main [hotfix-backport]"
|
|
82
|
+
git push origin main
|
|
83
|
+
|
|
84
|
+
echo "Synced $SOURCE → main"
|
|
@@ -10,34 +10,78 @@ on:
|
|
|
10
10
|
types: [completed]
|
|
11
11
|
branches:
|
|
12
12
|
- 'staging-*'
|
|
13
|
+
workflow_dispatch:
|
|
13
14
|
|
|
14
15
|
jobs:
|
|
15
16
|
create-pr:
|
|
16
|
-
#
|
|
17
|
-
if: github.event.workflow_run.conclusion == 'success'
|
|
17
|
+
# Run on successful Stores to Root completion, or explicit dispatch.
|
|
18
|
+
if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success'
|
|
18
19
|
runs-on: ubuntu-latest
|
|
19
20
|
permissions:
|
|
20
21
|
contents: read
|
|
21
22
|
pull-requests: write
|
|
22
23
|
env:
|
|
23
24
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
25
|
+
SHOPIFY_CLI_THEME_TOKEN: ${{ secrets.SHOPIFY_CLI_THEME_TOKEN }}
|
|
24
26
|
steps:
|
|
25
27
|
- uses: actions/checkout@v4
|
|
26
28
|
|
|
27
29
|
- name: Extract store alias
|
|
28
30
|
id: alias
|
|
29
31
|
run: |
|
|
30
|
-
|
|
32
|
+
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
33
|
+
BRANCH="${{ github.ref_name }}"
|
|
34
|
+
else
|
|
35
|
+
BRANCH="${{ github.event.workflow_run.head_branch }}"
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
if [[ "$BRANCH" != staging-* ]]; then
|
|
39
|
+
echo "Branch $BRANCH is not a staging-* branch, skipping."
|
|
40
|
+
exit 0
|
|
41
|
+
fi
|
|
42
|
+
|
|
31
43
|
ALIAS="${BRANCH#staging-}"
|
|
44
|
+
ALIAS_SECRET=$(echo "$ALIAS" | tr '[:lower:]-' '[:upper:]_')
|
|
32
45
|
echo "alias=$ALIAS" >> $GITHUB_OUTPUT
|
|
46
|
+
echo "alias_secret=$ALIAS_SECRET" >> $GITHUB_OUTPUT
|
|
33
47
|
echo "staging_branch=$BRANCH" >> $GITHUB_OUTPUT
|
|
34
48
|
echo "live_branch=live-${ALIAS}" >> $GITHUB_OUTPUT
|
|
35
49
|
|
|
50
|
+
- name: Resolve store domain from package.json
|
|
51
|
+
id: store
|
|
52
|
+
env:
|
|
53
|
+
SHOPIFY_STORE_URL_SCOPED: ${{ secrets[format('SHOPIFY_STORE_URL_{0}', steps.alias.outputs.alias_secret)] }}
|
|
54
|
+
SHOPIFY_STORE_URL_DEFAULT: ${{ secrets.SHOPIFY_STORE_URL }}
|
|
55
|
+
run: |
|
|
56
|
+
ALIAS="${{ steps.alias.outputs.alias }}"
|
|
57
|
+
DOMAIN_CONFIG=$(node -e "
|
|
58
|
+
const fs = require('fs');
|
|
59
|
+
const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
|
|
60
|
+
const domain = pkg?.config?.stores?.['${ALIAS}'] || '';
|
|
61
|
+
process.stdout.write(domain);
|
|
62
|
+
")
|
|
63
|
+
DOMAIN="${SHOPIFY_STORE_URL_SCOPED:-}"
|
|
64
|
+
if [ -z "$DOMAIN" ]; then
|
|
65
|
+
DOMAIN="${SHOPIFY_STORE_URL_DEFAULT:-}"
|
|
66
|
+
fi
|
|
67
|
+
if [ -z "$DOMAIN" ]; then
|
|
68
|
+
DOMAIN="$DOMAIN_CONFIG"
|
|
69
|
+
fi
|
|
70
|
+
DOMAIN=$(echo "$DOMAIN" | sed -E 's#^https?://##; s#/.*$##')
|
|
71
|
+
echo "domain=$DOMAIN" >> $GITHUB_OUTPUT
|
|
72
|
+
|
|
36
73
|
- name: Create PR to live branch
|
|
74
|
+
env:
|
|
75
|
+
SHOPIFY_CLI_THEME_TOKEN_SCOPED: ${{ secrets[format('SHOPIFY_CLI_THEME_TOKEN_{0}', steps.alias.outputs.alias_secret)] }}
|
|
76
|
+
SHOPIFY_CLI_THEME_TOKEN_DEFAULT: ${{ secrets.SHOPIFY_CLI_THEME_TOKEN }}
|
|
37
77
|
run: |
|
|
38
78
|
STAGING="${{ steps.alias.outputs.staging_branch }}"
|
|
39
79
|
LIVE="${{ steps.alias.outputs.live_branch }}"
|
|
40
80
|
ALIAS="${{ steps.alias.outputs.alias }}"
|
|
81
|
+
DOMAIN="${{ steps.store.outputs.domain }}"
|
|
82
|
+
STAGING_THEME_ID=""
|
|
83
|
+
REPO_NAME="${GITHUB_REPOSITORY#*/}"
|
|
84
|
+
SHOPIFY_TOKEN="${SHOPIFY_CLI_THEME_TOKEN_SCOPED:-$SHOPIFY_CLI_THEME_TOKEN_DEFAULT}"
|
|
41
85
|
|
|
42
86
|
# Check if live branch exists
|
|
43
87
|
if ! git ls-remote --heads origin "$LIVE" | grep -q "$LIVE"; then
|
|
@@ -54,15 +98,45 @@ jobs:
|
|
|
54
98
|
fi
|
|
55
99
|
|
|
56
100
|
# Create PR
|
|
101
|
+
BODY=$(printf "Deployment PR for **%s** store.\n\nReview the changes and merge to deploy." "$ALIAS")
|
|
102
|
+
|
|
103
|
+
if [ -n "$DOMAIN" ]; then
|
|
104
|
+
# Try to resolve a staging/non-live theme ID for this store.
|
|
105
|
+
if [ -n "$SHOPIFY_TOKEN" ]; then
|
|
106
|
+
if ! command -v shopify >/dev/null 2>&1; then
|
|
107
|
+
npm install -g @shopify/cli @shopify/theme >/dev/null 2>&1 || true
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
THEME_JSON=$(shopify theme list \
|
|
111
|
+
--store "$DOMAIN" \
|
|
112
|
+
--password "$SHOPIFY_TOKEN" \
|
|
113
|
+
--json 2>/dev/null || echo "[]")
|
|
114
|
+
|
|
115
|
+
STAGING_THEME_ID=$(echo "$THEME_JSON" | jq -r '
|
|
116
|
+
# Prefer exact branch theme naming: "<repo>/<staging-branch>"
|
|
117
|
+
(map(select(.role != "main" and .name == ($repo + "/" + $branch)))[0].id)
|
|
118
|
+
# Fallback: branch suffix match (still branch-specific)
|
|
119
|
+
// (map(select(.role != "main" and (.name | endswith("/" + $branch))))[0].id)
|
|
120
|
+
// empty
|
|
121
|
+
' --arg repo "$REPO_NAME" --arg branch "$STAGING")
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
if [ -n "$STAGING_THEME_ID" ]; then
|
|
125
|
+
PREVIEW_URL="https://${DOMAIN}?preview_theme_id=${STAGING_THEME_ID}"
|
|
126
|
+
CUSTOMIZE_URL="https://${DOMAIN}/admin/themes/${STAGING_THEME_ID}/editor"
|
|
127
|
+
BODY="${BODY}"$'\n\n'"**Customize URL (Staging Theme):** ${CUSTOMIZE_URL}"$'\n'"**Preview URL (Staging Theme):** ${PREVIEW_URL}"
|
|
128
|
+
else
|
|
129
|
+
BODY="${BODY}"$'\n\n'"⚠️ Staging theme link not found for branch \`${STAGING}\`."$'\n'"Expected naming: \`${REPO_NAME}/${STAGING}\` (or suffix match \`/${STAGING}\`)."
|
|
130
|
+
fi
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
BODY="${BODY}"$'\n\n'"*Generated by climaybe*"
|
|
134
|
+
|
|
57
135
|
PR_URL=$(gh pr create \
|
|
58
136
|
--base "$LIVE" \
|
|
59
137
|
--head "$STAGING" \
|
|
60
138
|
--title "Deploy to ${ALIAS}" \
|
|
61
|
-
--body "
|
|
62
|
-
|
|
63
|
-
Review the changes and merge to deploy.
|
|
64
|
-
|
|
65
|
-
*Generated by climaybe*" 2>/dev/null || echo "")
|
|
139
|
+
--body "$BODY" 2>/dev/null || echo "")
|
|
66
140
|
|
|
67
141
|
if [ -n "$PR_URL" ]; then
|
|
68
142
|
echo "Created PR: $PR_URL"
|
|
@@ -8,6 +8,7 @@ on:
|
|
|
8
8
|
push:
|
|
9
9
|
branches:
|
|
10
10
|
- 'staging-*'
|
|
11
|
+
workflow_dispatch:
|
|
11
12
|
|
|
12
13
|
# Prevent concurrent runs per branch
|
|
13
14
|
concurrency:
|
|
@@ -27,10 +28,16 @@ jobs:
|
|
|
27
28
|
- name: Skip if this is a sync commit
|
|
28
29
|
id: gate
|
|
29
30
|
run: |
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
echo "
|
|
31
|
+
if [ "${{ github.event_name }}" = "push" ]; then
|
|
32
|
+
COMMIT_MSG="${{ github.event.head_commit.message }}"
|
|
33
|
+
if echo "$COMMIT_MSG" | grep -q "\[stores-to-root\]"; then
|
|
34
|
+
echo "skip=true" >> $GITHUB_OUTPUT
|
|
35
|
+
else
|
|
36
|
+
echo "skip=false" >> $GITHUB_OUTPUT
|
|
37
|
+
fi
|
|
33
38
|
else
|
|
39
|
+
# Manual/API dispatch should always run for the selected branch ref.
|
|
40
|
+
echo "workflow_dispatch detected, bypassing sync-commit gate."
|
|
34
41
|
echo "skip=false" >> $GITHUB_OUTPUT
|
|
35
42
|
fi
|
|
36
43
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# climaybe — PR Close (Optional Preview Package)
|
|
2
|
+
# Cleans preview themes for the PR number and comments cleanup details.
|
|
3
|
+
|
|
4
|
+
name: PR Close
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
pull_request:
|
|
8
|
+
types: [closed]
|
|
9
|
+
branches: [main, staging, develop]
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
extract-pr-number:
|
|
13
|
+
uses: ./.github/workflows/reusable-extract-pr-number.yml
|
|
14
|
+
|
|
15
|
+
cleanup-theme:
|
|
16
|
+
needs: extract-pr-number
|
|
17
|
+
uses: ./.github/workflows/reusable-cleanup-themes.yml
|
|
18
|
+
with:
|
|
19
|
+
pr_number: ${{ needs.extract-pr-number.outputs.pr_number }}
|
|
20
|
+
secrets: inherit
|
|
21
|
+
|
|
22
|
+
comment-on-pr:
|
|
23
|
+
needs: cleanup-theme
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
steps:
|
|
26
|
+
- name: Comment on PR about cleanup
|
|
27
|
+
uses: actions/github-script@v7
|
|
28
|
+
with:
|
|
29
|
+
script: |
|
|
30
|
+
const prNumber = context.payload.pull_request.number;
|
|
31
|
+
const deletedCount = parseInt('${{ needs.cleanup-theme.outputs.deleted_count }}') || 0;
|
|
32
|
+
const deletedThemesRaw = `${{ needs.cleanup-theme.outputs.deleted_themes }}`.trim();
|
|
33
|
+
|
|
34
|
+
const lines = [
|
|
35
|
+
'## 🧹 Theme Cleanup Complete',
|
|
36
|
+
'',
|
|
37
|
+
'The preview theme cleanup has finished.',
|
|
38
|
+
'',
|
|
39
|
+
`**Branch:** ${context.payload.pull_request.head.ref}`,
|
|
40
|
+
`**PR Status:** ${context.payload.pull_request.state}`,
|
|
41
|
+
`**Deleted Themes:** ${deletedCount}`
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
if (deletedCount > 0 && deletedThemesRaw) {
|
|
45
|
+
const deletedThemeLines = deletedThemesRaw
|
|
46
|
+
.split('\n')
|
|
47
|
+
.map(name => name.trim())
|
|
48
|
+
.filter(Boolean)
|
|
49
|
+
.map(name => `- ${name}`);
|
|
50
|
+
|
|
51
|
+
if (deletedThemeLines.length > 0) {
|
|
52
|
+
lines.push('', '### Deleted Theme Names', ...deletedThemeLines);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
lines.push('', '---', '*Theme cleanup runs automatically when a PR is closed.*');
|
|
57
|
+
|
|
58
|
+
await github.rest.issues.createComment({
|
|
59
|
+
issue_number: prNumber,
|
|
60
|
+
owner: context.repo.owner,
|
|
61
|
+
repo: context.repo.repo,
|
|
62
|
+
body: lines.join('\n')
|
|
63
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# climaybe — PR Update (Optional Preview Package)
|
|
2
|
+
# Creates a Shopify preview theme on PR update, renames it with PR number,
|
|
3
|
+
# and posts preview/customize links back to the PR.
|
|
4
|
+
|
|
5
|
+
name: PR Update
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
pull_request:
|
|
9
|
+
types: [opened, synchronize, reopened]
|
|
10
|
+
branches: [main, staging, develop]
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
extract-pr-number:
|
|
14
|
+
uses: ./.github/workflows/reusable-extract-pr-number.yml
|
|
15
|
+
|
|
16
|
+
validate-environment:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
outputs:
|
|
19
|
+
store_alias: ${{ steps.resolve.outputs.alias }}
|
|
20
|
+
store_alias_secret: ${{ steps.resolve.outputs.alias_secret }}
|
|
21
|
+
steps:
|
|
22
|
+
- name: Checkout code
|
|
23
|
+
uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
- name: Resolve default store alias
|
|
26
|
+
id: resolve
|
|
27
|
+
run: |
|
|
28
|
+
ALIAS=$(node -e "
|
|
29
|
+
const fs = require('fs');
|
|
30
|
+
const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
|
|
31
|
+
const stores = pkg?.config?.stores || {};
|
|
32
|
+
const defaultStoreRaw = pkg?.config?.default_store;
|
|
33
|
+
const normalize = (v) => String(v || '')
|
|
34
|
+
.toLowerCase()
|
|
35
|
+
.replace(/^https?:\\/\\//, '')
|
|
36
|
+
.replace(/\\/.*$/, '');
|
|
37
|
+
const defaultStore = normalize(defaultStoreRaw);
|
|
38
|
+
let alias = '';
|
|
39
|
+
if (defaultStore) {
|
|
40
|
+
for (const [k, d] of Object.entries(stores)) {
|
|
41
|
+
if (normalize(d) === defaultStore) {
|
|
42
|
+
alias = k;
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (!alias) {
|
|
48
|
+
alias = Object.keys(stores)[0] || '';
|
|
49
|
+
}
|
|
50
|
+
if (!alias && defaultStore) {
|
|
51
|
+
const subdomain = defaultStore.split('.')[0] || 'default';
|
|
52
|
+
alias = subdomain.replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-') || 'default';
|
|
53
|
+
}
|
|
54
|
+
if (!alias) {
|
|
55
|
+
alias = 'default';
|
|
56
|
+
}
|
|
57
|
+
process.stdout.write(alias);
|
|
58
|
+
")
|
|
59
|
+
|
|
60
|
+
if [ -z "$ALIAS" ]; then
|
|
61
|
+
echo "Could not resolve default store alias from package.json config."
|
|
62
|
+
exit 1
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
ALIAS_SECRET=$(echo "$ALIAS" | tr '[:lower:]-' '[:upper:]_')
|
|
66
|
+
echo "alias=$ALIAS" >> $GITHUB_OUTPUT
|
|
67
|
+
echo "alias_secret=$ALIAS_SECRET" >> $GITHUB_OUTPUT
|
|
68
|
+
|
|
69
|
+
- name: Validate Shopify credentials
|
|
70
|
+
env:
|
|
71
|
+
SHOPIFY_STORE_URL_SCOPED: ${{ secrets[format('SHOPIFY_STORE_URL_{0}', steps.resolve.outputs.alias_secret)] }}
|
|
72
|
+
SHOPIFY_CLI_THEME_TOKEN_SCOPED: ${{ secrets[format('SHOPIFY_CLI_THEME_TOKEN_{0}', steps.resolve.outputs.alias_secret)] }}
|
|
73
|
+
SHOPIFY_STORE_URL_DEFAULT: ${{ secrets.SHOPIFY_STORE_URL }}
|
|
74
|
+
SHOPIFY_CLI_THEME_TOKEN_DEFAULT: ${{ secrets.SHOPIFY_CLI_THEME_TOKEN }}
|
|
75
|
+
run: |
|
|
76
|
+
SHOPIFY_STORE_URL="${SHOPIFY_STORE_URL_SCOPED:-$SHOPIFY_STORE_URL_DEFAULT}"
|
|
77
|
+
SHOPIFY_CLI_THEME_TOKEN="${SHOPIFY_CLI_THEME_TOKEN_SCOPED:-$SHOPIFY_CLI_THEME_TOKEN_DEFAULT}"
|
|
78
|
+
|
|
79
|
+
if [ -z "$SHOPIFY_STORE_URL" ]; then
|
|
80
|
+
echo "No store URL secret found. Expected SHOPIFY_STORE_URL_${{ steps.resolve.outputs.alias_secret }} or SHOPIFY_STORE_URL."
|
|
81
|
+
exit 1
|
|
82
|
+
fi
|
|
83
|
+
if [ -z "$SHOPIFY_CLI_THEME_TOKEN" ]; then
|
|
84
|
+
echo "No theme token secret found. Expected SHOPIFY_CLI_THEME_TOKEN_${{ steps.resolve.outputs.alias_secret }} or SHOPIFY_CLI_THEME_TOKEN."
|
|
85
|
+
exit 1
|
|
86
|
+
fi
|
|
87
|
+
echo "Shopify credentials are configured for alias: ${{ steps.resolve.outputs.alias }}"
|
|
88
|
+
|
|
89
|
+
share-theme:
|
|
90
|
+
needs: [validate-environment, extract-pr-number]
|
|
91
|
+
uses: ./.github/workflows/reusable-share-theme.yml
|
|
92
|
+
with:
|
|
93
|
+
pr_number: ${{ needs.extract-pr-number.outputs.pr_number }}
|
|
94
|
+
store_alias: ${{ needs.validate-environment.outputs.store_alias }}
|
|
95
|
+
secrets:
|
|
96
|
+
SHOPIFY_STORE_URL: ${{ secrets[format('SHOPIFY_STORE_URL_{0}', needs.validate-environment.outputs.store_alias_secret)] || secrets.SHOPIFY_STORE_URL }}
|
|
97
|
+
SHOPIFY_CLI_THEME_TOKEN: ${{ secrets[format('SHOPIFY_CLI_THEME_TOKEN_{0}', needs.validate-environment.outputs.store_alias_secret)] || secrets.SHOPIFY_CLI_THEME_TOKEN }}
|
|
98
|
+
|
|
99
|
+
rename-theme:
|
|
100
|
+
needs: [share-theme, extract-pr-number, validate-environment]
|
|
101
|
+
uses: ./.github/workflows/reusable-rename-theme.yml
|
|
102
|
+
with:
|
|
103
|
+
theme_id: ${{ needs.share-theme.outputs.theme_id }}
|
|
104
|
+
theme_name: ${{ needs.share-theme.outputs.theme_name }}
|
|
105
|
+
pr_number: ${{ needs.extract-pr-number.outputs.pr_number }}
|
|
106
|
+
store_alias: ${{ needs.validate-environment.outputs.store_alias }}
|
|
107
|
+
secrets:
|
|
108
|
+
SHOPIFY_STORE_URL: ${{ secrets[format('SHOPIFY_STORE_URL_{0}', needs.validate-environment.outputs.store_alias_secret)] || secrets.SHOPIFY_STORE_URL }}
|
|
109
|
+
SHOPIFY_CLI_THEME_TOKEN: ${{ secrets[format('SHOPIFY_CLI_THEME_TOKEN_{0}', needs.validate-environment.outputs.store_alias_secret)] || secrets.SHOPIFY_CLI_THEME_TOKEN }}
|
|
110
|
+
|
|
111
|
+
comment-on-pr:
|
|
112
|
+
needs: [share-theme, rename-theme, extract-pr-number, validate-environment]
|
|
113
|
+
uses: ./.github/workflows/reusable-comment-on-pr.yml
|
|
114
|
+
with:
|
|
115
|
+
theme_id: ${{ needs.share-theme.outputs.theme_id }}
|
|
116
|
+
share_output: ${{ needs.share-theme.outputs.share_output }}
|
|
117
|
+
pr_number: ${{ needs.extract-pr-number.outputs.pr_number_unpadded }}
|
|
118
|
+
store_alias: ${{ needs.validate-environment.outputs.store_alias }}
|
|
119
|
+
secrets:
|
|
120
|
+
SHOPIFY_STORE_URL: ${{ secrets[format('SHOPIFY_STORE_URL_{0}', needs.validate-environment.outputs.store_alias_secret)] || secrets.SHOPIFY_STORE_URL }}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
name: Cleanup Themes
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_call:
|
|
5
|
+
inputs:
|
|
6
|
+
pr_number:
|
|
7
|
+
required: true
|
|
8
|
+
type: string
|
|
9
|
+
description: "PR number to clean up themes for"
|
|
10
|
+
outputs:
|
|
11
|
+
deleted_count:
|
|
12
|
+
description: "Number of deleted themes"
|
|
13
|
+
value: ${{ jobs.cleanup.outputs.deleted_count }}
|
|
14
|
+
deleted_themes:
|
|
15
|
+
description: "Deleted theme names, one per line"
|
|
16
|
+
value: ${{ jobs.cleanup.outputs.deleted_themes }}
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
cleanup:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
outputs:
|
|
22
|
+
deleted_count: ${{ steps.cleanup.outputs.deleted_count }}
|
|
23
|
+
deleted_themes: ${{ steps.cleanup.outputs.deleted_themes }}
|
|
24
|
+
steps:
|
|
25
|
+
- name: Install Shopify CLI
|
|
26
|
+
run: npm install -g @shopify/cli @shopify/theme
|
|
27
|
+
|
|
28
|
+
- name: Cleanup PR preview themes
|
|
29
|
+
id: cleanup
|
|
30
|
+
env:
|
|
31
|
+
SHOPIFY_STORE_URL: ${{ secrets.SHOPIFY_STORE_URL }}
|
|
32
|
+
SHOPIFY_CLI_THEME_TOKEN: ${{ secrets.SHOPIFY_CLI_THEME_TOKEN }}
|
|
33
|
+
PR_NUMBER: ${{ inputs.pr_number }}
|
|
34
|
+
run: |
|
|
35
|
+
if [ -z "$SHOPIFY_STORE_URL" ] || [ -z "$SHOPIFY_CLI_THEME_TOKEN" ]; then
|
|
36
|
+
echo "Missing Shopify secrets."
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
THEME_LIST=$(shopify theme list \
|
|
41
|
+
--store "$SHOPIFY_STORE_URL" \
|
|
42
|
+
--password "$SHOPIFY_CLI_THEME_TOKEN" \
|
|
43
|
+
--json 2>/dev/null || echo "[]")
|
|
44
|
+
|
|
45
|
+
DELETED_COUNT=0
|
|
46
|
+
DELETED_THEMES=""
|
|
47
|
+
|
|
48
|
+
while IFS=$'\t' read -r THEME_ID THEME_NAME; do
|
|
49
|
+
[ -z "$THEME_ID" ] && continue
|
|
50
|
+
[ -z "$THEME_NAME" ] && continue
|
|
51
|
+
|
|
52
|
+
if printf "%s" "$THEME_NAME" | grep -q "PR${PR_NUMBER}"; then
|
|
53
|
+
if shopify theme delete \
|
|
54
|
+
--store "$SHOPIFY_STORE_URL" \
|
|
55
|
+
--password "$SHOPIFY_CLI_THEME_TOKEN" \
|
|
56
|
+
--force \
|
|
57
|
+
--theme "$THEME_ID" 2>/dev/null; then
|
|
58
|
+
DELETED_COUNT=$((DELETED_COUNT + 1))
|
|
59
|
+
DELETED_THEMES="${DELETED_THEMES}${THEME_NAME}\n"
|
|
60
|
+
else
|
|
61
|
+
echo "Failed to delete theme: $THEME_NAME ($THEME_ID)"
|
|
62
|
+
fi
|
|
63
|
+
fi
|
|
64
|
+
done < <(echo "$THEME_LIST" | jq -r '.[] | [.id, .name] | @tsv')
|
|
65
|
+
|
|
66
|
+
echo "deleted_count=$DELETED_COUNT" >> $GITHUB_OUTPUT
|
|
67
|
+
{
|
|
68
|
+
echo "deleted_themes<<DELETED_EOF"
|
|
69
|
+
printf "%b" "$DELETED_THEMES"
|
|
70
|
+
echo "DELETED_EOF"
|
|
71
|
+
} >> $GITHUB_OUTPUT
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
name: Comment on PR
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_call:
|
|
5
|
+
inputs:
|
|
6
|
+
theme_id:
|
|
7
|
+
required: false
|
|
8
|
+
type: string
|
|
9
|
+
share_output:
|
|
10
|
+
required: false
|
|
11
|
+
type: string
|
|
12
|
+
pr_number:
|
|
13
|
+
required: true
|
|
14
|
+
type: string
|
|
15
|
+
store_alias:
|
|
16
|
+
required: false
|
|
17
|
+
type: string
|
|
18
|
+
store_alias_secret:
|
|
19
|
+
required: false
|
|
20
|
+
type: string
|
|
21
|
+
secrets:
|
|
22
|
+
SHOPIFY_STORE_URL:
|
|
23
|
+
required: false
|
|
24
|
+
|
|
25
|
+
jobs:
|
|
26
|
+
comment:
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
steps:
|
|
29
|
+
- name: Post preview comment
|
|
30
|
+
uses: actions/github-script@v7
|
|
31
|
+
env:
|
|
32
|
+
SHOPIFY_STORE_URL: ${{ secrets.SHOPIFY_STORE_URL }}
|
|
33
|
+
THEME_ID: ${{ inputs.theme_id }}
|
|
34
|
+
SHARE_OUTPUT: ${{ inputs.share_output }}
|
|
35
|
+
PR_NUMBER: ${{ inputs.pr_number }}
|
|
36
|
+
with:
|
|
37
|
+
script: |
|
|
38
|
+
const issueNumber = parseInt(process.env.PR_NUMBER);
|
|
39
|
+
const storeDomain = (process.env.SHOPIFY_STORE_URL || '')
|
|
40
|
+
.replace(/^https?:\/\//, '')
|
|
41
|
+
.replace(/\/$/, '');
|
|
42
|
+
const themeId = process.env.THEME_ID || '';
|
|
43
|
+
const shareOutput = process.env.SHARE_OUTPUT || '';
|
|
44
|
+
|
|
45
|
+
let previewUrl = '';
|
|
46
|
+
let customizeUrl = '';
|
|
47
|
+
if (storeDomain && themeId) {
|
|
48
|
+
previewUrl = `https://${storeDomain}?preview_theme_id=${themeId}`;
|
|
49
|
+
customizeUrl = `https://${storeDomain}/admin/themes/${themeId}/editor`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const parts = [
|
|
53
|
+
'## 🎨 Theme Preview Generated',
|
|
54
|
+
'',
|
|
55
|
+
`**Store:** ${process.env.SHOPIFY_STORE_URL || 'N/A'}`,
|
|
56
|
+
`**Branch:** ${context.payload.pull_request?.head?.ref || context.ref.replace('refs/heads/', '')}`,
|
|
57
|
+
`**Commit:** ${(context.payload.pull_request?.head?.sha || context.sha || '').substring(0, 7)}`
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
if (customizeUrl) {
|
|
61
|
+
parts.push('', `**Customize URL:** ${customizeUrl}`);
|
|
62
|
+
}
|
|
63
|
+
if (previewUrl) {
|
|
64
|
+
parts.push('', `**Preview URL:** ${previewUrl}`);
|
|
65
|
+
}
|
|
66
|
+
if (shareOutput.trim()) {
|
|
67
|
+
parts.push('', '### Share Output', '```', shareOutput, '```');
|
|
68
|
+
}
|
|
69
|
+
parts.push('', '---', '*This preview will be available for 7 days.*');
|
|
70
|
+
|
|
71
|
+
await github.rest.issues.createComment({
|
|
72
|
+
issue_number: issueNumber,
|
|
73
|
+
owner: context.repo.owner,
|
|
74
|
+
repo: context.repo.repo,
|
|
75
|
+
body: parts.join('\n')
|
|
76
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Extract PR Number
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_call:
|
|
5
|
+
outputs:
|
|
6
|
+
pr_number:
|
|
7
|
+
description: "The PR number (padded: 01, 02, etc.)"
|
|
8
|
+
value: ${{ jobs.extract.outputs.pr_number }}
|
|
9
|
+
pr_number_unpadded:
|
|
10
|
+
description: "The PR number (unpadded: 1, 2, etc.)"
|
|
11
|
+
value: ${{ jobs.extract.outputs.pr_number_unpadded }}
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
extract:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
outputs:
|
|
17
|
+
pr_number: ${{ steps.extract.outputs.pr_number }}
|
|
18
|
+
pr_number_unpadded: ${{ steps.extract.outputs.pr_number_unpadded }}
|
|
19
|
+
steps:
|
|
20
|
+
- name: Extract PR number
|
|
21
|
+
id: extract
|
|
22
|
+
run: |
|
|
23
|
+
PR_NUMBER="${{ github.event.pull_request.number }}"
|
|
24
|
+
if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" = "" ]; then
|
|
25
|
+
PR_NUMBER="${{ github.event.number }}"
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" = "" ]; then
|
|
29
|
+
echo "Could not extract PR number from event."
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
PADDED_PR_NUMBER=$(printf "%02d" "$PR_NUMBER")
|
|
34
|
+
echo "pr_number=$PADDED_PR_NUMBER" >> $GITHUB_OUTPUT
|
|
35
|
+
echo "pr_number_unpadded=$PR_NUMBER" >> $GITHUB_OUTPUT
|