this repo has no description
1#!/bin/bash
2set -euo pipefail
3
4WORKFLOW_NAME="Release"
5WORKFLOW_FILE="release.yml"
6WORKFLOW_IDENTIFIER="$WORKFLOW_FILE"
7
8log_info() { printf '\n🔷 %s\n' "$1"; }
9log_error() { printf '❌ %s\n' "$1" >&2; exit 1; }
10log_success() { printf '\n✅ %s\n' "$1"; }
11
12usage() {
13 cat <<'USAGE'
14AXe Release Helper
15
16Usage: scripts/create-release.sh [VERSION|major|minor|patch] [--notes-file FILE] [--dry-run]
17 scripts/create-release.sh --help
18
19Arguments:
20 VERSION Explicit semantic version (e.g. 1.4.0 or 1.5.0-beta.1)
21 major|minor|patch Semantic version bump type (defaults to minor when omitted)
22
23Options:
24 --notes-file FILE Read release notes from FILE instead of prompting
25 --dry-run Preview actions without pushing
26 -h, --help Show this help text
27USAGE
28}
29
30ensure_command() {
31 if ! command -v "$1" >/dev/null 2>&1; then
32 log_error "$1 not found. Please install it first."
33 fi
34}
35
36# --- Version helpers ---
37parse_version() {
38 echo "$1" | sed -E 's/^([0-9]+)\.([0-9]+)\.([0-9]+)(-.*)?$/\1 \2 \3 \4/'
39}
40
41bump_version() {
42 local current_version=$1
43 local bump_type=$2
44
45 if [[ -z "$current_version" ]]; then
46 case $bump_type in
47 major) echo "1.0.0" ;;
48 minor) echo "0.1.0" ;;
49 patch) echo "0.0.1" ;;
50 esac
51 return
52 fi
53
54 local parsed=($(parse_version "$current_version"))
55 local major=${parsed[0]}
56 local minor=${parsed[1]}
57 local patch=${parsed[2]}
58
59 case $bump_type in
60 major) echo "$((major + 1)).0.0" ;;
61 minor) echo "${major}.$((minor + 1)).0" ;;
62 patch) echo "${major}.${minor}.$((patch + 1))" ;;
63 *) log_error "Unknown bump type: $bump_type" ;;
64 esac
65}
66
67latest_version() {
68 git tag | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$' | sed 's/^v//' | sort -V | tail -1
69}
70
71# --- Parse arguments ---
72VERSION=""
73BUMP_TYPE=""
74NOTES_FILE=""
75DRY_RUN=false
76
77while [[ $# -gt 0 ]]; do
78 case "$1" in
79 --notes-file)
80 shift
81 [[ $# -gt 0 ]] || log_error "--notes-file requires a path"
82 NOTES_FILE=$1
83 ;;
84 --dry-run)
85 DRY_RUN=true
86 ;;
87 -h|--help)
88 usage
89 exit 0
90 ;;
91 major|minor|patch)
92 if [[ -n "$VERSION" ]]; then
93 log_error "Cannot specify both explicit version and bump type."
94 fi
95 BUMP_TYPE=$1
96 ;;
97 *)
98 if [[ -z "$VERSION" ]]; then
99 VERSION=$1
100 else
101 log_error "Unexpected argument: $1"
102 fi
103 ;;
104 esac
105 shift || true
106done
107
108ensure_command gh
109ensure_command jq
110
111if ! gh auth status >/dev/null 2>&1; then
112 log_error "GitHub CLI is not authenticated. Run 'gh auth login'."
113fi
114
115REPO_NAME=$(gh repo view --json nameWithOwner -q .nameWithOwner)
116
117git fetch --tags >/dev/null 2>&1 || true
118
119LATEST_VERSION=$(latest_version)
120if [[ -n "$LATEST_VERSION" ]]; then
121 log_info "Latest version: $LATEST_VERSION"
122else
123 log_info "No previous versions found."
124fi
125
126if [[ -n "$VERSION" && -n "$BUMP_TYPE" ]]; then
127 log_error "Specify either an explicit version or a bump type, not both."
128fi
129
130if [[ -n "$BUMP_TYPE" ]]; then
131 VERSION=$(bump_version "$LATEST_VERSION" "$BUMP_TYPE")
132 log_info "Bump type '$BUMP_TYPE' selected -> $VERSION"
133fi
134
135if [[ -z "$VERSION" ]]; then
136 DEFAULT_VERSION=$(bump_version "$LATEST_VERSION" "minor")
137 read -rp "Enter version [$DEFAULT_VERSION]: " VERSION_INPUT
138 if [[ -z "$VERSION_INPUT" ]]; then
139 VERSION=$DEFAULT_VERSION
140 else
141 VERSION=$VERSION_INPUT
142 fi
143fi
144
145[[ -n "$VERSION" ]] || log_error "No version provided."
146
147if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then
148 log_error "Invalid version format '$VERSION'."
149fi
150
151TAG_NAME="v$VERSION"
152
153if git rev-parse "$TAG_NAME" >/dev/null 2>&1; then
154 log_error "Tag $TAG_NAME already exists locally."
155fi
156
157if git ls-remote --tags origin | grep -q "refs/tags/$TAG_NAME$"; then
158 log_error "Tag $TAG_NAME already exists on origin."
159fi
160
161if ! git diff-index --quiet HEAD --; then
162 log_error "Working tree is not clean. Commit or stash changes first."
163fi
164
165if [[ -n "$NOTES_FILE" ]]; then
166 [[ -f "$NOTES_FILE" ]] || log_error "Release notes file not found: $NOTES_FILE"
167 RELEASE_NOTES=$(<"$NOTES_FILE")
168else
169 log_info "Enter release notes (Ctrl+D to finish):"
170 RELEASE_NOTES=$(cat)
171fi
172
173if [[ -z "$RELEASE_NOTES" ]]; then
174 RELEASE_NOTES="Release $TAG_NAME"
175fi
176
177log_info "Preparing release for $TAG_NAME"
178log_info "Workflow: $WORKFLOW_NAME"
179
180TMP_NOTES=$(mktemp)
181printf '%s\n' "$RELEASE_NOTES" > "$TMP_NOTES"
182trap 'rm -f "$TMP_NOTES"' EXIT
183
184if $DRY_RUN; then
185 log_info "[dry-run] Would create annotated tag $TAG_NAME"
186else
187 git tag -a "$TAG_NAME" -F "$TMP_NOTES"
188 log_success "Tag $TAG_NAME created."
189fi
190
191CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
192HEAD_SHA=$(git rev-parse HEAD)
193
194if $DRY_RUN; then
195 log_info "[dry-run] Would push branch $CURRENT_BRANCH and tag $TAG_NAME"
196else
197 git push origin "$CURRENT_BRANCH"
198 git push origin "$TAG_NAME"
199 log_success "Tag $TAG_NAME pushed to origin."
200fi
201
202if $DRY_RUN; then
203 log_info "[dry-run] Skipping workflow monitoring."
204 exit 0
205fi
206
207log_info "Waiting for GitHub Actions workflow to start..."
208RUN_ID=""
209FALLBACK_USED=false
210for attempt in {1..30}; do
211 RUN_JSON=$(gh run list --workflow "$WORKFLOW_IDENTIFIER" --limit 10 --json databaseId,headSha,status,event 2>/dev/null || true)
212 if [[ -z "$RUN_JSON" ]]; then
213 RUN_JSON="[]"
214 fi
215 RUN_ID=$(echo "$RUN_JSON" | jq -r ".[] | select(.headSha == \"$HEAD_SHA\") | .databaseId" | head -n1)
216 if [[ -z "$RUN_ID" && "$WORKFLOW_IDENTIFIER" == "$WORKFLOW_FILE" && "$FALLBACK_USED" == "false" ]]; then
217 WORKFLOW_IDENTIFIER="$WORKFLOW_NAME"
218 FALLBACK_USED=true
219 continue
220 fi
221 if [[ -n "$RUN_ID" ]]; then
222 break
223 fi
224 sleep 10
225done
226
227if [[ -z "$RUN_ID" ]]; then
228 log_error "Could not find workflow run for commit $HEAD_SHA. Monitor manually on GitHub."
229fi
230
231log_info "Monitoring workflow run ID $RUN_ID..."
232if gh run watch "$RUN_ID" --exit-status; then
233 log_success "Workflow succeeded!"
234 log_info "Release will be published automatically."
235 log_info "https://github.com/$REPO_NAME/releases/tag/$TAG_NAME"
236else
237 log_info "Workflow failed. Cleaning up..."
238 if gh release view "$TAG_NAME" >/dev/null 2>&1; then
239 gh release delete "$TAG_NAME" --yes || true
240 fi
241 git push origin ":refs/tags/$TAG_NAME" || true
242 git tag -d "$TAG_NAME" || true
243 log_error "Release workflow failed. Fix issues and rerun the script with version $VERSION."
244fi