#!/usr/bin/env python3
"""
Release Manager for Siege Utilities

Updates version across all five locations (pyproject.toml, setup.py, __init__.py, docs/conf.py x2),
runs tests, builds the package, validates with twine, merges develop → main (gitflow),
tags the release, uploads to PyPI, and creates GitHub releases.

Usage:
    python scripts/release_manager.py --check          # Check version consistency
    python scripts/release_manager.py --bump patch     # Bump patch version
    python scripts/release_manager.py --build          # Build package
    python scripts/release_manager.py --build --clean  # Clean + build
    python scripts/release_manager.py --release --bump-type minor --release-notes "..."
    python scripts/release_manager.py --release --bump-type major --changelog --pypi
    python scripts/release_manager.py --release --target-version 3.1.0 --changelog --pypi
    python scripts/release_manager.py --release --bump-type patch --dry-run

Release workflow (12 steps):
    1. Check version consistency
    2. Run tests (unless --skip-tests)
    3. Bump or set version
    4. Verify import
    5. Clean build artifacts
    6. Build sdist + wheel
    7. Validate with twine
    8. Resolve release notes
    9. Merge develop → main (gitflow)
   10. Tag on main + push
   11. Upload to PyPI (if --pypi)
   12. Create GitHub release (unless --skip-github)
"""

import os
import sys
import subprocess
import shutil
import json
import re
import logging
import glob as globmod
from pathlib import Path
from typing import Dict, List, Optional
import argparse
from datetime import datetime

logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
logger = logging.getLogger(__name__)

# Resolve project root (parent of scripts/)
PROJECT_ROOT = Path(__file__).resolve().parent.parent

VERSION_FILES = {
    'pyproject.toml': {
        'path': PROJECT_ROOT / 'pyproject.toml',
        'pattern': r'(version\s*=\s*")[^"]+(")',
    },
    'setup.py': {
        'path': PROJECT_ROOT / 'setup.py',
        'pattern': r'(version\s*=\s*")[^"]+(")',
    },
    '__init__.py __version__': {
        'path': PROJECT_ROOT / 'siege_utilities' / '__init__.py',
        'pattern': r'(__version__\s*=\s*")[^"]+(")',
    },
    'docs/conf.py': {
        'path': PROJECT_ROOT / 'docs' / 'conf.py',
        'pattern': r"(release\s*=\s*')[^']+(')",
    },
    'docs/source/conf.py': {
        'path': PROJECT_ROOT / 'docs' / 'source' / 'conf.py',
        'pattern': r"(release\s*=\s*')[^']+(')",
    },
}


def read_versions() -> Dict[str, str]:
    """Read current version from each location."""
    versions = {}
    for name, spec in VERSION_FILES.items():
        content = spec['path'].read_text()
        match = re.search(spec['pattern'], content)
        if match:
            # The version is between the two capture groups
            full_match = re.search(spec['pattern'].replace(r'[^"]+', r'([^"]+)').replace(r"[^']+", r"([^']+)"), content)
            if full_match:
                versions[name] = full_match.group(2) if full_match.lastindex >= 2 else full_match.group(1)
        else:
            versions[name] = 'NOT FOUND'
    return versions


def get_current_version() -> str:
    """Get canonical version from pyproject.toml."""
    content = (PROJECT_ROOT / 'pyproject.toml').read_text()
    match = re.search(r'version\s*=\s*"([^"]+)"', content)
    if match:
        return match.group(1)
    raise ValueError("Could not find version in pyproject.toml")


def check_ci_status() -> bool:
    """Check that the latest CI run on the current branch passed.

    Returns True if all required jobs succeeded, False otherwise.
    Requires `gh` CLI to be authenticated.
    """
    try:
        branch = subprocess.run(
            ['git', 'branch', '--show-current'],
            capture_output=True, text=True, check=True,
            cwd=PROJECT_ROOT,
        ).stdout.strip()

        result = subprocess.run(
            ['gh', 'run', 'list', '--branch', branch, '--limit', '1',
             '--workflow', 'Siege Utilities CI/CD', '--json',
             'conclusion,status,name'],
            capture_output=True, text=True, check=True,
            cwd=PROJECT_ROOT,
        )
        runs = json.loads(result.stdout)
        if not runs:
            logger.warning(f"No CI runs found for branch {branch}")
            return False

        run = runs[0]
        if run.get('status') != 'completed':
            logger.warning(f"Latest CI run is {run.get('status')}, not completed")
            return False

        if run.get('conclusion') != 'success':
            logger.warning(f"Latest CI run conclusion: {run.get('conclusion')}")
            return False

        logger.info(f"CI check passed: {run.get('name')} on {branch}")
        return True
    except (subprocess.CalledProcessError, FileNotFoundError) as e:
        logger.warning(f"Could not check CI status: {e}")
        return False


