#!/bin/bash # Roo Code CLI Release Script # # Usage: # ./apps/cli/scripts/release.sh [options] [version] # # Options: # --dry-run Run all steps except creating the GitHub release # --local Build for local testing only (no GitHub checks, no changelog prompts) # --install Install locally after building (only with --local) # --skip-verify Skip end-to-end verification tests (faster local builds) # # Examples: # ./apps/cli/scripts/release.sh # Use version from package.json # ./apps/cli/scripts/release.sh 0.1.0 # Specify version # ./apps/cli/scripts/release.sh --dry-run # Test the release flow without pushing # ./apps/cli/scripts/release.sh --dry-run 0.1.0 # Dry run with specific version # ./apps/cli/scripts/release.sh --local # Build for local testing # ./apps/cli/scripts/release.sh --local --install # Build and install locally # ./apps/cli/scripts/release.sh --local --skip-verify # Fast local build # # This script: # 1. Builds the extension and CLI # 2. Creates a tarball for the current platform # 3. Creates a GitHub release and uploads the tarball (unless --dry-run or --local) # # Prerequisites: # - GitHub CLI (gh) installed and authenticated (not needed for --local) # - pnpm installed # - Run from the monorepo root directory set -e # Parse arguments DRY_RUN=false LOCAL_BUILD=false LOCAL_INSTALL=false SKIP_VERIFY=false VERSION_ARG="" while [[ $# -gt 0 ]]; do case $1 in --dry-run) DRY_RUN=true shift ;; --local) LOCAL_BUILD=true shift ;; --install) LOCAL_INSTALL=true shift ;; --skip-verify) SKIP_VERIFY=true shift ;; -*) echo "Unknown option: $1" >&2 exit 1 ;; *) VERSION_ARG="$1" shift ;; esac done # Validate option combinations if [ "$LOCAL_INSTALL" = true ] && [ "$LOCAL_BUILD" = false ]; then echo "Error: --install can only be used with --local" >&2 exit 1 fi # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' BOLD='\033[1m' NC='\033[0m' info() { printf "${GREEN}==>${NC} %s\n" "$1"; } warn() { printf "${YELLOW}Warning:${NC} %s\n" "$1"; } error() { printf "${RED}Error:${NC} %s\n" "$1" >&2; exit 1; } step() { printf "${BLUE}${BOLD}[%s]${NC} %s\n" "$1" "$2"; } # Get script directory and repo root SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" CLI_DIR="$REPO_ROOT/apps/cli" # Detect current platform detect_platform() { OS=$(uname -s | tr '[:upper:]' '[:lower:]') ARCH=$(uname -m) case "$OS" in darwin) OS="darwin" ;; linux) OS="linux" ;; *) error "Unsupported OS: $OS" ;; esac case "$ARCH" in x86_64|amd64) ARCH="x64" ;; arm64|aarch64) ARCH="arm64" ;; *) error "Unsupported architecture: $ARCH" ;; esac PLATFORM="${OS}-${ARCH}" } # Check prerequisites check_prerequisites() { step "1/8" "Checking prerequisites..." # Skip GitHub CLI checks for local builds if [ "$LOCAL_BUILD" = false ]; then if ! command -v gh &> /dev/null; then error "GitHub CLI (gh) is not installed. Install it with: brew install gh" fi if ! gh auth status &> /dev/null; then error "GitHub CLI is not authenticated. Run: gh auth login" fi fi if ! command -v pnpm &> /dev/null; then error "pnpm is not installed." fi if ! command -v node &> /dev/null; then error "Node.js is not installed." fi info "Prerequisites OK" } # Get version get_version() { if [ -n "$VERSION_ARG" ]; then VERSION="$VERSION_ARG" else VERSION=$(node -p "require('$CLI_DIR/package.json').version") fi # For local builds, append a local suffix with git short hash # This creates versions like: 0.1.0-local.abc1234 if [ "$LOCAL_BUILD" = true ]; then GIT_SHORT_HASH=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown") # Only append suffix if not already a local version if ! echo "$VERSION" | grep -qE '\-local\.'; then VERSION="${VERSION}-local.${GIT_SHORT_HASH}" fi fi # Validate semver format (allow -local.hash suffix) if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$'; then error "Invalid version format: $VERSION (expected semver like 0.1.0)" fi TAG="cli-v$VERSION" info "Version: $VERSION (tag: $TAG)" } # Extract changelog content for a specific version # Returns the content between the version header and the next version header (or EOF) get_changelog_content() { CHANGELOG_FILE="$CLI_DIR/CHANGELOG.md" if [ ! -f "$CHANGELOG_FILE" ]; then warn "No CHANGELOG.md found at $CHANGELOG_FILE" CHANGELOG_CONTENT="" return fi # Try to find the version section (handles both "[0.0.43]" and "[0.0.43] - date" formats) # Also handles "Unreleased" marker VERSION_PATTERN="^\#\# \[${VERSION}\]" # Check if the version exists in the changelog if ! grep -qE "$VERSION_PATTERN" "$CHANGELOG_FILE"; then warn "No changelog entry found for version $VERSION" # Skip prompts for local builds if [ "$LOCAL_BUILD" = true ]; then info "Skipping changelog prompt for local build" CHANGELOG_CONTENT="" return fi warn "Please add an entry to $CHANGELOG_FILE before releasing" echo "" echo "Expected format:" echo " ## [$VERSION] - $(date +%Y-%m-%d)" echo " " echo " ### Added" echo " - Your changes here" echo "" read -p "Continue without changelog content? [y/N] " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then error "Aborted. Please add a changelog entry and try again." fi CHANGELOG_CONTENT="" return fi # Extract content between this version and the next version header (or EOF) # Uses awk to capture everything between ## [VERSION] and the next ## [ # Using index() with "[VERSION]" ensures exact matching (1.0.1 won't match 1.0.10) CHANGELOG_CONTENT=$(awk -v version="$VERSION" ' BEGIN { found = 0; content = ""; target = "[" version "]" } /^## \[/ { if (found) { exit } if (index($0, target) > 0) { found = 1; next } } found { content = content $0 "\n" } END { print content } ' "$CHANGELOG_FILE") # Trim leading/trailing whitespace CHANGELOG_CONTENT=$(echo "$CHANGELOG_CONTENT" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') if [ -n "$CHANGELOG_CONTENT" ]; then info "Found changelog content for version $VERSION" else warn "Changelog entry for $VERSION appears to be empty" fi } # Build everything build() { step "2/8" "Building extension bundle..." cd "$REPO_ROOT" pnpm bundle step "3/8" "Building CLI..." pnpm --filter @roo-code/cli build info "Build complete" } # Create release tarball create_tarball() { step "4/8" "Creating release tarball for $PLATFORM..." RELEASE_DIR="$REPO_ROOT/roo-cli-${PLATFORM}" TARBALL="roo-cli-${PLATFORM}.tar.gz" # Clean up any previous build rm -rf "$RELEASE_DIR" rm -f "$REPO_ROOT/$TARBALL" # Create directory structure mkdir -p "$RELEASE_DIR/bin" mkdir -p "$RELEASE_DIR/lib" mkdir -p "$RELEASE_DIR/extension" # Copy CLI dist files info "Copying CLI files..." cp -r "$CLI_DIR/dist/"* "$RELEASE_DIR/lib/" # Create package.json for npm install (runtime dependencies that can't be bundled) info "Creating package.json..." node -e " const pkg = require('$CLI_DIR/package.json'); const newPkg = { name: '@roo-code/cli', version: '$VERSION', type: 'module', dependencies: { '@inkjs/ui': pkg.dependencies['@inkjs/ui'], '@trpc/client': pkg.dependencies['@trpc/client'], 'commander': pkg.dependencies.commander, 'fuzzysort': pkg.dependencies.fuzzysort, 'ink': pkg.dependencies.ink, 'p-wait-for': pkg.dependencies['p-wait-for'], 'react': pkg.dependencies.react, 'superjson': pkg.dependencies.superjson, 'zustand': pkg.dependencies.zustand } }; console.log(JSON.stringify(newPkg, null, 2)); " > "$RELEASE_DIR/package.json" # Copy extension bundle info "Copying extension bundle..." cp -r "$REPO_ROOT/src/dist/"* "$RELEASE_DIR/extension/" # Add package.json to extension directory to mark it as CommonJS # This is necessary because the main package.json has "type": "module" # but the extension bundle is CommonJS echo '{"type": "commonjs"}' > "$RELEASE_DIR/extension/package.json" # Find and copy ripgrep binary # The extension looks for ripgrep at: appRoot/node_modules/@vscode/ripgrep/bin/rg # The CLI sets appRoot to the CLI package root, so we need to put ripgrep there info "Looking for ripgrep binary..." RIPGREP_PATH=$(find "$REPO_ROOT/node_modules" -path "*/@vscode/ripgrep/bin/rg" -type f 2>/dev/null | head -1) if [ -n "$RIPGREP_PATH" ] && [ -f "$RIPGREP_PATH" ]; then info "Found ripgrep at: $RIPGREP_PATH" # Create the expected directory structure for the extension to find ripgrep mkdir -p "$RELEASE_DIR/node_modules/@vscode/ripgrep/bin" cp "$RIPGREP_PATH" "$RELEASE_DIR/node_modules/@vscode/ripgrep/bin/" chmod +x "$RELEASE_DIR/node_modules/@vscode/ripgrep/bin/rg" # Also keep a copy in bin/ for direct access mkdir -p "$RELEASE_DIR/bin" cp "$RIPGREP_PATH" "$RELEASE_DIR/bin/" chmod +x "$RELEASE_DIR/bin/rg" else warn "ripgrep binary not found - users will need ripgrep installed" fi # Create the wrapper script info "Creating wrapper script..." cat > "$RELEASE_DIR/bin/roo" << 'WRAPPER_EOF' #!/usr/bin/env node import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Set environment variables for the CLI // ROO_CLI_ROOT is the installed CLI package root (where node_modules/@vscode/ripgrep is) process.env.ROO_CLI_ROOT = join(__dirname, '..'); process.env.ROO_EXTENSION_PATH = join(__dirname, '..', 'extension'); process.env.ROO_RIPGREP_PATH = join(__dirname, 'rg'); // Import and run the actual CLI await import(join(__dirname, '..', 'lib', 'index.js')); WRAPPER_EOF chmod +x "$RELEASE_DIR/bin/roo" # Create empty .env file to suppress dotenvx warnings touch "$RELEASE_DIR/.env" # Create empty .env file to suppress dotenvx warnings touch "$RELEASE_DIR/.env" # Create tarball info "Creating tarball..." cd "$REPO_ROOT" tar -czvf "$TARBALL" "$(basename "$RELEASE_DIR")" # Clean up release directory rm -rf "$RELEASE_DIR" # Show size TARBALL_PATH="$REPO_ROOT/$TARBALL" TARBALL_SIZE=$(ls -lh "$TARBALL_PATH" | awk '{print $5}') info "Created: $TARBALL ($TARBALL_SIZE)" } # Verify local installation verify_local_install() { if [ "$SKIP_VERIFY" = true ]; then step "5/8" "Skipping verification (--skip-verify)" return fi step "5/8" "Verifying local installation..." VERIFY_DIR="$REPO_ROOT/.verify-release" VERIFY_INSTALL_DIR="$VERIFY_DIR/cli" VERIFY_BIN_DIR="$VERIFY_DIR/bin" # Clean up any previous verification directory rm -rf "$VERIFY_DIR" mkdir -p "$VERIFY_DIR" # Run the actual install script with the local tarball info "Running install script with local tarball..." TARBALL_PATH="$REPO_ROOT/$TARBALL" ROO_LOCAL_TARBALL="$TARBALL_PATH" \ ROO_INSTALL_DIR="$VERIFY_INSTALL_DIR" \ ROO_BIN_DIR="$VERIFY_BIN_DIR" \ ROO_VERSION="$VERSION" \ "$CLI_DIR/install.sh" || { echo "" warn "Install script failed. Showing tarball contents:" tar -tzf "$TARBALL_PATH" 2>&1 || true echo "" rm -rf "$VERIFY_DIR" error "Installation verification failed! The install script could not complete successfully." } # Verify the CLI runs correctly with basic commands info "Testing installed CLI..." # Test --help if ! "$VERIFY_BIN_DIR/roo" --help > /dev/null 2>&1; then echo "" warn "CLI --help output:" "$VERIFY_BIN_DIR/roo" --help 2>&1 || true echo "" rm -rf "$VERIFY_DIR" error "CLI --help check failed! The release tarball may have missing dependencies." fi info "CLI --help check passed" # Test --version if ! "$VERIFY_BIN_DIR/roo" --version > /dev/null 2>&1; then echo "" warn "CLI --version output:" "$VERIFY_BIN_DIR/roo" --version 2>&1 || true echo "" rm -rf "$VERIFY_DIR" error "CLI --version check failed! The release tarball may have missing dependencies." fi info "CLI --version check passed" # Run a simple end-to-end test to verify the CLI actually works info "Running end-to-end verification test..." # Create a temporary workspace for the test VERIFY_WORKSPACE="$VERIFY_DIR/workspace" mkdir -p "$VERIFY_WORKSPACE" # Run the CLI with a simple prompt if timeout 60 "$VERIFY_BIN_DIR/roo" --yes --oneshot -w "$VERIFY_WORKSPACE" "1+1=?" > "$VERIFY_DIR/test-output.log" 2>&1; then info "End-to-end test passed" else EXIT_CODE=$? echo "" warn "End-to-end test failed (exit code: $EXIT_CODE). Output:" cat "$VERIFY_DIR/test-output.log" 2>&1 || true echo "" rm -rf "$VERIFY_DIR" error "CLI end-to-end test failed! The CLI may be broken." fi # Clean up verification directory cd "$REPO_ROOT" rm -rf "$VERIFY_DIR" info "Local verification passed!" } # Create checksum create_checksum() { step "6/8" "Creating checksum..." cd "$REPO_ROOT" if command -v sha256sum &> /dev/null; then sha256sum "$TARBALL" > "${TARBALL}.sha256" elif command -v shasum &> /dev/null; then shasum -a 256 "$TARBALL" > "${TARBALL}.sha256" else warn "No sha256sum or shasum found, skipping checksum" return fi info "Checksum: $(cat "${TARBALL}.sha256")" } # Check if release already exists check_existing_release() { step "7/8" "Checking for existing release..." if gh release view "$TAG" &> /dev/null; then warn "Release $TAG already exists" read -p "Do you want to delete it and create a new one? [y/N] " -n 1 -r echo if [[ $REPLY =~ ^[Yy]$ ]]; then info "Deleting existing release..." gh release delete "$TAG" --yes # Also delete the tag if it exists git tag -d "$TAG" 2>/dev/null || true git push origin ":refs/tags/$TAG" 2>/dev/null || true else error "Aborted. Use a different version or delete the existing release manually." fi fi } # Create GitHub release create_release() { step "8/8" "Creating GitHub release..." cd "$REPO_ROOT" # Get the current commit SHA for the release target COMMIT_SHA=$(git rev-parse HEAD) # Verify the commit exists on GitHub before attempting to create the release # This prevents the "Release.target_commitish is invalid" error info "Verifying commit ${COMMIT_SHA:0:8} exists on GitHub..." git fetch origin 2>/dev/null || true if ! git branch -r --contains "$COMMIT_SHA" 2>/dev/null | grep -q "origin/"; then warn "Commit ${COMMIT_SHA:0:8} has not been pushed to GitHub" echo "" echo "The release script needs to create a release at your current commit," echo "but this commit hasn't been pushed to GitHub yet." echo "" read -p "Push current branch to origin now? [Y/n] " -n 1 -r echo if [[ ! $REPLY =~ ^[Nn]$ ]]; then info "Pushing to origin..." git push origin HEAD || error "Failed to push to origin. Please push manually and try again." else error "Aborted. Please push your commits to GitHub and try again." fi fi info "Commit verified on GitHub" # Build the What's New section from changelog content WHATS_NEW_SECTION="" if [ -n "$CHANGELOG_CONTENT" ]; then WHATS_NEW_SECTION="## What's New $CHANGELOG_CONTENT " fi RELEASE_NOTES=$(cat << EOF ${WHATS_NEW_SECTION}## Installation \`\`\`bash curl -fsSL https://raw.githubusercontent.com/RooCodeInc/Roo-Code/main/apps/cli/install.sh | sh \`\`\` Or install a specific version: \`\`\`bash ROO_VERSION=$VERSION curl -fsSL https://raw.githubusercontent.com/RooCodeInc/Roo-Code/main/apps/cli/install.sh | sh \`\`\` ## Requirements - Node.js 20 or higher - macOS (Intel or Apple Silicon) or Linux (x64 or ARM64) ## Usage \`\`\`bash # Run a task roo "What is this project?" # See all options roo --help \`\`\` ## Platform Support This release includes: - \`roo-cli-${PLATFORM}.tar.gz\` - Built on $(uname -s) $(uname -m) > **Note:** Additional platforms will be added as needed. If you need a different platform, please open an issue. ## Checksum \`\`\` $(cat "${TARBALL}.sha256" 2>/dev/null || echo "N/A") \`\`\` EOF ) info "Creating release at commit: ${COMMIT_SHA:0:8}" # Create release (gh will create the tag automatically) info "Creating release..." RELEASE_FILES="$TARBALL" if [ -f "${TARBALL}.sha256" ]; then RELEASE_FILES="$RELEASE_FILES ${TARBALL}.sha256" fi gh release create "$TAG" \ --title "Roo Code CLI v$VERSION" \ --notes "$RELEASE_NOTES" \ --prerelease \ --target "$COMMIT_SHA" \ $RELEASE_FILES info "Release created!" } # Cleanup cleanup() { info "Cleaning up..." cd "$REPO_ROOT" rm -f "$TARBALL" "${TARBALL}.sha256" } # Print summary print_summary() { echo "" printf "${GREEN}${BOLD}✓ Release v$VERSION created successfully!${NC}\n" echo "" echo " Release URL: https://github.com/RooCodeInc/Roo-Code/releases/tag/$TAG" echo "" echo " Install with:" echo " curl -fsSL https://raw.githubusercontent.com/RooCodeInc/Roo-Code/main/apps/cli/install.sh | sh" echo "" } # Print dry-run summary print_dry_run_summary() { echo "" printf "${YELLOW}${BOLD}✓ Dry run complete for v$VERSION${NC}\n" echo "" echo " The following artifacts were created:" echo " - $TARBALL" if [ -f "${TARBALL}.sha256" ]; then echo " - ${TARBALL}.sha256" fi echo "" echo " To complete the release, run without --dry-run:" echo " ./apps/cli/scripts/release.sh $VERSION" echo "" echo " Or manually upload the tarball to a new GitHub release." echo "" } # Print local build summary print_local_summary() { echo "" printf "${GREEN}${BOLD}✓ Local build complete for v$VERSION${NC}\n" echo "" echo " Tarball: $REPO_ROOT/$TARBALL" if [ -f "${TARBALL}.sha256" ]; then echo " Checksum: $REPO_ROOT/${TARBALL}.sha256" fi echo "" echo " To install manually:" echo " ROO_LOCAL_TARBALL=$REPO_ROOT/$TARBALL ./apps/cli/install.sh" echo "" echo " Or re-run with --install to install automatically:" echo " ./apps/cli/scripts/release.sh --local --install" echo "" } # Install locally using the install script install_local() { step "7/8" "Installing locally..." TARBALL_PATH="$REPO_ROOT/$TARBALL" ROO_LOCAL_TARBALL="$TARBALL_PATH" \ ROO_VERSION="$VERSION" \ "$CLI_DIR/install.sh" || { error "Local installation failed!" } info "Local installation complete!" } # Print local install summary print_local_install_summary() { echo "" printf "${GREEN}${BOLD}✓ Local build installed for v$VERSION${NC}\n" echo "" echo " Tarball: $REPO_ROOT/$TARBALL" echo " Installed to: ~/.roo/cli" echo " Binary: ~/.local/bin/roo" echo "" echo " Test it out:" echo " roo --version" echo " roo --help" echo "" } # Main main() { echo "" printf "${BLUE}${BOLD}" echo " ╭─────────────────────────────────╮" echo " │ Roo Code CLI Release Script │" echo " ╰─────────────────────────────────╯" printf "${NC}" if [ "$DRY_RUN" = true ]; then printf "${YELLOW} (DRY RUN MODE)${NC}\n" elif [ "$LOCAL_BUILD" = true ]; then printf "${YELLOW} (LOCAL BUILD MODE)${NC}\n" fi echo "" detect_platform check_prerequisites get_version get_changelog_content build create_tarball verify_local_install create_checksum if [ "$LOCAL_BUILD" = true ]; then step "7/8" "Skipping GitHub checks (local build)" if [ "$LOCAL_INSTALL" = true ]; then install_local print_local_install_summary else step "8/8" "Skipping installation (use --install to auto-install)" print_local_summary fi elif [ "$DRY_RUN" = true ]; then step "7/8" "Skipping existing release check (dry run)" step "8/8" "Skipping GitHub release creation (dry run)" print_dry_run_summary else check_existing_release create_release cleanup print_summary fi } main