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,94 @@
|
|
|
1
|
+
name: Share Theme
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_call:
|
|
5
|
+
inputs:
|
|
6
|
+
pr_number:
|
|
7
|
+
required: false
|
|
8
|
+
type: string
|
|
9
|
+
description: "PR number for theme naming context"
|
|
10
|
+
store_alias:
|
|
11
|
+
required: false
|
|
12
|
+
type: string
|
|
13
|
+
description: "Store alias for scoped secret lookup"
|
|
14
|
+
store_alias_secret:
|
|
15
|
+
required: false
|
|
16
|
+
type: string
|
|
17
|
+
description: "Upper snake-case alias for scoped secret lookup"
|
|
18
|
+
outputs:
|
|
19
|
+
theme_id:
|
|
20
|
+
description: "Shared theme ID"
|
|
21
|
+
value: ${{ jobs.share.outputs.theme_id }}
|
|
22
|
+
theme_name:
|
|
23
|
+
description: "Shared theme name"
|
|
24
|
+
value: ${{ jobs.share.outputs.theme_name }}
|
|
25
|
+
share_output:
|
|
26
|
+
description: "Raw share command output"
|
|
27
|
+
value: ${{ jobs.share.outputs.share_output }}
|
|
28
|
+
secrets:
|
|
29
|
+
SHOPIFY_STORE_URL:
|
|
30
|
+
required: false
|
|
31
|
+
SHOPIFY_CLI_THEME_TOKEN:
|
|
32
|
+
required: false
|
|
33
|
+
|
|
34
|
+
jobs:
|
|
35
|
+
share:
|
|
36
|
+
runs-on: ubuntu-latest
|
|
37
|
+
outputs:
|
|
38
|
+
theme_id: ${{ steps.share.outputs.theme_id }}
|
|
39
|
+
theme_name: ${{ steps.share.outputs.theme_name }}
|
|
40
|
+
share_output: ${{ steps.share.outputs.share_output }}
|
|
41
|
+
steps:
|
|
42
|
+
- name: Checkout code
|
|
43
|
+
uses: actions/checkout@v4
|
|
44
|
+
|
|
45
|
+
- name: Validate theme root
|
|
46
|
+
run: |
|
|
47
|
+
if [ ! -f "layout/theme.liquid" ]; then
|
|
48
|
+
echo "layout/theme.liquid not found. Ensure workflow runs at theme repository root."
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
- name: Install Shopify CLI
|
|
53
|
+
run: npm install -g @shopify/cli @shopify/theme
|
|
54
|
+
|
|
55
|
+
- name: Share theme
|
|
56
|
+
id: share
|
|
57
|
+
env:
|
|
58
|
+
SHOPIFY_STORE_URL: ${{ secrets.SHOPIFY_STORE_URL }}
|
|
59
|
+
SHOPIFY_CLI_THEME_TOKEN: ${{ secrets.SHOPIFY_CLI_THEME_TOKEN }}
|
|
60
|
+
run: |
|
|
61
|
+
if [ -z "$SHOPIFY_STORE_URL" ] || [ -z "$SHOPIFY_CLI_THEME_TOKEN" ]; then
|
|
62
|
+
echo "Missing Shopify secrets."
|
|
63
|
+
exit 1
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
OUTPUT=$(shopify theme share \
|
|
67
|
+
--store "$SHOPIFY_STORE_URL" \
|
|
68
|
+
--password "$SHOPIFY_CLI_THEME_TOKEN" 2>&1)
|
|
69
|
+
STATUS=$?
|
|
70
|
+
|
|
71
|
+
echo "$OUTPUT"
|
|
72
|
+
if [ $STATUS -ne 0 ]; then
|
|
73
|
+
echo "Theme share failed."
|
|
74
|
+
exit $STATUS
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
THEME_NAME=$(echo "$OUTPUT" | sed -n "s/.*The theme '\([^']*\)'.*/\1/p" | head -1)
|
|
78
|
+
THEME_ID=$(echo "$OUTPUT" | sed -n 's/.*#\([0-9]*\).*/\1/p' | head -1)
|
|
79
|
+
|
|
80
|
+
if [ -z "$THEME_ID" ]; then
|
|
81
|
+
echo "Could not parse theme id from share output."
|
|
82
|
+
exit 1
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
echo "theme_id=$THEME_ID" >> $GITHUB_OUTPUT
|
|
86
|
+
if [ -n "$THEME_NAME" ]; then
|
|
87
|
+
echo "theme_name=$THEME_NAME" >> $GITHUB_OUTPUT
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
{
|
|
91
|
+
echo "share_output<<SHARE_EOF"
|
|
92
|
+
echo "$OUTPUT"
|
|
93
|
+
echo "SHARE_EOF"
|
|
94
|
+
} >> $GITHUB_OUTPUT
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# climaybe — AI Changelog Generator (Reusable Workflow)
|
|
2
|
-
# Uses
|
|
2
|
+
# Uses Gemini when available, otherwise falls back to GitHub Models.
|
|
3
3
|
|
|
4
4
|
name: AI Changelog
|
|
5
5
|
|
|
@@ -21,11 +21,14 @@ on:
|
|
|
21
21
|
value: ${{ jobs.generate.outputs.changelog }}
|
|
22
22
|
secrets:
|
|
23
23
|
GEMINI_API_KEY:
|
|
24
|
-
required:
|
|
24
|
+
required: false
|
|
25
25
|
|
|
26
26
|
jobs:
|
|
27
27
|
generate:
|
|
28
28
|
runs-on: ubuntu-latest
|
|
29
|
+
permissions:
|
|
30
|
+
contents: read
|
|
31
|
+
models: read
|
|
29
32
|
outputs:
|
|
30
33
|
changelog: ${{ steps.ai.outputs.changelog }}
|
|
31
34
|
|
|
@@ -43,17 +46,23 @@ jobs:
|
|
|
43
46
|
echo "commits=" >> $GITHUB_OUTPUT
|
|
44
47
|
exit 0
|
|
45
48
|
fi
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
# Keep commits as multiline plain text
|
|
50
|
+
{
|
|
51
|
+
echo "commits<<COMMITS_EOF"
|
|
52
|
+
echo "$COMMITS"
|
|
53
|
+
echo "COMMITS_EOF"
|
|
54
|
+
} >> $GITHUB_OUTPUT
|
|
49
55
|
|
|
50
|
-
- name: Generate changelog
|
|
56
|
+
- name: Generate changelog (Gemini -> GitHub Models fallback)
|
|
51
57
|
id: ai
|
|
52
58
|
if: steps.commits.outputs.commits != ''
|
|
53
59
|
env:
|
|
54
60
|
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
|
61
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
62
|
+
COMMITS_RAW: ${{ steps.commits.outputs.commits }}
|
|
55
63
|
run: |
|
|
56
|
-
COMMITS
|
|
64
|
+
COMMITS="$COMMITS_RAW"
|
|
65
|
+
PROVIDER="fallback"
|
|
57
66
|
|
|
58
67
|
PROMPT=$(cat <<'PROMPT_EOF'
|
|
59
68
|
You are a changelog generator for a Shopify theme repository.
|
|
@@ -71,30 +80,79 @@ jobs:
|
|
|
71
80
|
PROMPT_EOF
|
|
72
81
|
)
|
|
73
82
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
CHANGELOG=""
|
|
84
|
+
|
|
85
|
+
# 1) Gemini path (if secret exists)
|
|
86
|
+
if [ -n "$GEMINI_API_KEY" ]; then
|
|
87
|
+
PAYLOAD=$(jq -n \
|
|
88
|
+
--arg prompt "$PROMPT" \
|
|
89
|
+
--arg commits "$COMMITS" \
|
|
90
|
+
'{
|
|
91
|
+
"contents": [{
|
|
92
|
+
"parts": [{"text": ($prompt + "\n" + $commits)}]
|
|
93
|
+
}],
|
|
94
|
+
"generationConfig": {
|
|
95
|
+
"temperature": 0.3,
|
|
96
|
+
"maxOutputTokens": 2048
|
|
97
|
+
}
|
|
98
|
+
}')
|
|
99
|
+
|
|
100
|
+
RESPONSE=$(curl -s -X POST \
|
|
101
|
+
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_API_KEY}" \
|
|
102
|
+
-H "Content-Type: application/json" \
|
|
103
|
+
-d "$PAYLOAD")
|
|
104
|
+
|
|
105
|
+
CHANGELOG=$(echo "$RESPONSE" | jq -r '.candidates[0].content.parts[0].text // empty' 2>/dev/null || true)
|
|
106
|
+
if [ -n "$CHANGELOG" ] && [ "$CHANGELOG" != "null" ]; then
|
|
107
|
+
PROVIDER="gemini"
|
|
108
|
+
fi
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
# 2) GitHub Models path (Copilot-backed models)
|
|
112
|
+
if [ -z "$CHANGELOG" ] || [ "$CHANGELOG" = "null" ]; then
|
|
113
|
+
GH_PAYLOAD=$(jq -n \
|
|
114
|
+
--arg prompt "$PROMPT" \
|
|
115
|
+
--arg commits "$COMMITS" \
|
|
116
|
+
'{
|
|
117
|
+
"model": "gpt-4o-mini",
|
|
118
|
+
"messages": [
|
|
119
|
+
{"role":"system","content":"You are a changelog generator for Shopify theme repos. Output markdown only."},
|
|
120
|
+
{"role":"user","content": ($prompt + "\n" + $commits)}
|
|
121
|
+
],
|
|
82
122
|
"temperature": 0.3,
|
|
83
|
-
"
|
|
84
|
-
}
|
|
85
|
-
|
|
123
|
+
"max_tokens": 1200
|
|
124
|
+
}')
|
|
125
|
+
|
|
126
|
+
GH_RESPONSE=$(curl -s -X POST \
|
|
127
|
+
"https://models.inference.ai.azure.com/chat/completions" \
|
|
128
|
+
-H "Content-Type: application/json" \
|
|
129
|
+
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
|
|
130
|
+
-d "$GH_PAYLOAD")
|
|
86
131
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
132
|
+
CHANGELOG=$(echo "$GH_RESPONSE" | jq -r '.choices[0].message.content // empty' 2>/dev/null || true)
|
|
133
|
+
if [ -n "$CHANGELOG" ] && [ "$CHANGELOG" != "null" ]; then
|
|
134
|
+
PROVIDER="github-models"
|
|
135
|
+
fi
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
# 3) Safe fallback if both providers fail
|
|
139
|
+
if [ -z "$CHANGELOG" ] || [ "$CHANGELOG" = "null" ]; then
|
|
140
|
+
COMMIT_LINES="$COMMITS"
|
|
141
|
+
CHANGELOG=$(printf "## Chore\n")
|
|
142
|
+
while IFS= read -r line; do
|
|
143
|
+
[ -z "$line" ] && continue
|
|
144
|
+
SUBJECT="${line#* }"
|
|
145
|
+
CHANGELOG="${CHANGELOG}- ${SUBJECT}\n"
|
|
146
|
+
done <<< "$COMMIT_LINES"
|
|
147
|
+
PROVIDER="static-fallback"
|
|
148
|
+
fi
|
|
91
149
|
|
|
92
|
-
|
|
150
|
+
echo "Changelog provider: $PROVIDER"
|
|
93
151
|
|
|
94
152
|
# Write to output (multiline)
|
|
95
153
|
{
|
|
96
154
|
echo "changelog<<CHANGELOG_EOF"
|
|
97
|
-
|
|
155
|
+
printf "%b\n" "$CHANGELOG"
|
|
98
156
|
echo "CHANGELOG_EOF"
|
|
99
157
|
} >> $GITHUB_OUTPUT
|
|
100
158
|
|
|
@@ -52,8 +52,12 @@ jobs:
|
|
|
52
52
|
EXPLICIT_VERSION="${{ inputs.version }}"
|
|
53
53
|
|
|
54
54
|
if [ -n "$EXPLICIT_VERSION" ]; then
|
|
55
|
-
# Use explicit version
|
|
55
|
+
# Use explicit version; ensure v prefix and always three-part (e.g. v3.2.0)
|
|
56
56
|
NEW_VERSION="${EXPLICIT_VERSION#v}"
|
|
57
|
+
PARTS=$(echo "$NEW_VERSION" | tr '.' '\n' | wc -l)
|
|
58
|
+
if [ "$PARTS" -eq "2" ]; then
|
|
59
|
+
NEW_VERSION="${NEW_VERSION}.0"
|
|
60
|
+
fi
|
|
57
61
|
NEW_VERSION="v${NEW_VERSION}"
|
|
58
62
|
else
|
|
59
63
|
# Get latest tag
|
|
@@ -102,6 +106,7 @@ jobs:
|
|
|
102
106
|
- name: Commit and tag
|
|
103
107
|
run: |
|
|
104
108
|
NEW_VERSION="${{ steps.bump.outputs.new_version }}"
|
|
109
|
+
TAG_CREATED="false"
|
|
105
110
|
|
|
106
111
|
# Stage changes
|
|
107
112
|
git add -A
|
|
@@ -111,12 +116,22 @@ jobs:
|
|
|
111
116
|
git commit -m "chore(release): bump version to ${NEW_VERSION}"
|
|
112
117
|
fi
|
|
113
118
|
|
|
114
|
-
#
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
119
|
+
# Guard against duplicate tag creation (local or remote).
|
|
120
|
+
if git ls-remote --exit-code --tags origin "refs/tags/${NEW_VERSION}" >/dev/null 2>&1; then
|
|
121
|
+
echo "Tag ${NEW_VERSION} already exists on origin, reusing."
|
|
122
|
+
elif git rev-parse -q --verify "refs/tags/${NEW_VERSION}" >/dev/null; then
|
|
123
|
+
echo "Tag ${NEW_VERSION} already exists locally, reusing."
|
|
118
124
|
else
|
|
119
|
-
|
|
125
|
+
CHANGELOG="${{ inputs.changelog }}"
|
|
126
|
+
if [ -n "$CHANGELOG" ]; then
|
|
127
|
+
git tag -a "$NEW_VERSION" -m "$CHANGELOG"
|
|
128
|
+
else
|
|
129
|
+
git tag -a "$NEW_VERSION" -m "Release ${NEW_VERSION}"
|
|
130
|
+
fi
|
|
131
|
+
TAG_CREATED="true"
|
|
120
132
|
fi
|
|
121
133
|
|
|
122
|
-
git push origin HEAD
|
|
134
|
+
git push origin HEAD
|
|
135
|
+
if [ "$TAG_CREATED" = "true" ]; then
|
|
136
|
+
git push origin "$NEW_VERSION"
|
|
137
|
+
fi
|
|
@@ -30,6 +30,8 @@ jobs:
|
|
|
30
30
|
|
|
31
31
|
- name: Check for untagged commits
|
|
32
32
|
id: check
|
|
33
|
+
env:
|
|
34
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
33
35
|
run: |
|
|
34
36
|
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
|
|
35
37
|
|
|
@@ -41,8 +43,42 @@ jobs:
|
|
|
41
43
|
|
|
42
44
|
echo "last_tag=$LATEST_TAG" >> $GITHUB_OUTPUT
|
|
43
45
|
|
|
44
|
-
#
|
|
45
|
-
|
|
46
|
+
# Collect candidate commits since last tag.
|
|
47
|
+
# Exclude known workflow/system commits first, then exclude commits
|
|
48
|
+
# that are associated with PRs whose source branch is staging-*.
|
|
49
|
+
CANDIDATES=$(git log ${LATEST_TAG}..HEAD --pretty=format:"%H%x09%s" | \
|
|
50
|
+
grep -v "chore(release): bump version" | \
|
|
51
|
+
grep -v "\[hotfix-backport\]" | \
|
|
52
|
+
grep -v "\[skip-store-sync\]" | \
|
|
53
|
+
grep -v "\[stores-to-root\]" | \
|
|
54
|
+
grep -v "\[root-to-stores\]" || true)
|
|
55
|
+
|
|
56
|
+
COMMITS=""
|
|
57
|
+
SKIPPED_UNKNOWN=0
|
|
58
|
+
while IFS=$'\t' read -r SHA SUBJECT; do
|
|
59
|
+
[ -z "$SHA" ] && continue
|
|
60
|
+
|
|
61
|
+
HEAD_REF=$(gh api \
|
|
62
|
+
repos/${{ github.repository }}/commits/$SHA/pulls \
|
|
63
|
+
--jq '.[0].head.ref // ""' 2>/dev/null || true)
|
|
64
|
+
|
|
65
|
+
if [ -z "$HEAD_REF" ]; then
|
|
66
|
+
SKIPPED_UNKNOWN=$((SKIPPED_UNKNOWN + 1))
|
|
67
|
+
echo "Skipping commit due to missing PR metadata: $SHA"
|
|
68
|
+
continue
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
if [[ "$HEAD_REF" == staging-* ]]; then
|
|
72
|
+
echo "Skipping store-backport commit from $HEAD_REF: $SHA"
|
|
73
|
+
continue
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
COMMITS="${COMMITS}${SUBJECT}"$'\n'
|
|
77
|
+
done <<< "$CANDIDATES"
|
|
78
|
+
|
|
79
|
+
if [ "$SKIPPED_UNKNOWN" -gt 0 ]; then
|
|
80
|
+
echo "Skipped $SKIPPED_UNKNOWN commit(s) due to missing PR metadata (fail-safe)."
|
|
81
|
+
fi
|
|
46
82
|
|
|
47
83
|
if [ -n "$COMMITS" ]; then
|
|
48
84
|
COMMIT_COUNT=$(echo "$COMMITS" | wc -l | tr -d ' ')
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# climaybe — Post-Merge Tag (Single-store)
|
|
2
|
-
# After a staging → main PR is merged
|
|
3
|
-
#
|
|
4
|
-
# PR title convention: "Release v3.2" or "Release v3.2.0"
|
|
1
|
+
# climaybe — Post-Merge Tag (Single-store & Multi-store)
|
|
2
|
+
# After a staging → main PR is merged: detects target version from PR title, minor bump (e.g. v3.2.0).
|
|
3
|
+
# After staging-<store> or live-<store> is synced to main (multistore-hotfix-to-main): patch bump (e.g. v3.2.1).
|
|
4
|
+
# Version format is always three-part: v3.2.0. PR title convention for release: "Release v3.2" or "Release v3.2.0"
|
|
5
5
|
|
|
6
6
|
name: Post-Merge Tag
|
|
7
7
|
|
|
@@ -20,8 +20,9 @@ jobs:
|
|
|
20
20
|
outputs:
|
|
21
21
|
is_release: ${{ steps.check.outputs.is_release }}
|
|
22
22
|
version: ${{ steps.check.outputs.version }}
|
|
23
|
+
is_hotfix_backport: ${{ steps.check.outputs.is_hotfix_backport }}
|
|
23
24
|
steps:
|
|
24
|
-
- name: Check if this push is from a merged staging PR
|
|
25
|
+
- name: Check if this push is from a merged staging PR or hotfix backport
|
|
25
26
|
id: check
|
|
26
27
|
env:
|
|
27
28
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -29,19 +30,23 @@ jobs:
|
|
|
29
30
|
# Check the commit message for merge indicator
|
|
30
31
|
COMMIT_MSG=$(echo "${{ github.event.head_commit.message }}" | head -1)
|
|
31
32
|
|
|
32
|
-
# Skip
|
|
33
|
-
if echo "$COMMIT_MSG" | grep -q "
|
|
33
|
+
# Skip version bump commits from this workflow (avoid loop)
|
|
34
|
+
if echo "$COMMIT_MSG" | grep -q "chore(release): bump version"; then
|
|
35
|
+
echo "Skipping post-merge tag: version bump commit."
|
|
34
36
|
echo "is_release=false" >> $GITHUB_OUTPUT
|
|
37
|
+
echo "is_hotfix_backport=false" >> $GITHUB_OUTPUT
|
|
35
38
|
exit 0
|
|
36
39
|
fi
|
|
37
40
|
|
|
38
|
-
#
|
|
39
|
-
if echo "$COMMIT_MSG" | grep -q "
|
|
41
|
+
# Automatic hotfix sync (multistore-hotfix-to-main merge commit)
|
|
42
|
+
if echo "$COMMIT_MSG" | grep -q "\[hotfix-backport\]"; then
|
|
40
43
|
echo "is_release=false" >> $GITHUB_OUTPUT
|
|
44
|
+
echo "is_hotfix_backport=true" >> $GITHUB_OUTPUT
|
|
45
|
+
echo "Detected hotfix backport merge, will trigger patch bump."
|
|
41
46
|
exit 0
|
|
42
47
|
fi
|
|
43
48
|
|
|
44
|
-
# Look for merged PR that targeted main
|
|
49
|
+
# Look for merged PR that targeted main
|
|
45
50
|
PR_NUMBER=$(echo "$COMMIT_MSG" | grep -oP '#\K\d+' | head -1 || true)
|
|
46
51
|
|
|
47
52
|
if [ -n "$PR_NUMBER" ]; then
|
|
@@ -51,27 +56,36 @@ jobs:
|
|
|
51
56
|
HEAD_REF=$(echo "$PR_DATA" | jq -r '.head.ref')
|
|
52
57
|
TITLE=$(echo "$PR_DATA" | jq -r '.title')
|
|
53
58
|
|
|
59
|
+
# Staging → main: release (minor bump)
|
|
54
60
|
if [ "$HEAD_REF" = "staging" ]; then
|
|
55
|
-
# Extract version from PR title: "Release v3.2" or "Release v3.2.0"
|
|
56
61
|
VERSION=$(echo "$TITLE" | grep -oP 'v\d+\.\d+(\.\d+)?' || true)
|
|
57
|
-
|
|
58
62
|
if [ -n "$VERSION" ]; then
|
|
59
|
-
# Ensure it has 3 parts
|
|
60
63
|
PARTS=$(echo "$VERSION" | tr '.' '\n' | wc -l)
|
|
61
64
|
if [ "$PARTS" -eq "2" ]; then
|
|
62
65
|
VERSION="${VERSION}.0"
|
|
63
66
|
fi
|
|
64
|
-
|
|
65
67
|
echo "is_release=true" >> $GITHUB_OUTPUT
|
|
66
68
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
69
|
+
echo "is_hotfix_backport=false" >> $GITHUB_OUTPUT
|
|
67
70
|
echo "Detected release version: $VERSION"
|
|
68
71
|
exit 0
|
|
69
72
|
fi
|
|
73
|
+
echo "Skipping post-merge tag: staging PR title missing release version."
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
# live-<store> → main: hotfix backport (patch bump)
|
|
77
|
+
if [[ "$HEAD_REF" == live-* ]]; then
|
|
78
|
+
echo "is_release=false" >> $GITHUB_OUTPUT
|
|
79
|
+
echo "is_hotfix_backport=true" >> $GITHUB_OUTPUT
|
|
80
|
+
echo "Detected hotfix backport from $HEAD_REF, will trigger patch bump."
|
|
81
|
+
exit 0
|
|
70
82
|
fi
|
|
71
83
|
fi
|
|
72
84
|
fi
|
|
73
85
|
|
|
86
|
+
echo "Skipping post-merge tag: push is not a merged staging release or hotfix backport PR."
|
|
74
87
|
echo "is_release=false" >> $GITHUB_OUTPUT
|
|
88
|
+
echo "is_hotfix_backport=false" >> $GITHUB_OUTPUT
|
|
75
89
|
|
|
76
90
|
# Generate changelog for the release
|
|
77
91
|
changelog:
|
|
@@ -84,7 +98,7 @@ jobs:
|
|
|
84
98
|
secrets:
|
|
85
99
|
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
|
86
100
|
|
|
87
|
-
# Apply version bump
|
|
101
|
+
# Apply version bump (minor for release)
|
|
88
102
|
bump:
|
|
89
103
|
needs: [detect, changelog]
|
|
90
104
|
if: needs.detect.outputs.is_release == 'true'
|
|
@@ -94,3 +108,42 @@ jobs:
|
|
|
94
108
|
version: ${{ needs.detect.outputs.version }}
|
|
95
109
|
changelog: ${{ needs.changelog.outputs.changelog }}
|
|
96
110
|
secrets: inherit
|
|
111
|
+
|
|
112
|
+
# Resolve last tag for hotfix changelog (base_ref)
|
|
113
|
+
hotfix-base:
|
|
114
|
+
needs: detect
|
|
115
|
+
if: needs.detect.outputs.is_hotfix_backport == 'true'
|
|
116
|
+
runs-on: ubuntu-latest
|
|
117
|
+
outputs:
|
|
118
|
+
last_tag: ${{ steps.tag.outputs.last_tag }}
|
|
119
|
+
steps:
|
|
120
|
+
- uses: actions/checkout@v4
|
|
121
|
+
with:
|
|
122
|
+
fetch-depth: 0
|
|
123
|
+
- name: Get latest tag
|
|
124
|
+
id: tag
|
|
125
|
+
run: |
|
|
126
|
+
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
|
|
127
|
+
echo "last_tag=$LAST_TAG" >> $GITHUB_OUTPUT
|
|
128
|
+
echo "Base ref for hotfix changelog: $LAST_TAG"
|
|
129
|
+
|
|
130
|
+
# Generate changelog for hotfix (since last tag)
|
|
131
|
+
changelog-hotfix:
|
|
132
|
+
needs: [detect, hotfix-base]
|
|
133
|
+
if: needs.detect.outputs.is_hotfix_backport == 'true'
|
|
134
|
+
uses: ./.github/workflows/ai-changelog.yml
|
|
135
|
+
with:
|
|
136
|
+
base_ref: ${{ needs.hotfix-base.outputs.last_tag }}
|
|
137
|
+
head_ref: HEAD
|
|
138
|
+
secrets:
|
|
139
|
+
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
|
140
|
+
|
|
141
|
+
# Apply patch version bump after hotfix backport merge
|
|
142
|
+
bump-hotfix:
|
|
143
|
+
needs: [detect, hotfix-base, changelog-hotfix]
|
|
144
|
+
if: needs.detect.outputs.is_hotfix_backport == 'true'
|
|
145
|
+
uses: ./.github/workflows/version-bump.yml
|
|
146
|
+
with:
|
|
147
|
+
bump_type: patch
|
|
148
|
+
changelog: ${{ needs.changelog-hotfix.outputs.changelog }}
|
|
149
|
+
secrets: inherit
|
|
@@ -68,10 +68,16 @@ jobs:
|
|
|
68
68
|
PATCH=$((PATCH + 1))
|
|
69
69
|
NEW_TAG="v${MAJOR}.${MINOR}.${PATCH}"
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
git
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
# Avoid duplicate push attempts when a tag already exists.
|
|
72
|
+
if git rev-parse -q --verify "refs/tags/$NEW_TAG" >/dev/null; then
|
|
73
|
+
echo "new_tag=$NEW_TAG" >> $GITHUB_OUTPUT
|
|
74
|
+
echo "Tag $NEW_TAG already exists, reusing it."
|
|
75
|
+
else
|
|
76
|
+
git tag -a "$NEW_TAG" -m "Pre-release lock before staging merge"
|
|
77
|
+
git push origin "$NEW_TAG"
|
|
78
|
+
echo "new_tag=$NEW_TAG" >> $GITHUB_OUTPUT
|
|
79
|
+
echo "Created pre-release tag: $NEW_TAG"
|
|
80
|
+
fi
|
|
75
81
|
else
|
|
76
82
|
echo "new_tag=$LATEST_TAG" >> $GITHUB_OUTPUT
|
|
77
83
|
echo "No new commits since $LATEST_TAG, skipping tag."
|
|
@@ -98,10 +104,14 @@ jobs:
|
|
|
98
104
|
steps:
|
|
99
105
|
- name: Post changelog comment
|
|
100
106
|
uses: actions/github-script@v7
|
|
107
|
+
env:
|
|
108
|
+
PRE_RELEASE_TAG: ${{ needs.pre-release-tag.outputs.new_tag }}
|
|
109
|
+
CHANGELOG_TEXT: ${{ needs.changelog.outputs.changelog }}
|
|
101
110
|
with:
|
|
102
111
|
script: |
|
|
103
|
-
const tag =
|
|
104
|
-
const
|
|
112
|
+
const tag = process.env.PRE_RELEASE_TAG || '';
|
|
113
|
+
const rawChangelog = process.env.CHANGELOG_TEXT || '';
|
|
114
|
+
const changelog = rawChangelog.trim() ? rawChangelog : 'No changes detected.';
|
|
105
115
|
|
|
106
116
|
const body = `## Release Changelog\n\n` +
|
|
107
117
|
`**Pre-release tag:** \`${tag}\`\n\n` +
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
# climaybe — Hotfix Backport (Multi-store)
|
|
2
|
-
# After root-to-stores completes on a live-* branch,
|
|
3
|
-
# creates a PR to main with [hotfix-backport] flag.
|
|
4
|
-
# This ensures hotfixes on live stores are propagated back to the dev branch.
|
|
5
|
-
# Triggers a patch version bump on main.
|
|
6
|
-
|
|
7
|
-
name: Hotfix Backport
|
|
8
|
-
|
|
9
|
-
on:
|
|
10
|
-
workflow_run:
|
|
11
|
-
workflows: ["Root to Stores"]
|
|
12
|
-
types: [completed]
|
|
13
|
-
branches:
|
|
14
|
-
- 'live-*'
|
|
15
|
-
|
|
16
|
-
jobs:
|
|
17
|
-
backport:
|
|
18
|
-
if: github.event.workflow_run.conclusion == 'success'
|
|
19
|
-
runs-on: ubuntu-latest
|
|
20
|
-
permissions:
|
|
21
|
-
contents: write
|
|
22
|
-
pull-requests: write
|
|
23
|
-
env:
|
|
24
|
-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
25
|
-
steps:
|
|
26
|
-
- uses: actions/checkout@v4
|
|
27
|
-
with:
|
|
28
|
-
ref: ${{ github.event.workflow_run.head_branch }}
|
|
29
|
-
fetch-depth: 0
|
|
30
|
-
token: ${{ secrets.GITHUB_TOKEN }}
|
|
31
|
-
|
|
32
|
-
- name: Extract store alias
|
|
33
|
-
id: alias
|
|
34
|
-
run: |
|
|
35
|
-
BRANCH="${{ github.event.workflow_run.head_branch }}"
|
|
36
|
-
ALIAS="${BRANCH#live-}"
|
|
37
|
-
echo "alias=$ALIAS" >> $GITHUB_OUTPUT
|
|
38
|
-
echo "live_branch=$BRANCH" >> $GITHUB_OUTPUT
|
|
39
|
-
|
|
40
|
-
- name: Check if backport is needed
|
|
41
|
-
id: check
|
|
42
|
-
run: |
|
|
43
|
-
LIVE="${{ steps.alias.outputs.live_branch }}"
|
|
44
|
-
|
|
45
|
-
# Get the last common ancestor with main
|
|
46
|
-
MERGE_BASE=$(git merge-base origin/main origin/$LIVE 2>/dev/null || echo "")
|
|
47
|
-
|
|
48
|
-
if [ -z "$MERGE_BASE" ]; then
|
|
49
|
-
echo "needs_backport=false" >> $GITHUB_OUTPUT
|
|
50
|
-
exit 0
|
|
51
|
-
fi
|
|
52
|
-
|
|
53
|
-
# Check for commits on live that aren't on main
|
|
54
|
-
COMMITS=$(git log --oneline ${MERGE_BASE}..origin/$LIVE -- . ':!stores/' 2>/dev/null | grep -v "\[stores-to-root\]" | grep -v "\[root-to-stores\]" | grep -v "chore(release)" || true)
|
|
55
|
-
|
|
56
|
-
if [ -n "$COMMITS" ]; then
|
|
57
|
-
echo "needs_backport=true" >> $GITHUB_OUTPUT
|
|
58
|
-
echo "Commits to backport:"
|
|
59
|
-
echo "$COMMITS"
|
|
60
|
-
else
|
|
61
|
-
echo "needs_backport=false" >> $GITHUB_OUTPUT
|
|
62
|
-
echo "No new commits to backport."
|
|
63
|
-
fi
|
|
64
|
-
|
|
65
|
-
- name: Create backport PR
|
|
66
|
-
if: steps.check.outputs.needs_backport == 'true'
|
|
67
|
-
run: |
|
|
68
|
-
LIVE="${{ steps.alias.outputs.live_branch }}"
|
|
69
|
-
ALIAS="${{ steps.alias.outputs.alias }}"
|
|
70
|
-
|
|
71
|
-
# Check for existing open backport PR
|
|
72
|
-
EXISTING_PR=$(gh pr list --base main --head "$LIVE" --state open --json number --jq '.[0].number' 2>/dev/null || echo "")
|
|
73
|
-
|
|
74
|
-
if [ -n "$EXISTING_PR" ]; then
|
|
75
|
-
echo "Backport PR #$EXISTING_PR already exists"
|
|
76
|
-
exit 0
|
|
77
|
-
fi
|
|
78
|
-
|
|
79
|
-
PR_URL=$(gh pr create \
|
|
80
|
-
--base main \
|
|
81
|
-
--head "$LIVE" \
|
|
82
|
-
--title "[hotfix-backport] ${ALIAS}: hotfix from live" \
|
|
83
|
-
--body "Backporting hotfix changes from **${LIVE}** to main.
|
|
84
|
-
|
|
85
|
-
This PR was automatically created because direct commits were detected on the live branch.
|
|
86
|
-
|
|
87
|
-
**Important:** Merging this will trigger a patch version bump on main.
|
|
88
|
-
|
|
89
|
-
*Generated by climaybe*" 2>/dev/null || echo "")
|
|
90
|
-
|
|
91
|
-
if [ -n "$PR_URL" ]; then
|
|
92
|
-
echo "Created backport PR: $PR_URL"
|
|
93
|
-
else
|
|
94
|
-
echo "PR creation failed or no diff between branches."
|
|
95
|
-
fi
|
|
96
|
-
|
|
97
|
-
# Patch version bump after backport merge
|
|
98
|
-
# This is handled by the nightly-hotfix workflow or can be triggered manually.
|
|
99
|
-
# The [hotfix-backport] flag in the commit message ensures main-to-staging-stores
|
|
100
|
-
# does NOT push this back to stores.
|