@neferbyte/cherry-release-cli 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/bin/cherry-release.sh +191 -0
- package/package.json +12 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
# ─── Constants ────────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
BRANCHES=(staging production)
|
|
7
|
+
ORIGINAL_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
8
|
+
WORKTREE_DIR="/tmp/cherry-release-test"
|
|
9
|
+
WORKTREES_TO_CLEAN=()
|
|
10
|
+
|
|
11
|
+
# ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
fail() {
|
|
14
|
+
echo -e "\n\033[1;31mError:\033[0m $1" >&2
|
|
15
|
+
exit 1
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
step() {
|
|
19
|
+
echo -e "\n\033[1;34m→\033[0m $1"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
check_ok() {
|
|
23
|
+
echo -e " \033[1;32m✓\033[0m $1"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
cleanup() {
|
|
27
|
+
for wt in "${WORKTREES_TO_CLEAN[@]}"; do
|
|
28
|
+
if [[ -d "$wt" ]]; then
|
|
29
|
+
git worktree remove --force "$wt" 2>/dev/null || true
|
|
30
|
+
fi
|
|
31
|
+
done
|
|
32
|
+
|
|
33
|
+
current=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)
|
|
34
|
+
if [[ "$current" != "$ORIGINAL_BRANCH" ]]; then
|
|
35
|
+
git checkout "$ORIGINAL_BRANCH" 2>/dev/null || true
|
|
36
|
+
fi
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
trap cleanup EXIT
|
|
40
|
+
|
|
41
|
+
# ─── Phase 1: Pre-flight checks ──────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
step "Running pre-flight checks"
|
|
44
|
+
|
|
45
|
+
# 1. At least one commit hash argument
|
|
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
|
|
52
|
+
if [[ -n "$(git status --porcelain)" ]]; then
|
|
53
|
+
fail "Working tree is dirty. Commit or stash your changes first."
|
|
54
|
+
fi
|
|
55
|
+
check_ok "Working tree is clean"
|
|
56
|
+
|
|
57
|
+
# 3. Required tools available
|
|
58
|
+
for tool in jq sed commit-and-tag-version pnpm; do
|
|
59
|
+
if ! command -v "$tool" &>/dev/null; then
|
|
60
|
+
fail "Required tool '$tool' is not installed or not in PATH."
|
|
61
|
+
fi
|
|
62
|
+
done
|
|
63
|
+
check_ok "Required tools available (jq, sed, commit-and-tag-version, pnpm)"
|
|
64
|
+
|
|
65
|
+
# 4. Remote write access
|
|
66
|
+
if ! git push --dry-run origin HEAD &>/dev/null; then
|
|
67
|
+
fail "No write access to the remote repository."
|
|
68
|
+
fi
|
|
69
|
+
check_ok "Remote write access confirmed"
|
|
70
|
+
|
|
71
|
+
# 5. Fetch latest remote refs
|
|
72
|
+
if ! git fetch origin development staging production 2>/dev/null; then
|
|
73
|
+
fail "Failed to fetch remote branches (development, staging, production)."
|
|
74
|
+
fi
|
|
75
|
+
check_ok "Fetched latest remote refs"
|
|
76
|
+
|
|
77
|
+
# 6. All commit hashes are valid commit objects
|
|
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)
|
|
95
|
+
local_dev=$(git rev-parse development 2>/dev/null)
|
|
96
|
+
remote_dev=$(git rev-parse origin/development 2>/dev/null)
|
|
97
|
+
merge_base_dev=$(git merge-base development origin/development 2>/dev/null)
|
|
98
|
+
if [[ "$local_dev" != "$remote_dev" && "$merge_base_dev" != "$local_dev" ]]; then
|
|
99
|
+
fail "Local 'development' has diverged from origin/development.\nResolve this manually before deploying."
|
|
100
|
+
fi
|
|
101
|
+
check_ok "development can fast-forward to origin"
|
|
102
|
+
|
|
103
|
+
# 9. staging and production have no local-only commits ahead of origin
|
|
104
|
+
for branch in "${BRANCHES[@]}"; do
|
|
105
|
+
local_ref=$(git rev-parse "$branch" 2>/dev/null || true)
|
|
106
|
+
remote_ref=$(git rev-parse "origin/$branch" 2>/dev/null || true)
|
|
107
|
+
|
|
108
|
+
if [[ -z "$local_ref" || -z "$remote_ref" ]]; then
|
|
109
|
+
continue # branch may not exist locally yet — that's fine, reset --hard will create it
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
ahead=$(git rev-list --count "origin/$branch..$branch" 2>/dev/null || echo "0")
|
|
113
|
+
if [[ "$ahead" -gt 0 ]]; then
|
|
114
|
+
fail "Local '$branch' is $ahead commit(s) ahead of origin/$branch.\nThis would be lost on reset. Push or discard those commits first."
|
|
115
|
+
fi
|
|
116
|
+
done
|
|
117
|
+
check_ok "No local-only commits on staging/production"
|
|
118
|
+
|
|
119
|
+
# 10. Dry-run cherry-picks in temporary worktrees
|
|
120
|
+
for branch in "${BRANCHES[@]}"; do
|
|
121
|
+
wt_path="${WORKTREE_DIR}-${branch}"
|
|
122
|
+
WORKTREES_TO_CLEAN+=("$wt_path")
|
|
123
|
+
|
|
124
|
+
# Clean up any leftover worktree from a previous failed run
|
|
125
|
+
if [[ -d "$wt_path" ]]; then
|
|
126
|
+
git worktree remove --force "$wt_path" 2>/dev/null || true
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
git worktree add "$wt_path" "origin/$branch" --detach --quiet 2>/dev/null \
|
|
130
|
+
|| fail "Failed to create test worktree for $branch."
|
|
131
|
+
|
|
132
|
+
for hash in "$@"; do
|
|
133
|
+
short=$(git rev-parse --short "$hash")
|
|
134
|
+
if ! git -C "$wt_path" cherry-pick --no-commit "$hash" &>/dev/null; then
|
|
135
|
+
git -C "$wt_path" cherry-pick --abort 2>/dev/null || true
|
|
136
|
+
fail "Cherry-pick of $short will fail on $branch.\nResolve conflicts before deploying."
|
|
137
|
+
fi
|
|
138
|
+
git -C "$wt_path" reset --hard HEAD --quiet 2>/dev/null
|
|
139
|
+
done
|
|
140
|
+
|
|
141
|
+
git worktree remove --force "$wt_path" 2>/dev/null || true
|
|
142
|
+
done
|
|
143
|
+
check_ok "Dry-run cherry-picks succeeded on staging and production"
|
|
144
|
+
|
|
145
|
+
echo -e "\n\033[1;32mAll pre-flight checks passed.\033[0m"
|
|
146
|
+
|
|
147
|
+
# ─── Phase 2: Execution ──────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
# --- development ---
|
|
150
|
+
step "Deploying to development"
|
|
151
|
+
|
|
152
|
+
git checkout development
|
|
153
|
+
git pull --ff-only
|
|
154
|
+
pnpm release patch
|
|
155
|
+
git push --follow-tags origin development
|
|
156
|
+
echo -e " \033[1;32m✓\033[0m development released"
|
|
157
|
+
|
|
158
|
+
# --- staging ---
|
|
159
|
+
step "Deploying to staging"
|
|
160
|
+
|
|
161
|
+
git checkout staging
|
|
162
|
+
git reset --hard origin/staging --quiet
|
|
163
|
+
for hash in "$@"; do
|
|
164
|
+
git cherry-pick "$hash"
|
|
165
|
+
done
|
|
166
|
+
git push origin staging
|
|
167
|
+
pnpm release patch
|
|
168
|
+
git push --follow-tags origin staging
|
|
169
|
+
echo -e " \033[1;32m✓\033[0m staging released"
|
|
170
|
+
|
|
171
|
+
# --- production ---
|
|
172
|
+
step "Deploying to production"
|
|
173
|
+
|
|
174
|
+
git checkout production
|
|
175
|
+
git reset --hard origin/production --quiet
|
|
176
|
+
for hash in "$@"; do
|
|
177
|
+
git cherry-pick "$hash"
|
|
178
|
+
done
|
|
179
|
+
git push origin production
|
|
180
|
+
pnpm release patch
|
|
181
|
+
git push --follow-tags origin production
|
|
182
|
+
echo -e " \033[1;32m✓\033[0m production released"
|
|
183
|
+
|
|
184
|
+
# ─── Phase 3: Cleanup ────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
step "Returning to development"
|
|
187
|
+
|
|
188
|
+
git checkout development
|
|
189
|
+
git pull --ff-only
|
|
190
|
+
|
|
191
|
+
echo -e "\n\033[1;32mDeploy complete.\033[0m"
|
package/package.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@neferbyte/cherry-release-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Cherry-pick commits across environment branches and tag releases",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cherry-release": "./bin/cherry-release.sh"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/"
|
|
11
|
+
]
|
|
12
|
+
}
|