def check_consistency() -> Dict:
    """Check version consistency across all files."""
    versions = read_versions()
    unique = set(versions.values()) - {'NOT FOUND'}
    return {
        'versions': versions,
        'consistent': len(unique) <= 1,
        'canonical': get_current_version(),
    }


def set_version(new_version: str):
    """Set version across all files."""
    for name, spec in VERSION_FILES.items():
        path = spec['path']
        content = path.read_text()
        new_content = re.sub(spec['pattern'], rf'\g<1>{new_version}\2', content)
        if new_content == content:
            logger.warning(f"No replacement made in {name}")
        else:
            path.write_text(new_content)
            logger.info(f"Updated {name} -> {new_version}")


def validate_target_version(target: str) -> str:
    """Validate an explicit target version string.

    Checks:
      1. Format is X.Y.Z (three non-negative integers)
      2. Target is greater than or equal to the current version

    If the target equals the current version, the version is already bumped
    and the release proceeds without a version change (merge + tag + publish).

    Returns the validated version string, or raises ValueError.
    """
    parts = target.split('.')
    if len(parts) != 3:
        raise ValueError(f"Version must be X.Y.Z format, got: {target}")

    try:
        target_tuple = tuple(int(p) for p in parts)
    except ValueError:
        raise ValueError(f"Version components must be integers, got: {target}")

    if any(p < 0 for p in target_tuple):
        raise ValueError(f"Version components must be non-negative, got: {target}")

    current = get_current_version()
    current_tuple = tuple(int(p) for p in current.split('.'))

    if target_tuple < current_tuple:
        raise ValueError(
            f"Target version {target} must be >= current version {current}"
        )

    if target_tuple == current_tuple:
        logger.info(f"Target version {target} matches current — version already bumped")
    else:
        logger.info(f"Target version {target} validated (current: {current})")
    return target


def bump_version(bump_type: str) -> str:
    """Bump version and return new version string."""
    current = get_current_version()
    parts = current.split('.')
    if len(parts) != 3:
        raise ValueError(f"Unexpected version format: {current}")

    major, minor, patch = int(parts[0]), int(parts[1]), int(parts[2])

    if bump_type == 'major':
        major += 1; minor = 0; patch = 0
    elif bump_type == 'minor':
        minor += 1; patch = 0
    elif bump_type == 'patch':
        patch += 1
    else:
        raise ValueError(f"Invalid bump type: {bump_type}")

    new_version = f"{major}.{minor}.{patch}"
    set_version(new_version)
    return new_version


def run_tests() -> bool:
    """Run the test suite."""
    logger.info("Running tests...")
    result = subprocess.run(
        [sys.executable, '-m', 'pytest', 'tests/', '-x', '-q'],
        cwd=PROJECT_ROOT,
    )
    return result.returncode == 0


def clean_build_artifacts() -> bool:
    """Remove dist/, build/, and *.egg-info directories."""
    logger.info("Cleaning build artifacts...")
    cleaned = []
    for dirname in ['dist', 'build']:
        target = PROJECT_ROOT / dirname
        if target.exists():
            shutil.rmtree(target)
            cleaned.append(str(dirname))

    for egg_info in PROJECT_ROOT.glob('*.egg-info'):
        shutil.rmtree(egg_info)
        cleaned.append(egg_info.name)

    for egg_info in (PROJECT_ROOT / 'siege_utilities').glob('*.egg-info'):
        shutil.rmtree(egg_info)
        cleaned.append(egg_info.name)

    if cleaned:
        logger.info(f"Cleaned: {', '.join(cleaned)}")
    else:
        logger.info("No build artifacts to clean")
    return True


def build_package() -> bool:
    """Build sdist and wheel using python -m build."""
    logger.info("Building package...")
    result = subprocess.run(
        [sys.executable, '-m', 'build'],
        cwd=PROJECT_ROOT,
    )
    if result.returncode == 0:
        logger.info("Package built successfully")
        return True
    logger.error("Package build failed")
    return False


