Initial commit
This commit is contained in:
340
skills/adw-bootstrap/reference/scaled/workflows/adw_ship_iso.py
Executable file
340
skills/adw-bootstrap/reference/scaled/workflows/adw_ship_iso.py
Executable file
@@ -0,0 +1,340 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user