340 lines
12 KiB
Python
Executable File
340 lines
12 KiB
Python
Executable File
#!/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 <issue-number> <adw-id>
|
|
|
|
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 <issue-number> <adw-id>")
|
|
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() |