climaybe 1.5.2 → 1.6.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 +44 -18
- package/bin/version.txt +1 -1
- package/package.json +1 -1
- package/src/commands/add-cursor-skill.js +17 -0
- package/src/commands/init.js +23 -0
- package/src/commands/setup-commitlint.js +21 -0
- package/src/index.js +12 -0
- package/src/lib/commit-tooling.js +110 -0
- package/src/lib/config.js +16 -0
- package/src/lib/prompts.js +28 -0
- package/src/workflows/build/build-pipeline.yml +15 -15
- package/src/workflows/multi/main-to-staging-stores.yml +40 -69
- package/src/workflows/multi/multistore-hotfix-to-main.yml +15 -4
- package/src/workflows/multi/root-to-stores.yml +63 -16
- package/src/workflows/multi/stores-to-root.yml +77 -43
- package/src/workflows/shared/version-bump.yml +19 -2
- package/src/workflows/single/nightly-hotfix.yml +43 -23
- package/src/workflows/single/post-merge-tag.yml +67 -89
- package/src/workflows/single/release-pr-check.yml +30 -4
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
# climaybe — Multistore Hotfix to Main
|
|
2
|
-
#
|
|
2
|
+
# Syncs hotfixes from staging-<store> or live-<store> back to main.
|
|
3
|
+
# Only when the push is NOT from main (main→staging merge would create a loop).
|
|
3
4
|
# 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
5
|
|
|
7
6
|
name: Multistore Hotfix to Main
|
|
8
7
|
|
|
@@ -49,8 +48,20 @@ jobs:
|
|
|
49
48
|
run: |
|
|
50
49
|
SOURCE="${{ steps.ref.outputs.branch }}"
|
|
51
50
|
|
|
52
|
-
|
|
51
|
+
# Do not backport when this push was a merge FROM main (staging-* or live-*; would loop)
|
|
52
|
+
if [[ "$SOURCE" == staging-* || "$SOURCE" == live-* ]]; then
|
|
53
|
+
MAIN_SHA=$(git rev-parse origin/main 2>/dev/null || echo "")
|
|
54
|
+
if [ -n "$MAIN_SHA" ]; then
|
|
55
|
+
SECOND_PARENT=$(git rev-parse origin/$SOURCE^2 2>/dev/null || echo "")
|
|
56
|
+
if [ "$SECOND_PARENT" = "$MAIN_SHA" ]; then
|
|
57
|
+
echo "needs_backport=false" >> $GITHUB_OUTPUT
|
|
58
|
+
echo "Push is merge from main into $SOURCE; skipping to avoid loop."
|
|
59
|
+
exit 0
|
|
60
|
+
fi
|
|
61
|
+
fi
|
|
62
|
+
fi
|
|
53
63
|
|
|
64
|
+
MERGE_BASE=$(git merge-base origin/main origin/$SOURCE 2>/dev/null || echo "")
|
|
54
65
|
if [ -z "$MERGE_BASE" ]; then
|
|
55
66
|
echo "needs_backport=false" >> $GITHUB_OUTPUT
|
|
56
67
|
exit 0
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# climaybe — Root to Stores (Multi-store)
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
1
|
+
# climaybe — Root to Stores / Stores to Root (Multi-store, live-*)
|
|
2
|
+
# Same logic as stores-to-root on staging-*: direction depends on source.
|
|
3
|
+
# Push to live-* from MAIN (merge): copy stores/<alias>/ → root.
|
|
4
|
+
# Push to live-* from ELSEWHERE (direct, Shopify, etc.): copy root → stores/<alias>/.
|
|
5
5
|
|
|
6
6
|
name: Root to Stores
|
|
7
7
|
|
|
@@ -24,16 +24,30 @@ jobs:
|
|
|
24
24
|
- uses: actions/checkout@v4
|
|
25
25
|
with:
|
|
26
26
|
token: ${{ secrets.GITHUB_TOKEN }}
|
|
27
|
+
fetch-depth: 0
|
|
27
28
|
|
|
28
|
-
- name: Skip
|
|
29
|
+
- name: Skip if our own sync commit; detect if push from main
|
|
29
30
|
id: gate
|
|
30
31
|
run: |
|
|
31
32
|
COMMIT_MSG="${{ github.event.head_commit.message }}"
|
|
32
33
|
if echo "$COMMIT_MSG" | grep -qE "\[root-to-stores\]|\[stores-to-root\]|\[hotfix-backport\]|chore\(release\)"; then
|
|
33
34
|
echo "skip=true" >> $GITHUB_OUTPUT
|
|
35
|
+
echo "from_main=false" >> $GITHUB_OUTPUT
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
# Detect: is this push a merge FROM main? (second parent = main)
|
|
39
|
+
MAIN_SHA=$(git rev-parse origin/main 2>/dev/null || echo "")
|
|
40
|
+
if [ -z "$MAIN_SHA" ]; then
|
|
41
|
+
echo "from_main=false" >> $GITHUB_OUTPUT
|
|
34
42
|
else
|
|
35
|
-
echo "
|
|
43
|
+
SECOND_PARENT=$(git rev-parse HEAD^2 2>/dev/null || echo "")
|
|
44
|
+
if [ "$SECOND_PARENT" = "$MAIN_SHA" ]; then
|
|
45
|
+
echo "from_main=true" >> $GITHUB_OUTPUT
|
|
46
|
+
else
|
|
47
|
+
echo "from_main=false" >> $GITHUB_OUTPUT
|
|
48
|
+
fi
|
|
36
49
|
fi
|
|
50
|
+
echo "skip=false" >> $GITHUB_OUTPUT
|
|
37
51
|
|
|
38
52
|
- name: Extract store alias
|
|
39
53
|
if: steps.gate.outputs.skip != 'true'
|
|
@@ -42,25 +56,59 @@ jobs:
|
|
|
42
56
|
BRANCH="${{ github.ref_name }}"
|
|
43
57
|
ALIAS="${BRANCH#live-}"
|
|
44
58
|
echo "alias=$ALIAS" >> $GITHUB_OUTPUT
|
|
59
|
+
echo "Store alias: $ALIAS (from_main=${{ steps.gate.outputs.from_main }})"
|
|
45
60
|
|
|
46
|
-
|
|
47
|
-
|
|
61
|
+
# --- When push is FROM MAIN: stores → root ---
|
|
62
|
+
- name: Sync stores/<alias>/ to root (main merged in)
|
|
63
|
+
if: steps.gate.outputs.skip != 'true' && steps.gate.outputs.from_main == 'true'
|
|
48
64
|
run: |
|
|
49
65
|
ALIAS="${{ steps.alias.outputs.alias }}"
|
|
50
66
|
STORE_DIR="stores/${ALIAS}"
|
|
67
|
+
if [ ! -d "$STORE_DIR" ]; then
|
|
68
|
+
echo "Store directory $STORE_DIR does not exist, skipping."
|
|
69
|
+
exit 0
|
|
70
|
+
fi
|
|
71
|
+
for DIR in config templates sections; do
|
|
72
|
+
SRC="${STORE_DIR}/${DIR}"
|
|
73
|
+
if [ -d "$SRC" ]; then
|
|
74
|
+
find "$SRC" -name "*.json" | while read -r FILE; do
|
|
75
|
+
REL_PATH="${FILE#$STORE_DIR/}"
|
|
76
|
+
if [ "$REL_PATH" = "config/settings_schema.json" ]; then
|
|
77
|
+
continue
|
|
78
|
+
fi
|
|
79
|
+
mkdir -p "$(dirname "$REL_PATH")"
|
|
80
|
+
cp "$FILE" "$REL_PATH"
|
|
81
|
+
echo " Copied: $REL_PATH"
|
|
82
|
+
done
|
|
83
|
+
fi
|
|
84
|
+
done
|
|
51
85
|
|
|
52
|
-
|
|
53
|
-
|
|
86
|
+
- name: Commit stores→root
|
|
87
|
+
if: steps.gate.outputs.skip != 'true' && steps.gate.outputs.from_main == 'true'
|
|
88
|
+
run: |
|
|
89
|
+
git config user.name "github-actions[bot]"
|
|
90
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
91
|
+
git add -A
|
|
92
|
+
if ! git diff --cached --quiet; then
|
|
93
|
+
git commit -m "chore: sync stores/${{ steps.alias.outputs.alias }}/ to root [stores-to-root]"
|
|
94
|
+
git push
|
|
95
|
+
else
|
|
96
|
+
echo "No changes to commit."
|
|
97
|
+
fi
|
|
54
98
|
|
|
55
|
-
|
|
99
|
+
# --- When push is NOT from main: root → stores/<alias>/ ---
|
|
100
|
+
- name: Sync root to stores/<alias>/ (external push)
|
|
101
|
+
if: steps.gate.outputs.skip != 'true' && steps.gate.outputs.from_main == 'false'
|
|
102
|
+
run: |
|
|
103
|
+
ALIAS="${{ steps.alias.outputs.alias }}"
|
|
104
|
+
STORE_DIR="stores/${ALIAS}"
|
|
105
|
+
mkdir -p "${STORE_DIR}/config" "${STORE_DIR}/templates" "${STORE_DIR}/sections"
|
|
56
106
|
for DIR in config templates sections; do
|
|
57
107
|
if [ -d "$DIR" ]; then
|
|
58
108
|
find "$DIR" -name "*.json" | while read -r FILE; do
|
|
59
|
-
# Skip excluded files
|
|
60
109
|
if [ "$FILE" = "config/settings_schema.json" ]; then
|
|
61
110
|
continue
|
|
62
111
|
fi
|
|
63
|
-
|
|
64
112
|
DEST="${STORE_DIR}/${FILE}"
|
|
65
113
|
mkdir -p "$(dirname "$DEST")"
|
|
66
114
|
cp "$FILE" "$DEST"
|
|
@@ -69,12 +117,11 @@ jobs:
|
|
|
69
117
|
fi
|
|
70
118
|
done
|
|
71
119
|
|
|
72
|
-
- name: Commit
|
|
73
|
-
if: steps.gate.outputs.skip != 'true'
|
|
120
|
+
- name: Commit root→stores
|
|
121
|
+
if: steps.gate.outputs.skip != 'true' && steps.gate.outputs.from_main == 'false'
|
|
74
122
|
run: |
|
|
75
123
|
git config user.name "github-actions[bot]"
|
|
76
124
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
77
|
-
|
|
78
125
|
git add -A
|
|
79
126
|
if ! git diff --cached --quiet; then
|
|
80
127
|
git commit -m "chore: sync root to stores/${{ steps.alias.outputs.alias }}/ [root-to-stores]"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
# climaybe — Stores to Root (Multi-store)
|
|
2
|
-
#
|
|
3
|
-
#
|
|
1
|
+
# climaybe — Stores to Root / Root to Stores (Multi-store, staging-*)
|
|
2
|
+
# Push to staging-* from MAIN (merge): copy stores/<alias>/ → root (store-specific on top of main).
|
|
3
|
+
# Push to staging-* from ELSEWHERE (Shopify, direct, feature branch): copy root → stores/<alias>/
|
|
4
|
+
# so store-specific JSON changes are persisted, then hotfix-to-main can backport.
|
|
4
5
|
|
|
5
6
|
name: Stores to Root
|
|
6
7
|
|
|
@@ -24,80 +25,80 @@ jobs:
|
|
|
24
25
|
- uses: actions/checkout@v4
|
|
25
26
|
with:
|
|
26
27
|
token: ${{ secrets.GITHUB_TOKEN }}
|
|
27
|
-
fetch-depth:
|
|
28
|
+
fetch-depth: 0
|
|
28
29
|
|
|
29
|
-
- name: Skip if this is
|
|
30
|
+
- name: Skip if this is our own sync commit
|
|
30
31
|
id: gate
|
|
31
32
|
run: |
|
|
32
|
-
if [ "${{ github.event_name }}" = "
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
34
|
+
echo "skip=false" >> $GITHUB_OUTPUT
|
|
35
|
+
echo "from_main=false" >> $GITHUB_OUTPUT
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
COMMIT_MSG="${{ github.event.head_commit.message }}"
|
|
39
|
+
if echo "$COMMIT_MSG" | grep -qE "\[stores-to-root\]|\[root-to-stores\]"; then
|
|
40
|
+
echo "skip=true" >> $GITHUB_OUTPUT
|
|
41
|
+
echo "from_main=false" >> $GITHUB_OUTPUT
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
# Detect: is this push a merge FROM main? (merge commit with second parent = main)
|
|
45
|
+
MAIN_SHA=$(git rev-parse origin/main 2>/dev/null || echo "")
|
|
46
|
+
if [ -z "$MAIN_SHA" ]; then
|
|
47
|
+
echo "from_main=false" >> $GITHUB_OUTPUT
|
|
48
|
+
else
|
|
49
|
+
SECOND_PARENT=$(git rev-parse HEAD^2 2>/dev/null || echo "")
|
|
50
|
+
if [ "$SECOND_PARENT" = "$MAIN_SHA" ]; then
|
|
51
|
+
echo "from_main=true" >> $GITHUB_OUTPUT
|
|
36
52
|
else
|
|
37
|
-
echo "
|
|
53
|
+
echo "from_main=false" >> $GITHUB_OUTPUT
|
|
38
54
|
fi
|
|
39
|
-
else
|
|
40
|
-
# Manual/API dispatch should always run for the selected branch ref.
|
|
41
|
-
echo "workflow_dispatch detected, bypassing sync-commit gate."
|
|
42
|
-
echo "skip=false" >> $GITHUB_OUTPUT
|
|
43
55
|
fi
|
|
56
|
+
echo "skip=false" >> $GITHUB_OUTPUT
|
|
44
57
|
|
|
45
|
-
- name:
|
|
58
|
+
- name: Extract store alias from branch name
|
|
46
59
|
if: steps.gate.outputs.skip != 'true'
|
|
60
|
+
id: alias
|
|
61
|
+
run: |
|
|
62
|
+
BRANCH="${{ github.ref_name }}"
|
|
63
|
+
ALIAS="${BRANCH#staging-}"
|
|
64
|
+
echo "alias=$ALIAS" >> $GITHUB_OUTPUT
|
|
65
|
+
echo "Store alias: $ALIAS (from_main=${{ steps.gate.outputs.from_main }})"
|
|
66
|
+
|
|
67
|
+
# --- When push is FROM MAIN: stores → root, and restore store-specific locales ---
|
|
68
|
+
- name: Keep store-specific paths (e.g. locales) after main sync
|
|
69
|
+
if: steps.gate.outputs.skip != 'true' && steps.gate.outputs.from_main == 'true'
|
|
47
70
|
run: |
|
|
48
|
-
# After a main→staging merge, restore store-specific paths from the pre-merge commit
|
|
49
|
-
# so they are not overwritten by main. Only run when paths exist (no pathspec error).
|
|
50
71
|
if [ ! -d "locales" ]; then
|
|
51
|
-
echo "No locales/ directory, skipping."
|
|
52
72
|
exit 0
|
|
53
73
|
fi
|
|
54
74
|
PARENT=$(git rev-parse HEAD^1 2>/dev/null || echo "")
|
|
55
75
|
if [ -z "$PARENT" ]; then
|
|
56
|
-
echo "Not a merge commit or shallow clone, skipping."
|
|
57
76
|
exit 0
|
|
58
77
|
fi
|
|
59
78
|
find locales -name "*.json" 2>/dev/null | while read -r f; do
|
|
60
79
|
git checkout "$PARENT" -- "$f" 2>/dev/null || true
|
|
61
80
|
done
|
|
62
|
-
if git diff --quiet; then
|
|
63
|
-
echo "No store-specific locale changes to restore."
|
|
64
|
-
else
|
|
81
|
+
if ! git diff --quiet; then
|
|
65
82
|
echo "Restored store-specific locale files from pre-merge commit."
|
|
66
83
|
fi
|
|
67
84
|
|
|
68
|
-
- name:
|
|
69
|
-
if: steps.gate.outputs.skip != 'true'
|
|
70
|
-
id: alias
|
|
71
|
-
run: |
|
|
72
|
-
BRANCH="${{ github.ref_name }}"
|
|
73
|
-
ALIAS="${BRANCH#staging-}"
|
|
74
|
-
echo "alias=$ALIAS" >> $GITHUB_OUTPUT
|
|
75
|
-
echo "Store alias: $ALIAS"
|
|
76
|
-
|
|
77
|
-
- name: Sync stores/<alias>/ to root
|
|
78
|
-
if: steps.gate.outputs.skip != 'true'
|
|
85
|
+
- name: Sync stores/<alias>/ to root (main merged in)
|
|
86
|
+
if: steps.gate.outputs.skip != 'true' && steps.gate.outputs.from_main == 'true'
|
|
79
87
|
run: |
|
|
80
88
|
ALIAS="${{ steps.alias.outputs.alias }}"
|
|
81
89
|
STORE_DIR="stores/${ALIAS}"
|
|
82
|
-
|
|
83
90
|
if [ ! -d "$STORE_DIR" ]; then
|
|
84
91
|
echo "Store directory $STORE_DIR does not exist, skipping."
|
|
85
92
|
exit 0
|
|
86
93
|
fi
|
|
87
|
-
|
|
88
|
-
# Sync directories: config, templates, sections
|
|
89
94
|
for DIR in config templates sections; do
|
|
90
95
|
SRC="${STORE_DIR}/${DIR}"
|
|
91
96
|
if [ -d "$SRC" ]; then
|
|
92
|
-
# Copy JSON files only, preserving directory structure
|
|
93
97
|
find "$SRC" -name "*.json" | while read -r FILE; do
|
|
94
98
|
REL_PATH="${FILE#$STORE_DIR/}"
|
|
95
|
-
|
|
96
|
-
# Skip excluded files
|
|
97
99
|
if [ "$REL_PATH" = "config/settings_schema.json" ]; then
|
|
98
100
|
continue
|
|
99
101
|
fi
|
|
100
|
-
|
|
101
102
|
mkdir -p "$(dirname "$REL_PATH")"
|
|
102
103
|
cp "$FILE" "$REL_PATH"
|
|
103
104
|
echo " Copied: $REL_PATH"
|
|
@@ -105,12 +106,11 @@ jobs:
|
|
|
105
106
|
fi
|
|
106
107
|
done
|
|
107
108
|
|
|
108
|
-
- name: Commit
|
|
109
|
-
if: steps.gate.outputs.skip != 'true'
|
|
109
|
+
- name: Commit stores→root
|
|
110
|
+
if: steps.gate.outputs.skip != 'true' && steps.gate.outputs.from_main == 'true'
|
|
110
111
|
run: |
|
|
111
112
|
git config user.name "github-actions[bot]"
|
|
112
113
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
113
|
-
|
|
114
114
|
git add -A
|
|
115
115
|
if ! git diff --cached --quiet; then
|
|
116
116
|
git commit -m "chore: sync stores/${{ steps.alias.outputs.alias }}/ to root [stores-to-root]"
|
|
@@ -118,3 +118,37 @@ jobs:
|
|
|
118
118
|
else
|
|
119
119
|
echo "No changes to commit."
|
|
120
120
|
fi
|
|
121
|
+
|
|
122
|
+
# --- When push is NOT from main: root → stores/<alias>/ (persist store JSONs) ---
|
|
123
|
+
- name: Sync root to stores/<alias>/ (external push, e.g. Shopify)
|
|
124
|
+
if: steps.gate.outputs.skip != 'true' && steps.gate.outputs.from_main == 'false'
|
|
125
|
+
run: |
|
|
126
|
+
ALIAS="${{ steps.alias.outputs.alias }}"
|
|
127
|
+
STORE_DIR="stores/${ALIAS}"
|
|
128
|
+
mkdir -p "${STORE_DIR}/config" "${STORE_DIR}/templates" "${STORE_DIR}/sections"
|
|
129
|
+
for DIR in config templates sections; do
|
|
130
|
+
if [ -d "$DIR" ]; then
|
|
131
|
+
find "$DIR" -name "*.json" | while read -r FILE; do
|
|
132
|
+
if [ "$FILE" = "config/settings_schema.json" ]; then
|
|
133
|
+
continue
|
|
134
|
+
fi
|
|
135
|
+
DEST="${STORE_DIR}/${FILE}"
|
|
136
|
+
mkdir -p "$(dirname "$DEST")"
|
|
137
|
+
cp "$FILE" "$DEST"
|
|
138
|
+
echo " Synced: $FILE → $DEST"
|
|
139
|
+
done
|
|
140
|
+
fi
|
|
141
|
+
done
|
|
142
|
+
|
|
143
|
+
- name: Commit root→stores
|
|
144
|
+
if: steps.gate.outputs.skip != 'true' && steps.gate.outputs.from_main == 'false'
|
|
145
|
+
run: |
|
|
146
|
+
git config user.name "github-actions[bot]"
|
|
147
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
148
|
+
git add -A
|
|
149
|
+
if ! git diff --cached --quiet; then
|
|
150
|
+
git commit -m "chore: sync root to stores/${{ steps.alias.outputs.alias }}/ [root-to-stores]"
|
|
151
|
+
git push
|
|
152
|
+
else
|
|
153
|
+
echo "No changes to commit."
|
|
154
|
+
fi
|
|
@@ -60,8 +60,25 @@ jobs:
|
|
|
60
60
|
fi
|
|
61
61
|
NEW_VERSION="v${NEW_VERSION}"
|
|
62
62
|
else
|
|
63
|
-
# Get latest tag
|
|
64
|
-
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null ||
|
|
63
|
+
# Get latest tag (or theme_version from settings_schema.json when no tags)
|
|
64
|
+
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || true)
|
|
65
|
+
if [ -z "$LATEST_TAG" ]; then
|
|
66
|
+
SCHEMA="config/settings_schema.json"
|
|
67
|
+
if [ -f "$SCHEMA" ]; then
|
|
68
|
+
LATEST_TAG=$(node -e "
|
|
69
|
+
const fs = require('fs');
|
|
70
|
+
const arr = JSON.parse(fs.readFileSync('$SCHEMA', 'utf-8'));
|
|
71
|
+
const info = arr.find(x => x.name === 'theme_info');
|
|
72
|
+
const v = (info && info.theme_version) ? String(info.theme_version).trim() : '';
|
|
73
|
+
if (!v) process.exit(1);
|
|
74
|
+
let parts = v.replace(/^v/i, '').split('.');
|
|
75
|
+
if (parts.length === 2) parts.push('0');
|
|
76
|
+
if (parts.length < 3) parts = parts.concat(Array(3 - parts.length).fill('0')).slice(0, 3);
|
|
77
|
+
console.log('v' + parts.slice(0, 3).join('.'));
|
|
78
|
+
" 2>/dev/null || true)
|
|
79
|
+
fi
|
|
80
|
+
[ -z "$LATEST_TAG" ] && LATEST_TAG="v0.0.0"
|
|
81
|
+
fi
|
|
65
82
|
LATEST_TAG="${LATEST_TAG#v}"
|
|
66
83
|
|
|
67
84
|
IFS='.' read -r MAJOR MINOR PATCH <<< "$LATEST_TAG"
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
# climaybe — Nightly Hotfix Tagger (Single-store)
|
|
1
|
+
# climaybe — Nightly Hotfix Tagger (Single-store & Multi-store)
|
|
2
2
|
# Runs at 02:00 US Eastern (07:00 UTC) every night.
|
|
3
|
-
#
|
|
4
|
-
# generates
|
|
3
|
+
# Collects commits since latest tag (excluding release bumps and store-sync noise),
|
|
4
|
+
# generates AI changelog, and creates a patch version tag.
|
|
5
|
+
# Hotfix / non-staging merges are tagged here only (not at commit time).
|
|
5
6
|
|
|
6
7
|
name: Nightly Hotfix Tag
|
|
7
8
|
|
|
@@ -20,6 +21,8 @@ concurrency:
|
|
|
20
21
|
jobs:
|
|
21
22
|
check:
|
|
22
23
|
runs-on: ubuntu-latest
|
|
24
|
+
permissions:
|
|
25
|
+
contents: write
|
|
23
26
|
outputs:
|
|
24
27
|
has_changes: ${{ steps.check.outputs.has_changes }}
|
|
25
28
|
last_tag: ${{ steps.check.outputs.last_tag }}
|
|
@@ -27,34 +30,60 @@ jobs:
|
|
|
27
30
|
- uses: actions/checkout@v4
|
|
28
31
|
with:
|
|
29
32
|
fetch-depth: 0
|
|
33
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
30
34
|
|
|
31
|
-
- name:
|
|
35
|
+
- name: Configure git
|
|
36
|
+
run: |
|
|
37
|
+
git config user.name "github-actions[bot]"
|
|
38
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
39
|
+
|
|
40
|
+
- name: Check for untagged commits (or create initial tag from settings_schema.json)
|
|
32
41
|
id: check
|
|
33
42
|
env:
|
|
34
43
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
35
44
|
run: |
|
|
36
|
-
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null ||
|
|
45
|
+
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || true)
|
|
46
|
+
if [ -z "$LATEST_TAG" ]; then
|
|
47
|
+
SCHEMA="config/settings_schema.json"
|
|
48
|
+
if [ -f "$SCHEMA" ]; then
|
|
49
|
+
LATEST_TAG=$(node -e "
|
|
50
|
+
const fs = require('fs');
|
|
51
|
+
const arr = JSON.parse(fs.readFileSync('$SCHEMA', 'utf-8'));
|
|
52
|
+
const info = arr.find(x => x.name === 'theme_info');
|
|
53
|
+
const v = (info && info.theme_version) ? String(info.theme_version).trim() : '';
|
|
54
|
+
if (!v) process.exit(1);
|
|
55
|
+
let parts = v.replace(/^v/i, '').split('.');
|
|
56
|
+
if (parts.length === 2) parts.push('0');
|
|
57
|
+
if (parts.length < 3) parts = parts.concat(Array(3 - parts.length).fill('0')).slice(0, 3);
|
|
58
|
+
console.log('v' + parts.slice(0, 3).join('.'));
|
|
59
|
+
" 2>/dev/null || true)
|
|
60
|
+
fi
|
|
61
|
+
if [ -n "$LATEST_TAG" ]; then
|
|
62
|
+
if ! git rev-parse -q --verify "refs/tags/$LATEST_TAG" >/dev/null; then
|
|
63
|
+
git tag -a "$LATEST_TAG" -m "Initial version from settings_schema.json"
|
|
64
|
+
git push origin "$LATEST_TAG"
|
|
65
|
+
echo "Created initial tag $LATEST_TAG from settings_schema.json"
|
|
66
|
+
fi
|
|
67
|
+
fi
|
|
68
|
+
fi
|
|
37
69
|
|
|
38
70
|
if [ -z "$LATEST_TAG" ]; then
|
|
39
|
-
echo "No tags
|
|
71
|
+
echo "No tags and no settings_schema.json theme_version, skipping."
|
|
40
72
|
echo "has_changes=false" >> $GITHUB_OUTPUT
|
|
41
73
|
exit 0
|
|
42
74
|
fi
|
|
43
75
|
|
|
44
76
|
echo "last_tag=$LATEST_TAG" >> $GITHUB_OUTPUT
|
|
45
77
|
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
# that are associated with PRs whose source branch is staging-*.
|
|
78
|
+
# Commits since last tag; exclude only version-bump and store-sync noise.
|
|
79
|
+
# Include [hotfix-backport] and commits with no PR (e.g. direct merges).
|
|
49
80
|
CANDIDATES=$(git log ${LATEST_TAG}..HEAD --pretty=format:"%H%x09%s" | \
|
|
50
81
|
grep -v "chore(release): bump version" | \
|
|
51
|
-
grep -v "\[hotfix-backport\]" | \
|
|
52
82
|
grep -v "\[skip-store-sync\]" | \
|
|
53
83
|
grep -v "\[stores-to-root\]" | \
|
|
54
84
|
grep -v "\[root-to-stores\]" || true)
|
|
55
85
|
|
|
56
86
|
COMMITS=""
|
|
57
|
-
SKIPPED_UNKNOWN=0
|
|
58
87
|
while IFS=$'\t' read -r SHA SUBJECT; do
|
|
59
88
|
[ -z "$SHA" ] && continue
|
|
60
89
|
|
|
@@ -62,13 +91,8 @@ jobs:
|
|
|
62
91
|
repos/${{ github.repository }}/commits/$SHA/pulls \
|
|
63
92
|
--jq '.[0].head.ref // ""' 2>/dev/null || true)
|
|
64
93
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
echo "Skipping commit due to missing PR metadata: $SHA"
|
|
68
|
-
continue
|
|
69
|
-
fi
|
|
70
|
-
|
|
71
|
-
if [[ "$HEAD_REF" == staging-* ]]; then
|
|
94
|
+
# Skip only store-sync PRs (staging-* → main). Include staging and everything else.
|
|
95
|
+
if [[ -n "$HEAD_REF" && "$HEAD_REF" == staging-* ]]; then
|
|
72
96
|
echo "Skipping store-backport commit from $HEAD_REF: $SHA"
|
|
73
97
|
continue
|
|
74
98
|
fi
|
|
@@ -76,16 +100,12 @@ jobs:
|
|
|
76
100
|
COMMITS="${COMMITS}${SUBJECT}"$'\n'
|
|
77
101
|
done <<< "$CANDIDATES"
|
|
78
102
|
|
|
79
|
-
if [ "$SKIPPED_UNKNOWN" -gt 0 ]; then
|
|
80
|
-
echo "Skipped $SKIPPED_UNKNOWN commit(s) due to missing PR metadata (fail-safe)."
|
|
81
|
-
fi
|
|
82
|
-
|
|
83
103
|
if [ -n "$COMMITS" ]; then
|
|
84
104
|
COMMIT_COUNT=$(echo "$COMMITS" | wc -l | tr -d ' ')
|
|
85
105
|
echo "Found $COMMIT_COUNT untagged commit(s) since $LATEST_TAG"
|
|
86
106
|
echo "has_changes=true" >> $GITHUB_OUTPUT
|
|
87
107
|
else
|
|
88
|
-
echo "No untagged
|
|
108
|
+
echo "No untagged commits since $LATEST_TAG"
|
|
89
109
|
echo "has_changes=false" >> $GITHUB_OUTPUT
|
|
90
110
|
fi
|
|
91
111
|
|