22
33set -euo pipefail
44
5+ export GIT_PAGER=cat
6+ export PAGER=cat
7+ export LESS=-F
8+
9+ ORIG_BRANCH=$( git rev-parse --abbrev-ref HEAD 2> /dev/null || echo " " )
10+ ORIG_COMMIT=" "
11+ if [ " $ORIG_BRANCH " = " HEAD" ]; then
12+ ORIG_COMMIT=$( git rev-parse HEAD 2> /dev/null || echo " " )
13+ fi
14+ AUTO_STASHES=()
15+ RESTORE_COMPLETED=0
16+ ORIG_UNTRACKED=()
17+ while IFS=$' \0' read -r -d ' ' path; do
18+ ORIG_UNTRACKED+=(" $path " )
19+ done < <( git ls-files --others --exclude-standard -z 2> /dev/null || printf ' ' )
20+
21+ restore_workspace () {
22+ local restore_messages=()
23+
24+ if [ -n " $ORIG_BRANCH " ] && [ " $ORIG_BRANCH " != " HEAD" ]; then
25+ local current_branch
26+ current_branch=$( git rev-parse --abbrev-ref HEAD 2> /dev/null || echo " " )
27+ if [ -n " $current_branch " ] && [ " $current_branch " != " $ORIG_BRANCH " ]; then
28+ if git checkout " $ORIG_BRANCH " > /dev/null 2>&1 ; then
29+ restore_messages+=(" Returned to original branch $ORIG_BRANCH ." )
30+ else
31+ echo " Warning: failed to return to branch $ORIG_BRANCH . Current branch: $current_branch ."
32+ fi
33+ fi
34+ elif [ -n " $ORIG_COMMIT " ]; then
35+ local current_ref
36+ current_ref=$( git rev-parse HEAD 2> /dev/null || echo " " )
37+ if [ -n " $current_ref " ] && [ " $current_ref " != " $ORIG_COMMIT " ]; then
38+ if git checkout --detach " $ORIG_COMMIT " > /dev/null 2>&1 ; then
39+ restore_messages+=(" Returned to detached HEAD at $ORIG_COMMIT ." )
40+ else
41+ echo " Warning: failed to return to original commit $ORIG_COMMIT ."
42+ fi
43+ fi
44+ fi
45+
46+ for (( idx= ${# AUTO_STASHES[@]} - 1 ; idx>= 0 ; idx-- )) ; do
47+ local stash_ref=${AUTO_STASHES[idx]}
48+ if [ -n " $stash_ref " ]; then
49+ if git stash pop " $stash_ref " > /dev/null 2>&1 ; then
50+ restore_messages+=(" Restored auto-stash $stash_ref ." )
51+ else
52+ echo " Warning: failed to restore auto-stash $stash_ref . Run 'git stash pop $stash_ref ' manually."
53+ fi
54+ fi
55+ done
56+
57+ if [ ${# ORIG_UNTRACKED[@]} -gt 0 ]; then
58+ for path in " ${ORIG_UNTRACKED[@]} " ; do
59+ if git ls-files --error-unmatch -- " $path " > /dev/null 2>&1 ; then
60+ git update-index --force-remove -- " $path " > /dev/null 2>&1
61+ fi
62+ done
63+ fi
64+
65+ if [ ${# restore_messages[@]} -gt 0 ]; then
66+ printf ' %s\n' " ${restore_messages[@]} "
67+ fi
68+
69+ RESTORE_COMPLETED=1
70+ }
71+
72+ cleanup_release_context () {
73+ local exit_code=$?
74+ set +e
75+
76+ if [ " $RESTORE_COMPLETED " -ne 1 ]; then
77+ restore_workspace
78+ fi
79+
80+ exit $exit_code
81+ }
82+ trap cleanup_release_context EXIT
83+
584ask_consent () {
685 local explanation=$1
786 shift
@@ -29,15 +108,86 @@ require_clean_worktree() {
29108 fi
30109}
31110
111+
32112ensure_checkout_safe () {
33113 local target=$1
34- if ! git checkout --dry-run " $target " > /dev/null 2>&1 ; then
35- echo " Checkout preview detected conflicts (tracked files or untracked files would be overwritten)."
36- echo " Resolve or stash those files before continuing."
114+
115+ if ! git rev-parse --verify " $target " > /dev/null 2>&1 ; then
116+ echo " Unable to verify target ref $target . Ensure the branch exists locally."
117+ exit 1
118+ fi
119+
120+ local tracked_blockers
121+ tracked_blockers=$( git status --porcelain --untracked-files=no)
122+ if [ -n " $tracked_blockers " ]; then
123+ echo " Tracked changes detected that would block switching to $target ."
124+ echo " Tracked files (showing up to 20):"
125+ printf ' %s\n' " $tracked_blockers " | head -n 20 | sed ' s/^/ - /'
126+ if [ " $( printf ' %s\n' " $tracked_blockers " | wc -l | tr -d ' ' ) " -gt 20 ]; then
127+ echo " - ... (additional files omitted)"
128+ fi
129+ echo " Clean or stash the tracked changes before continuing."
130+ exit 1
131+ fi
132+
133+ local untracked_blockers=()
134+ while IFS= read -r -d ' ' path; do
135+ untracked_blockers+=(" $path " )
136+ done < <( git ls-files --others --exclude-standard -z)
137+
138+ if [ ${# untracked_blockers[@]} -eq 0 ]; then
139+ return 0
140+ fi
141+
142+ echo " Checkout preview detected untracked files that could be overwritten when switching to $target ."
143+ echo " Affected untracked files (showing up to 20):"
144+ local idx=0
145+ for path in " ${untracked_blockers[@]} " ; do
146+ idx=$(( idx + 1 ))
147+ if [ $idx -le 20 ]; then
148+ echo " - $path "
149+ fi
150+ done
151+ if [ $idx -gt 20 ]; then
152+ echo " - ... ($idx total files)"
153+ fi
154+
155+ local stash_choice
156+ while true ; do
157+ read -r -p " Auto-stash these untracked files and continue? (y/n): " stash_choice
158+ case " $stash_choice " in
159+ y|Y) break ;;
160+ n|N)
161+ echo " Aborting at user request; resolve untracked files before rerunning."
162+ exit 1
163+ ;;
164+ " " ) echo " Please answer y or n." ;;
165+ * ) echo " Invalid selection. Enter y or n." ;;
166+ esac
167+ done
168+
169+ echo " Auto-stashing untracked files before retrying checkout."
170+ local stash_marker
171+ stash_marker=" $( date +%s) -$$ -$RANDOM "
172+ local stash_message=" release.sh:auto-stash:$target :$stash_marker "
173+ if git stash push --include-untracked --message " $stash_message " > /dev/null 2>&1 ; then
174+ local stash_ref
175+ stash_ref=$( git stash list | awk -F: -v msg=" $stash_message " ' $0 ~ msg {print $1; exit}' )
176+ if [ -n " $stash_ref " ]; then
177+ AUTO_STASHES+=(" $stash_ref " )
178+ else
179+ echo " Warning: auto-stashed files recorded but stash reference could not be determined."
180+ fi
181+ echo " Untracked files stashed temporarily; they will be restored after release.sh completes."
182+ ensure_checkout_safe " $target "
183+ return
184+ else
185+ echo " Failed to auto-stash untracked files. Resolve them manually and rerun the script."
37186 exit 1
38187 fi
39188}
40189
190+
41191# Step 1: Get the first argument from the command line
42192echo " ==== Read Tag ===="
43193TAG=${1:- }
@@ -68,13 +218,34 @@ if [ -z "$TAG" ]; then
68218 MINOR_TAG=" v${MAJOR} .${MINOR_SUGGESTED} .0"
69219 PATCH_TAG=" v${MAJOR} .${MINOR} .${PATCH_SUGGESTED} "
70220 echo " Suggested tags:"
71- echo " - Minor bump (includes these features): $MINOR_TAG "
72- echo " - Patch bump: $PATCH_TAG "
73- read -r -p " Enter desired tag (copy one of the suggestions or provide a custom value): " TAG
74- if [ -z " $TAG " ]; then
75- TAG=$PATCH_TAG
76- echo " Defaulting to patch bump: $TAG "
77- fi
221+ echo " 1) Minor bump (includes these features): $MINOR_TAG "
222+ echo " 2) Patch bump: $PATCH_TAG "
223+ echo " 3) Enter custom tag"
224+ while true ; do
225+ read -r -p " Selection [1-3, default 2]: " TAG_CHOICE
226+ case " $TAG_CHOICE " in
227+ " " |" 2" )
228+ TAG=$PATCH_TAG
229+ echo " Using patch bump: $TAG "
230+ break
231+ ;;
232+ " 1" )
233+ TAG=$MINOR_TAG
234+ echo " Using minor bump: $TAG "
235+ break
236+ ;;
237+ " 3" )
238+ read -r -p " Enter custom tag (vX.Y.Z): " TAG
239+ if [ -n " $TAG " ]; then
240+ break
241+ fi
242+ echo " Custom tag cannot be empty."
243+ ;;
244+ * )
245+ echo " Invalid selection. Enter 1, 2, or 3."
246+ ;;
247+ esac
248+ done
78249 else
79250 TAG=" v${MAJOR} .${MINOR} .${PATCH_SUGGESTED} "
80251 read -r -p " Suggested tag is $TAG (no feature commits detected). Is this appropriate? (y/n): " CONFIRM
@@ -95,6 +266,23 @@ VERSION=${TAG#v}
95266
96267require_clean_worktree " Resolve tracked changes before starting the release (common causes: leftover ./version.sh runs or generated dist files)."
97268
269+ # Step 4: Run dependency audit, update dependencies, update versions, and rebuild bundles
270+ echo " ==== Update versions ===="
271+ ask_consent " Update package.json versions to $VERSION across all actions." bash ./version.sh " $VERSION "
272+ require_clean_worktree " Version update introduced tracked changes. Review and commit or stash them before continuing."
273+
274+ echo " ==== Rebuild bundles ===="
275+ ask_consent " Regenerate dist outputs so release $TAG contains fresh builds." bash ./build.sh
276+ require_clean_worktree " Build step introduced tracked changes. Review and commit or stash them before continuing."
277+
278+ echo " ==== Dependency audit ===="
279+ ask_consent " Run depcheck across all actions to verify dependency health." bash ./depcheck.sh
280+ require_clean_worktree " Depcheck introduced tracked changes. Review and commit or stash them before continuing."
281+
282+ echo " ==== Dependency updates ===="
283+ ask_consent " Run update-dependencies to refresh package versions before release." bash ./update-dependencies.sh
284+ require_clean_worktree " Dependency updates introduced tracked changes. Review and commit or stash them before continuing."
285+
98286# Step 4: Check out the develop branch locally and check if it matches the remote
99287echo " ==== Develop matches remote ===="
100288ask_consent " Fetch latest refs from origin to ensure develop is up to date before tagging." git fetch origin
@@ -104,6 +292,7 @@ if [ "$LOCAL_DEVELOP" != "$REMOTE_DEVELOP" ]; then
104292 echo " Local develop branch is not up to date with remote. "
105293 echo " Local develop: $LOCAL_DEVELOP ."
106294 git log -1 --pretty=format:" %s" refs/heads/develop
295+ echo " \n"
107296 echo " Remote develop: $REMOTE_DEVELOP ."
108297 git log -1 --pretty=format:" %s" origin/develop
109298 echo " Exiting"
112301
113302# Step 5: Check out the master branch locally and check if it matches the remote develop
114303echo " ==== Master matches develop ===="
115- ensure_checkout_safe master
116- ask_consent " Check out the master branch locally to prepare for release tagging." git checkout master
117- require_clean_worktree " Checkout left tracked changes (frequently produced by post-checkout hooks). Clean them up before continuing."
304+ LOCAL_MASTER=$( git rev-parse refs/heads/master 2> /dev/null || echo " " )
305+ if [ -z " $LOCAL_MASTER " ]; then
306+ echo " Local master branch not found; fetch or create it before running the release."
307+ exit 1
308+ fi
118309
119- LOCAL_MASTER=$( git rev-parse refs/heads/master)
120310if [ " $LOCAL_MASTER " != " $REMOTE_DEVELOP " ]; then
311+ ensure_checkout_safe master
312+ ask_consent " Check out the master branch locally to prepare for release tagging." git checkout master
313+ require_clean_worktree " Checkout left tracked changes (frequently produced by post-checkout hooks). Clean them up before continuing."
314+
315+ LOCAL_MASTER=$( git rev-parse refs/heads/master)
121316 echo " Local master branch is not up to date with remote develop."
122317 echo " Local master: $LOCAL_MASTER ."
123318 git log -1 --pretty=format:" %s" refs/heads/master
319+ printf " \n"
124320 echo " Remote develop: $REMOTE_DEVELOP ."
125321 git log -1 --pretty=format:" %s" origin/develop
126- git log refs/head/master..origin/develop
322+ printf " \nCommits on remote develop not in local master:\n"
323+ git log refs/heads/master..origin/develop
127324 read -r -p " Do you want to rebase local master on top of remote develop? (y/n): " REBASE_CONFIRM
128325 if [ " $REBASE_CONFIRM " != " y" ]; then
129326 echo " Exiting."
134331
135332require_clean_worktree " Rebase introduced tracked changes (e.g., conflict resolution artifacts). Review and resolve them before proceeding."
136333
137- echo " ==== Dependency audit ===="
138- ask_consent " Run depcheck across all actions to verify dependency health." ./depcheck.sh
139-
140- echo " ==== Dependency updates ===="
141- ask_consent " Run update-dependencies to refresh package versions before release." ./update-dependencies.sh
142-
143- echo " ==== Update versions ===="
144- ask_consent " Update package.json versions to $VERSION across all actions." ./version.sh " $VERSION "
145-
146- echo " ==== Rebuild bundles ===="
147- ask_consent " Regenerate dist outputs so release $TAG contains fresh builds." ./build.sh
148-
149334echo " ==== Stage release artifacts ===="
150335if [ -z " $( git status --porcelain) " ]; then
151336 echo " No changes detected after dependency updates and rebuild. Exiting to avoid an empty release commit."
@@ -170,4 +355,8 @@ ask_consent "Create local tag $TAG so the release can be pushed." git tag "$TAG"
170355echo " ==== Push tag ===="
171356ask_consent " Push tag $TAG to origin to make the release available to users." git push origin " $TAG "
172357
358+ # Step 9: Restore original branch
359+ echo " ==== Restore original branch ===="
360+ restore_workspace
361+
173362echo " Tag $TAG has been created and pushed to remote."
0 commit comments