def validate_build() -> bool:
    """Validate built distributions with twine check."""
    dist_dir = PROJECT_ROOT / 'dist'
    dist_files = list(dist_dir.glob('*')) if dist_dir.exists() else []
    if not dist_files:
        logger.error("No distribution files found in dist/")
        return False

    logger.info("Validating package with twine...")
    result = subprocess.run(
        [sys.executable, '-m', 'twine', 'check'] + [str(f) for f in dist_files],
        cwd=PROJECT_ROOT,
        capture_output=True, text=True,
    )
    if result.returncode == 0:
        logger.info("Package validation passed")
        return True
    logger.error(f"Package validation failed:\n{result.stdout}\n{result.stderr}")
    return False


def upload_to_pypi(repository: str = 'pypi', token: Optional[str] = None,
                   dry_run: bool = False) -> bool:
    """Upload distributions to PyPI using twine.

    Args:
        repository: 'pypi' or 'testpypi'
        token: PyPI API token. Falls back to PYPI_API_TOKEN env var.
        dry_run: If True, log what would happen but don't upload.
    """
    resolved_token = token or os.environ.get('PYPI_API_TOKEN')
    if not resolved_token:
        logger.error("No PyPI token provided (use --token or PYPI_API_TOKEN env var)")
        return False

    dist_dir = PROJECT_ROOT / 'dist'
    dist_files = list(dist_dir.glob('*')) if dist_dir.exists() else []
    if not dist_files:
        logger.error("No distribution files found in dist/")
        return False

    if dry_run:
        logger.info(f"[DRY RUN] Would upload {len(dist_files)} file(s) to {repository}")
        return True

    cmd = [sys.executable, '-m', 'twine', 'upload',
           '--username', '__token__', '--password', resolved_token]
    if repository == 'testpypi':
        cmd.extend(['--repository', 'testpypi'])
    cmd.extend([str(f) for f in dist_files])

    logger.info(f"Uploading to {repository}...")
    result = subprocess.run(cmd, cwd=PROJECT_ROOT)
    if result.returncode == 0:
        logger.info(f"Upload to {repository} succeeded")
        return True
    logger.error(f"Upload to {repository} failed")
    return False


def _git(args: List[str], check: bool = True, capture: bool = False) -> subprocess.CompletedProcess:
    """Run a git command in the project root."""
    return subprocess.run(
        ['git'] + args,
        cwd=PROJECT_ROOT,
        check=check,
        capture_output=capture,
        text=True,
    )


def _current_branch() -> str:
    """Return the name of the current git branch."""
    return _git(['branch', '--show-current'], capture=True).stdout.strip()


def _branch_exists(branch: str) -> bool:
    """Check whether a local branch exists."""
    result = _git(['rev-parse', '--verify', branch], check=False, capture=True)
    return result.returncode == 0


def _is_clean() -> bool:
    """Return True when the working tree has no uncommitted changes."""
    result = _git(['status', '--porcelain'], capture=True)
    return len(result.stdout.strip()) == 0


def merge_develop_to_main(dry_run: bool = False) -> bool:
    """Merge develop into main (gitflow release merge).

    Workflow:
      1. Verify we are on develop with a clean worktree
      2. Checkout main and pull latest
      3. Merge develop into main (no fast-forward for a merge commit)
      4. Push main
      5. Return to develop

    Args:
        dry_run: If True, log what would happen but don't execute.

    Returns:
        True on success, False on failure.
    """
    starting_branch = _current_branch()

    if starting_branch != 'develop':
        logger.error(f"Expected to be on 'develop', currently on '{starting_branch}'")
        return False

    if not _is_clean():
        logger.error("Working tree is not clean — commit or stash changes first")
        return False

    if dry_run:
        logger.info("[DRY RUN] Would merge develop → main and push")
        return True

    try:
        # Pull latest develop
        logger.info("Pulling latest develop...")
        _git(['pull', 'origin', 'develop'])

        # Checkout main and pull
        logger.info("Switching to main...")
        _git(['checkout', 'main'])
        _git(['pull', 'origin', 'main'])

        # Merge develop into main
        logger.info("Merging develop into main...")
        _git(['merge', 'develop', '--no-ff', '-m', 'Merge branch \'develop\''])

        # Push main
        logger.info("Pushing main...")
        _git(['push', 'origin', 'main'])

        # Return to develop
        logger.info("Switching back to develop...")
        _git(['checkout', 'develop'])

        logger.info("Merge develop → main complete")
        return True
    except subprocess.CalledProcessError as e:
        logger.error(f"Merge failed: {e}")
        # Try to return to starting branch
        _git(['checkout', starting_branch], check=False)
        return False


