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
|
@@ -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
|
|
@@ -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` +
|