From 75726329c7f9ee821789fa0567c1efb5dbff25a0 Mon Sep 17 00:00:00 2001 From: "Sean P. Kane" Date: Mon, 12 Jan 2026 12:06:11 -0800 Subject: [PATCH] chore: Prepare for 0.6.1 release (#29) * fix: version in wiki-source.txt fix * chore: small release-fixes * chore: prep for 0.6.1 release --- .../freecad_mcp_bridge/__init__.py | 2 +- addon/FreecadRobustMCPBridge/wiki-source.txt | 2 +- just/release.just | 408 ++++++++++++++++-- .../CutObjectForMagnets.FCMacro | 2 +- .../README-CutObjectForMagnets.md | 2 +- macros/Cut_Object_for_Magnets/wiki-source.txt | 2 +- macros/Multi_Export/MultiExport.FCMacro | 2 +- macros/Multi_Export/README-MultiExport.md | 2 +- macros/Multi_Export/wiki-source.txt | 2 +- package.xml | 6 +- scripts/release-helpers.sh | 90 ++++ 11 files changed, 477 insertions(+), 43 deletions(-) create mode 100755 scripts/release-helpers.sh diff --git a/addon/FreecadRobustMCPBridge/freecad_mcp_bridge/__init__.py b/addon/FreecadRobustMCPBridge/freecad_mcp_bridge/__init__.py index e698dca..dd72da3 100644 --- a/addon/FreecadRobustMCPBridge/freecad_mcp_bridge/__init__.py +++ b/addon/FreecadRobustMCPBridge/freecad_mcp_bridge/__init__.py @@ -9,5 +9,5 @@ It is bundled with the workbench addon for self-contained installation. from .server import FreecadMCPPlugin -__version__ = "0.6.0" # Updated by release workflow +__version__ = "0.6.1" # Updated by release workflow __all__ = ["FreecadMCPPlugin", "__version__"] diff --git a/addon/FreecadRobustMCPBridge/wiki-source.txt b/addon/FreecadRobustMCPBridge/wiki-source.txt index c70c1b0..7fd6f2c 100644 --- a/addon/FreecadRobustMCPBridge/wiki-source.txt +++ b/addon/FreecadRobustMCPBridge/wiki-source.txt @@ -10,7 +10,7 @@ |Icon=FreecadRobustMCPBridge.svg |Description=Bridge workbench designed to provide an interface between FreeCAD and the Robust MCP Server to enable AI assistants (like Claude) to control FreeCAD via the Model Context Protocol (MCP). It provides XML-RPC and JSON-RPC interfaces for external automation. |Author=Sean P. Kane -|Version=0.6.0 +|Version=0.6.1 |Date=2026-01-12 |FCVersion=0.21+ |Download=[https://github.com/spkane/freecad-robust-mcp-and-more/releases Latest Release] diff --git a/just/release.just b/just/release.just index f27c769..31adfaf 100644 --- a/just/release.just +++ b/just/release.just @@ -208,19 +208,14 @@ bump-macro-export version: (_bump-macro "Multi_Export" "Multi Export" "MultiExpo tag-mcp-server version: #!/usr/bin/env bash set -euo pipefail + + # Source shared release helper functions + . "{{project_root}}/scripts/release-helpers.sh" + TAG="robust-mcp-server-v{{version}}" - # Validate version format - if [[ ! "{{version}}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then - echo "ERROR: Version '{{version}}' is not valid semver (X.Y.Z or X.Y.Z-prerelease)" - exit 1 - fi - - # Check for uncommitted changes - if ! git diff --quiet HEAD; then - echo "ERROR: You have uncommitted changes. Please commit or stash them first." - exit 1 - fi + # Run all pre-release checks (version format, main branch, up-to-date, clean tree) + pre_release_checks "{{version}}" || exit 1 echo "Creating tag: $TAG" echo "" @@ -246,19 +241,15 @@ tag-mcp-server version: tag-workbench version: #!/usr/bin/env bash set -euo pipefail + + # Source shared release helper functions + . "{{project_root}}/scripts/release-helpers.sh" + TAG="robust-mcp-workbench-v{{version}}" VERSION="{{version}}" - if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then - echo "ERROR: Version '$VERSION' is not valid semver" - exit 1 - fi - - # Check for uncommitted changes - if ! git diff --quiet HEAD; then - echo "ERROR: You have uncommitted changes. Please commit or stash them first." - exit 1 - fi + # Run all pre-release checks (version format, main branch, up-to-date, clean tree) + pre_release_checks "$VERSION" || exit 1 # Verify version in source files matches tag version echo "Verifying version in source files..." @@ -332,22 +323,16 @@ _tag-macro macro_dir macro_name macro_file_basename tag_prefix bump_command tag_ #!/usr/bin/env bash set -euo pipefail + # Source shared release helper functions + . "{{project_root}}/scripts/release-helpers.sh" + VERSION="{{version}}" TAG="{{tag_prefix}}v{{version}}" MACRO_DIR="{{project_root}}/macros/{{macro_dir}}" MACRO_NAME="{{macro_name}}" - # Validate version format (SemVer 2.0) - if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then - echo "ERROR: Version '$VERSION' is not valid semver" - exit 1 - fi - - # Check for uncommitted changes - if ! git diff --quiet HEAD; then - echo "ERROR: You have uncommitted changes. Please commit or stash them first." - exit 1 - fi + # Run all pre-release checks (version format, main branch, up-to-date, clean tree) + pre_release_checks "$VERSION" || exit 1 # Verify version in source files matches tag version echo "Verifying version in source files..." @@ -536,6 +521,365 @@ delete-tag tag: echo "" echo "Tag '{{tag}}' deleted." +# Show information about a release (useful before rollback) +show-release-info tag: + #!/usr/bin/env bash + set -euo pipefail + + TAG="{{tag}}" + + echo "==========================================" + echo "Release Information: $TAG" + echo "==========================================" + echo "" + + # Check if tag exists locally + if git tag -l "$TAG" | grep -qF "$TAG"; then + echo "Local tag: EXISTS" + COMMIT=$(git rev-parse "$TAG" 2>/dev/null) + echo " Commit: $COMMIT" + echo " Date: $(git log -1 --format=%ai "$TAG")" + echo " Author: $(git log -1 --format='%an <%ae>' "$TAG")" + else + echo "Local tag: NOT FOUND" + fi + + # Check if tag exists on remote + if git ls-remote --tags origin | grep -qF "refs/tags/$TAG"; then + echo "Remote tag: EXISTS" + else + echo "Remote tag: NOT FOUND" + fi + + echo "" + + # Check which branches contain the tag + echo "Branches containing this tag:" + BRANCHES=$(git branch -a --contains "$TAG" 2>/dev/null || true) + if [ -n "$BRANCHES" ]; then + echo "$BRANCHES" | sed 's/^/ /' + else + echo " (none or tag not found)" + fi + + echo "" + + # Check if GitHub Release exists + echo "GitHub Release:" + if command -v gh &> /dev/null; then + if gh release view "$TAG" --json tagName,name,isDraft,isPrerelease,createdAt &>/dev/null; then + gh release view "$TAG" --json tagName,name,isDraft,isPrerelease,createdAt | \ + jq -r '" Name: \(.name)\n Draft: \(.isDraft)\n Prerelease: \(.isPrerelease)\n Created: \(.createdAt)"' + echo "" + echo " Assets:" + gh release view "$TAG" --json assets | jq -r '.assets[].name' | sed 's/^/ - /' + else + echo " NOT FOUND (no GitHub Release for this tag)" + fi + else + echo " (gh CLI not installed - cannot check)" + fi + + echo "" + + # Determine component type and show additional info + case "$TAG" in + robust-mcp-server-v*) + VERSION="${TAG#robust-mcp-server-v}" + echo "Component: Robust MCP Server" + echo "Version: $VERSION" + echo "" + echo "Published to:" + echo " - PyPI: https://pypi.org/project/freecad-robust-mcp/$VERSION/" + echo " - Docker Hub: spkane/freecad-robust-mcp:$VERSION" + echo "" + echo "NOTE: PyPI packages cannot be fully deleted, only yanked." + echo " Docker tags can be deleted from Docker Hub." + ;; + robust-mcp-workbench-v*) + VERSION="${TAG#robust-mcp-workbench-v}" + echo "Component: Robust MCP Bridge Workbench" + echo "Version: $VERSION" + ;; + macro-cut-object-for-magnets-v*) + VERSION="${TAG#macro-cut-object-for-magnets-v}" + echo "Component: Cut Object for Magnets Macro" + echo "Version: $VERSION" + ;; + macro-multi-export-v*) + VERSION="${TAG#macro-multi-export-v}" + echo "Component: Multi Export Macro" + echo "Version: $VERSION" + ;; + *) + echo "Component: Unknown (not a recognized release tag format)" + ;; + esac + +# Rollback a release (delete tag and GitHub Release) +rollback-release tag: + #!/usr/bin/env bash + set -euo pipefail + + TAG="{{tag}}" + + echo "==========================================" + echo "ROLLBACK RELEASE: $TAG" + echo "==========================================" + echo "" + + # Determine component type and version + COMPONENT="" + VERSION="" + case "$TAG" in + robust-mcp-server-v*) + COMPONENT="mcp-server" + VERSION="${TAG#robust-mcp-server-v}" + ;; + robust-mcp-workbench-v*) + COMPONENT="workbench" + VERSION="${TAG#robust-mcp-workbench-v}" + ;; + macro-cut-object-for-magnets-v*) + COMPONENT="macro-magnets" + VERSION="${TAG#macro-cut-object-for-magnets-v}" + ;; + macro-multi-export-v*) + COMPONENT="macro-export" + VERSION="${TAG#macro-multi-export-v}" + ;; + *) + COMPONENT="unknown" + ;; + esac + + # Show what will be affected + echo "This will:" + echo " 1. Delete the GitHub Release (if exists)" + echo " 2. Delete the git tag (local and remote)" + if [ "$COMPONENT" = "mcp-server" ]; then + echo " 3. Optionally yank from PyPI" + echo " 4. Provide Docker Hub cleanup instructions" + fi + echo "" + + # Check what exists + LOCAL_TAG_EXISTS=false + REMOTE_TAG_EXISTS=false + GH_RELEASE_EXISTS=false + + if git tag -l "$TAG" | grep -qF "$TAG"; then + LOCAL_TAG_EXISTS=true + echo " Local tag: WILL BE DELETED" + else + echo " Local tag: not found (already deleted?)" + fi + + if git ls-remote --tags origin | grep -qF "refs/tags/$TAG"; then + REMOTE_TAG_EXISTS=true + echo " Remote tag: WILL BE DELETED" + else + echo " Remote tag: not found (already deleted?)" + fi + + if command -v gh &> /dev/null; then + if gh release view "$TAG" &>/dev/null; then + GH_RELEASE_EXISTS=true + echo " GitHub Release: WILL BE DELETED" + else + echo " GitHub Release: not found (already deleted?)" + fi + else + echo " GitHub Release: (gh CLI not installed - cannot check/delete)" + fi + + echo "" + + # Component-specific info + case "$COMPONENT" in + mcp-server) + echo "Component: Robust MCP Server v$VERSION" + echo "" + echo "IMPORTANT: MCP Server releases publish to multiple registries:" + echo " - PyPI: Package cannot be deleted, only yanked (hidden from pip install)" + echo " - Docker Hub: Tags can be deleted via web UI or API" + echo "" + ;; + workbench) + echo "Component: Robust MCP Bridge Workbench v$VERSION" + echo "" + echo "This release only creates a GitHub Release with archive files." + echo "Rollback will fully clean up this release." + echo "" + ;; + macro-magnets) + echo "Component: Cut Object for Magnets Macro v$VERSION" + echo "" + echo "This release only creates a GitHub Release with archive files." + echo "Rollback will fully clean up this release." + echo "" + ;; + macro-export) + echo "Component: Multi Export Macro v$VERSION" + echo "" + echo "This release only creates a GitHub Release with archive files." + echo "Rollback will fully clean up this release." + echo "" + ;; + *) + echo "WARNING: Unrecognized tag format. Proceeding with generic rollback." + echo "" + ;; + esac + + # Nothing to do? + if [ "$LOCAL_TAG_EXISTS" = false ] && [ "$REMOTE_TAG_EXISTS" = false ] && [ "$GH_RELEASE_EXISTS" = false ]; then + echo "Nothing to rollback - tag and release not found." + exit 0 + fi + + read -p "Proceed with rollback? [y/N] " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Aborted." + exit 1 + fi + + echo "" + + # Delete GitHub Release first (before tag, since release references tag) + if [ "$GH_RELEASE_EXISTS" = true ]; then + echo "Deleting GitHub Release..." + if gh release delete "$TAG" --yes; then + echo " ✓ GitHub Release deleted." + else + echo " WARNING: Failed to delete GitHub Release." + fi + fi + + # Delete remote tag + if [ "$REMOTE_TAG_EXISTS" = true ]; then + echo "Deleting remote tag..." + if git push origin --delete "$TAG"; then + echo " ✓ Remote tag deleted." + else + echo " WARNING: Failed to delete remote tag." + fi + fi + + # Delete local tag + if [ "$LOCAL_TAG_EXISTS" = true ]; then + echo "Deleting local tag..." + if git tag -d "$TAG"; then + echo " ✓ Local tag deleted." + else + echo " WARNING: Failed to delete local tag." + fi + fi + + echo "" + echo "==========================================" + echo "Git/GitHub Rollback complete!" + echo "==========================================" + + # MCP Server: Offer to yank from PyPI and provide Docker cleanup info + if [ "$COMPONENT" = "mcp-server" ]; then + echo "" + echo "--- PyPI Cleanup ---" + echo "" + echo "Would you like to yank version $VERSION from PyPI?" + echo " - Yanking hides the version from 'pip install freecad-robust-mcp'" + echo " - Users who pin to this version can still install it" + echo " - This can be undone later with 'twine yank --undo'" + echo "" + read -p "Yank from PyPI? [y/N] " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + echo "" + echo "Attempting to yank from PyPI..." + echo "(This requires PyPI authentication - you may be prompted to login)" + echo "" + if command -v twine &> /dev/null || uv run twine --version &>/dev/null; then + if uv run twine yank freecad-robust-mcp "$VERSION" 2>&1; then + echo " ✓ Version $VERSION yanked from PyPI." + else + echo "" + echo " Yank failed. You can try manually:" + echo " uv run twine yank freecad-robust-mcp $VERSION" + echo "" + echo " Or use the PyPI web interface:" + echo " https://pypi.org/manage/project/freecad-robust-mcp/release/$VERSION/" + fi + else + echo " twine not available. Install with: uv add twine" + echo "" + echo " Or use the PyPI web interface:" + echo " https://pypi.org/manage/project/freecad-robust-mcp/release/$VERSION/" + fi + else + echo "" + echo "Skipped PyPI yank. You can do this later with:" + echo " uv run twine yank freecad-robust-mcp $VERSION" + echo "" + echo "Or via web interface:" + echo " https://pypi.org/manage/project/freecad-robust-mcp/release/$VERSION/" + fi + + echo "" + echo "--- Docker Hub Cleanup ---" + echo "" + echo "Docker Hub tags must be deleted manually." + echo "Tags to delete: $VERSION, latest (if this was the latest release)" + echo "" + echo "Web interface:" + echo " https://hub.docker.com/r/spkane/freecad-robust-mcp/tags" + echo "" + echo "Or via API (requires DOCKER_HUB_TOKEN):" + echo " curl -X DELETE -H \"Authorization: Bearer \$DOCKER_HUB_TOKEN\" \\" + echo " https://hub.docker.com/v2/repositories/spkane/freecad-robust-mcp/tags/$VERSION/" + fi + + echo "" + echo "==========================================" + echo "Rollback process complete!" + echo "==========================================" + +# Delete only the GitHub Release (keep tag) +delete-github-release tag: + #!/usr/bin/env bash + set -euo pipefail + + TAG="{{tag}}" + + if ! command -v gh &> /dev/null; then + echo "ERROR: gh CLI is required but not installed." + echo "Install: https://cli.github.com/" + exit 1 + fi + + if ! gh release view "$TAG" &>/dev/null; then + echo "No GitHub Release found for tag: $TAG" + exit 0 + fi + + echo "This will delete the GitHub Release for: $TAG" + echo "(The git tag will be preserved)" + echo "" + + gh release view "$TAG" --json name,createdAt,assets | \ + jq -r '"Release: \(.name)\nCreated: \(.createdAt)\nAssets: \(.assets | length)"' + + echo "" + read -p "Delete this release? [y/N] " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Aborted." + exit 1 + fi + + gh release delete "$TAG" --yes + echo "GitHub Release deleted. Tag '$TAG' preserved." + # ============================================================================= # Release Status # ============================================================================= diff --git a/macros/Cut_Object_for_Magnets/CutObjectForMagnets.FCMacro b/macros/Cut_Object_for_Magnets/CutObjectForMagnets.FCMacro index b801e6f..faf6809 100644 --- a/macros/Cut_Object_for_Magnets/CutObjectForMagnets.FCMacro +++ b/macros/Cut_Object_for_Magnets/CutObjectForMagnets.FCMacro @@ -21,7 +21,7 @@ Usage: __Name__ = "Cut Object for Magnets" __Comment__ = "Cut an object along a plane and add aligned magnet holes with surface collision detection" __Author__ = "Sean P. Kane" -__Version__ = "0.6.0" +__Version__ = "0.6.1" __Date__ = "2026-01-12" __License__ = "MIT" __Web__ = "https://github.com/spkane/freecad-robust-mcp-and-more" diff --git a/macros/Cut_Object_for_Magnets/README-CutObjectForMagnets.md b/macros/Cut_Object_for_Magnets/README-CutObjectForMagnets.md index 0d9cb0c..79126fe 100644 --- a/macros/Cut_Object_for_Magnets/README-CutObjectForMagnets.md +++ b/macros/Cut_Object_for_Magnets/README-CutObjectForMagnets.md @@ -2,7 +2,7 @@ ![Macro Icon](CutObjectForMagnets.svg) -**Version:** 0.6.0 +**Version:** 0.6.1 **FreeCAD Version:** 0.19 or later **License:** MIT diff --git a/macros/Cut_Object_for_Magnets/wiki-source.txt b/macros/Cut_Object_for_Magnets/wiki-source.txt index 957b769..a0e6c72 100644 --- a/macros/Cut_Object_for_Magnets/wiki-source.txt +++ b/macros/Cut_Object_for_Magnets/wiki-source.txt @@ -7,7 +7,7 @@ |Icon=Macro_Cut_Object_For_Magnets.png |Description=Cut an object along a plane and add aligned magnet holes with surface collision detection. Creates two parts with perfectly aligned holes for embedding magnets that allow the parts to snap together. |Author=Sean P. Kane -|Version=0.6.0 +|Version=0.6.1 |Date=2026-01-12 |FCVersion=0.19+ |Download=[https://wiki.freecad.org/images/thumb/e/e3/Macro_Cut_Object_For_Magnets.png/48px-Macro_Cut_Object_For_Magnets.png ToolBar Icon] diff --git a/macros/Multi_Export/MultiExport.FCMacro b/macros/Multi_Export/MultiExport.FCMacro index e73547f..26a62ce 100644 --- a/macros/Multi_Export/MultiExport.FCMacro +++ b/macros/Multi_Export/MultiExport.FCMacro @@ -22,7 +22,7 @@ Usage: __Name__ = "Multi Export" __Comment__ = "Export selected bodies to multiple file formats (STL, STEP, 3MF, OBJ, IGES, BREP, PLY, AMF) simultaneously" __Author__ = "Sean P. Kane" -__Version__ = "0.6.0" +__Version__ = "0.6.1" __Date__ = "2026-01-12" __License__ = "MIT" __Web__ = "https://github.com/spkane/freecad-robust-mcp-and-more" diff --git a/macros/Multi_Export/README-MultiExport.md b/macros/Multi_Export/README-MultiExport.md index 845778b..503316f 100644 --- a/macros/Multi_Export/README-MultiExport.md +++ b/macros/Multi_Export/README-MultiExport.md @@ -2,7 +2,7 @@ ![Macro Icon](MultiExport.svg) -**Version:** 0.6.0 +**Version:** 0.6.1 **FreeCAD Version:** 0.19 or later **License:** MIT diff --git a/macros/Multi_Export/wiki-source.txt b/macros/Multi_Export/wiki-source.txt index 315744b..a304a37 100644 --- a/macros/Multi_Export/wiki-source.txt +++ b/macros/Multi_Export/wiki-source.txt @@ -7,7 +7,7 @@ |Icon=Macro_Multi_Export.png |Description=Export selected bodies to multiple file formats (STL, STEP, 3MF, OBJ, IGES, BREP, PLY, AMF) simultaneously with a user-friendly dialog for format selection and output configuration. |Author=Sean P. Kane -|Version=0.6.0 +|Version=0.6.1 |Date=2026-01-12 |FCVersion=0.21+ |Download=[https://wiki.freecad.org/images/thumb/1/19/Macro_Multi_Export.png/48px-Macro_Multi_Export.png ToolBar Icon] diff --git a/package.xml b/package.xml index 3a125d5..4753f79 100644 --- a/package.xml +++ b/package.xml @@ -37,7 +37,7 @@ Robust MCP Bridge - 0.6.0 + 0.6.1 2026-01-12 Robust MCP Bridge workbench for AI assistant integration with FreeCAD. Uses the Model Context Protocol (MCP) to connect AI assistants like Claude Code and Cursor to FreeCAD. Works in both GUI mode (with full visual features including screenshots, colors, and camera control) and headless mode (for automation and CI/CD). Provides toolbar commands to start, stop, and monitor the MCP bridge server. Supports XML-RPC and JSON-RPC protocols with 82+ CAD tools. FreecadRobustMCPBridgeWorkbench @@ -48,7 +48,7 @@ Multi Export - 0.6.0 + 0.6.1 2026-01-12 Export selected bodies to multiple file formats (STL, STEP, 3MF, OBJ, IGES, BREP, PLY, AMF) simultaneously with configurable mesh options. ./macros/Multi_Export/ @@ -58,7 +58,7 @@ Cut Object for Magnets - 0.6.0 + 0.6.1 2026-01-12 Cut an object along a plane and add aligned magnet holes with surface collision detection. Perfect for creating 3D printed parts that snap together with embedded magnets. ./macros/Cut_Object_for_Magnets/ diff --git a/scripts/release-helpers.sh b/scripts/release-helpers.sh new file mode 100755 index 0000000..b29bfa2 --- /dev/null +++ b/scripts/release-helpers.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# Shared helper functions for release management +# Source this file in Just recipes: . ./scripts/release-helpers.sh + +# Validate semver version format +# Usage: validate_semver "1.0.0" || exit 1 +validate_semver() { + local version="$1" + if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then + echo "ERROR: Version '$version' is not valid semver (X.Y.Z or X.Y.Z-prerelease)" + echo "Examples: 1.0.0, 1.0.0-alpha, 1.0.0-beta.1, 1.0.0-rc.1" + return 1 + fi + return 0 +} + +# Verify we're on the main branch +# Usage: require_main_branch || exit 1 +require_main_branch() { + local current_branch + current_branch=$(git rev-parse --abbrev-ref HEAD) + if [[ "$current_branch" != "main" ]]; then + echo "ERROR: Release tags must be created from the 'main' branch." + echo " Current branch: $current_branch" + echo "" + echo "Please merge your changes to main first, then run this command from main." + return 1 + fi + return 0 +} + +# Verify local main is up to date with origin/main +# Usage: require_up_to_date || exit 1 +require_up_to_date() { + # Fetch latest from origin (quietly) + git fetch origin main --quiet 2>/dev/null || { + echo "WARNING: Could not fetch from origin. Continuing with local state." + return 0 + } + + local local_sha remote_sha base_sha + local_sha=$(git rev-parse HEAD) + remote_sha=$(git rev-parse origin/main 2>/dev/null) || { + echo "WARNING: Could not find origin/main. Continuing with local state." + return 0 + } + + if [[ "$local_sha" != "$remote_sha" ]]; then + # Check if we're behind, ahead, or diverged + base_sha=$(git merge-base HEAD origin/main 2>/dev/null) + + if [[ "$base_sha" == "$local_sha" ]]; then + echo "ERROR: Local main is behind origin/main." + echo " Please run: git pull origin main" + return 1 + elif [[ "$base_sha" == "$remote_sha" ]]; then + echo "ERROR: Local main is ahead of origin/main." + echo " Please push your changes first: git push origin main" + return 1 + else + echo "ERROR: Local main has diverged from origin/main." + echo " Please reconcile the branches before releasing." + return 1 + fi + fi + return 0 +} + +# Check for uncommitted changes +# Usage: require_clean_tree || exit 1 +require_clean_tree() { + if ! git diff --quiet HEAD 2>/dev/null; then + echo "ERROR: You have uncommitted changes. Please commit or stash them first." + return 1 + fi + return 0 +} + +# Run all pre-release checks +# Usage: pre_release_checks "1.0.0" || exit 1 +pre_release_checks() { + local version="$1" + + validate_semver "$version" || return 1 + require_main_branch || return 1 + require_up_to_date || return 1 + require_clean_tree || return 1 + + return 0 +}