def git_tag_and_push(version: str, message: Optional[str] = None,
                     dry_run: bool = False) -> bool:
    """Create annotated tag on main and push it.

    Must be called after merge_develop_to_main so the tag lands on main.

    Args:
        version: Version string (tag will be vX.Y.Z)
        message: Tag annotation message. Defaults to 'Release vX.Y.Z'.
        dry_run: If True, log what would happen but don't execute.
    """
    tag = f"v{version}"
    msg = message or f"Release {tag}"

    if dry_run:
        logger.info(f"[DRY RUN] Would tag {tag} on main and push")
        return True

    try:
        # Switch to main to tag
        current = _current_branch()
        if current != 'main':
            _git(['checkout', 'main'])

        _git(['tag', '-a', tag, '-m', msg])
        _git(['push', 'origin', '--tags'])
        logger.info(f"Tagged {tag} on main and pushed")

        # Return to develop
        if current != 'main':
            _git(['checkout', current])

        return True
    except subprocess.CalledProcessError as e:
        logger.error(f"Tagging failed: {e}")
        return False


def git_commit_and_tag(version: str, message: Optional[str] = None,
                       dry_run: bool = False) -> bool:
    """Commit version file changes, create annotated tag, push both.

    .. deprecated::
        Use merge_develop_to_main + git_tag_and_push for gitflow releases.
        This function is kept for backward compatibility with non-gitflow repos.

    Args:
        version: Version string (tag will be vX.Y.Z)
        message: Tag annotation message. Defaults to 'Release vX.Y.Z'.
        dry_run: If True, log what would happen but don't execute.
    """
    tag = f"v{version}"
    msg = message or f"Release {tag}"

    version_paths = [str(spec['path'].relative_to(PROJECT_ROOT))
                     for spec in VERSION_FILES.values()]

    changelog = PROJECT_ROOT / 'CHANGELOG.md'
    if changelog.exists():
        version_paths.append('CHANGELOG.md')

    if dry_run:
        logger.info(f"[DRY RUN] Would commit {version_paths}, tag {tag}, push")
        return True

    try:
        subprocess.run(['git', 'add'] + version_paths,
                       cwd=PROJECT_ROOT, check=True)
        subprocess.run(['git', 'commit', '-m', f"chore: release {tag}"],
                       cwd=PROJECT_ROOT, check=True)
        subprocess.run(['git', 'tag', '-a', tag, '-m', msg],
                       cwd=PROJECT_ROOT, check=True)
        subprocess.run(['git', 'push'], cwd=PROJECT_ROOT, check=True)
        subprocess.run(['git', 'push', '--tags'], cwd=PROJECT_ROOT, check=True)
        logger.info(f"Committed, tagged {tag}, and pushed")
        return True
    except subprocess.CalledProcessError as e:
        logger.error(f"Git operation failed: {e}")
        return False


def extract_changelog_notes(version: str) -> Optional[str]:
    """Parse CHANGELOG.md and return the section for a given version.

    Looks for a heading like ``## [X.Y.Z]`` or ``## [Unreleased]`` and
    returns all content until the next ``## `` heading.
    """
    changelog = PROJECT_ROOT / 'CHANGELOG.md'
    if not changelog.exists():
        logger.warning("CHANGELOG.md not found")
        return None

    content = changelog.read_text()
    # Match ## [version] or ## [Unreleased]
    pattern = rf'^## \[{re.escape(version)}\].*$'
    match = re.search(pattern, content, re.MULTILINE)
    if not match:
        # Also try "Unreleased" if version not found explicitly
        if version.lower() == 'unreleased':
            match = re.search(r'^## \[Unreleased\].*$', content, re.MULTILINE)
        if not match:
            logger.warning(f"No changelog section found for {version}")
            return None

    start = match.end()
    # Find the next ## heading
    next_heading = re.search(r'^## \[', content[start:], re.MULTILINE)
    if next_heading:
        section = content[start:start + next_heading.start()]
    else:
        section = content[start:]

    return section.strip()


