#!/usr/bin/env fish
# release.fish - Automated version release script for arch-toolkit
#
# What: Automates the entire release workflow including version bumps,
# documentation, building, and crates.io publishing.
#
# Usage:
# ./release.fish [--dry-run] [version]
#
# Options:
# --dry-run Preview all changes without executing them
# version New version (e.g., 0.1.0). If not provided, will prompt.
#
# Details:
# This script guides through the complete release process:
# 1. Version update in Cargo.toml
# 2. Documentation (release notes, README)
# 3. Build and test
# 4. GitHub release and crates.io publishing
# ============================================================================
# Configuration
# ============================================================================
set -g ARCH_TOOLKIT_DIR (realpath (dirname (status filename))/../..)
set -g DRY_RUN false
# Colors for output - use functions to avoid variable interpolation issues
function _red; set_color red; end
function _green; set_color green; end
function _yellow; set_color yellow; end
function _blue; set_color blue; end
function _cyan; set_color cyan; end
function _magenta; set_color magenta; end
function _reset; set_color normal; end
function _bold; set_color --bold; end
function _bold_cyan; set_color --bold cyan; end
function _bold_green; set_color --bold green; end
# ============================================================================
# Helper Functions
# ============================================================================
function log_info
_blue; echo -n "[INFO] "; _reset; echo $argv
end
function log_success
_green; echo -n "[SUCCESS] "; _reset; echo $argv
end
function log_warn
_yellow; echo -n "[WARN] "; _reset; echo $argv
end
function log_error
_red; echo -n "[ERROR] "; _reset; echo $argv
end
function log_step
echo
_magenta; echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"; _reset
_bold_cyan; echo " STEP: $argv"; _reset
_magenta; echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"; _reset
end
function log_phase
echo
_bold_green; echo "════════════════════════════════════════════════════════════════════════"; _reset
_bold_green; echo " PHASE: $argv"; _reset
_bold_green; echo "════════════════════════════════════════════════════════════════════════"; _reset
end
function dry_run_cmd
if test "$DRY_RUN" = true
_yellow; echo -n "[DRY-RUN] Would execute: "; _reset; echo $argv
return 0
else
eval $argv
return $status
end
end
function confirm_continue
set -l msg $argv[1]
if test -z "$msg"
set msg "Continue?"
end
while true
_cyan; echo -n "$msg [Y/n]: "; _reset
read response
switch (string lower $response)
case '' y yes
return 0
case n no
return 1
case '*'
echo "Please answer y or n"
end
end
end
function wait_for_user
set -l msg $argv[1]
if test -z "$msg"
set msg "Press Enter to continue..."
end
_cyan; echo -n $msg; _reset
read
end
function validate_semver
set -l ver_str $argv[1]
if string match -qr -- '^[0-9]+\.[0-9]+\.[0-9]+$' $ver_str
return 0
else
return 1
end
end
function get_current_version
grep -m1 '^version = ' "$ARCH_TOOLKIT_DIR/Cargo.toml" | sed 's/version = "\(.*\)"/\1/'
end
function is_prerelease_version
# Returns 0 (true) if version is < 1.0.0 (prerelease)
# Returns 1 (false) if version is >= 1.0.0 (stable)
set -l ver_str $argv[1]
set -l major (string split '.' $ver_str)[1]
if test "$major" -lt 1 2>/dev/null
return 0
else
return 1
end
end
function is_major_or_minor_change
# Returns 0 (true) if major or minor version changed (not just patch)
# Returns 1 (false) if only patch version changed
set -l old_ver $argv[1]
set -l new_ver $argv[2]
set -l old_parts (string split '.' $old_ver)
set -l new_parts (string split '.' $new_ver)
set -l old_major $old_parts[1]
set -l old_minor $old_parts[2]
set -l new_major $new_parts[1]
set -l new_minor $new_parts[2]
# Check if major or minor changed
if test "$old_major" != "$new_major"
return 0
else if test "$old_minor" != "$new_minor"
return 0
else
return 1
end
end
# ============================================================================
# Phase 1: Version Update
# ============================================================================
function phase1_version_update
set -l new_ver $argv[1]
log_phase "1. Version Update"
set -l current_ver (get_current_version)
_blue; echo -n "[INFO] "; _reset; echo -n "Current version: "; _bold; echo $current_ver; _reset
_blue; echo -n "[INFO] "; _reset; echo -n "New version: "; _bold; echo $new_ver; _reset
# Step 1.1: Update Cargo.toml
log_step "Updating Cargo.toml"
if test "$DRY_RUN" = true
log_info "[DRY-RUN] Would update version in Cargo.toml from $current_ver to $new_ver"
else
sed -i "s/^version = \"$current_ver\"/version = \"$new_ver\"/" "$ARCH_TOOLKIT_DIR/Cargo.toml"
if test $status -eq 0
log_success "Updated Cargo.toml"
else
log_error "Failed to update Cargo.toml"
return 1
end
end
# Step 1.2: Run cargo check to update Cargo.lock
log_step "Updating Cargo.lock"
cd "$ARCH_TOOLKIT_DIR"
dry_run_cmd "cargo check"
if test $status -eq 0
log_success "Cargo.lock updated"
else
log_error "cargo check failed"
return 1
end
return 0
end
# ============================================================================
# Phase 2: Documentation
# ============================================================================
function phase2_documentation
set -l new_ver $argv[1]
set -l old_ver $argv[2]
log_phase "2. Documentation"
# Step 2.1: Generate release notes with Cursor
log_step "Generate Release Notes"
_blue; echo -n "[INFO] "; _reset; echo -n "Please run: "; _bold; echo "/release-new $new_ver"; _reset
set -l release_file "$ARCH_TOOLKIT_DIR/docs/RELEASE_v$new_ver.md"
if test "$DRY_RUN" = true
log_info "[DRY-RUN] Would wait for release notes generation"
else
wait_for_user "After completing /release-new in Cursor, press Enter..."
# Verify release file was created
if not test -f "$release_file"
log_warn "Release file not found at: $release_file"
if not confirm_continue "Continue anyway?"
return 1
end
else
log_success "Release file created: $release_file"
end
end
# Step 2.2: Update CHANGELOG.md
update_changelog "$new_ver"
# Step 2.3: README update with Cursor (optional)
log_step "Update README (optional)"
_blue; echo -n "[INFO] "; _reset; echo -n "Please run: "; _bold; echo "/readme-update"; _reset
if test "$DRY_RUN" = true
log_info "[DRY-RUN] Would wait for README update"
else
if confirm_continue "Update README now?"
wait_for_user "After completing /readme-update in Cursor, press Enter..."
log_success "README update complete"
else
log_info "Skipping README update"
end
end
# Step 2.4: Update SECURITY.md if major/minor version changed
if is_major_or_minor_change "$old_ver" "$new_ver"
log_step "Update SECURITY.md"
update_security_md "$new_ver"
else
log_info "Skipping SECURITY.md update (patch release only)"
end
return 0
end
# Phase 3 removed - PKGBUILD updates not applicable to library
# ============================================================================
# Phase 4: Build and Release
# ============================================================================
function phase4_build_release
set -l new_ver $argv[1]
log_phase "3. Build and Test"
cd "$ARCH_TOOLKIT_DIR"
# Step 3.1: Run pre-commit checks (tests/checks)
log_step "Running pre-commit checks (tests and checks)"
dry_run_cmd "make -C dev/ pre-commit"
if test $status -ne 0
log_error "pre-commit checks failed"
if not confirm_continue "Continue despite pre-commit failure?"
return 1
end
else
log_success "pre-commit checks passed"
end
# Step 3.2: Build library with all features
log_step "Building library with all features"
dry_run_cmd "cargo build --all-features"
if test $status -ne 0
log_error "cargo build --all-features failed"
return 1
end
log_success "Library built successfully"
# Step 3.3: Commit and push all changes (before publish verification)
log_step "Committing and pushing changes"
if test "$DRY_RUN" = true
log_info "[DRY-RUN] Would commit all changes with message: Release v$new_ver"
log_info "[DRY-RUN] Would push to origin"
else
cd "$ARCH_TOOLKIT_DIR"
git add -A
git commit -m "Release v$new_ver"
if test $status -eq 0
log_success "Changes committed"
else
log_warn "Nothing to commit or commit failed"
end
git push origin
if test $status -eq 0
log_success "Changes pushed to origin"
else
log_error "Failed to push changes"
return 1
end
end
# Step 3.4: Dry-run crates.io publish (after commit)
log_step "Verifying crates.io publish (dry-run)"
dry_run_cmd "cargo publish --dry-run"
if test $status -ne 0
log_error "cargo publish --dry-run failed"
return 1
end
log_success "crates.io publish verification passed"
log_phase "4. Release"
# Step 4.1: Create git tag
log_step "Creating git tag"
set -l tag "v$new_ver"
if test "$DRY_RUN" = true
log_info "[DRY-RUN] Would create tag: $tag"
else
# Check if tag already exists
if git tag -l | grep -q "^$tag\$"
log_warn "Tag $tag already exists"
if confirm_continue "Delete and recreate tag?"
git tag -d "$tag"
git push origin --delete "$tag" 2>/dev/null
else
log_info "Skipping tag creation"
end
end
git tag "$tag"
if test $status -eq 0
log_success "Created tag: $tag"
else
log_error "Failed to create tag"
return 1
end
end
# Step 4.2: Push tag to GitHub
log_step "Pushing tag to GitHub"
dry_run_cmd "git push origin $tag"
if test $status -eq 0
log_success "Tag pushed to GitHub"
else
log_error "Failed to push tag"
return 1
end
# Step 4.3: Create GitHub release (binary uploaded by GitHub Action)
log_step "Creating GitHub Release"
set -l release_file "$ARCH_TOOLKIT_DIR/docs/RELEASE_v$new_ver.md"
# Determine if this is a prerelease (version < 1.0.0)
set -l prerelease_flag ""
if is_prerelease_version "$new_ver"
set prerelease_flag "--prerelease"
log_info "Version < 1.0.0: Creating as prerelease"
else
log_info "Version >= 1.0.0: Creating as stable release"
end
if test "$DRY_RUN" = true
log_info "[DRY-RUN] Would create GitHub release $tag with notes from $release_file"
log_info "[DRY-RUN] Binary will be uploaded by GitHub Action"
if test -n "$prerelease_flag"
log_info "[DRY-RUN] Release would be marked as prerelease"
end
else
if test -f "$release_file"
# Create release with notes (binary uploaded by GitHub Action)
if test -n "$prerelease_flag"
gh release create "$tag" \
--title "v$new_ver" \
--prerelease \
--notes-file "$release_file"
else
gh release create "$tag" \
--title "v$new_ver" \
--notes-file "$release_file"
end
if test $status -eq 0
log_success "GitHub release created (binary will be uploaded by GitHub Action)"
else
log_error "Failed to create GitHub release"
return 1
end
end
end
# Step 4.4: Publish to crates.io
log_step "Publishing to crates.io"
if test "$DRY_RUN" = true
log_info "[DRY-RUN] Would run 'cargo publish' to publish to crates.io"
else
cd "$ARCH_TOOLKIT_DIR"
cargo publish
if test $status -eq 0
log_success "Published to crates.io"
else
log_error "Failed to publish to crates.io"
if not confirm_continue "Continue anyway?"
return 1
end
end
end
return 0
end
# Phase 5 removed - AUR updates not applicable to library
# ============================================================================
# Prerequisites Check
# ============================================================================
function check_prerequisites
log_info "Checking prerequisites..."
set -l missing ""
# Check for required commands
if not command -q cursor
set missing $missing "cursor"
end
if not command -q gh
set missing $missing "gh"
end
if not command -q cargo
set missing $missing "cargo"
end
if not command -q git
set missing $missing "git"
end
if not command -q make
set missing $missing "make"
end
# python3 not required for library releases
if test -n "$missing"
log_error "Missing required commands: $missing"
return 1
end
# Check directories exist
if not test -d "$ARCH_TOOLKIT_DIR"
log_error "arch-toolkit directory not found: $ARCH_TOOLKIT_DIR"
return 1
end
log_success "All prerequisites met"
return 0
end
# ============================================================================
# Pre-flight Checks
# ============================================================================
function check_preflight
log_info "Running pre-flight checks..."
cd "$ARCH_TOOLKIT_DIR"
# Check if on main branch
set -l current_branch (git branch --show-current)
if test "$current_branch" != "main"
log_error "Not on main branch (current: $current_branch)"
if not confirm_continue "Continue on branch '$current_branch'?"
return 1
end
else
log_success "On main branch"
end
# Check for clean working directory
set -l git_status (git status --porcelain)
if test -n "$git_status"
log_error "Working directory is not clean"
log_info "Uncommitted changes:"
git status --short
echo
if not confirm_continue "Continue with uncommitted changes?"
return 1
end
else
log_success "Working directory is clean"
end
return 0
end
# ============================================================================
# SECURITY.md Update
# ============================================================================
function update_security_md
set -l new_ver $argv[1]
set -l security_file "$ARCH_TOOLKIT_DIR/SECURITY.md"
log_step "Updating SECURITY.md"
if test "$DRY_RUN" = true
log_info "[DRY-RUN] Would update SECURITY.md with new version $new_ver"
return 0
end
if not test -f "$security_file"
log_error "SECURITY.md not found at: $security_file"
return 1
end
# Parse version to get major.minor
set -l ver_parts (string split '.' $new_ver)
set -l major $ver_parts[1]
set -l minor $ver_parts[2]
set -l major_minor "$major.$minor"
# Create temporary file
set -l tmp_file (mktemp)
# Read file and process line by line
set -l in_table false
set -l table_updated false
set -l header_written false
set -l separator_written false
while read -l line
# Detect the start of the version table
if string match -qr -- '^\|\s*Version\s*\|\s*Supported' "$line"
set in_table true
set header_written false
set separator_written false
echo "$line" >> "$tmp_file"
set header_written true
continue
end
# Handle table separator line
if test "$in_table" = true; and string match -qr -- '^\|\s*-' "$line"
if not test "$separator_written" = true
echo "$line" >> "$tmp_file"
set separator_written true
# Add new version row after separator
echo "| $major_minor.x | :white_check_mark: |" >> "$tmp_file"
set table_updated true
end
continue
end
# Handle table rows
if test "$in_table" = true
if string match -qr -- '^\|\s*<' "$line"
# This is the "< X.Y.Z" line - update it
echo "| < $major_minor.0 | :x: |" >> "$tmp_file"
continue
else if string match -qr -- '^\|\s*[0-9]' "$line"
# Another version row - skip old version entries
continue
else if test -z (string trim "$line")
# Empty line - end of table
set in_table false
echo "$line" >> "$tmp_file"
continue
else if string match -qr -- '^##' "$line"
# New section - end of table
set in_table false
echo "" >> "$tmp_file"
echo "$line" >> "$tmp_file"
continue
else if not string match -qr -- '^\|' "$line"
# Not a table line anymore
set in_table false
echo "$line" >> "$tmp_file"
continue
end
end
# Write line as-is if not in table or not a table row
echo "$line" >> "$tmp_file"
end < "$security_file"
# Replace original file
mv "$tmp_file" "$security_file"
if test "$table_updated" = true
log_success "SECURITY.md updated: $major_minor.x is now supported"
else
log_warn "Could not find version table in SECURITY.md"
return 1
end
return 0
end
# ============================================================================
# CHANGELOG Update
# ============================================================================
function update_changelog
set -l new_ver $argv[1]
set -l changelog_file "$ARCH_TOOLKIT_DIR/CHANGELOG.md"
set -l release_file "$ARCH_TOOLKIT_DIR/docs/RELEASE_v$new_ver.md"
log_step "Updating CHANGELOG.md"
if test "$DRY_RUN" = true
log_info "[DRY-RUN] Would update CHANGELOG.md with release notes"
return 0
end
# Create CHANGELOG.md if it doesn't exist
if not test -f "$changelog_file"
log_info "Creating CHANGELOG.md..."
echo "# Changelog
All notable changes to arch-toolkit will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
---
" > "$changelog_file"
end
# Check if release file exists
if not test -f "$release_file"
log_warn "Release file not found: $release_file"
log_warn "Skipping CHANGELOG update"
return 0
end
# Get current date
set -l release_date (date +%Y-%m-%d)
# Create a temporary file for the new changelog
set -l tmp_file (mktemp)
# Check if this version already exists
set -l existing_version_line 0
set -l line_num 0
while read -l line
set line_num (math $line_num + 1)
if string match -r -- '^##\s*\[' "$line"
if string match -qr -- "\[$new_ver\]" "$line"
set existing_version_line $line_num
break
end
end
end < "$changelog_file"
if test $existing_version_line -gt 0
# Version exists - replace it in place
log_info "Version $new_ver already exists, replacing in place..."
# Find where the version entry ends (next --- separator or next version entry)
set -l version_start $existing_version_line
set -l version_end 0
set -l line_num 0
set -l in_version_section false
set -l found_start false
while read -l line
set line_num (math $line_num + 1)
# Check if we've reached the start of the version entry
if test $line_num -eq $version_start
set found_start true
set in_version_section true
continue
end
# If we're in the version section, look for the end
if test "$in_version_section" = true
# End at next --- separator (but not the one right after the version header)
if string match -qr -- '^---$' "$line"
if test $line_num -gt (math $version_start + 2)
set version_end $line_num
break
end
end
# Or end at next version entry
if string match -r -- '^##\s*\[' "$line"
set version_end $line_num
break
end
end
end < "$changelog_file"
# If we didn't find an end, use end of file
if test $version_end -eq 0
set version_end (wc -l < "$changelog_file" | string trim)
set version_end (math $version_end + 1)
end
# Write everything before the version entry
if test $version_start -gt 1
head -n (math $version_start - 1) "$changelog_file" > "$tmp_file"
else
touch "$tmp_file"
end
# Add the new version entry
echo "## [$new_ver] - $release_date" >> "$tmp_file"
echo "" >> "$tmp_file"
cat "$release_file" >> "$tmp_file"
echo "" >> "$tmp_file"
echo "---" >> "$tmp_file"
echo "" >> "$tmp_file"
# Append everything after the old version entry
tail -n +$version_end "$changelog_file" >> "$tmp_file"
else
# Version doesn't exist - add it to the top
log_info "Version $new_ver not found, adding to the top..."
# Find the first version entry (## [version])
set -l first_version_line (grep -n '^##\s*\[.*\]' "$changelog_file" | head -1 | cut -d: -f1)
if test -n "$first_version_line"; and test $first_version_line -gt 1
# Write everything before the first version entry (header if exists)
head -n (math $first_version_line - 1) "$changelog_file" > "$tmp_file"
# Add new version entry
echo "## [$new_ver] - $release_date" >> "$tmp_file"
echo "" >> "$tmp_file"
# Append release content (preserving newlines)
cat "$release_file" >> "$tmp_file"
# Add separator
echo "" >> "$tmp_file"
echo "---" >> "$tmp_file"
echo "" >> "$tmp_file"
# Append rest of changelog (starting from first version entry)
tail -n +$first_version_line "$changelog_file" >> "$tmp_file"
else if test -n "$first_version_line"
# First version is at line 1, just prepend new entry
echo "## [$new_ver] - $release_date" > "$tmp_file"
echo "" >> "$tmp_file"
cat "$release_file" >> "$tmp_file"
echo "" >> "$tmp_file"
echo "---" >> "$tmp_file"
echo "" >> "$tmp_file"
cat "$changelog_file" >> "$tmp_file"
else
# No version entry found, prepend new entry at the very top
# Check if file starts with a header (like "# Changelog")
set -l first_line (head -n 1 "$changelog_file")
set -l header_end 0
if string match -qr -- '^#\s+Changelog' "$first_line"
# File has a header, find where it ends (first blank line or first "## [version]" entry)
set -l line_num 1
while read -l line
set line_num (math $line_num + 1)
# Stop at first version entry (## [version])
if string match -qr -- '^##\s*\[.*\]' "$line"
set header_end (math $line_num - 1)
break
end
# If we hit a blank line after some header content, check if next line is a version entry
if test -z (string trim "$line"); and test $line_num -gt 2
# Check next line
set -l next_line (sed -n (math $line_num + 1)p "$changelog_file")
if string match -qr -- '^##\s*\[.*\]' "$next_line"
set header_end $line_num
break
end
end
end < "$changelog_file"
if test $header_end -gt 0
# Write header (up to header_end)
head -n $header_end "$changelog_file" > "$tmp_file"
echo "" >> "$tmp_file"
echo "## [$new_ver] - $release_date" >> "$tmp_file"
echo "" >> "$tmp_file"
cat "$release_file" >> "$tmp_file"
echo "" >> "$tmp_file"
echo "---" >> "$tmp_file"
echo "" >> "$tmp_file"
set -l next_line (math $header_end + 1)
tail -n +$next_line "$changelog_file" >> "$tmp_file"
else
# No header or versions, just write new entry
echo "# Changelog
All notable changes to arch-toolkit will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
---
" > "$tmp_file"
echo "## [$new_ver] - $release_date" >> "$tmp_file"
echo "" >> "$tmp_file"
cat "$release_file" >> "$tmp_file"
echo "" >> "$tmp_file"
echo "---" >> "$tmp_file"
echo "" >> "$tmp_file"
cat "$changelog_file" >> "$tmp_file"
end
else
# No header or versions, just write new entry
echo "## [$new_ver] - $release_date" > "$tmp_file"
echo "" >> "$tmp_file"
cat "$release_file" >> "$tmp_file"
echo "" >> "$tmp_file"
echo "---" >> "$tmp_file"
echo "" >> "$tmp_file"
cat "$changelog_file" >> "$tmp_file"
end
end
end
# Replace original file
mv "$tmp_file" "$changelog_file"
log_success "CHANGELOG.md updated"
return 0
end
# ============================================================================
# Main Function
# ============================================================================
function main
set -l new_version ""
# Parse arguments
for arg in $argv
switch $arg
case '--dry-run'
set DRY_RUN true
log_warn "DRY RUN MODE - No changes will be made"
case '-h' '--help'
echo "Usage: release.fish [--dry-run] [version]"
echo
echo "Options:"
echo " --dry-run Preview all changes without executing them"
echo " -h, --help Show this help message"
echo
echo "If version is not provided, you will be prompted to enter it."
return 0
case '*'
if test -z "$new_version"
set new_version $arg
end
end
end
# Print banner
echo
_bold_cyan; echo "╔════════════════════════════════════════════════════════════════════════╗"; _reset
_bold_cyan; echo "║ ARCH-TOOLKIT RELEASE AUTOMATION ║"; _reset
_bold_cyan; echo "╚════════════════════════════════════════════════════════════════════════╝"; _reset
echo
# Check prerequisites
check_prerequisites
if test $status -ne 0
return 1
end
# Run pre-flight checks
check_preflight
if test $status -ne 0
return 1
end
# Get version if not provided
if test -z "$new_version"
set -l current (get_current_version)
_cyan; echo -n "Enter new version (current: $current): "; _reset
read new_version
end
# Validate version
if not validate_semver "$new_version"
log_error "Invalid version format: $new_version (expected: X.Y.Z)"
return 1
end
# Confirm before starting
echo
_blue; echo -n "[INFO] "; _reset; echo -n "Release version: "; _bold; echo $new_version; _reset
_blue; echo -n "[INFO] "; _reset; echo -n "Current version: "; _bold; echo (get_current_version); _reset
echo
if not confirm_continue "Start release process?"
log_info "Release cancelled"
return 0
end
# Store old version before updating
set -l old_version (get_current_version)
# Execute phases
phase1_version_update "$new_version"
or return 1
phase2_documentation "$new_version" "$old_version"
or return 1
phase4_build_release "$new_version"
or return 1
# Final summary
echo
_bold_green; echo "╔════════════════════════════════════════════════════════════════════════╗"; _reset
_bold_green; echo "║ RELEASE COMPLETE! 🎉 ║"; _reset
_bold_green; echo "╚════════════════════════════════════════════════════════════════════════╝"; _reset
echo
log_success "Version $new_version has been released!"
echo
log_info "Don't forget to verify:"
echo " • GitHub release: https://github.com/Firstp1ck/arch-toolkit/releases"
echo " • Crates.io: https://crates.io/crates/arch-toolkit"
echo
return 0
end
# Run main
main $argv