@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.
@@ -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. 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
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
- # 3. Required tools available
58
- for tool in jq sed commit-and-tag-version pnpm; do
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, pnpm)"
58
+ check_ok "Required tools available (jq, sed, commit-and-tag-version)"
64
59
 
65
- # 4. Remote write access
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
- # 5. Fetch latest remote refs
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
- # 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)
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
- # 9. staging and production have no local-only commits ahead of origin
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 # branch may not exist locally yet — that's fine, reset --hard will create it
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
- # 10. Dry-run cherry-picks in temporary worktrees
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 "$@"; do
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 on staging and production"
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
- pnpm release patch
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
- step "Deploying to staging"
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
- for hash in "$@"; do
163
- git cherry-pick "$hash"
164
- done
165
- git push origin staging
166
- pnpm release patch
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
- step "Deploying to production"
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
- for hash in "$@"; do
176
- git cherry-pick "$hash"
177
- done
178
- git push origin production
179
- pnpm release patch
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.1",
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/"