#!/bin/bash set -e # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' print_status() { echo -e "${BLUE}[INFO]${NC} $1" >&2; } print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1" >&2; } print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1" >&2; } print_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; } # Global variables COMMIT_MESSAGE="" RELEASE_MODE=false VERSION_INCREMENT_TYPE="patch" # "patch", "minor", or "major" show_usage() { echo "Didactyl Increment and Push Script" echo "" echo "USAGE:" echo " $0 [OPTIONS] \"commit message\"" echo "" echo "COMMANDS:" echo " $0 \"commit message\" Default: increment patch, commit & push" echo " $0 -p \"commit message\" Increment patch version" echo " $0 -m \"commit message\" Increment minor version" echo " $0 -M \"commit message\" Increment major version" echo " $0 -r \"commit message\" Create release with assets (no version increment)" echo " $0 -r -m \"commit message\" Create release with minor version increment" echo " $0 -h Show this help message" echo "" echo "OPTIONS:" echo " -p, --patch Increment patch version (default)" echo " -m, --minor Increment minor version" echo " -M, --major Increment major version" echo " -r, --release Create release with assets" echo " -h, --help Show this help message" echo "" echo "EXAMPLES:" echo " $0 \"Fixed event signing bug\"" echo " $0 -m \"Added tool-use system\"" echo " $0 -M \"Breaking API changes\"" echo " $0 -r \"Release current version\"" echo " $0 -r -m \"Release with minor increment\"" echo "" echo "VERSION INCREMENT MODES:" echo " -p, --patch (default): Increment patch version (v0.1.0 → v0.1.1)" echo " -m, --minor: Increment minor version, zero patch (v0.1.0 → v0.2.0)" echo " -M, --major: Increment major version, zero minor+patch (v0.1.0 → v1.0.0)" echo "" echo "RELEASE MODE (-r flag):" echo " - Build static x86_64 and arm64 binaries using build_static.sh" echo " - Git add, commit, push, and create Gitea release with assets" echo " - Can be combined with version increment flags" echo "" echo "REQUIREMENTS FOR RELEASE MODE:" echo " - Gitea token in ~/.gitea_token for release uploads" echo " - Docker installed for static binary builds" } # Parse command line arguments while [[ $# -gt 0 ]]; do case $1 in -r|--release) RELEASE_MODE=true shift ;; -p|--patch) VERSION_INCREMENT_TYPE="patch" shift ;; -m|--minor) VERSION_INCREMENT_TYPE="minor" shift ;; -M|--major) VERSION_INCREMENT_TYPE="major" shift ;; -h|--help) show_usage exit 0 ;; *) # First non-flag argument is the commit message if [[ -z "$COMMIT_MESSAGE" ]]; then COMMIT_MESSAGE="$1" fi shift ;; esac done # Validate inputs if [[ -z "$COMMIT_MESSAGE" ]]; then print_error "Commit message is required" echo "" show_usage exit 1 fi # Check if we're in a git repository check_git_repo() { if ! git rev-parse --git-dir > /dev/null 2>&1; then print_error "Not in a git repository" exit 1 fi } # Ensure git identity exists so commits do not fail on clean machines ensure_git_identity() { local current_name="" local current_email="" current_name=$(git config --get user.name 2>/dev/null || true) current_email=$(git config --get user.email 2>/dev/null || true) if [[ -n "$current_name" && -n "$current_email" ]]; then return 0 fi print_warning "Git user.name / user.email not fully configured for this repository" local fallback_name local fallback_email fallback_name="${GIT_AUTHOR_NAME:-Didactyl User}" fallback_email="${GIT_AUTHOR_EMAIL:-didactyl@local}" if [[ -z "$current_name" ]]; then git config user.name "$fallback_name" print_status "Set local git user.name to '$fallback_name'" fi if [[ -z "$current_email" ]]; then git config user.email "$fallback_email" print_status "Set local git user.email to '$fallback_email'" fi } # Ensure origin remote uses SSH for git.laantungir.net to avoid HTTPS username prompts ensure_origin_ssh_remote() { local origin_url origin_url=$(git remote get-url origin 2>/dev/null || true) if [[ -z "$origin_url" ]]; then print_warning "No 'origin' remote found; skipping remote URL normalization" return 0 fi # Convert only this host from HTTPS to SSH (uses ~/.ssh/config Host git.laantungir.net) if [[ "$origin_url" =~ ^https://git\.laantungir\.net/(.+)\.git$ ]]; then local repo_path local ssh_url repo_path="${BASH_REMATCH[1]}" ssh_url="git@git.laantungir.net:${repo_path}.git" git remote set-url origin "$ssh_url" print_status "Updated origin remote to SSH: $ssh_url" fi } # Function to get current version and increment appropriately increment_version() { local increment_type="$1" # "patch", "minor", or "major" print_status "Getting current version..." # Get the highest version tag (not chronologically latest) LATEST_TAG=$(git tag -l 'v*.*.*' | sort -V | tail -n 1 || echo "") if [[ -z "$LATEST_TAG" ]]; then LATEST_TAG="v0.0.0" print_warning "No version tags found, starting from $LATEST_TAG" fi # Extract version components (remove 'v' prefix) VERSION=${LATEST_TAG#v} # Parse major.minor.patch using regex if [[ $VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then MAJOR=${BASH_REMATCH[1]} MINOR=${BASH_REMATCH[2]} PATCH=${BASH_REMATCH[3]} else print_error "Invalid version format in tag: $LATEST_TAG" print_error "Expected format: v0.1.0" exit 1 fi # Increment version based on type if [[ "$increment_type" == "major" ]]; then NEW_MAJOR=$((MAJOR + 1)) NEW_MINOR=0 NEW_PATCH=0 NEW_VERSION="v${NEW_MAJOR}.${NEW_MINOR}.${NEW_PATCH}" print_status "Major version increment: incrementing major version" elif [[ "$increment_type" == "minor" ]]; then NEW_MAJOR=$MAJOR NEW_MINOR=$((MINOR + 1)) NEW_PATCH=0 NEW_VERSION="v${NEW_MAJOR}.${NEW_MINOR}.${NEW_PATCH}" print_status "Minor version increment: incrementing minor version" else NEW_MAJOR=$MAJOR NEW_MINOR=$MINOR NEW_PATCH=$((PATCH + 1)) NEW_VERSION="v${NEW_MAJOR}.${NEW_MINOR}.${NEW_PATCH}" print_status "Patch version increment: incrementing patch version" fi print_status "Current version: $LATEST_TAG" print_status "New version: $NEW_VERSION" # Update version in src/main.h update_version_in_header "$NEW_VERSION" "$NEW_MAJOR" "$NEW_MINOR" "$NEW_PATCH" # Export for use in other functions export NEW_VERSION } # Function to update version macros in src/main.h update_version_in_header() { local new_version="$1" local major="$2" local minor="$3" local patch="$4" print_status "Updating version in src/main.h..." # Check if src/main.h exists if [[ ! -f "src/main.h" ]]; then print_error "src/main.h not found" exit 1 fi # Update DIDACTYL_VERSION macro sed -i "s/#define DIDACTYL_VERSION \".*\"/#define DIDACTYL_VERSION \"$new_version\"/" src/main.h # Update DIDACTYL_VERSION_MAJOR macro sed -i "s/#define DIDACTYL_VERSION_MAJOR [0-9]\+/#define DIDACTYL_VERSION_MAJOR $major/" src/main.h # Update DIDACTYL_VERSION_MINOR macro sed -i "s/#define DIDACTYL_VERSION_MINOR .*/#define DIDACTYL_VERSION_MINOR $minor/" src/main.h # Update DIDACTYL_VERSION_PATCH macro sed -i "s/#define DIDACTYL_VERSION_PATCH [0-9]\+/#define DIDACTYL_VERSION_PATCH $patch/" src/main.h print_success "Updated version in src/main.h to $new_version" } # Function to update README Current Status version and release comment update_readme_current_status() { local new_version="$1" local release_comment="$2" if [[ ! -f "README.md" ]]; then print_warning "README.md not found, skipping Current Status update" return 0 fi print_status "Updating README Current Status section..." if python3 - "$new_version" "$release_comment" <<'PY' import sys from pathlib import Path version = sys.argv[1] comment = sys.argv[2] path = Path("README.md") text = path.read_text(encoding="utf-8") lines = text.splitlines() heading_idx = None for i, line in enumerate(lines): if line.startswith("## Current Status"): heading_idx = i break if heading_idx is None: raise SystemExit("README Current Status heading not found") lines[heading_idx] = f"## Current Status — {version}" section_end = len(lines) for i in range(heading_idx + 1, len(lines)): if lines[i].startswith("## "): section_end = i break comment_line = f"> Last release update: {version} — {comment}" # Replace existing autogenerated release comment inside Current Status section existing_idx = None for i in range(heading_idx + 1, section_end): if lines[i].startswith("> Last release update:"): existing_idx = i break if existing_idx is not None: lines[existing_idx] = comment_line else: insert_at = None for i in range(heading_idx + 1, section_end): if lines[i].startswith("**Active build"): insert_at = i + 1 break if insert_at is None: insert_at = heading_idx + 1 payload = ["", comment_line] lines[insert_at:insert_at] = payload path.write_text("\n".join(lines) + "\n", encoding="utf-8") PY then print_success "Updated README Current Status section" else print_error "Failed to update README Current Status section" exit 1 fi } # Function to commit and push changes without creating a tag (tag already created) git_commit_and_push_no_tag() { print_status "Preparing git commit..." # Stage all changes if git add . > /dev/null 2>&1; then print_success "Staged all changes" else print_error "Failed to stage changes" exit 1 fi # Check if there are changes to commit if git diff --staged --quiet; then print_warning "No changes to commit" else # Commit changes if git commit -m "$NEW_VERSION - $COMMIT_MESSAGE" > /dev/null 2>&1; then print_success "Committed changes" else print_error "Failed to commit changes" print_error "git commit output:" git commit -m "$NEW_VERSION - $COMMIT_MESSAGE" 2>&1 || true exit 1 fi fi # Push changes print_status "Pushing to remote repository..." local current_branch current_branch=$(git branch --show-current 2>/dev/null || echo "") if git rev-parse --abbrev-ref --symbolic-full-name "@{u}" > /dev/null 2>&1; then local push_output if push_output=$(git push 2>&1); then print_success "Pushed changes" else print_error "Failed to push changes" print_error "git push output:" echo "$push_output" >&2 print_warning "If this is an SSH auth error, verify ~/.ssh/config and that the correct private key is available to SSH" exit 1 fi else if [[ -z "$current_branch" ]]; then print_error "Unable to determine current branch for push" exit 1 fi print_warning "No upstream configured for branch '$current_branch'; setting upstream to origin/$current_branch" local push_output if push_output=$(git push -u origin "$current_branch" 2>&1); then print_success "Pushed changes and configured upstream" else print_error "Failed to push changes while configuring upstream" print_error "git push output:" echo "$push_output" >&2 print_warning "If this is an SSH auth error, verify ~/.ssh/config and that the correct private key is available to SSH" exit 1 fi fi # Push only the new tag to avoid conflicts with existing tags local tag_push_output if tag_push_output=$(git push origin "$NEW_VERSION" 2>&1); then print_success "Pushed tag: $NEW_VERSION" else print_warning "Tag push failed, trying force push..." if tag_push_output=$(git push --force origin "$NEW_VERSION" 2>&1); then print_success "Force-pushed updated tag: $NEW_VERSION" else print_error "Failed to push tag: $NEW_VERSION" print_error "git tag push output:" echo "$tag_push_output" >&2 exit 1 fi fi } # Function to build release binaries build_release_binary() { print_status "Building release binaries (x86_64 + arm64)..." # Check if build_static.sh exists if [[ ! -f "build_static.sh" ]]; then print_error "build_static.sh not found" return 1 fi # Run the static build script for both platforms if ./build_static.sh --all-platforms > /dev/null 2>&1; then print_success "Built static binaries successfully" return 0 else print_error "Failed to build one or more static binaries" return 1 fi } # Function to upload release assets to Gitea upload_release_assets() { local release_id="$1" local binary_x86_path="$2" local binary_arm64_path="$3" print_status "Uploading release assets..." # Check for Gitea token if [[ ! -f "$HOME/.gitea_token" ]]; then print_warning "No ~/.gitea_token found. Skipping asset uploads." return 0 fi local token=$(cat "$HOME/.gitea_token" | tr -d '\n\r') local api_url="https://git.laantungir.net/api/v1/repos/laantungir/didactyl" local assets_url="$api_url/releases/$release_id/assets" print_status "Assets URL: $assets_url" # Upload x86_64 binary if [[ -f "$binary_x86_path" ]]; then print_status "Uploading binary: $(basename "$binary_x86_path")" # Retry loop for eventual consistency local max_attempts=3 local attempt=1 while [[ $attempt -le $max_attempts ]]; do print_status "Upload attempt $attempt/$max_attempts" local binary_response=$(curl -fS -X POST "$assets_url" \ -H "Authorization: token $token" \ -F "attachment=@$binary_x86_path;filename=$(basename "$binary_x86_path")" \ -F "name=$(basename "$binary_x86_path")") if echo "$binary_response" | grep -q '"id"'; then print_success "Uploaded x86_64 binary successfully" break else print_warning "Upload attempt $attempt failed" if [[ $attempt -lt $max_attempts ]]; then print_status "Retrying in 2 seconds..." sleep 2 else print_error "Failed to upload x86_64 binary after $max_attempts attempts" print_error "Response: $binary_response" fi fi ((attempt++)) done fi # Upload arm64 binary if [[ -f "$binary_arm64_path" ]]; then print_status "Uploading binary: $(basename "$binary_arm64_path")" local max_attempts=3 local attempt=1 while [[ $attempt -le $max_attempts ]]; do print_status "Upload attempt $attempt/$max_attempts" local binary_response=$(curl -fS -X POST "$assets_url" \ -H "Authorization: token $token" \ -F "attachment=@$binary_arm64_path;filename=$(basename "$binary_arm64_path")" \ -F "name=$(basename "$binary_arm64_path")") if echo "$binary_response" | grep -q '"id"'; then print_success "Uploaded arm64 binary successfully" break else print_warning "Upload attempt $attempt failed" if [[ $attempt -lt $max_attempts ]]; then print_status "Retrying in 2 seconds..." sleep 2 else print_error "Failed to upload arm64 binary after $max_attempts attempts" print_error "Response: $binary_response" fi fi ((attempt++)) done fi } # Function to create Gitea release create_gitea_release() { print_status "Creating Gitea release..." # Check for Gitea token if [[ ! -f "$HOME/.gitea_token" ]]; then print_warning "No ~/.gitea_token found. Skipping release creation." print_warning "Create ~/.gitea_token with your Gitea access token to enable releases." return 0 fi local token=$(cat "$HOME/.gitea_token" | tr -d '\n\r') local api_url="https://git.laantungir.net/api/v1/repos/laantungir/didactyl" # Create release print_status "Creating release $NEW_VERSION..." local response=$(curl -s -X POST "$api_url/releases" \ -H "Authorization: token $token" \ -H "Content-Type: application/json" \ -d "{\"tag_name\": \"$NEW_VERSION\", \"name\": \"$NEW_VERSION\", \"body\": \"$COMMIT_MESSAGE\"}") if echo "$response" | grep -q '"id"'; then print_success "Created release $NEW_VERSION" local release_id=$(echo "$response" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2) echo $release_id elif echo "$response" | grep -q "already exists"; then print_warning "Release $NEW_VERSION already exists" local check_response=$(curl -s -H "Authorization: token $token" "$api_url/releases/tags/$NEW_VERSION") if echo "$check_response" | grep -q '"id"'; then local release_id=$(echo "$check_response" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2) print_status "Using existing release ID: $release_id" echo $release_id else print_error "Could not find existing release ID" return 1 fi else print_error "Failed to create release $NEW_VERSION" print_error "Response: $response" local check_response=$(curl -s -H "Authorization: token $token" "$api_url/releases/tags/$NEW_VERSION") if echo "$check_response" | grep -q '"id"'; then print_warning "Release exists but creation response was unexpected" local release_id=$(echo "$check_response" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2) echo $release_id else print_error "Release does not exist and creation failed" return 1 fi fi } # Main execution main() { print_status "Didactyl Increment and Push Script" # Check prerequisites check_git_repo ensure_git_identity ensure_origin_ssh_remote if [[ "$RELEASE_MODE" == true ]]; then print_status "=== RELEASE MODE ===" # Only increment version if explicitly requested if [[ "$VERSION_INCREMENT_TYPE" != "patch" ]]; then increment_version "$VERSION_INCREMENT_TYPE" else LATEST_TAG=$(git tag -l 'v*.*.*' | sort -V | tail -n 1 || echo "v0.0.0") NEW_VERSION="$LATEST_TAG" export NEW_VERSION fi # Keep README Current Status in sync with the version and commit message update_readme_current_status "$NEW_VERSION" "$COMMIT_MESSAGE" # Create new git tag BEFORE compilation so version picks it up if git tag "$NEW_VERSION" > /dev/null 2>&1; then print_success "Created tag: $NEW_VERSION" else print_warning "Tag $NEW_VERSION already exists, removing and recreating..." git tag -d "$NEW_VERSION" > /dev/null 2>&1 git tag "$NEW_VERSION" > /dev/null 2>&1 fi # Commit and push git_commit_and_push_no_tag # Build release binaries local binary_x86_path="" local binary_arm64_path="" if build_release_binary; then [[ -f "build/didactyl_static_x86_64" ]] && binary_x86_path="build/didactyl_static_x86_64" [[ -f "build/didactyl_static_arm64" ]] && binary_arm64_path="build/didactyl_static_arm64" else print_warning "Binary build failed, continuing with release creation" if [[ -f "build/didactyl_static_x86_64" ]]; then print_status "Using existing x86_64 binary from previous build" binary_x86_path="build/didactyl_static_x86_64" fi if [[ -f "build/didactyl_static_arm64" ]]; then print_status "Using existing arm64 binary from previous build" binary_arm64_path="build/didactyl_static_arm64" fi fi # Create Gitea release local release_id="" if release_id=$(create_gitea_release); then if [[ "$release_id" =~ ^[0-9]+$ ]]; then if [[ -n "$release_id" && (-n "$binary_x86_path" || -n "$binary_arm64_path") ]]; then upload_release_assets "$release_id" "$binary_x86_path" "$binary_arm64_path" fi print_success "Release $NEW_VERSION completed successfully!" else print_error "Invalid release_id: $release_id" exit 1 fi else print_error "Release creation failed" fi else print_status "=== DEFAULT MODE ===" # Increment version based on type (default to patch) increment_version "$VERSION_INCREMENT_TYPE" # Keep README Current Status in sync with the version and commit message update_readme_current_status "$NEW_VERSION" "$COMMIT_MESSAGE" # Create new git tag BEFORE compilation so version picks it up if git tag "$NEW_VERSION" > /dev/null 2>&1; then print_success "Created tag: $NEW_VERSION" else print_warning "Tag $NEW_VERSION already exists, removing and recreating..." git tag -d "$NEW_VERSION" > /dev/null 2>&1 git tag "$NEW_VERSION" > /dev/null 2>&1 fi # Commit and push git_commit_and_push_no_tag print_success "Increment and push completed successfully!" print_status "Version $NEW_VERSION pushed to repository" fi } # Execute main function main