def verify_import() -> bool:
    """Verify the package imports and reports the correct version."""
    result = subprocess.run(
        [sys.executable, '-c',
         'import siege_utilities; print(siege_utilities.__version__)'],
        cwd=PROJECT_ROOT,
        capture_output=True, text=True,
    )
    if result.returncode != 0:
        logger.error(f"Import failed: {result.stderr}")
        return False
    reported = result.stdout.strip().split('\n')[-1]
    expected = get_current_version()
    if reported != expected:
        logger.error(f"Version mismatch: import says {reported}, pyproject says {expected}")
        return False
    logger.info(f"Import OK, version {reported}")
    return True


def create_github_release(version: str, notes: str, dry_run: bool = False) -> bool:
    """Create a GitHub release using gh CLI."""
    if dry_run:
        logger.info(f"[DRY RUN] Would create GitHub release v{version}")
        return True

    result = subprocess.run(
        ['gh', 'release', 'create', f'v{version}',
         '--repo', 'siege-analytics/siege_utilities',
         '--title', f'v{version}',
         '--notes', notes],
        cwd=PROJECT_ROOT,
    )
    return result.returncode == 0


def main():
    parser = argparse.ArgumentParser(description="Release Manager for Siege Utilities")
    parser.add_argument('--check', action='store_true', help='Check version consistency')
    parser.add_argument('--bump', choices=['major', 'minor', 'patch'], help='Bump version')
    parser.add_argument('--set-version', type=str, help='Set explicit version')
    parser.add_argument('--build', action='store_true', help='Build package')
    parser.add_argument('--test', action='store_true', help='Run tests')
    parser.add_argument('--verify', action='store_true', help='Verify import + version')
    parser.add_argument('--release', action='store_true', help='Full release workflow')
    parser.add_argument('--bump-type', choices=['major', 'minor', 'patch'],
                        help='Bump type for --release (mutually exclusive with --target-version)')
    parser.add_argument('--target-version', type=str,
                        help='Explicit target version X.Y.Z for --release (validated against current)')
    parser.add_argument('--release-notes', type=str, help='Release notes for --release')
    parser.add_argument('--changelog', action='store_true',
                        help='Use CHANGELOG.md for release notes (instead of --release-notes)')
    parser.add_argument('--skip-tests', action='store_true', help='Skip tests during release')
    parser.add_argument('--skip-github', action='store_true', help='Skip GitHub release creation')
    parser.add_argument('--pypi', action='store_true', help='Upload to PyPI after build')
    parser.add_argument('--repository', choices=['pypi', 'testpypi'], default='pypi',
                        help='PyPI repository target (default: pypi)')
    parser.add_argument('--token', type=str,
                        help='PyPI API token (or set PYPI_API_TOKEN env var)')
    parser.add_argument('--dry-run', action='store_true',
                        help='Show actions without executing side effects')
    parser.add_argument('--clean', action='store_true',
                        help='Clean build artifacts before building')

    args = parser.parse_args()

    if args.check:
        status = check_consistency()
        print(json.dumps(status, indent=2))
        sys.exit(0 if status['consistent'] else 1)

    elif args.bump:
        new_ver = bump_version(args.bump)
        print(f"Bumped to {new_ver}")

    elif args.set_version:
        set_version(args.set_version)
        print(f"Set version to {args.set_version}")

    elif args.build:
        if args.clean:
            clean_build_artifacts()
        if not build_package():
            sys.exit(1)
        if not validate_build():
            sys.exit(1)
        if args.pypi:
            if not upload_to_pypi(args.repository, args.token, args.dry_run):
                sys.exit(1)

    elif args.test:
        sys.exit(0 if run_tests() else 1)

    elif args.verify:
        sys.exit(0 if verify_import() else 1)

    elif args.release:
        if not args.bump_type and not args.target_version:
            parser.error("--release requires --bump-type or --target-version")
        if args.bump_type and args.target_version:
            parser.error("--bump-type and --target-version are mutually exclusive")
        if not args.release_notes and not args.changelog:
            parser.error("--release requires --release-notes or --changelog")

        # 1. Check consistency
        logger.info("Step 1/12: Checking version consistency...")
        status = check_consistency()
        if not status['consistent']:
            logger.error(f"Version inconsistency: {status['versions']}")
            sys.exit(1)

        # 1b. Validate target version (if provided)
        if args.target_version:
            try:
                validate_target_version(args.target_version)
            except ValueError as e:
                logger.error(str(e))
                sys.exit(1)

        # 1c. Check CI status
        logger.info("Checking CI status on current branch...")
        if not check_ci_status():
            logger.warning("CI check did not pass — proceeding anyway (verify manually)")

        # 2. Tests
        logger.info("Step 2/12: Running tests...")
        if not args.skip_tests:
            if not run_tests():
                logger.error("Tests failed, aborting release")
                sys.exit(1)
        else:
            logger.info("Tests skipped (--skip-tests)")

        # 3. Set version
        logger.info("Step 3/12: Setting version...")
        if args.target_version:
            new_ver = args.target_version
            current_ver = get_current_version()
            if new_ver == current_ver:
                logger.info(f"Version already at {new_ver} — skipping bump")
            elif args.dry_run:
                logger.info(f"[DRY RUN] Would set version to {new_ver}")
            else:
                set_version(new_ver)
                logger.info(f"Version set to {new_ver}")
        else:
            if args.dry_run:
                current = get_current_version()
                parts = current.split('.')
                major, minor, patch = int(parts[0]), int(parts[1]), int(parts[2])
                if args.bump_type == 'major':
                    new_ver = f"{major + 1}.0.0"
                elif args.bump_type == 'minor':
                    new_ver = f"{major}.{minor + 1}.0"
                else:
                    new_ver = f"{major}.{minor}.{patch + 1}"
                logger.info(f"[DRY RUN] Would bump {current} -> {new_ver}")
            else:
                new_ver = bump_version(args.bump_type)
                logger.info(f"Version bumped to {new_ver}")

        # 4. Verify import
        logger.info("Step 4/12: Verifying import...")
        if not args.dry_run:
            if not verify_import():
                logger.error("Import verification failed")
                sys.exit(1)
        else:
            logger.info("[DRY RUN] Would verify import")

        # 5. Clean build artifacts
        logger.info("Step 5/12: Cleaning build artifacts...")
        clean_build_artifacts()

        # 6. Build
        logger.info("Step 6/12: Building package...")
        if not args.dry_run:
            if not build_package():
                sys.exit(1)
        else:
            logger.info("[DRY RUN] Would build package")

        # 7. Validate
        logger.info("Step 7/12: Validating package...")
        if not args.dry_run:
            if not validate_build():
                sys.exit(1)
        else:
            logger.info("[DRY RUN] Would validate package")

        # 8. Resolve release notes (needed for tag + GitHub release)
        logger.info("Step 8/12: Resolving release notes...")
        if args.changelog:
            notes = extract_changelog_notes(new_ver) or extract_changelog_notes('Unreleased')
            if not notes:
                logger.warning("Could not extract changelog notes, using default")
                notes = f"Release v{new_ver}"
        else:
            notes = args.release_notes

        # 9. Merge develop → main (gitflow)
        logger.info("Step 9/12: Merging develop → main...")
        if _current_branch() == 'develop' or args.dry_run:
            if not merge_develop_to_main(args.dry_run):
                logger.error("Merge develop → main failed")
                sys.exit(1)
        else:
            logger.warning(f"Not on develop (on {_current_branch()}), skipping merge")

        # 10. Tag on main + push
        logger.info("Step 10/12: Tagging release on main...")
        if not git_tag_and_push(new_ver, notes, args.dry_run):
            logger.warning("Tagging failed (non-fatal)")

        # 11. Upload to PyPI
        logger.info("Step 11/12: PyPI upload...")
        if args.pypi:
            if not upload_to_pypi(args.repository, args.token, args.dry_run):
                sys.exit(1)
        else:
            logger.info("PyPI upload skipped (use --pypi to enable)")

        # 12. GitHub release
        logger.info("Step 12/12: GitHub release...")
        if not args.skip_github:
            if not create_github_release(new_ver, notes, args.dry_run):
                logger.warning("GitHub release creation failed (non-fatal)")
        else:
            logger.info("GitHub release skipped (--skip-github)")

        logger.info(f"Release {new_ver} complete" +
                     (" (dry run)" if args.dry_run else ""))

    else:
        parser.print_help()


if __name__ == '__main__':
    main()
