climaybe 1.0.0 → 1.1.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 +62 -11
- package/package.json +7 -1
- package/src/commands/add-store.js +5 -3
- package/src/commands/init.js +11 -2
- package/src/commands/update-workflows.js +4 -2
- package/src/lib/config.js +16 -0
- package/src/lib/prompts.js +55 -5
- 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 +112 -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
|
@@ -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,112 @@
|
|
|
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 normalize = (v) => String(v || '')
|
|
33
|
+
.toLowerCase()
|
|
34
|
+
.replace(/^https?:\\/\\//, '')
|
|
35
|
+
.replace(/\\/.*$/, '');
|
|
36
|
+
const defaultStore = normalize(pkg?.config?.default_store);
|
|
37
|
+
let alias = '';
|
|
38
|
+
if (defaultStore) {
|
|
39
|
+
for (const [k, d] of Object.entries(stores)) {
|
|
40
|
+
if (normalize(d) === defaultStore) {
|
|
41
|
+
alias = k;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (!alias) {
|
|
47
|
+
alias = Object.keys(stores)[0] || '';
|
|
48
|
+
}
|
|
49
|
+
process.stdout.write(alias);
|
|
50
|
+
")
|
|
51
|
+
|
|
52
|
+
if [ -z "$ALIAS" ]; then
|
|
53
|
+
echo "Could not resolve default store alias from package.json config."
|
|
54
|
+
exit 1
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
ALIAS_SECRET=$(echo "$ALIAS" | tr '[:lower:]-' '[:upper:]_')
|
|
58
|
+
echo "alias=$ALIAS" >> $GITHUB_OUTPUT
|
|
59
|
+
echo "alias_secret=$ALIAS_SECRET" >> $GITHUB_OUTPUT
|
|
60
|
+
|
|
61
|
+
- name: Validate Shopify credentials
|
|
62
|
+
env:
|
|
63
|
+
SHOPIFY_STORE_URL_SCOPED: ${{ secrets[format('SHOPIFY_STORE_URL_{0}', steps.resolve.outputs.alias_secret)] }}
|
|
64
|
+
SHOPIFY_CLI_THEME_TOKEN_SCOPED: ${{ secrets[format('SHOPIFY_CLI_THEME_TOKEN_{0}', steps.resolve.outputs.alias_secret)] }}
|
|
65
|
+
SHOPIFY_STORE_URL_DEFAULT: ${{ secrets.SHOPIFY_STORE_URL }}
|
|
66
|
+
SHOPIFY_CLI_THEME_TOKEN_DEFAULT: ${{ secrets.SHOPIFY_CLI_THEME_TOKEN }}
|
|
67
|
+
run: |
|
|
68
|
+
SHOPIFY_STORE_URL="${SHOPIFY_STORE_URL_SCOPED:-$SHOPIFY_STORE_URL_DEFAULT}"
|
|
69
|
+
SHOPIFY_CLI_THEME_TOKEN="${SHOPIFY_CLI_THEME_TOKEN_SCOPED:-$SHOPIFY_CLI_THEME_TOKEN_DEFAULT}"
|
|
70
|
+
|
|
71
|
+
if [ -z "$SHOPIFY_STORE_URL" ]; then
|
|
72
|
+
echo "No store URL secret found. Expected SHOPIFY_STORE_URL_${{ steps.resolve.outputs.alias_secret }} or SHOPIFY_STORE_URL."
|
|
73
|
+
exit 1
|
|
74
|
+
fi
|
|
75
|
+
if [ -z "$SHOPIFY_CLI_THEME_TOKEN" ]; then
|
|
76
|
+
echo "No theme token secret found. Expected SHOPIFY_CLI_THEME_TOKEN_${{ steps.resolve.outputs.alias_secret }} or SHOPIFY_CLI_THEME_TOKEN."
|
|
77
|
+
exit 1
|
|
78
|
+
fi
|
|
79
|
+
echo "Shopify credentials are configured for alias: ${{ steps.resolve.outputs.alias }}"
|
|
80
|
+
|
|
81
|
+
share-theme:
|
|
82
|
+
needs: [validate-environment, extract-pr-number]
|
|
83
|
+
uses: ./.github/workflows/reusable-share-theme.yml
|
|
84
|
+
with:
|
|
85
|
+
pr_number: ${{ needs.extract-pr-number.outputs.pr_number }}
|
|
86
|
+
store_alias: ${{ needs.validate-environment.outputs.store_alias }}
|
|
87
|
+
secrets:
|
|
88
|
+
SHOPIFY_STORE_URL: ${{ secrets[format('SHOPIFY_STORE_URL_{0}', needs.validate-environment.outputs.store_alias_secret)] || secrets.SHOPIFY_STORE_URL }}
|
|
89
|
+
SHOPIFY_CLI_THEME_TOKEN: ${{ secrets[format('SHOPIFY_CLI_THEME_TOKEN_{0}', needs.validate-environment.outputs.store_alias_secret)] || secrets.SHOPIFY_CLI_THEME_TOKEN }}
|
|
90
|
+
|
|
91
|
+
rename-theme:
|
|
92
|
+
needs: [share-theme, extract-pr-number, validate-environment]
|
|
93
|
+
uses: ./.github/workflows/reusable-rename-theme.yml
|
|
94
|
+
with:
|
|
95
|
+
theme_id: ${{ needs.share-theme.outputs.theme_id }}
|
|
96
|
+
theme_name: ${{ needs.share-theme.outputs.theme_name }}
|
|
97
|
+
pr_number: ${{ needs.extract-pr-number.outputs.pr_number }}
|
|
98
|
+
store_alias: ${{ needs.validate-environment.outputs.store_alias }}
|
|
99
|
+
secrets:
|
|
100
|
+
SHOPIFY_STORE_URL: ${{ secrets[format('SHOPIFY_STORE_URL_{0}', needs.validate-environment.outputs.store_alias_secret)] || secrets.SHOPIFY_STORE_URL }}
|
|
101
|
+
SHOPIFY_CLI_THEME_TOKEN: ${{ secrets[format('SHOPIFY_CLI_THEME_TOKEN_{0}', needs.validate-environment.outputs.store_alias_secret)] || secrets.SHOPIFY_CLI_THEME_TOKEN }}
|
|
102
|
+
|
|
103
|
+
comment-on-pr:
|
|
104
|
+
needs: [share-theme, rename-theme, extract-pr-number, validate-environment]
|
|
105
|
+
uses: ./.github/workflows/reusable-comment-on-pr.yml
|
|
106
|
+
with:
|
|
107
|
+
theme_id: ${{ needs.share-theme.outputs.theme_id }}
|
|
108
|
+
share_output: ${{ needs.share-theme.outputs.share_output }}
|
|
109
|
+
pr_number: ${{ needs.extract-pr-number.outputs.pr_number_unpadded }}
|
|
110
|
+
store_alias: ${{ needs.validate-environment.outputs.store_alias }}
|
|
111
|
+
secrets:
|
|
112
|
+
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
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
name: Rename Theme
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_call:
|
|
5
|
+
inputs:
|
|
6
|
+
theme_id:
|
|
7
|
+
required: true
|
|
8
|
+
type: string
|
|
9
|
+
description: "Theme ID to rename"
|
|
10
|
+
theme_name:
|
|
11
|
+
required: true
|
|
12
|
+
type: string
|
|
13
|
+
description: "Current theme name"
|
|
14
|
+
pr_number:
|
|
15
|
+
required: true
|
|
16
|
+
type: string
|
|
17
|
+
description: "PR number to append"
|
|
18
|
+
store_alias:
|
|
19
|
+
required: false
|
|
20
|
+
type: string
|
|
21
|
+
description: "Store alias for scoped secret lookup"
|
|
22
|
+
store_alias_secret:
|
|
23
|
+
required: false
|
|
24
|
+
type: string
|
|
25
|
+
description: "Upper snake-case alias for scoped secret lookup"
|
|
26
|
+
secrets:
|
|
27
|
+
SHOPIFY_STORE_URL:
|
|
28
|
+
required: false
|
|
29
|
+
SHOPIFY_CLI_THEME_TOKEN:
|
|
30
|
+
required: false
|
|
31
|
+
|
|
32
|
+
jobs:
|
|
33
|
+
rename:
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
steps:
|
|
36
|
+
- name: Install Shopify CLI
|
|
37
|
+
run: npm install -g @shopify/cli @shopify/theme
|
|
38
|
+
|
|
39
|
+
- name: Rename theme
|
|
40
|
+
env:
|
|
41
|
+
SHOPIFY_STORE_URL: ${{ secrets.SHOPIFY_STORE_URL }}
|
|
42
|
+
SHOPIFY_CLI_THEME_TOKEN: ${{ secrets.SHOPIFY_CLI_THEME_TOKEN }}
|
|
43
|
+
THEME_ID: ${{ inputs.theme_id }}
|
|
44
|
+
THEME_NAME: ${{ inputs.theme_name }}
|
|
45
|
+
PR_NUMBER: ${{ inputs.pr_number }}
|
|
46
|
+
run: |
|
|
47
|
+
if [ -z "$THEME_ID" ] || [ -z "$THEME_NAME" ]; then
|
|
48
|
+
echo "Missing theme_id/theme_name."
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
if [ -z "$SHOPIFY_STORE_URL" ] || [ -z "$SHOPIFY_CLI_THEME_TOKEN" ]; then
|
|
52
|
+
echo "Missing Shopify store URL/token for rename."
|
|
53
|
+
exit 1
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
NEW_THEME_NAME="${THEME_NAME}-PR${PR_NUMBER}"
|
|
57
|
+
echo "Renaming theme $THEME_ID to '$NEW_THEME_NAME'..."
|
|
58
|
+
|
|
59
|
+
if shopify theme rename \
|
|
60
|
+
--store "$SHOPIFY_STORE_URL" \
|
|
61
|
+
--password "$SHOPIFY_CLI_THEME_TOKEN" \
|
|
62
|
+
--theme "$THEME_ID" \
|
|
63
|
+
--name "$NEW_THEME_NAME" 2>&1; then
|
|
64
|
+
echo "Rename succeeded with password auth."
|
|
65
|
+
elif shopify theme rename \
|
|
66
|
+
--store "$SHOPIFY_STORE_URL" \
|
|
67
|
+
--theme "$THEME_ID" \
|
|
68
|
+
--name "$NEW_THEME_NAME" 2>&1; then
|
|
69
|
+
echo "Rename succeeded with authenticated session."
|
|
70
|
+
else
|
|
71
|
+
echo "Failed to rename theme."
|
|
72
|
+
exit 1
|
|
73
|
+
fi
|