climaybe 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +204 -0
- package/bin/cli.js +5 -0
- package/package.json +31 -0
- package/src/commands/add-store.js +55 -0
- package/src/commands/init.js +78 -0
- package/src/commands/switch.js +27 -0
- package/src/commands/sync.js +42 -0
- package/src/commands/update-workflows.js +18 -0
- package/src/index.js +44 -0
- package/src/lib/config.js +92 -0
- package/src/lib/git.js +90 -0
- package/src/lib/prompts.js +117 -0
- package/src/lib/store-sync.js +142 -0
- package/src/lib/workflows.js +99 -0
- package/src/workflows/multi/hotfix-backport.yml +100 -0
- package/src/workflows/multi/main-to-staging-stores.yml +118 -0
- package/src/workflows/multi/pr-to-live.yml +71 -0
- package/src/workflows/multi/root-to-stores.yml +84 -0
- package/src/workflows/multi/stores-to-root.yml +89 -0
- package/src/workflows/shared/ai-changelog.yml +108 -0
- package/src/workflows/shared/version-bump.yml +122 -0
- package/src/workflows/single/nightly-hotfix.yml +73 -0
- package/src/workflows/single/post-merge-tag.yml +96 -0
- package/src/workflows/single/release-pr-check.yml +136 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# climaybe — Root to Stores (Multi-store)
|
|
2
|
+
# When direct commits (hotfixes) land on live-* branches,
|
|
3
|
+
# syncs root JSON files back to stores/<alias>/ directory.
|
|
4
|
+
# This prepares the data for backporting to main.
|
|
5
|
+
|
|
6
|
+
name: Root to Stores
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
branches:
|
|
11
|
+
- 'live-*'
|
|
12
|
+
|
|
13
|
+
# Prevent concurrent runs per branch
|
|
14
|
+
concurrency:
|
|
15
|
+
group: root-to-stores-${{ github.ref_name }}
|
|
16
|
+
cancel-in-progress: false
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
sync:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
permissions:
|
|
22
|
+
contents: write
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
with:
|
|
26
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
27
|
+
|
|
28
|
+
- name: Skip workflow commits
|
|
29
|
+
id: gate
|
|
30
|
+
run: |
|
|
31
|
+
COMMIT_MSG="${{ github.event.head_commit.message }}"
|
|
32
|
+
if echo "$COMMIT_MSG" | grep -qE "\[root-to-stores\]|\[stores-to-root\]|\[hotfix-backport\]|chore\(release\)"; then
|
|
33
|
+
echo "skip=true" >> $GITHUB_OUTPUT
|
|
34
|
+
else
|
|
35
|
+
echo "skip=false" >> $GITHUB_OUTPUT
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
- name: Extract store alias
|
|
39
|
+
if: steps.gate.outputs.skip != 'true'
|
|
40
|
+
id: alias
|
|
41
|
+
run: |
|
|
42
|
+
BRANCH="${{ github.ref_name }}"
|
|
43
|
+
ALIAS="${BRANCH#live-}"
|
|
44
|
+
echo "alias=$ALIAS" >> $GITHUB_OUTPUT
|
|
45
|
+
|
|
46
|
+
- name: Sync root to stores/<alias>/
|
|
47
|
+
if: steps.gate.outputs.skip != 'true'
|
|
48
|
+
run: |
|
|
49
|
+
ALIAS="${{ steps.alias.outputs.alias }}"
|
|
50
|
+
STORE_DIR="stores/${ALIAS}"
|
|
51
|
+
|
|
52
|
+
# Ensure store directory exists
|
|
53
|
+
mkdir -p "${STORE_DIR}/config" "${STORE_DIR}/templates" "${STORE_DIR}/sections"
|
|
54
|
+
|
|
55
|
+
# Sync directories: config, templates, sections
|
|
56
|
+
for DIR in config templates sections; do
|
|
57
|
+
if [ -d "$DIR" ]; then
|
|
58
|
+
find "$DIR" -name "*.json" | while read -r FILE; do
|
|
59
|
+
# Skip excluded files
|
|
60
|
+
if [ "$FILE" = "config/settings_schema.json" ]; then
|
|
61
|
+
continue
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
DEST="${STORE_DIR}/${FILE}"
|
|
65
|
+
mkdir -p "$(dirname "$DEST")"
|
|
66
|
+
cp "$FILE" "$DEST"
|
|
67
|
+
echo " Synced: $FILE → $DEST"
|
|
68
|
+
done
|
|
69
|
+
fi
|
|
70
|
+
done
|
|
71
|
+
|
|
72
|
+
- name: Commit changes
|
|
73
|
+
if: steps.gate.outputs.skip != 'true'
|
|
74
|
+
run: |
|
|
75
|
+
git config user.name "github-actions[bot]"
|
|
76
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
77
|
+
|
|
78
|
+
git add -A
|
|
79
|
+
if ! git diff --cached --quiet; then
|
|
80
|
+
git commit -m "chore: sync root to stores/${{ steps.alias.outputs.alias }}/ [root-to-stores]"
|
|
81
|
+
git push
|
|
82
|
+
else
|
|
83
|
+
echo "No changes to commit."
|
|
84
|
+
fi
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# climaybe — Stores to Root (Multi-store)
|
|
2
|
+
# When a push happens on staging-* branches (e.g., after main sync),
|
|
3
|
+
# copies JSON files from stores/<alias>/ to the repo root.
|
|
4
|
+
|
|
5
|
+
name: Stores to Root
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
push:
|
|
9
|
+
branches:
|
|
10
|
+
- 'staging-*'
|
|
11
|
+
|
|
12
|
+
# Prevent concurrent runs per branch
|
|
13
|
+
concurrency:
|
|
14
|
+
group: stores-to-root-${{ github.ref_name }}
|
|
15
|
+
cancel-in-progress: false
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
sync:
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
permissions:
|
|
21
|
+
contents: write
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
with:
|
|
25
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
26
|
+
|
|
27
|
+
- name: Skip if this is a sync commit
|
|
28
|
+
id: gate
|
|
29
|
+
run: |
|
|
30
|
+
COMMIT_MSG="${{ github.event.head_commit.message }}"
|
|
31
|
+
if echo "$COMMIT_MSG" | grep -q "\[stores-to-root\]"; then
|
|
32
|
+
echo "skip=true" >> $GITHUB_OUTPUT
|
|
33
|
+
else
|
|
34
|
+
echo "skip=false" >> $GITHUB_OUTPUT
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
- name: Extract store alias from branch name
|
|
38
|
+
if: steps.gate.outputs.skip != 'true'
|
|
39
|
+
id: alias
|
|
40
|
+
run: |
|
|
41
|
+
BRANCH="${{ github.ref_name }}"
|
|
42
|
+
ALIAS="${BRANCH#staging-}"
|
|
43
|
+
echo "alias=$ALIAS" >> $GITHUB_OUTPUT
|
|
44
|
+
echo "Store alias: $ALIAS"
|
|
45
|
+
|
|
46
|
+
- name: Sync stores/<alias>/ to root
|
|
47
|
+
if: steps.gate.outputs.skip != 'true'
|
|
48
|
+
run: |
|
|
49
|
+
ALIAS="${{ steps.alias.outputs.alias }}"
|
|
50
|
+
STORE_DIR="stores/${ALIAS}"
|
|
51
|
+
|
|
52
|
+
if [ ! -d "$STORE_DIR" ]; then
|
|
53
|
+
echo "Store directory $STORE_DIR does not exist, skipping."
|
|
54
|
+
exit 0
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# Sync directories: config, templates, sections
|
|
58
|
+
for DIR in config templates sections; do
|
|
59
|
+
SRC="${STORE_DIR}/${DIR}"
|
|
60
|
+
if [ -d "$SRC" ]; then
|
|
61
|
+
# Copy JSON files only, preserving directory structure
|
|
62
|
+
find "$SRC" -name "*.json" | while read -r FILE; do
|
|
63
|
+
REL_PATH="${FILE#$STORE_DIR/}"
|
|
64
|
+
|
|
65
|
+
# Skip excluded files
|
|
66
|
+
if [ "$REL_PATH" = "config/settings_schema.json" ]; then
|
|
67
|
+
continue
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
mkdir -p "$(dirname "$REL_PATH")"
|
|
71
|
+
cp "$FILE" "$REL_PATH"
|
|
72
|
+
echo " Copied: $REL_PATH"
|
|
73
|
+
done
|
|
74
|
+
fi
|
|
75
|
+
done
|
|
76
|
+
|
|
77
|
+
- name: Commit changes
|
|
78
|
+
if: steps.gate.outputs.skip != 'true'
|
|
79
|
+
run: |
|
|
80
|
+
git config user.name "github-actions[bot]"
|
|
81
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
82
|
+
|
|
83
|
+
git add -A
|
|
84
|
+
if ! git diff --cached --quiet; then
|
|
85
|
+
git commit -m "chore: sync stores/${{ steps.alias.outputs.alias }}/ to root [stores-to-root]"
|
|
86
|
+
git push
|
|
87
|
+
else
|
|
88
|
+
echo "No changes to commit."
|
|
89
|
+
fi
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# climaybe — AI Changelog Generator (Reusable Workflow)
|
|
2
|
+
# Uses Google Gemini to classify commits and generate a changelog.
|
|
3
|
+
|
|
4
|
+
name: AI Changelog
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
workflow_call:
|
|
8
|
+
inputs:
|
|
9
|
+
base_ref:
|
|
10
|
+
description: 'Base ref (tag or commit) to collect commits from'
|
|
11
|
+
required: true
|
|
12
|
+
type: string
|
|
13
|
+
head_ref:
|
|
14
|
+
description: 'Head ref to collect commits to (default: HEAD)'
|
|
15
|
+
required: false
|
|
16
|
+
type: string
|
|
17
|
+
default: 'HEAD'
|
|
18
|
+
outputs:
|
|
19
|
+
changelog:
|
|
20
|
+
description: 'Generated changelog in markdown format'
|
|
21
|
+
value: ${{ jobs.generate.outputs.changelog }}
|
|
22
|
+
secrets:
|
|
23
|
+
GEMINI_API_KEY:
|
|
24
|
+
required: true
|
|
25
|
+
|
|
26
|
+
jobs:
|
|
27
|
+
generate:
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
outputs:
|
|
30
|
+
changelog: ${{ steps.ai.outputs.changelog }}
|
|
31
|
+
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/checkout@v4
|
|
34
|
+
with:
|
|
35
|
+
fetch-depth: 0
|
|
36
|
+
|
|
37
|
+
- name: Collect commits
|
|
38
|
+
id: commits
|
|
39
|
+
run: |
|
|
40
|
+
COMMITS=$(git log --pretty=format:"%h %s" ${{ inputs.base_ref }}..${{ inputs.head_ref }} 2>/dev/null || echo "")
|
|
41
|
+
if [ -z "$COMMITS" ]; then
|
|
42
|
+
echo "No commits found between ${{ inputs.base_ref }} and ${{ inputs.head_ref }}"
|
|
43
|
+
echo "commits=" >> $GITHUB_OUTPUT
|
|
44
|
+
exit 0
|
|
45
|
+
fi
|
|
46
|
+
# Escape for JSON
|
|
47
|
+
COMMITS_ESCAPED=$(echo "$COMMITS" | jq -Rs .)
|
|
48
|
+
echo "commits=$COMMITS_ESCAPED" >> $GITHUB_OUTPUT
|
|
49
|
+
|
|
50
|
+
- name: Generate changelog with Gemini
|
|
51
|
+
id: ai
|
|
52
|
+
if: steps.commits.outputs.commits != ''
|
|
53
|
+
env:
|
|
54
|
+
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
|
55
|
+
run: |
|
|
56
|
+
COMMITS=${{ steps.commits.outputs.commits }}
|
|
57
|
+
|
|
58
|
+
PROMPT=$(cat <<'PROMPT_EOF'
|
|
59
|
+
You are a changelog generator for a Shopify theme repository.
|
|
60
|
+
Given the following git commits, classify each into one of these categories:
|
|
61
|
+
- **Features**: New functionality
|
|
62
|
+
- **Fixes**: Bug fixes
|
|
63
|
+
- **Improvements**: Enhancements to existing features
|
|
64
|
+
- **Chore**: Maintenance, refactoring, docs
|
|
65
|
+
|
|
66
|
+
Format the output as a markdown changelog. Group by category.
|
|
67
|
+
Omit empty categories. Write concise, merchant-friendly descriptions.
|
|
68
|
+
Do NOT include commit hashes in the output.
|
|
69
|
+
|
|
70
|
+
Commits:
|
|
71
|
+
PROMPT_EOF
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
PAYLOAD=$(jq -n \
|
|
75
|
+
--arg prompt "$PROMPT" \
|
|
76
|
+
--argjson commits "$COMMITS" \
|
|
77
|
+
'{
|
|
78
|
+
"contents": [{
|
|
79
|
+
"parts": [{"text": ($prompt + "\n" + $commits)}]
|
|
80
|
+
}],
|
|
81
|
+
"generationConfig": {
|
|
82
|
+
"temperature": 0.3,
|
|
83
|
+
"maxOutputTokens": 2048
|
|
84
|
+
}
|
|
85
|
+
}')
|
|
86
|
+
|
|
87
|
+
RESPONSE=$(curl -s -X POST \
|
|
88
|
+
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_API_KEY}" \
|
|
89
|
+
-H "Content-Type: application/json" \
|
|
90
|
+
-d "$PAYLOAD")
|
|
91
|
+
|
|
92
|
+
CHANGELOG=$(echo "$RESPONSE" | jq -r '.candidates[0].content.parts[0].text // "Changelog generation failed."')
|
|
93
|
+
|
|
94
|
+
# Write to output (multiline)
|
|
95
|
+
{
|
|
96
|
+
echo "changelog<<CHANGELOG_EOF"
|
|
97
|
+
echo "$CHANGELOG"
|
|
98
|
+
echo "CHANGELOG_EOF"
|
|
99
|
+
} >> $GITHUB_OUTPUT
|
|
100
|
+
|
|
101
|
+
- name: Fallback if no commits
|
|
102
|
+
if: steps.commits.outputs.commits == ''
|
|
103
|
+
run: |
|
|
104
|
+
{
|
|
105
|
+
echo "changelog<<CHANGELOG_EOF"
|
|
106
|
+
echo "No changes detected."
|
|
107
|
+
echo "CHANGELOG_EOF"
|
|
108
|
+
} >> $GITHUB_OUTPUT
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# climaybe — Version Bump (Reusable Workflow)
|
|
2
|
+
# Bumps version in settings_schema.json, creates a git tag, and pushes.
|
|
3
|
+
|
|
4
|
+
name: Version Bump
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
workflow_call:
|
|
8
|
+
inputs:
|
|
9
|
+
bump_type:
|
|
10
|
+
description: 'Type of version bump: minor or patch'
|
|
11
|
+
required: true
|
|
12
|
+
type: string
|
|
13
|
+
version:
|
|
14
|
+
description: 'Explicit version to set (e.g., v3.2.0). If empty, auto-increments based on bump_type.'
|
|
15
|
+
required: false
|
|
16
|
+
type: string
|
|
17
|
+
default: ''
|
|
18
|
+
changelog:
|
|
19
|
+
description: 'Changelog text for the tag annotation'
|
|
20
|
+
required: false
|
|
21
|
+
type: string
|
|
22
|
+
default: ''
|
|
23
|
+
outputs:
|
|
24
|
+
new_version:
|
|
25
|
+
description: 'The new version tag that was created'
|
|
26
|
+
value: ${{ jobs.bump.outputs.new_version }}
|
|
27
|
+
|
|
28
|
+
jobs:
|
|
29
|
+
bump:
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
outputs:
|
|
32
|
+
new_version: ${{ steps.bump.outputs.new_version }}
|
|
33
|
+
|
|
34
|
+
permissions:
|
|
35
|
+
contents: write
|
|
36
|
+
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/checkout@v4
|
|
39
|
+
with:
|
|
40
|
+
fetch-depth: 0
|
|
41
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
42
|
+
|
|
43
|
+
- name: Configure git
|
|
44
|
+
run: |
|
|
45
|
+
git config user.name "github-actions[bot]"
|
|
46
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
47
|
+
|
|
48
|
+
- name: Determine new version
|
|
49
|
+
id: bump
|
|
50
|
+
run: |
|
|
51
|
+
BUMP_TYPE="${{ inputs.bump_type }}"
|
|
52
|
+
EXPLICIT_VERSION="${{ inputs.version }}"
|
|
53
|
+
|
|
54
|
+
if [ -n "$EXPLICIT_VERSION" ]; then
|
|
55
|
+
# Use explicit version, ensure it starts with v
|
|
56
|
+
NEW_VERSION="${EXPLICIT_VERSION#v}"
|
|
57
|
+
NEW_VERSION="v${NEW_VERSION}"
|
|
58
|
+
else
|
|
59
|
+
# Get latest tag
|
|
60
|
+
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
|
|
61
|
+
LATEST_TAG="${LATEST_TAG#v}"
|
|
62
|
+
|
|
63
|
+
IFS='.' read -r MAJOR MINOR PATCH <<< "$LATEST_TAG"
|
|
64
|
+
MAJOR=${MAJOR:-0}
|
|
65
|
+
MINOR=${MINOR:-0}
|
|
66
|
+
PATCH=${PATCH:-0}
|
|
67
|
+
|
|
68
|
+
if [ "$BUMP_TYPE" = "minor" ]; then
|
|
69
|
+
MINOR=$((MINOR + 1))
|
|
70
|
+
PATCH=0
|
|
71
|
+
elif [ "$BUMP_TYPE" = "patch" ]; then
|
|
72
|
+
PATCH=$((PATCH + 1))
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
NEW_VERSION="v${MAJOR}.${MINOR}.${PATCH}"
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
|
79
|
+
echo "New version: $NEW_VERSION"
|
|
80
|
+
|
|
81
|
+
- name: Update settings_schema.json
|
|
82
|
+
run: |
|
|
83
|
+
NEW_VERSION="${{ steps.bump.outputs.new_version }}"
|
|
84
|
+
SCHEMA_FILE="config/settings_schema.json"
|
|
85
|
+
|
|
86
|
+
if [ -f "$SCHEMA_FILE" ]; then
|
|
87
|
+
# Update the theme_info version in settings_schema.json
|
|
88
|
+
node -e "
|
|
89
|
+
const fs = require('fs');
|
|
90
|
+
const schema = JSON.parse(fs.readFileSync('$SCHEMA_FILE', 'utf-8'));
|
|
91
|
+
const themeInfo = schema.find(s => s.name === 'theme_info');
|
|
92
|
+
if (themeInfo) {
|
|
93
|
+
themeInfo.theme_version = '${NEW_VERSION#v}';
|
|
94
|
+
}
|
|
95
|
+
fs.writeFileSync('$SCHEMA_FILE', JSON.stringify(schema, null, 2) + '\n');
|
|
96
|
+
"
|
|
97
|
+
echo "Updated $SCHEMA_FILE to ${NEW_VERSION}"
|
|
98
|
+
else
|
|
99
|
+
echo "No $SCHEMA_FILE found, skipping schema update."
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
- name: Commit and tag
|
|
103
|
+
run: |
|
|
104
|
+
NEW_VERSION="${{ steps.bump.outputs.new_version }}"
|
|
105
|
+
|
|
106
|
+
# Stage changes
|
|
107
|
+
git add -A
|
|
108
|
+
|
|
109
|
+
# Only commit if there are changes
|
|
110
|
+
if ! git diff --cached --quiet; then
|
|
111
|
+
git commit -m "chore(release): bump version to ${NEW_VERSION}"
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
# Create annotated tag
|
|
115
|
+
CHANGELOG="${{ inputs.changelog }}"
|
|
116
|
+
if [ -n "$CHANGELOG" ]; then
|
|
117
|
+
git tag -a "$NEW_VERSION" -m "$CHANGELOG"
|
|
118
|
+
else
|
|
119
|
+
git tag -a "$NEW_VERSION" -m "Release ${NEW_VERSION}"
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
git push origin HEAD --follow-tags
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# climaybe — Nightly Hotfix Tagger (Single-store)
|
|
2
|
+
# Runs at 02:00 US Eastern (07:00 UTC) every night.
|
|
3
|
+
# If there are untagged commits on main (not from staging merges),
|
|
4
|
+
# generates a changelog and creates a patch version tag.
|
|
5
|
+
|
|
6
|
+
name: Nightly Hotfix Tag
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
schedule:
|
|
10
|
+
# 02:00 US Eastern = 07:00 UTC
|
|
11
|
+
- cron: '0 7 * * *'
|
|
12
|
+
# Allow manual trigger for testing
|
|
13
|
+
workflow_dispatch:
|
|
14
|
+
|
|
15
|
+
# Prevent concurrent runs
|
|
16
|
+
concurrency:
|
|
17
|
+
group: nightly-hotfix
|
|
18
|
+
cancel-in-progress: false
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
check:
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
outputs:
|
|
24
|
+
has_changes: ${{ steps.check.outputs.has_changes }}
|
|
25
|
+
last_tag: ${{ steps.check.outputs.last_tag }}
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v4
|
|
28
|
+
with:
|
|
29
|
+
fetch-depth: 0
|
|
30
|
+
|
|
31
|
+
- name: Check for untagged commits
|
|
32
|
+
id: check
|
|
33
|
+
run: |
|
|
34
|
+
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
|
|
35
|
+
|
|
36
|
+
if [ -z "$LATEST_TAG" ]; then
|
|
37
|
+
echo "No tags found, skipping."
|
|
38
|
+
echo "has_changes=false" >> $GITHUB_OUTPUT
|
|
39
|
+
exit 0
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
echo "last_tag=$LATEST_TAG" >> $GITHUB_OUTPUT
|
|
43
|
+
|
|
44
|
+
# Count commits since last tag, excluding version bump commits
|
|
45
|
+
COMMITS=$(git log ${LATEST_TAG}..HEAD --pretty=format:"%s" | grep -v "chore(release): bump version" | grep -v "\[hotfix-backport\]" || true)
|
|
46
|
+
|
|
47
|
+
if [ -n "$COMMITS" ]; then
|
|
48
|
+
COMMIT_COUNT=$(echo "$COMMITS" | wc -l | tr -d ' ')
|
|
49
|
+
echo "Found $COMMIT_COUNT untagged commit(s) since $LATEST_TAG"
|
|
50
|
+
echo "has_changes=true" >> $GITHUB_OUTPUT
|
|
51
|
+
else
|
|
52
|
+
echo "No untagged hotfix commits since $LATEST_TAG"
|
|
53
|
+
echo "has_changes=false" >> $GITHUB_OUTPUT
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
changelog:
|
|
57
|
+
needs: check
|
|
58
|
+
if: needs.check.outputs.has_changes == 'true'
|
|
59
|
+
uses: ./.github/workflows/ai-changelog.yml
|
|
60
|
+
with:
|
|
61
|
+
base_ref: ${{ needs.check.outputs.last_tag }}
|
|
62
|
+
head_ref: HEAD
|
|
63
|
+
secrets:
|
|
64
|
+
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
|
65
|
+
|
|
66
|
+
bump:
|
|
67
|
+
needs: [check, changelog]
|
|
68
|
+
if: needs.check.outputs.has_changes == 'true'
|
|
69
|
+
uses: ./.github/workflows/version-bump.yml
|
|
70
|
+
with:
|
|
71
|
+
bump_type: patch
|
|
72
|
+
changelog: ${{ needs.changelog.outputs.changelog }}
|
|
73
|
+
secrets: inherit
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# climaybe — Post-Merge Tag (Single-store)
|
|
2
|
+
# After a staging → main PR is merged, detects the target version from the PR title
|
|
3
|
+
# and bumps the minor version accordingly.
|
|
4
|
+
# PR title convention: "Release v3.2" or "Release v3.2.0"
|
|
5
|
+
|
|
6
|
+
name: Post-Merge Tag
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
branches: [main]
|
|
11
|
+
|
|
12
|
+
# Prevent concurrent version bumps
|
|
13
|
+
concurrency:
|
|
14
|
+
group: post-merge-tag
|
|
15
|
+
cancel-in-progress: false
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
detect:
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
outputs:
|
|
21
|
+
is_release: ${{ steps.check.outputs.is_release }}
|
|
22
|
+
version: ${{ steps.check.outputs.version }}
|
|
23
|
+
steps:
|
|
24
|
+
- name: Check if this push is from a merged staging PR
|
|
25
|
+
id: check
|
|
26
|
+
env:
|
|
27
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
28
|
+
run: |
|
|
29
|
+
# Check the commit message for merge indicator
|
|
30
|
+
COMMIT_MSG=$(echo "${{ github.event.head_commit.message }}" | head -1)
|
|
31
|
+
|
|
32
|
+
# Skip hotfix backports
|
|
33
|
+
if echo "$COMMIT_MSG" | grep -q "\[hotfix-backport\]"; then
|
|
34
|
+
echo "is_release=false" >> $GITHUB_OUTPUT
|
|
35
|
+
exit 0
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# Skip version bump commits from this workflow
|
|
39
|
+
if echo "$COMMIT_MSG" | grep -q "chore(release): bump version"; then
|
|
40
|
+
echo "is_release=false" >> $GITHUB_OUTPUT
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# Look for merged PR that targeted main from staging
|
|
45
|
+
PR_NUMBER=$(echo "$COMMIT_MSG" | grep -oP '#\K\d+' | head -1 || true)
|
|
46
|
+
|
|
47
|
+
if [ -n "$PR_NUMBER" ]; then
|
|
48
|
+
PR_DATA=$(gh api repos/${{ github.repository }}/pulls/$PR_NUMBER 2>/dev/null || echo "")
|
|
49
|
+
|
|
50
|
+
if [ -n "$PR_DATA" ]; then
|
|
51
|
+
HEAD_REF=$(echo "$PR_DATA" | jq -r '.head.ref')
|
|
52
|
+
TITLE=$(echo "$PR_DATA" | jq -r '.title')
|
|
53
|
+
|
|
54
|
+
if [ "$HEAD_REF" = "staging" ]; then
|
|
55
|
+
# Extract version from PR title: "Release v3.2" or "Release v3.2.0"
|
|
56
|
+
VERSION=$(echo "$TITLE" | grep -oP 'v\d+\.\d+(\.\d+)?' || true)
|
|
57
|
+
|
|
58
|
+
if [ -n "$VERSION" ]; then
|
|
59
|
+
# Ensure it has 3 parts
|
|
60
|
+
PARTS=$(echo "$VERSION" | tr '.' '\n' | wc -l)
|
|
61
|
+
if [ "$PARTS" -eq "2" ]; then
|
|
62
|
+
VERSION="${VERSION}.0"
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
echo "is_release=true" >> $GITHUB_OUTPUT
|
|
66
|
+
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
67
|
+
echo "Detected release version: $VERSION"
|
|
68
|
+
exit 0
|
|
69
|
+
fi
|
|
70
|
+
fi
|
|
71
|
+
fi
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
echo "is_release=false" >> $GITHUB_OUTPUT
|
|
75
|
+
|
|
76
|
+
# Generate changelog for the release
|
|
77
|
+
changelog:
|
|
78
|
+
needs: detect
|
|
79
|
+
if: needs.detect.outputs.is_release == 'true'
|
|
80
|
+
uses: ./.github/workflows/ai-changelog.yml
|
|
81
|
+
with:
|
|
82
|
+
base_ref: ${{ needs.detect.outputs.version }}
|
|
83
|
+
head_ref: HEAD
|
|
84
|
+
secrets:
|
|
85
|
+
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
|
86
|
+
|
|
87
|
+
# Apply version bump
|
|
88
|
+
bump:
|
|
89
|
+
needs: [detect, changelog]
|
|
90
|
+
if: needs.detect.outputs.is_release == 'true'
|
|
91
|
+
uses: ./.github/workflows/version-bump.yml
|
|
92
|
+
with:
|
|
93
|
+
bump_type: minor
|
|
94
|
+
version: ${{ needs.detect.outputs.version }}
|
|
95
|
+
changelog: ${{ needs.changelog.outputs.changelog }}
|
|
96
|
+
secrets: inherit
|