@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.
Files changed (2) hide show
  1. package/bin/cherry-release.sh +191 -0
  2. 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
+ }