#!/bin/bash # ============================================================================= # MedAssist Release Script (non-interactive) # ============================================================================= # Usage: # ./scripts/release.sh patch # 1.0.0 -> 1.0.1 (bugfixes) # ./scripts/release.sh minor # 1.0.0 -> 1.1.0 (new features) # ./scripts/release.sh major # 1.0.0 -> 2.0.0 (breaking changes) # ./scripts/release.sh 1.2.3 # explicit version # # Fully non-interactive: no y/N prompts. Designed to be called by AI agents # or CI systems. Creates a PR for the version bump (required due to branch # protection), waits for CI with retry logic, merges, and creates a signed tag. # ============================================================================= set -e # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Configuration GITHUB_REPO="DanielVolz/medassist-ng" CI_POLL_INTERVAL=15 # seconds between CI status polls CI_INITIAL_DELAY=20 # seconds to wait before first CI check CI_MAX_WAIT=600 # maximum seconds to wait for CI (10 minutes) # Get script directory and project root SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" cd "$PROJECT_ROOT" # Detect git remote name (prefer 'origin', fall back to 'github') detect_remote() { if git remote | grep -q '^origin$'; then echo "origin" elif git remote | grep -q '^github$'; then echo "github" else echo -e "${RED}Error: No 'origin' or 'github' remote found.${NC}" >&2 exit 1 fi } GIT_REMOTE=$(detect_remote) # Wait for CI checks on a PR with retry logic # GitHub Actions can take 10-30 seconds before checks are reported. # This function polls until checks appear and then watches them. wait_for_ci() { local pr_number="$1" local elapsed=0 echo -e "${BLUE}Waiting ${CI_INITIAL_DELAY}s for CI checks to be registered...${NC}" sleep "${CI_INITIAL_DELAY}" elapsed=$CI_INITIAL_DELAY while [[ $elapsed -lt $CI_MAX_WAIT ]]; do # Check if any checks have been reported local check_output check_output=$(gh pr checks "${pr_number}" --repo "${GITHUB_REPO}" 2>&1) || true if echo "$check_output" | grep -q "no checks reported"; then echo -e "${YELLOW}No checks reported yet (${elapsed}s elapsed). Retrying in ${CI_POLL_INTERVAL}s...${NC}" sleep "${CI_POLL_INTERVAL}" elapsed=$((elapsed + CI_POLL_INTERVAL)) continue fi # Checks are registered — use --watch to wait for completion echo -e "${BLUE}CI checks registered. Watching for completion...${NC}" if gh pr checks "${pr_number}" --repo "${GITHUB_REPO}" --watch; then echo -e "${GREEN}CI checks passed!${NC}" return 0 else echo -e "${RED}CI checks failed!${NC}" return 1 fi done echo -e "${RED}Timed out waiting for CI checks after ${CI_MAX_WAIT}s.${NC}" return 1 } # ─── Preflight checks ──────────────────────────────────────────────────────── if ! command -v gh &> /dev/null; then echo -e "${RED}Error: GitHub CLI (gh) is required but not installed.${NC}" echo "Install it with: brew install gh" exit 1 fi if ! gh auth status &> /dev/null; then echo -e "${RED}Error: Not authenticated with GitHub CLI.${NC}" echo "Run: gh auth login" exit 1 fi if [[ -n $(git status --porcelain) ]]; then echo -e "${RED}Error: You have uncommitted changes. Commit or stash them first.${NC}" git status --short exit 1 fi # ─── Determine version ─────────────────────────────────────────────────────── echo -e "${BLUE}Updating main branch...${NC}" git checkout main git pull "${GIT_REMOTE}" main CURRENT_VERSION=$(grep '"version"' backend/package.json | sed 's/.*"version": "\(.*\)".*/\1/') echo -e "${BLUE}Current version: ${YELLOW}v${CURRENT_VERSION}${NC}" if [[ -z "$1" ]]; then echo -e "${RED}Usage: $0 ${NC}" exit 1 fi case "$1" in patch) IFS='.' read -r major minor patch <<< "$CURRENT_VERSION" NEW_VERSION="$major.$minor.$((patch + 1))" ;; minor) IFS='.' read -r major minor patch <<< "$CURRENT_VERSION" NEW_VERSION="$major.$((minor + 1)).0" ;; major) IFS='.' read -r major minor patch <<< "$CURRENT_VERSION" NEW_VERSION="$((major + 1)).0.0" ;; *) if [[ ! "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo -e "${RED}Invalid version format. Use: x.y.z${NC}" exit 1 fi NEW_VERSION="$1" ;; esac echo -e "${GREEN}Releasing: ${YELLOW}v${CURRENT_VERSION} → v${NEW_VERSION}${NC}" # ─── Create release branch and PR ──────────────────────────────────────────── RELEASE_BRANCH="chore/release-${NEW_VERSION}" if git show-ref --verify --quiet "refs/heads/${RELEASE_BRANCH}"; then echo -e "${YELLOW}Branch ${RELEASE_BRANCH} already exists locally. Deleting...${NC}" git branch -D "${RELEASE_BRANCH}" fi echo -e "${BLUE}Creating release branch...${NC}" git checkout -b "${RELEASE_BRANCH}" echo -e "${BLUE}Updating package.json files...${NC}" sed -i '' "s/\"version\": \"${CURRENT_VERSION}\"/\"version\": \"${NEW_VERSION}\"/" backend/package.json sed -i '' "s/\"version\": \"${CURRENT_VERSION}\"/\"version\": \"${NEW_VERSION}\"/" frontend/package.json 2>/dev/null || true echo -e "${BLUE}Committing version bump...${NC}" git add backend/package.json frontend/package.json 2>/dev/null || git add backend/package.json git commit -m "chore: release v${NEW_VERSION}" echo -e "${BLUE}Pushing release branch...${NC}" git push -u "${GIT_REMOTE}" "${RELEASE_BRANCH}" echo -e "${BLUE}Creating Pull Request...${NC}" PR_URL=$(gh pr create \ --repo "${GITHUB_REPO}" \ --head "${RELEASE_BRANCH}" \ --title "chore: release v${NEW_VERSION}" \ --body "## Release v${NEW_VERSION} Automated version bump for release v${NEW_VERSION}. This PR was created by the release script.") echo -e "${GREEN}PR created: ${YELLOW}${PR_URL}${NC}" PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$') # ─── Wait for CI and merge ──────────────────────────────────────────────────── if ! wait_for_ci "${PR_NUMBER}"; then echo -e "${RED}CI checks failed! Please fix the issues and try again.${NC}" echo -e "${RED}Release branch '${RELEASE_BRANCH}' and PR #${PR_NUMBER} are still open.${NC}" exit 1 fi echo -e "${BLUE}Merging PR #${PR_NUMBER}...${NC}" gh pr merge "${PR_NUMBER}" --repo "${GITHUB_REPO}" --squash --delete-branch echo -e "${BLUE}Updating main branch...${NC}" git checkout main git pull "${GIT_REMOTE}" main # ─── Create and push signed tag ────────────────────────────────────────────── if git rev-parse "v${NEW_VERSION}" >/dev/null 2>&1; then echo -e "${YELLOW}Tag v${NEW_VERSION} already exists locally. Deleting...${NC}" git tag -d "v${NEW_VERSION}" fi if git ls-remote --tags "${GIT_REMOTE}" "v${NEW_VERSION}" 2>/dev/null | grep -q "v${NEW_VERSION}"; then echo -e "${YELLOW}Tag v${NEW_VERSION} exists on remote. Deleting...${NC}" git push "${GIT_REMOTE}" ":refs/tags/v${NEW_VERSION}" 2>/dev/null || true fi echo -e "${BLUE}Creating signed tag v${NEW_VERSION}...${NC}" git tag -s "v${NEW_VERSION}" -m "Release v${NEW_VERSION}" echo -e "${BLUE}Pushing tag...${NC}" git push "${GIT_REMOTE}" "v${NEW_VERSION}" # ─── Done ───────────────────────────────────────────────────────────────────── echo "" echo -e "${GREEN}════════════════════════════════════════════════════════════${NC}" echo -e "${GREEN} ✓ Released v${NEW_VERSION}${NC}" echo -e "${GREEN}════════════════════════════════════════════════════════════${NC}" echo "" echo -e "${BLUE}GitHub Actions will now build and publish Docker images.${NC}" echo -e "Track progress: ${YELLOW}https://github.com/${GITHUB_REPO}/actions${NC}" echo -e "Release page: ${YELLOW}https://github.com/${GITHUB_REPO}/releases/tag/v${NEW_VERSION}${NC}"