Files
gh-joshuaoliphant-claude-pl…/skills/adw-bootstrap/reference/scaled/workflows/adw_ship_iso.py
2025-11-30 08:28:42 +08:00

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()