@neferbyte/cherry-release-cli 1.0.1 → 1.0.3
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/bin/cherry-release.sh +64 -49
- package/bin/release.sh +113 -0
- package/package.json +3 -2
package/bin/cherry-release.sh
CHANGED
|
@@ -7,6 +7,7 @@ BRANCHES=(staging production)
|
|
|
7
7
|
ORIGINAL_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
8
8
|
WORKTREE_DIR="/tmp/cherry-release-test"
|
|
9
9
|
WORKTREES_TO_CLEAN=()
|
|
10
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
10
11
|
|
|
11
12
|
# ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
12
13
|
|
|
@@ -42,56 +43,33 @@ trap cleanup EXIT
|
|
|
42
43
|
|
|
43
44
|
step "Running pre-flight checks"
|
|
44
45
|
|
|
45
|
-
# 1.
|
|
46
|
-
if [[ $# -eq 0 ]]; then
|
|
47
|
-
fail "No commit hashes provided.\nUsage: cherry-release <hash1> [hash2] ..."
|
|
48
|
-
fi
|
|
49
|
-
check_ok "Received $# commit hash(es)"
|
|
50
|
-
|
|
51
|
-
# 2. Working tree is clean
|
|
46
|
+
# 1. Working tree is clean
|
|
52
47
|
if [[ -n "$(git status --porcelain)" ]]; then
|
|
53
48
|
fail "Working tree is dirty. Commit or stash your changes first."
|
|
54
49
|
fi
|
|
55
50
|
check_ok "Working tree is clean"
|
|
56
51
|
|
|
57
|
-
#
|
|
58
|
-
for tool in jq sed commit-and-tag-version
|
|
52
|
+
# 2. Required tools available
|
|
53
|
+
for tool in jq sed commit-and-tag-version; do
|
|
59
54
|
if ! command -v "$tool" &>/dev/null; then
|
|
60
55
|
fail "Required tool '$tool' is not installed or not in PATH."
|
|
61
56
|
fi
|
|
62
57
|
done
|
|
63
|
-
check_ok "Required tools available (jq, sed, commit-and-tag-version
|
|
58
|
+
check_ok "Required tools available (jq, sed, commit-and-tag-version)"
|
|
64
59
|
|
|
65
|
-
#
|
|
60
|
+
# 3. Remote write access
|
|
66
61
|
if ! git push --dry-run origin HEAD &>/dev/null; then
|
|
67
62
|
fail "No write access to the remote repository."
|
|
68
63
|
fi
|
|
69
64
|
check_ok "Remote write access confirmed"
|
|
70
65
|
|
|
71
|
-
#
|
|
66
|
+
# 4. Fetch latest remote refs
|
|
72
67
|
if ! git fetch origin development staging production 2>/dev/null; then
|
|
73
68
|
fail "Failed to fetch remote branches (development, staging, production)."
|
|
74
69
|
fi
|
|
75
70
|
check_ok "Fetched latest remote refs"
|
|
76
71
|
|
|
77
|
-
#
|
|
78
|
-
for hash in "$@"; do
|
|
79
|
-
obj_type=$(git cat-file -t "$hash" 2>/dev/null || true)
|
|
80
|
-
if [[ "$obj_type" != "commit" ]]; then
|
|
81
|
-
fail "Invalid commit hash: $hash (type: ${obj_type:-not found})"
|
|
82
|
-
fi
|
|
83
|
-
done
|
|
84
|
-
check_ok "All commit hashes are valid commit objects"
|
|
85
|
-
|
|
86
|
-
# 7. All commits are reachable from origin/development
|
|
87
|
-
for hash in "$@"; do
|
|
88
|
-
if ! git merge-base --is-ancestor "$hash" origin/development 2>/dev/null; then
|
|
89
|
-
fail "Commit $hash is not reachable from origin/development.\nOnly deploy commits that are already merged to development."
|
|
90
|
-
fi
|
|
91
|
-
done
|
|
92
|
-
check_ok "All commits are reachable from origin/development"
|
|
93
|
-
|
|
94
|
-
# 8. development is not diverged from origin (can fast-forward)
|
|
72
|
+
# 5. development is not diverged from origin (can fast-forward)
|
|
95
73
|
local_dev=$(git rev-parse development 2>/dev/null)
|
|
96
74
|
remote_dev=$(git rev-parse origin/development 2>/dev/null)
|
|
97
75
|
merge_base_dev=$(git merge-base development origin/development 2>/dev/null)
|
|
@@ -100,13 +78,13 @@ if [[ "$local_dev" != "$remote_dev" && "$merge_base_dev" != "$local_dev" ]]; the
|
|
|
100
78
|
fi
|
|
101
79
|
check_ok "development can fast-forward to origin"
|
|
102
80
|
|
|
103
|
-
#
|
|
81
|
+
# 6. staging and production have no local-only commits ahead of origin
|
|
104
82
|
for branch in "${BRANCHES[@]}"; do
|
|
105
83
|
local_ref=$(git rev-parse "$branch" 2>/dev/null || true)
|
|
106
84
|
remote_ref=$(git rev-parse "origin/$branch" 2>/dev/null || true)
|
|
107
85
|
|
|
108
86
|
if [[ -z "$local_ref" || -z "$remote_ref" ]]; then
|
|
109
|
-
continue
|
|
87
|
+
continue
|
|
110
88
|
fi
|
|
111
89
|
|
|
112
90
|
ahead=$(git rev-list --count "origin/$branch..$branch" 2>/dev/null || echo "0")
|
|
@@ -116,12 +94,43 @@ for branch in "${BRANCHES[@]}"; do
|
|
|
116
94
|
done
|
|
117
95
|
check_ok "No local-only commits on staging/production"
|
|
118
96
|
|
|
119
|
-
#
|
|
97
|
+
# 7. Auto-discover commits to promote (per target branch)
|
|
98
|
+
declare -A COMMITS_FOR
|
|
99
|
+
for branch in "${BRANCHES[@]}"; do
|
|
100
|
+
hashes=$(git cherry "origin/$branch" origin/development | grep '^+' | awk '{print $2}')
|
|
101
|
+
|
|
102
|
+
# Filter out version bump commits (commit-and-tag-version)
|
|
103
|
+
filtered=()
|
|
104
|
+
for hash in $hashes; do
|
|
105
|
+
msg=$(git log -1 --format="%s" "$hash")
|
|
106
|
+
if [[ ! "$msg" =~ ^chore\(release\): ]]; then
|
|
107
|
+
filtered+=("$hash")
|
|
108
|
+
fi
|
|
109
|
+
done
|
|
110
|
+
|
|
111
|
+
COMMITS_FOR[$branch]="${filtered[*]}"
|
|
112
|
+
done
|
|
113
|
+
|
|
114
|
+
staging_count=$(echo "${COMMITS_FOR[staging]}" | wc -w | tr -d ' ')
|
|
115
|
+
production_count=$(echo "${COMMITS_FOR[production]}" | wc -w | tr -d ' ')
|
|
116
|
+
|
|
117
|
+
if [[ "$staging_count" -eq 0 && "$production_count" -eq 0 ]]; then
|
|
118
|
+
echo -e "\n\033[1;32mNothing to promote.\033[0m staging and production are up to date."
|
|
119
|
+
exit 0
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
check_ok "Discovered commits to promote (staging: $staging_count, production: $production_count)"
|
|
123
|
+
|
|
124
|
+
# 8. Dry-run cherry-picks in temporary worktrees
|
|
120
125
|
for branch in "${BRANCHES[@]}"; do
|
|
126
|
+
commits=(${COMMITS_FOR[$branch]})
|
|
127
|
+
if [[ ${#commits[@]} -eq 0 ]]; then
|
|
128
|
+
continue
|
|
129
|
+
fi
|
|
130
|
+
|
|
121
131
|
wt_path="${WORKTREE_DIR}-${branch}"
|
|
122
132
|
WORKTREES_TO_CLEAN+=("$wt_path")
|
|
123
133
|
|
|
124
|
-
# Clean up any leftover worktree from a previous failed run
|
|
125
134
|
if [[ -d "$wt_path" ]]; then
|
|
126
135
|
git worktree remove --force "$wt_path" 2>/dev/null || true
|
|
127
136
|
fi
|
|
@@ -129,7 +138,7 @@ for branch in "${BRANCHES[@]}"; do
|
|
|
129
138
|
git worktree add "$wt_path" "origin/$branch" --detach --quiet 2>/dev/null \
|
|
130
139
|
|| fail "Failed to create test worktree for $branch."
|
|
131
140
|
|
|
132
|
-
for hash in "
|
|
141
|
+
for hash in "${commits[@]}"; do
|
|
133
142
|
short=$(git rev-parse --short "$hash")
|
|
134
143
|
if ! git -C "$wt_path" cherry-pick "$hash" &>/dev/null; then
|
|
135
144
|
git -C "$wt_path" cherry-pick --abort 2>/dev/null || true
|
|
@@ -139,7 +148,7 @@ for branch in "${BRANCHES[@]}"; do
|
|
|
139
148
|
|
|
140
149
|
git worktree remove --force "$wt_path" 2>/dev/null || true
|
|
141
150
|
done
|
|
142
|
-
check_ok "Dry-run cherry-picks succeeded
|
|
151
|
+
check_ok "Dry-run cherry-picks succeeded"
|
|
143
152
|
|
|
144
153
|
echo -e "\n\033[1;32mAll pre-flight checks passed.\033[0m"
|
|
145
154
|
|
|
@@ -150,33 +159,39 @@ step "Deploying to development"
|
|
|
150
159
|
|
|
151
160
|
git checkout development
|
|
152
161
|
git pull --ff-only
|
|
153
|
-
|
|
162
|
+
"$SCRIPT_DIR/release.sh" patch
|
|
154
163
|
git push --follow-tags origin development
|
|
155
164
|
echo -e " \033[1;32m✓\033[0m development released"
|
|
156
165
|
|
|
157
166
|
# --- staging ---
|
|
158
|
-
|
|
167
|
+
staging_commits=(${COMMITS_FOR[staging]})
|
|
168
|
+
step "Deploying to staging (${#staging_commits[@]} commit(s))"
|
|
159
169
|
|
|
160
170
|
git checkout staging
|
|
161
171
|
git reset --hard origin/staging --quiet
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
172
|
+
if [[ ${#staging_commits[@]} -gt 0 ]]; then
|
|
173
|
+
for hash in "${staging_commits[@]}"; do
|
|
174
|
+
git cherry-pick "$hash"
|
|
175
|
+
done
|
|
176
|
+
git push origin staging
|
|
177
|
+
fi
|
|
178
|
+
"$SCRIPT_DIR/release.sh" patch
|
|
167
179
|
git push --follow-tags origin staging
|
|
168
180
|
echo -e " \033[1;32m✓\033[0m staging released"
|
|
169
181
|
|
|
170
182
|
# --- production ---
|
|
171
|
-
|
|
183
|
+
production_commits=(${COMMITS_FOR[production]})
|
|
184
|
+
step "Deploying to production (${#production_commits[@]} commit(s))"
|
|
172
185
|
|
|
173
186
|
git checkout production
|
|
174
187
|
git reset --hard origin/production --quiet
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
188
|
+
if [[ ${#production_commits[@]} -gt 0 ]]; then
|
|
189
|
+
for hash in "${production_commits[@]}"; do
|
|
190
|
+
git cherry-pick "$hash"
|
|
191
|
+
done
|
|
192
|
+
git push origin production
|
|
193
|
+
fi
|
|
194
|
+
"$SCRIPT_DIR/release.sh" patch
|
|
180
195
|
git push --follow-tags origin production
|
|
181
196
|
echo -e " \033[1;32m✓\033[0m production released"
|
|
182
197
|
|
package/bin/release.sh
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
if [[ -n $1 && ! $1 =~ ^(major|minor|patch)$ ]]; then
|
|
6
|
+
echo "Usage: $0 [major|minor|patch]"
|
|
7
|
+
exit 1
|
|
8
|
+
fi
|
|
9
|
+
|
|
10
|
+
for cmd in jq git sed commit-and-tag-version; do
|
|
11
|
+
command -v $cmd >/dev/null 2>&1 || {
|
|
12
|
+
echo "$cmd is required but not installed. Aborting."
|
|
13
|
+
exit 1
|
|
14
|
+
}
|
|
15
|
+
done
|
|
16
|
+
|
|
17
|
+
removePrerelease() {
|
|
18
|
+
newVersion=$(echo "$currentVersion" | sed 's/-.*//')
|
|
19
|
+
jq ".version = \"$newVersion\"" package.json >package.json.tmp
|
|
20
|
+
mv package.json.tmp package.json
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
currentVersion=$(jq -r '.version' package.json)
|
|
24
|
+
branch=$(git branch --show-current)
|
|
25
|
+
|
|
26
|
+
if [[ $currentVersion =~ ^[0-9]+\.[0-9]+\.[0-9]+-(dev|rc)\.[0-9]+$ ]]; then
|
|
27
|
+
currentRelease="${BASH_REMATCH[1]}"
|
|
28
|
+
mainVersion="${currentVersion%%-*}"
|
|
29
|
+
else
|
|
30
|
+
currentRelease=""
|
|
31
|
+
mainVersion="${currentVersion}"
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
case $1 in
|
|
35
|
+
major)
|
|
36
|
+
releaseType="--release-as major"
|
|
37
|
+
;;
|
|
38
|
+
|
|
39
|
+
minor)
|
|
40
|
+
releaseType="--release-as minor"
|
|
41
|
+
;;
|
|
42
|
+
|
|
43
|
+
patch)
|
|
44
|
+
releaseType="--release-as patch"
|
|
45
|
+
;;
|
|
46
|
+
|
|
47
|
+
*)
|
|
48
|
+
releaseType=""
|
|
49
|
+
;;
|
|
50
|
+
esac
|
|
51
|
+
|
|
52
|
+
case $branch in
|
|
53
|
+
development)
|
|
54
|
+
# No change - it comes from development release
|
|
55
|
+
if [[ $currentRelease = 'dev' ]]; then
|
|
56
|
+
# Release type is explicitly defined; remove prerelease to enforce it
|
|
57
|
+
if [[ -n $releaseType ]]; then
|
|
58
|
+
removePrerelease
|
|
59
|
+
fi
|
|
60
|
+
# Carry out operation; variations depend on $releaseType's value
|
|
61
|
+
commit-and-tag-version --tag-prefix "" --prerelease dev $releaseType
|
|
62
|
+
# It comes from a later stage release; pump up is mandatory; remove prerelease
|
|
63
|
+
else
|
|
64
|
+
removePrerelease
|
|
65
|
+
# No explicitly defined release type; go forward with minor
|
|
66
|
+
if [[ -z $releaseType ]]; then
|
|
67
|
+
commit-and-tag-version --tag-prefix "" --prerelease dev --release-as minor
|
|
68
|
+
# Explicitly defined release type; just go forward with it
|
|
69
|
+
else
|
|
70
|
+
commit-and-tag-version --tag-prefix "" --prerelease dev $releaseType
|
|
71
|
+
fi
|
|
72
|
+
fi
|
|
73
|
+
;;
|
|
74
|
+
|
|
75
|
+
staging)
|
|
76
|
+
# No change - it comes from staging release
|
|
77
|
+
if [[ $currentRelease = 'rc' ]]; then
|
|
78
|
+
if [[ -n $releaseType ]]; then
|
|
79
|
+
removePrerelease
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
commit-and-tag-version --tag-prefix "" --prerelease rc $releaseType
|
|
83
|
+
# It comes from dev; several things could happen
|
|
84
|
+
elif [[ $currentRelease = 'dev' ]]; then
|
|
85
|
+
# No explicitly defined release type; adopts main version by just replacing dev with rc
|
|
86
|
+
if [[ -z $releaseType ]]; then
|
|
87
|
+
commit-and-tag-version --tag-prefix "" --prerelease rc --release-as $mainVersion
|
|
88
|
+
# Explicitly defined release type; just go forward with it
|
|
89
|
+
else
|
|
90
|
+
commit-and-tag-version --tag-prefix "" --prerelease rc $releaseType
|
|
91
|
+
fi
|
|
92
|
+
# It comes from production; pump up is mandatory; remove prerelease
|
|
93
|
+
else
|
|
94
|
+
removePrerelease
|
|
95
|
+
# No explicitly defined release type; continue with minor
|
|
96
|
+
if [[ -z $releaseType ]]; then
|
|
97
|
+
commit-and-tag-version --tag-prefix "" --prerelease rc --release-as minor
|
|
98
|
+
# Explicitly defined release type; just go forward with it
|
|
99
|
+
else
|
|
100
|
+
commit-and-tag-version --tag-prefix "" --prerelease rc $releaseType
|
|
101
|
+
fi
|
|
102
|
+
fi
|
|
103
|
+
;;
|
|
104
|
+
|
|
105
|
+
production)
|
|
106
|
+
commit-and-tag-version --tag-prefix "" $releaseType
|
|
107
|
+
;;
|
|
108
|
+
|
|
109
|
+
*)
|
|
110
|
+
echo "Release is not allowed in the current branch"
|
|
111
|
+
exit 1
|
|
112
|
+
;;
|
|
113
|
+
esac
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neferbyte/cherry-release-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Cherry-pick commits across environment branches and tag releases",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
7
|
-
"cherry-release": "./bin/cherry-release.sh"
|
|
7
|
+
"cherry-release": "./bin/cherry-release.sh",
|
|
8
|
+
"cherry-release-version": "./bin/release.sh"
|
|
8
9
|
},
|
|
9
10
|
"files": [
|
|
10
11
|
"bin/"
|