climaybe 1.5.2 → 1.6.1
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 +45 -18
- package/bin/version.txt +1 -1
- package/package.json +16 -2
- 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 +66 -79
- 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 +39 -9
- 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,7 +1,10 @@
|
|
|
1
1
|
# climaybe — Main to Staging <store> (Multi-store)
|
|
2
|
-
# When
|
|
3
|
-
#
|
|
4
|
-
#
|
|
2
|
+
# When main is pushed (e.g. after staging→main merge), syncs main to each staging-<alias>.
|
|
3
|
+
# Root JSON files (config/settings_data.json, templates/*.json, sections/*.json) are ignored:
|
|
4
|
+
# merge brings main in but we immediately restore root from stores/<alias>/ so store-specific data is kept.
|
|
5
|
+
# Runs on every push to main except pure store-sync commits ([stores-to-root], [root-to-stores]), so that:
|
|
6
|
+
# - Version-bump commits: staging-<alias> get the new version.
|
|
7
|
+
# - Hotfix backports (live or staging-<alias> → main): other staging-<alias> get those changes; the store that sent the hotfix is skipped (no need to merge back to the same branch).
|
|
5
8
|
|
|
6
9
|
name: Main to Staging Stores
|
|
7
10
|
|
|
@@ -15,30 +18,38 @@ concurrency:
|
|
|
15
18
|
cancel-in-progress: false
|
|
16
19
|
|
|
17
20
|
jobs:
|
|
18
|
-
# Gate: skip hotfix
|
|
21
|
+
# Gate: skip only pure store-sync commits. When hotfix-backport, extract source branch alias so we skip merging back into that same store.
|
|
19
22
|
gate:
|
|
20
23
|
runs-on: ubuntu-latest
|
|
21
24
|
outputs:
|
|
22
25
|
should_run: ${{ steps.check.outputs.should_run }}
|
|
26
|
+
hotfix_source_alias: ${{ steps.check.outputs.hotfix_source_alias }}
|
|
23
27
|
steps:
|
|
24
|
-
- name: Check commit message
|
|
28
|
+
- name: Check commit message and hotfix source
|
|
25
29
|
id: check
|
|
26
30
|
run: |
|
|
27
31
|
COMMIT_MSG="${{ github.event.head_commit.message }}"
|
|
28
32
|
|
|
29
|
-
if echo "$COMMIT_MSG" | grep -
|
|
30
|
-
echo "Skipping —
|
|
31
|
-
echo "should_run=false" >> $GITHUB_OUTPUT
|
|
32
|
-
elif echo "$COMMIT_MSG" | grep -q "chore(release): bump version"; then
|
|
33
|
-
echo "Skipping — version bump commit"
|
|
33
|
+
if echo "$COMMIT_MSG" | grep -qE "\[skip-store-sync\]|\[stores-to-root\]|\[root-to-stores\]"; then
|
|
34
|
+
echo "Skipping — store sync commit (avoid loop)"
|
|
34
35
|
echo "should_run=false" >> $GITHUB_OUTPUT
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
echo "should_run=false" >> $GITHUB_OUTPUT
|
|
38
|
-
else
|
|
39
|
-
echo "should_run=true" >> $GITHUB_OUTPUT
|
|
36
|
+
echo "hotfix_source_alias=" >> $GITHUB_OUTPUT
|
|
37
|
+
exit 0
|
|
40
38
|
fi
|
|
41
39
|
|
|
40
|
+
echo "should_run=true" >> $GITHUB_OUTPUT
|
|
41
|
+
|
|
42
|
+
# If hotfix-backport, extract source branch alias (e.g. staging-norway or live-norway → norway) so we skip that store in sync
|
|
43
|
+
HOTFIX_SOURCE_ALIAS=""
|
|
44
|
+
if echo "$COMMIT_MSG" | grep -q "\[hotfix-backport\]"; then
|
|
45
|
+
SOURCE_BRANCH=$(echo "$COMMIT_MSG" | sed -n 's/.*Merge \([^ ]*\) into main.*/\1/p' | head -1)
|
|
46
|
+
if [ -n "$SOURCE_BRANCH" ]; then
|
|
47
|
+
HOTFIX_SOURCE_ALIAS="${SOURCE_BRANCH#staging-}"
|
|
48
|
+
HOTFIX_SOURCE_ALIAS="${HOTFIX_SOURCE_ALIAS#live-}"
|
|
49
|
+
fi
|
|
50
|
+
fi
|
|
51
|
+
echo "hotfix_source_alias=$HOTFIX_SOURCE_ALIAS" >> $GITHUB_OUTPUT
|
|
52
|
+
|
|
42
53
|
# Read store list from package.json
|
|
43
54
|
config:
|
|
44
55
|
needs: gate
|
|
@@ -60,7 +71,7 @@ jobs:
|
|
|
60
71
|
echo "stores=$STORES" >> $GITHUB_OUTPUT
|
|
61
72
|
echo "Store aliases: $STORES"
|
|
62
73
|
|
|
63
|
-
#
|
|
74
|
+
# Merge main into each staging-<alias>; root JSONs ignored (restored from stores/<alias>/). Skip the store that sent the hotfix (no need to merge back).
|
|
64
75
|
sync:
|
|
65
76
|
needs: [gate, config]
|
|
66
77
|
if: needs.gate.outputs.should_run == 'true'
|
|
@@ -75,85 +86,61 @@ jobs:
|
|
|
75
86
|
actions: write
|
|
76
87
|
env:
|
|
77
88
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
89
|
+
HOTFIX_SOURCE_ALIAS: ${{ needs.gate.outputs.hotfix_source_alias }}
|
|
78
90
|
steps:
|
|
79
91
|
- uses: actions/checkout@v4
|
|
80
92
|
with:
|
|
81
93
|
fetch-depth: 0
|
|
82
94
|
|
|
83
|
-
- name: Sync main to staging-${{ matrix.store }}
|
|
95
|
+
- name: Sync main to staging-${{ matrix.store }} (root JSONs ignored)
|
|
84
96
|
run: |
|
|
85
97
|
BRANCH="staging-${{ matrix.store }}"
|
|
98
|
+
ALIAS="${{ matrix.store }}"
|
|
99
|
+
STORE_DIR="stores/${ALIAS}"
|
|
100
|
+
|
|
101
|
+
if [ -n "$HOTFIX_SOURCE_ALIAS" ] && [ "$HOTFIX_SOURCE_ALIAS" = "$ALIAS" ]; then
|
|
102
|
+
echo "Hotfix came from staging-$ALIAS or live-$ALIAS, skipping (no need to merge back to same store)."
|
|
103
|
+
exit 0
|
|
104
|
+
fi
|
|
86
105
|
|
|
87
|
-
# Check if branch exists
|
|
88
106
|
if ! git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then
|
|
89
107
|
echo "Branch $BRANCH does not exist on remote, skipping."
|
|
90
108
|
exit 0
|
|
91
109
|
fi
|
|
92
110
|
|
|
93
|
-
|
|
94
|
-
|
|
111
|
+
git config user.name "github-actions[bot]"
|
|
112
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
95
113
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
114
|
+
git fetch origin "$BRANCH"
|
|
115
|
+
git checkout "$BRANCH"
|
|
116
|
+
git merge origin/main --no-ff -m "Sync main → $BRANCH" || true
|
|
117
|
+
if [ $? -ne 0 ]; then
|
|
118
|
+
echo "Merge had conflicts; aborting. Manual resolution may be needed."
|
|
119
|
+
git merge --abort 2>/dev/null || true
|
|
120
|
+
exit 1
|
|
100
121
|
fi
|
|
101
122
|
|
|
102
|
-
#
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if [ -n "$PR_URL" ]; then
|
|
112
|
-
echo "Created PR: $PR_URL"
|
|
113
|
-
|
|
114
|
-
# Auto-merge
|
|
115
|
-
PR_NUM=$(echo "$PR_URL" | grep -oP '\d+$')
|
|
116
|
-
if gh pr merge "$PR_NUM" --merge --admin 2>/dev/null; then
|
|
117
|
-
echo "Auto-merge succeeded for $BRANCH"
|
|
118
|
-
|
|
119
|
-
# Explicitly trigger Stores to Root to avoid missing downstream runs
|
|
120
|
-
# when merges are performed by GITHUB_TOKEN-based automation.
|
|
121
|
-
gh workflow run "Stores to Root" --ref "$BRANCH" 2>/dev/null || \
|
|
122
|
-
echo "Failed to trigger Stores to Root for $BRANCH"
|
|
123
|
-
|
|
124
|
-
# Wait for the latest Stores to Root run on this branch to complete,
|
|
125
|
-
# then trigger PR to Live to keep ordering deterministic.
|
|
126
|
-
for i in $(seq 1 30); do
|
|
127
|
-
RUN_STATUS=$(gh run list \
|
|
128
|
-
--workflow "Stores to Root" \
|
|
129
|
-
--branch "$BRANCH" \
|
|
130
|
-
--limit 1 \
|
|
131
|
-
--json status,conclusion \
|
|
132
|
-
--jq '.[0].status + "|" + (.[0].conclusion // "")' 2>/dev/null || echo "")
|
|
133
|
-
|
|
134
|
-
if [ -z "$RUN_STATUS" ]; then
|
|
135
|
-
sleep 5
|
|
136
|
-
continue
|
|
137
|
-
fi
|
|
138
|
-
|
|
139
|
-
STATUS="${RUN_STATUS%%|*}"
|
|
140
|
-
CONCLUSION="${RUN_STATUS##*|}"
|
|
141
|
-
|
|
142
|
-
if [ "$STATUS" = "completed" ]; then
|
|
143
|
-
if [ "$CONCLUSION" = "success" ]; then
|
|
144
|
-
gh workflow run "PR to Live" --ref "$BRANCH" 2>/dev/null || \
|
|
145
|
-
echo "Failed to trigger PR to Live for $BRANCH"
|
|
146
|
-
else
|
|
147
|
-
echo "Stores to Root did not succeed for $BRANCH (conclusion: $CONCLUSION)"
|
|
123
|
+
# Ignore root JSONs: restore from stores/<alias>/ so main's versions are not kept
|
|
124
|
+
if [ -d "$STORE_DIR" ]; then
|
|
125
|
+
for DIR in config templates sections; do
|
|
126
|
+
SRC="${STORE_DIR}/${DIR}"
|
|
127
|
+
if [ -d "$SRC" ]; then
|
|
128
|
+
find "$SRC" -name "*.json" | while read -r FILE; do
|
|
129
|
+
REL_PATH="${FILE#$STORE_DIR/}"
|
|
130
|
+
if [ "$REL_PATH" = "config/settings_schema.json" ]; then
|
|
131
|
+
continue
|
|
148
132
|
fi
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
133
|
+
mkdir -p "$(dirname "$REL_PATH")"
|
|
134
|
+
cp "$FILE" "$REL_PATH"
|
|
135
|
+
done
|
|
136
|
+
fi
|
|
137
|
+
done
|
|
138
|
+
git add config templates sections 2>/dev/null || true
|
|
139
|
+
if ! git diff --cached --quiet; then
|
|
140
|
+
git commit -m "chore: keep store root JSONs from stores/$ALIAS/ [stores-to-root]"
|
|
156
141
|
fi
|
|
157
|
-
else
|
|
158
|
-
echo "No changes to sync for $BRANCH"
|
|
159
142
|
fi
|
|
143
|
+
|
|
144
|
+
git push origin "$BRANCH" && echo "Pushed $BRANCH" || { echo "Push failed."; exit 1; }
|
|
145
|
+
|
|
146
|
+
gh workflow run "PR to Live" --ref "$BRANCH" 2>/dev/null || echo "Failed to trigger PR to Live for $BRANCH"
|
|
@@ -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
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# climaybe — Version Bump (Reusable Workflow)
|
|
2
|
-
# Bumps version in settings_schema.json, creates a git tag, and pushes.
|
|
2
|
+
# Bumps version in settings_schema.json and package.json (if present), creates a git tag, and pushes.
|
|
3
|
+
# Only runs on main (called from post-merge-tag and nightly-hotfix). staging / staging-* / live-* do not run version bump.
|
|
3
4
|
|
|
4
5
|
name: Version Bump
|
|
5
6
|
|
|
@@ -60,8 +61,25 @@ jobs:
|
|
|
60
61
|
fi
|
|
61
62
|
NEW_VERSION="v${NEW_VERSION}"
|
|
62
63
|
else
|
|
63
|
-
# Get latest tag
|
|
64
|
-
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null ||
|
|
64
|
+
# Get latest tag (or theme_version from settings_schema.json when no tags)
|
|
65
|
+
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || true)
|
|
66
|
+
if [ -z "$LATEST_TAG" ]; then
|
|
67
|
+
SCHEMA="config/settings_schema.json"
|
|
68
|
+
if [ -f "$SCHEMA" ]; then
|
|
69
|
+
LATEST_TAG=$(node -e "
|
|
70
|
+
const fs = require('fs');
|
|
71
|
+
const arr = JSON.parse(fs.readFileSync('$SCHEMA', 'utf-8'));
|
|
72
|
+
const info = arr.find(x => x.name === 'theme_info');
|
|
73
|
+
const v = (info && info.theme_version) ? String(info.theme_version).trim() : '';
|
|
74
|
+
if (!v) process.exit(1);
|
|
75
|
+
let parts = v.replace(/^v/i, '').split('.');
|
|
76
|
+
if (parts.length === 2) parts.push('0');
|
|
77
|
+
if (parts.length < 3) parts = parts.concat(Array(3 - parts.length).fill('0')).slice(0, 3);
|
|
78
|
+
console.log('v' + parts.slice(0, 3).join('.'));
|
|
79
|
+
" 2>/dev/null || true)
|
|
80
|
+
fi
|
|
81
|
+
[ -z "$LATEST_TAG" ] && LATEST_TAG="v0.0.0"
|
|
82
|
+
fi
|
|
65
83
|
LATEST_TAG="${LATEST_TAG#v}"
|
|
66
84
|
|
|
67
85
|
IFS='.' read -r MAJOR MINOR PATCH <<< "$LATEST_TAG"
|
|
@@ -82,20 +100,20 @@ jobs:
|
|
|
82
100
|
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
|
83
101
|
echo "New version: $NEW_VERSION"
|
|
84
102
|
|
|
85
|
-
- name: Update settings_schema.json
|
|
103
|
+
- name: Update settings_schema.json and package.json
|
|
104
|
+
env:
|
|
105
|
+
NEW_VERSION: ${{ steps.bump.outputs.new_version }}
|
|
86
106
|
run: |
|
|
87
|
-
|
|
107
|
+
export VERSION_PLAIN="${NEW_VERSION#v}"
|
|
88
108
|
SCHEMA_FILE="config/settings_schema.json"
|
|
89
109
|
|
|
90
110
|
if [ -f "$SCHEMA_FILE" ]; then
|
|
91
|
-
# Update the theme_info version in settings_schema.json
|
|
92
111
|
node -e "
|
|
93
112
|
const fs = require('fs');
|
|
113
|
+
const v = process.env.VERSION_PLAIN;
|
|
94
114
|
const schema = JSON.parse(fs.readFileSync('$SCHEMA_FILE', 'utf-8'));
|
|
95
115
|
const themeInfo = schema.find(s => s.name === 'theme_info');
|
|
96
|
-
if (themeInfo)
|
|
97
|
-
themeInfo.theme_version = '${NEW_VERSION#v}';
|
|
98
|
-
}
|
|
116
|
+
if (themeInfo) themeInfo.theme_version = v;
|
|
99
117
|
fs.writeFileSync('$SCHEMA_FILE', JSON.stringify(schema, null, 2) + '\n');
|
|
100
118
|
"
|
|
101
119
|
echo "Updated $SCHEMA_FILE to ${NEW_VERSION}"
|
|
@@ -103,6 +121,18 @@ jobs:
|
|
|
103
121
|
echo "No $SCHEMA_FILE found, skipping schema update."
|
|
104
122
|
fi
|
|
105
123
|
|
|
124
|
+
if [ -f "package.json" ]; then
|
|
125
|
+
node -e "
|
|
126
|
+
const fs = require('fs');
|
|
127
|
+
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
|
|
128
|
+
pkg.version = process.env.VERSION_PLAIN;
|
|
129
|
+
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
|
|
130
|
+
"
|
|
131
|
+
echo "Updated package.json version to ${VERSION_PLAIN}"
|
|
132
|
+
else
|
|
133
|
+
echo "No package.json found, skipping."
|
|
134
|
+
fi
|
|
135
|
+
|
|
106
136
|
- name: Commit and tag
|
|
107
137
|
run: |
|
|
108
138
|
NEW_VERSION="${{ steps.bump.outputs.new_version }}"
|