Skip to content

Commit ae9342b

Browse files
committed
Add release.sh script for automated NuGet releases
Copied and adapted from micro-plumberd release workflow. Features: - Auto-increment version (major/minor/patch/build) - Tag format: eventpi/X.Y.Z.W - Git status validation and cross-platform compatibility - Interactive and non-interactive modes (--dry-run, -y) - Triggers GitHub Actions NuGet publishing workflow Usage: ./release.sh # Auto-increment build ./release.sh 1.0.11.32 # Specific version ./release.sh --dry-run # Preview release ./release.sh --build -y -m "Fix" # Auto-confirm
1 parent 328c2c3 commit ae9342b

1 file changed

Lines changed: 385 additions & 0 deletions

File tree

release.sh

Lines changed: 385 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,385 @@
1+
#!/bin/bash
2+
3+
# EventPi Release Script
4+
# Automates version tagging and triggering NuGet package publishing
5+
6+
set -e
7+
8+
# Check if running in interactive terminal
9+
check_interactive_terminal() {
10+
if [[ ! -t 0 ]] || [[ ! -t 1 ]]; then
11+
echo "# This script requires an interactive terminal for user prompts and confirmations."
12+
echo "# Please run this script from a terminal session, not from CI/CD or automated context."
13+
echo "# For automated releases, use the GitHub Actions workflow or implement a non-interactive version."
14+
return 1
15+
fi
16+
return 0
17+
}
18+
19+
# Colors for output
20+
RED='\033[0;31m'
21+
GREEN='\033[0;32m'
22+
YELLOW='\033[1;33m'
23+
BLUE='\033[0;34m'
24+
NC='\033[0m' # No Color
25+
26+
# Functions
27+
print_usage() {
28+
echo -e "${BLUE}EventPi Release Script${NC}"
29+
echo
30+
echo "Usage: ./release.sh [VERSION] [OPTIONS]"
31+
echo
32+
echo "Arguments:"
33+
echo " VERSION 4-part version (e.g., 1.0.122.154, 1.1.0.0)"
34+
echo " If not provided, script will auto-increment build number"
35+
echo
36+
echo "Options:"
37+
echo " -m, --message TEXT Release notes/message (optional)"
38+
echo " -b, --build Auto-increment build number (default)"
39+
echo " -p, --patch Auto-increment patch version"
40+
echo " -n, --minor Auto-increment minor version"
41+
echo " -M, --major Auto-increment major version"
42+
echo " -y, --yes Auto-confirm release without prompts"
43+
echo " --dry-run Show what would be done without executing"
44+
echo " -h, --help Show this help message"
45+
echo
46+
echo "Examples:"
47+
echo " ./release.sh 1.0.123.155 # Release specific version"
48+
echo " ./release.sh 1.0.123.155 -m \"Bug fixes\" # With release notes"
49+
echo " ./release.sh --build -m \"New features\" # Auto-increment build (default)"
50+
echo " ./release.sh --patch -m \"API changes\" # Auto-increment patch"
51+
echo " ./release.sh --minor -y -m \"New feature\" # Auto-confirm release"
52+
echo " ./release.sh --dry-run # Preview next build release"
53+
}
54+
55+
print_error() {
56+
echo -e "${RED}Error: $1${NC}" >&2
57+
}
58+
59+
print_warning() {
60+
echo -e "${YELLOW}Warning: $1${NC}"
61+
}
62+
63+
print_success() {
64+
echo -e "${GREEN}$1${NC}"
65+
}
66+
67+
print_info() {
68+
echo -e "${BLUE}$1${NC}"
69+
}
70+
71+
# Get the latest version tag
72+
get_latest_version() {
73+
git tag --sort=-version:refname | grep -E '^eventpi/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -n1 | sed 's/^eventpi\///' || echo "1.0.0.0"
74+
}
75+
76+
# Increment version
77+
increment_version() {
78+
local version=$1
79+
local part=$2
80+
81+
IFS='.' read -r -a parts <<< "$version"
82+
local major=${parts[0]:-1}
83+
local minor=${parts[1]:-0}
84+
local patch=${parts[2]:-0}
85+
local build=${parts[3]:-0}
86+
87+
case $part in
88+
"major")
89+
major=$((major + 1))
90+
minor=0
91+
patch=0
92+
build=0
93+
;;
94+
"minor")
95+
minor=$((minor + 1))
96+
patch=0
97+
build=0
98+
;;
99+
"patch")
100+
patch=$((patch + 1))
101+
build=0
102+
;;
103+
"build"|*)
104+
build=$((build + 1))
105+
;;
106+
esac
107+
108+
echo "$major.$minor.$patch.$build"
109+
}
110+
111+
# Validate 4-part version
112+
validate_version() {
113+
local version=$1
114+
if [[ ! $version =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
115+
print_error "Invalid version format: $version. Expected format: X.Y.Z.W (e.g., 1.0.122.154)"
116+
return 1
117+
fi
118+
return 0
119+
}
120+
121+
# Check if version already exists
122+
check_version_exists() {
123+
local version=$1
124+
if git tag --list | grep -q "^eventpi/$version$"; then
125+
print_error "Version eventpi/$version already exists!"
126+
echo "Existing tags:"
127+
git tag --sort=-version:refname | head -5
128+
return 1
129+
fi
130+
return 0
131+
}
132+
133+
# Initialize git configuration for cross-platform compatibility
134+
init_git_config() {
135+
# Set core.autocrlf to false to prevent line ending issues between Windows/Linux
136+
git config --local core.autocrlf false 2>/dev/null || true
137+
138+
# Set core.filemode to false to ignore file permission changes (Windows/WSL compatibility)
139+
git config --local core.filemode false 2>/dev/null || true
140+
141+
# Ensure consistent behavior for git status and diff operations
142+
git config --local status.submodulesummary false 2>/dev/null || true
143+
144+
# Set safe directory (helps with WSL/Windows shared folders)
145+
git config --global --add safe.directory "$(git rev-parse --show-toplevel)" 2>/dev/null || true
146+
}
147+
148+
# Check git status
149+
check_git_status() {
150+
local auto_confirm="$1"
151+
152+
# Initialize git config for cross-platform compatibility
153+
init_git_config
154+
155+
if ! git diff-index --quiet HEAD --; then
156+
print_error "Working directory is not clean. Please commit or stash changes first."
157+
echo
158+
echo "Uncommitted changes:"
159+
git status --porcelain
160+
return 1
161+
fi
162+
163+
# Check if we're on master/main
164+
local branch=$(git branch --show-current)
165+
if [[ "$branch" != "master" && "$branch" != "main" ]]; then
166+
print_warning "You're not on master/main branch (current: $branch)"
167+
if [[ "$auto_confirm" != "true" ]]; then
168+
read -p "Continue anyway? [y/N]: " -n 1 -r
169+
echo
170+
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
171+
print_error "Aborted by user"
172+
return 1
173+
fi
174+
else
175+
print_info "Auto-confirming: continuing on $branch branch"
176+
fi
177+
fi
178+
179+
# Check for unpushed commits
180+
git fetch origin "$branch" 2>/dev/null || true
181+
local unpushed_commits=$(git rev-list --count origin/"$branch"..HEAD 2>/dev/null || echo "0")
182+
if [[ "$unpushed_commits" -gt 0 ]]; then
183+
print_error "There are $unpushed_commits unpushed commit(s) on branch $branch"
184+
echo
185+
echo "Unpushed commits:"
186+
git log --oneline origin/"$branch"..HEAD
187+
echo
188+
print_info "Please push your commits first:"
189+
print_info " git push origin $branch"
190+
return 1
191+
fi
192+
193+
return 0
194+
}
195+
196+
# Create and push tag
197+
create_release() {
198+
local version=$1
199+
local message=$2
200+
local dry_run=$3
201+
202+
local tag="eventpi/$version"
203+
204+
print_info "Creating release $tag..."
205+
206+
if [[ "$dry_run" == "true" ]]; then
207+
echo -e "${YELLOW}[DRY RUN]${NC} Would create tag: $tag"
208+
if [[ -n "$message" ]]; then
209+
echo -e "${YELLOW}[DRY RUN]${NC} With message: $message"
210+
fi
211+
echo -e "${YELLOW}[DRY RUN]${NC} Would push to origin"
212+
return 0
213+
fi
214+
215+
# Create tag
216+
if [[ -n "$message" ]]; then
217+
git tag -a "$tag" -m "$message"
218+
print_success "✓ Created annotated tag $tag with message"
219+
else
220+
git tag "$tag"
221+
print_success "✓ Created tag $tag"
222+
fi
223+
224+
# Push tag
225+
print_info "Pushing tag to origin..."
226+
git push origin "$tag"
227+
print_success "✓ Pushed tag $tag to origin"
228+
229+
# Show next steps
230+
echo
231+
print_info "🚀 Release $tag created successfully!"
232+
echo
233+
echo "Next steps:"
234+
echo "1. Monitor the GitHub Actions workflow: https://github.com/modelingevolution/EventPi/actions"
235+
echo "2. Verify NuGet package is published to NuGet.org:"
236+
echo " - EventPi.Abstractions $version"
237+
echo "3. Check packages on NuGet.org: https://www.nuget.org/profiles/modelingevolution"
238+
239+
return 0
240+
}
241+
242+
# Main script
243+
main() {
244+
local version=""
245+
local message=""
246+
local increment_type="build"
247+
local dry_run="false"
248+
local auto_confirm="false"
249+
local needs_interactive="true"
250+
251+
# Parse arguments first to check for help, dry-run, or auto-confirm
252+
while [[ $# -gt 0 ]]; do
253+
case $1 in
254+
-h|--help)
255+
print_usage
256+
exit 0
257+
;;
258+
--dry-run)
259+
dry_run="true"
260+
needs_interactive="false"
261+
shift
262+
;;
263+
-y|--yes)
264+
auto_confirm="true"
265+
needs_interactive="false"
266+
shift
267+
;;
268+
-m|--message)
269+
message="$2"
270+
shift 2
271+
;;
272+
-b|--build)
273+
increment_type="build"
274+
shift
275+
;;
276+
-p|--patch)
277+
increment_type="patch"
278+
shift
279+
;;
280+
-n|--minor)
281+
increment_type="minor"
282+
shift
283+
;;
284+
-M|--major)
285+
increment_type="major"
286+
shift
287+
;;
288+
-*)
289+
print_error "Unknown option: $1"
290+
print_usage
291+
exit 1
292+
;;
293+
*)
294+
if [[ -z "$version" ]]; then
295+
version="$1"
296+
else
297+
print_error "Multiple versions specified: $version and $1"
298+
exit 1
299+
fi
300+
shift
301+
;;
302+
esac
303+
done
304+
305+
# If no arguments provided, show helpful guidance
306+
if [[ -z "$version" ]] && [[ "$increment_type" == "build" ]] && [[ -z "$message" ]] && [[ "$dry_run" == "false" ]] && [[ "$auto_confirm" == "false" ]]; then
307+
echo "No arguments provided. Here are some examples:"
308+
echo
309+
print_usage
310+
echo
311+
echo "For non-interactive environments:"
312+
echo " ./release.sh --dry-run # Preview release"
313+
echo " ./release.sh --build -y -m \"Bug fixes\" # Auto-confirm release"
314+
exit 1
315+
fi
316+
317+
# Check if running in interactive terminal (only when interactive input is needed)
318+
if [[ "$needs_interactive" == "true" ]] && ! check_interactive_terminal; then
319+
echo
320+
echo "To run this script in non-interactive mode:"
321+
echo " ./release.sh --dry-run # Preview release"
322+
echo " ./release.sh --minor -y -m \"New feature\" # Auto-confirm release"
323+
exit 1
324+
fi
325+
326+
# Check if we're in a git repository
327+
if ! git rev-parse --git-dir > /dev/null 2>&1; then
328+
print_error "Not in a git repository"
329+
exit 1
330+
fi
331+
332+
# Get current version if not specified
333+
if [[ -z "$version" ]]; then
334+
local current_version=$(get_latest_version)
335+
version=$(increment_version "$current_version" "$increment_type")
336+
print_info "Auto-incrementing $increment_type version: $current_version$version"
337+
fi
338+
339+
# Validate version
340+
if ! validate_version "$version"; then
341+
exit 1
342+
fi
343+
344+
# Check if version already exists
345+
if ! check_version_exists "$version"; then
346+
exit 1
347+
fi
348+
349+
# Check git status (skip for dry run)
350+
if [[ "$dry_run" != "true" ]]; then
351+
if ! check_git_status "$auto_confirm"; then
352+
exit 1
353+
fi
354+
fi
355+
356+
# Show summary
357+
echo
358+
print_info "Release Summary:"
359+
echo " Version: eventpi/$version"
360+
echo " Message: ${message:-"(none)"}"
361+
echo " Branch: $(git branch --show-current)"
362+
echo " Commit: $(git rev-parse --short HEAD)"
363+
if [[ "$dry_run" == "true" ]]; then
364+
echo -e " Mode: ${YELLOW}DRY RUN${NC}"
365+
fi
366+
echo
367+
368+
# Confirm (skip for dry run and auto-confirm)
369+
if [[ "$dry_run" != "true" ]] && [[ "$auto_confirm" != "true" ]]; then
370+
read -p "Create release? [y/N]: " -n 1 -r
371+
echo
372+
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
373+
print_error "Aborted by user"
374+
exit 1
375+
fi
376+
elif [[ "$auto_confirm" == "true" ]] && [[ "$dry_run" != "true" ]]; then
377+
print_info "Auto-confirming release creation"
378+
fi
379+
380+
# Create release
381+
create_release "$version" "$message" "$dry_run"
382+
}
383+
384+
# Run main function
385+
main "$@"

0 commit comments

Comments
 (0)