#!/usr/bin/env -S uv run # /// script # dependencies = ["python-dotenv", "pydantic"] # /// """ ADW Ship Iso - AI Developer Workflow for shipping (merging) to main Usage: uv run adw_ship_iso.py Workflow: 1. Load state and validate worktree exists 2. Validate ALL state fields are populated (not None) 3. Perform manual git merge in main repository: - Fetch latest from origin - Checkout main - Merge feature branch - Push to origin/main 4. Post success message to issue This workflow REQUIRES that all previous workflows have been run and that every field in ADWState has a value. This is our final approval step. Note: Merge operations happen in the main repository root, not in the worktree, to preserve the worktree's state. """ import sys import os import logging import json import subprocess from typing import Optional, Dict, Any, Tuple from dotenv import load_dotenv from adw_modules.state import ADWState from adw_modules.github import ( make_issue_comment, get_repo_url, extract_repo_path, ) from adw_modules.beads_integration import is_beads_issue, close_beads_issue from adw_modules.workflow_ops import format_issue_message from adw_modules.worktree_ops import validate_worktree from adw_modules.data_types import ADWStateData # Setup logging logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) logger = logging.getLogger(__name__) # Agent name constant AGENT_SHIPPER = "shipper" def get_main_repo_root() -> str: """Get the main repository root directory (parent of adws).""" # This script is in adws/, so go up one level to get repo root return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) def manual_merge_to_main(branch_name: str, logger: logging.Logger) -> Tuple[bool, Optional[str]]: """Manually merge a branch to main using git commands. This runs in the main repository root, not in a worktree. Args: branch_name: The feature branch to merge logger: Logger instance Returns: Tuple of (success, error_message) """ repo_root = get_main_repo_root() logger.info(f"Performing manual merge in main repository: {repo_root}") try: # Save current branch to restore later result = subprocess.run( ["git", "rev-parse", "--abbrev-ref", "HEAD"], capture_output=True, text=True, cwd=repo_root ) original_branch = result.stdout.strip() logger.debug(f"Original branch: {original_branch}") # Step 1: Fetch latest from origin logger.info("Fetching latest from origin...") result = subprocess.run( ["git", "fetch", "origin"], capture_output=True, text=True, cwd=repo_root ) if result.returncode != 0: return False, f"Failed to fetch from origin: {result.stderr}" # Step 2: Checkout main logger.info("Checking out main branch...") result = subprocess.run( ["git", "checkout", "main"], capture_output=True, text=True, cwd=repo_root ) if result.returncode != 0: return False, f"Failed to checkout main: {result.stderr}" # Step 3: Pull latest main logger.info("Pulling latest main...") result = subprocess.run( ["git", "pull", "origin", "main"], capture_output=True, text=True, cwd=repo_root ) if result.returncode != 0: # Try to restore original branch subprocess.run(["git", "checkout", original_branch], cwd=repo_root) return False, f"Failed to pull latest main: {result.stderr}" # Step 4: Merge the feature branch (no-ff to preserve all commits) logger.info(f"Merging branch {branch_name} (no-ff to preserve all commits)...") result = subprocess.run( ["git", "merge", branch_name, "--no-ff", "-m", f"Merge branch '{branch_name}' via ADW Ship workflow"], capture_output=True, text=True, cwd=repo_root ) if result.returncode != 0: # Try to restore original branch subprocess.run(["git", "checkout", original_branch], cwd=repo_root) return False, f"Failed to merge {branch_name}: {result.stderr}" # Step 5: Push to origin/main logger.info("Pushing to origin/main...") result = subprocess.run( ["git", "push", "origin", "main"], capture_output=True, text=True, cwd=repo_root ) if result.returncode != 0: # Try to restore original branch subprocess.run(["git", "checkout", original_branch], cwd=repo_root) return False, f"Failed to push to origin/main: {result.stderr}" # Step 6: Restore original branch logger.info(f"Restoring original branch: {original_branch}") subprocess.run(["git", "checkout", original_branch], cwd=repo_root) logger.info("✅ Successfully merged and pushed to main!") return True, None except Exception as e: logger.error(f"Unexpected error during merge: {e}") # Try to restore original branch try: subprocess.run(["git", "checkout", original_branch], cwd=repo_root) except: pass return False, str(e) def validate_state_completeness(state: ADWState, logger: logging.Logger) -> tuple[bool, list[str]]: """Validate that all fields in ADWState have values (not None). Returns: tuple of (is_valid, missing_fields) """ # Get the expected fields from ADWStateData model expected_fields = { "adw_id", "issue_number", "branch_name", "plan_file", "issue_class", "worktree_path", "backend_port", "frontend_port", } missing_fields = [] for field in expected_fields: value = state.get(field) if value is None: missing_fields.append(field) logger.warning(f"Missing required field: {field}") else: logger.debug(f"✓ {field}: {value}") return len(missing_fields) == 0, missing_fields def main(): """Main entry point.""" # Load environment variables load_dotenv() # Parse command line args # INTENTIONAL: adw-id is REQUIRED - we need it to find the worktree and state if len(sys.argv) < 3: print("Usage: uv run adw_ship_iso.py ") print("\nError: Both issue-number and adw-id are required") print("Run the complete SDLC workflow before shipping") sys.exit(1) issue_number = sys.argv[1] adw_id = sys.argv[2] # Try to load existing state state = ADWState.load(adw_id, logger) if not state: # No existing state found logger.error(f"No state found for ADW ID: {adw_id}") logger.error("Run the complete SDLC workflow before shipping") print(f"\nError: No state found for ADW ID: {adw_id}") print("Run the complete SDLC workflow before shipping") sys.exit(1) # Update issue number from state if available issue_number = state.get("issue_number", issue_number) # Track that this ADW workflow has run state.append_adw_id("adw_ship_iso") logger.info(f"ADW Ship Iso starting - ID: {adw_id}, Issue: {issue_number}") # Check if this is a beads issue is_beads = is_beads_issue(issue_number) logger.info(f"Issue type: {'beads' if is_beads else 'GitHub'}") # Post initial status (only for GitHub issues) if not is_beads: make_issue_comment( issue_number, format_issue_message(adw_id, "ops", f"🚢 Starting ship workflow\n" f"📋 Validating state completeness...") ) # Step 1: Validate state completeness logger.info("Validating state completeness...") is_valid, missing_fields = validate_state_completeness(state, logger) if not is_valid: error_msg = f"State validation failed. Missing fields: {', '.join(missing_fields)}" logger.error(error_msg) if not is_beads: make_issue_comment( issue_number, format_issue_message(adw_id, AGENT_SHIPPER, f"❌ {error_msg}\n\n" "Please ensure all workflows have been run:\n" "- adw_plan_iso.py (creates plan_file, branch_name, issue_class)\n" "- adw_build_iso.py (implements the plan)\n" "- adw_test_iso.py (runs tests)\n" "- adw_review_iso.py (reviews implementation)\n" "- adw_document_iso.py (generates docs)") ) sys.exit(1) logger.info("✅ State validation passed - all fields have values") # Step 2: Validate worktree exists valid, error = validate_worktree(adw_id, state) if not valid: logger.error(f"Worktree validation failed: {error}") if not is_beads: make_issue_comment( issue_number, format_issue_message(adw_id, AGENT_SHIPPER, f"❌ Worktree validation failed: {error}") ) sys.exit(1) worktree_path = state.get("worktree_path") logger.info(f"✅ Worktree validated at: {worktree_path}") # Step 3: Get branch name branch_name = state.get("branch_name") logger.info(f"Preparing to merge branch: {branch_name}") if not is_beads: make_issue_comment( issue_number, format_issue_message(adw_id, AGENT_SHIPPER, f"📋 State validation complete\n" f"🔍 Preparing to merge branch: {branch_name}") ) # Step 4: Perform manual merge logger.info(f"Starting manual merge of {branch_name} to main...") if not is_beads: make_issue_comment( issue_number, format_issue_message(adw_id, AGENT_SHIPPER, f"🔀 Merging {branch_name} to main...\n" "Using manual git operations in main repository") ) success, error = manual_merge_to_main(branch_name, logger) if not success: logger.error(f"Failed to merge: {error}") if not is_beads: make_issue_comment( issue_number, format_issue_message(adw_id, AGENT_SHIPPER, f"❌ Failed to merge: {error}") ) sys.exit(1) logger.info(f"✅ Successfully merged {branch_name} to main") # Step 5: Close beads issue if applicable if is_beads: logger.info(f"Closing beads issue: {issue_number}") success, error = close_beads_issue( issue_number, f"Completed via ADW {adw_id} - merged to main" ) if not success: logger.warning(f"Failed to close beads issue: {error}") else: logger.info(f"✅ Closed beads issue: {issue_number}") # Step 6: Post success message (only for GitHub issues) if not is_beads: make_issue_comment( issue_number, format_issue_message(adw_id, AGENT_SHIPPER, f"🎉 **Successfully shipped!**\n\n" f"✅ Validated all state fields\n" f"✅ Merged branch `{branch_name}` to main\n" f"✅ Pushed to origin/main\n\n" f"🚢 Code has been deployed to production!") ) # Save final state state.save("adw_ship_iso") # Post final state summary (only for GitHub issues) if not is_beads: make_issue_comment( issue_number, f"{adw_id}_ops: 📋 Final ship state:\n```json\n{json.dumps(state.data, indent=2)}\n```" ) logger.info("Ship workflow completed successfully") if __name__ == "__main__": main()