Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
b841a5e
feat: implement catalog workflow automation (issues #22, #23, #24)
ChristopherJHart Oct 28, 2025
10d2be2
refactor: use catalog_destined per-issue instead of global CLI flag
ChristopherJHart Oct 28, 2025
fa36308
fix: update default catalog repository to Testing-as-Code/tac-catalog
ChristopherJHart Oct 28, 2025
8ec7f14
refactor: extract OS from Test Tags instead of filename parsing
ChristopherJHart Oct 28, 2025
eda2d24
refactor: simplify regex pattern for OS tag extraction
ChristopherJHart Oct 28, 2025
7d3dfd2
fix: add console script entry point for CLI
ChristopherJHart Oct 28, 2025
c00c35a
fix: use non-recursive glob to avoid backup files in test_cases_dir
ChristopherJHart Oct 28, 2025
95a0f9c
refactor: separate catalog PR workflow from issues workflow
ChristopherJHart Oct 28, 2025
b7db17f
fix: remove double slash in catalog repo URL
ChristopherJHart Oct 30, 2025
48e29e8
feat: use conventional branch naming for catalog PRs
ChristopherJHart Oct 30, 2025
dfd519a
refactor: include OS name in catalog branch naming
ChristopherJHart Oct 30, 2025
993afcb
feat: add tracking issue creation for catalog PRs
ChristopherJHart Nov 7, 2025
ff12796
fix: strip OS tags from test case titles in CLI commands
ChristopherJHart Nov 7, 2025
f486113
feat: suggest project branch name based on catalog branch
ChristopherJHart Nov 7, 2025
e5222ae
refactor: use fenced code blocks for copyable commands
ChristopherJHart Nov 7, 2025
2d333eb
fix: prevent data loss when test case not found in source file
ChristopherJHart Nov 13, 2025
fb37bf4
fix: add defensive check to prevent wiping files with empty test_cases
ChristopherJHart Nov 13, 2025
85b0322
fix: use atomic file writes to prevent data loss from truncation
ChristopherJHart Nov 13, 2025
436a5ca
fix: handle large files (>1MB) in fetch-files command
ChristopherJHart Nov 13, 2025
2f2f022
Revert "fix: add defensive check to prevent wiping files with empty t…
ChristopherJHart Nov 13, 2025
3332dc6
feat: add test requirement section to catalog tracking issues
ChristopherJHart Nov 14, 2025
f72d9af
fix: improve YAML formatting in test requirement section
ChristopherJHart Nov 14, 2025
9d5fd87
Merge remote-tracking branch 'origin/master' into feature/catalog-wor…
ChristopherJHart Dec 19, 2025
60c230a
feat: add unified test requirements processing command
ChristopherJHart Dec 19, 2025
05d9470
feat: add backwards-compatible issues.yaml migration
ChristopherJHart Dec 19, 2025
5147dda
fix: issues.yaml migration fetches metadata from GitHub
ChristopherJHart Dec 20, 2025
2b268cb
fix: preserve YAML formatting when saving test_cases files
ChristopherJHart Dec 20, 2025
2688199
fix: convert ruamel.yaml CommentedMap to dict for Jinja2 templates
ChristopherJHart Dec 20, 2025
11f5232
fix: add Jinja2 |default filter for missing command keys in template
ChristopherJHart Dec 20, 2025
d1be4a4
feat: integrate issue body truncation in test_requirements processor
ChristopherJHart Dec 20, 2025
bebae75
fix: process all YAML files in test_cases directory
ChristopherJHart Dec 20, 2025
661fcd1
fix: project PR body now includes issue reference with closing keywords
ChristopherJHart Dec 20, 2025
8362aa1
feat: change project PR title prefix to "GenAI, Review:"
ChristopherJHart Dec 20, 2025
342e250
merge: resolve conflicts with master, keeping feature branch changes
ChristopherJHart Jan 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 212 additions & 0 deletions github_ops_manager/configuration/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,218 @@ def repo_callback(
repo_app.callback()(repo_callback)


# --- New unified test requirements processing command ---
@repo_app.command(name="process-test-requirements")
def process_test_requirements_cli(
ctx: typer.Context,
test_cases_dir: Annotated[
Path,
Argument(
envvar="TEST_CASES_DIR",
help="Directory containing test_cases.yaml files.",
),
],
base_directory: Annotated[
Path,
Option(
envvar="BASE_DIRECTORY",
help="Base directory for resolving script file paths. Defaults to parent of test_cases_dir.",
),
] = None,
issue_template: Annotated[
Path | None,
Option(
envvar="ISSUE_TEMPLATE",
help="Path to Jinja2 template for issue bodies.",
),
] = None,
issue_labels: Annotated[
str | None,
Option(
envvar="ISSUE_LABELS",
help="Comma-separated list of labels to apply to issues.",
),
] = None,
catalog_repo: Annotated[
str,
Option(
envvar="CATALOG_REPO",
help="Catalog repository name (owner/repo) for catalog-destined test cases.",
),
] = "Testing-as-Code/tac-catalog",
# ⚠️ DEPRECATED: Migration option - remove post-migration
issues_yaml: Annotated[
Path | None,
Option(
envvar="ISSUES_YAML",
help="[DEPRECATED] Path to legacy issues.yaml file for migration. "
"If provided, migrates existing issue/PR metadata to test_cases.yaml before processing.",
),
] = None,
) -> None:
"""Process test requirements directly from test_cases.yaml files.

This command eliminates the need for issues.yaml by:
- Reading test requirements directly from test_cases.yaml files
- Creating GitHub issues for test cases that don't have issue metadata
- Creating PRs (project or catalog) for test cases with generated scripts
- Writing metadata back to test_cases.yaml files

For each test case:
- If project_issue_number is missing, creates an issue
- If generated_script_path exists and PR metadata is missing:
- Non-catalog: creates PR in project repo
- Catalog-destined: creates PR in catalog repo

MIGRATION: If --issues-yaml is provided, existing metadata from issues.yaml
will be migrated to test_cases.yaml before normal processing begins.
"""
from github_ops_manager.synchronize.test_requirements import process_test_requirements

repo: str = ctx.obj["repo"]
github_api_url: str = ctx.obj["github_api_url"]
github_pat_token: str = ctx.obj["github_pat_token"]
github_app_id: int = ctx.obj["github_app_id"]
github_app_private_key_path: Path | None = ctx.obj["github_app_private_key_path"]
github_app_installation_id: int = ctx.obj["github_app_installation_id"]
github_auth_type = ctx.obj["github_auth_type"]

# Validate test cases directory
if not test_cases_dir.exists():
typer.echo(f"Test cases directory not found: {test_cases_dir.absolute()}", err=True)
raise typer.Exit(1)

if not test_cases_dir.is_dir():
typer.echo(f"Test cases path is not a directory: {test_cases_dir.absolute()}", err=True)
raise typer.Exit(1)

# Default base directory to parent of test_cases_dir
if base_directory is None:
base_directory = test_cases_dir.parent

typer.echo(f"Processing test requirements from: {test_cases_dir.absolute()}")
typer.echo(f"Base directory for scripts: {base_directory.absolute()}")

# Parse labels
parsed_labels = None
if issue_labels:
parsed_labels = [label.strip() for label in issue_labels.split(",") if label.strip()]

# Use default template if not specified
if issue_template is None:
issue_template = Path(__file__).parent.parent / "templates" / "tac_issues_body.j2"
if issue_template.exists():
typer.echo(f"Using default issue template: {issue_template}")
else:
issue_template = None
typer.echo("No issue template specified, using simple default body")

# Build repo URL
if "api.github.com" in github_api_url:
base_url = "https://github.com"
else:
base_url = github_api_url.replace("/api/v3", "").replace("/api", "").rstrip("/")
project_repo_url = f"{base_url}/{repo}"
catalog_repo_url = f"{base_url}/{catalog_repo}"

async def run_processing() -> dict:
# Create project adapter
project_adapter = await GitHubKitAdapter.create(
repo=repo,
github_auth_type=github_auth_type,
github_pat_token=github_pat_token,
github_app_id=github_app_id,
github_app_private_key_path=github_app_private_key_path,
github_app_installation_id=github_app_installation_id,
github_api_url=github_api_url,
)

# Get project default branch
project_repo_info = await project_adapter.get_repository()
project_default_branch = project_repo_info.default_branch

typer.echo(f"Project repository: {repo} (default branch: {project_default_branch})")

# ═══════════════════════════════════════════════════════════════════════════
# ⚠️ DEPRECATED: issues.yaml migration - TODO: Remove this block post-migration
# ═══════════════════════════════════════════════════════════════════════════
if issues_yaml is not None:
from github_ops_manager.synchronize.issues_yaml_migration import run_issues_yaml_migration

typer.echo("\n--- Running issues.yaml Migration (DEPRECATED) ---")
typer.echo(f"Migrating from: {issues_yaml.absolute()}")

migration_results = await run_issues_yaml_migration(
issues_yaml_path=issues_yaml,
test_cases_dir=test_cases_dir,
repo_url=project_repo_url,
github_adapter=project_adapter,
)

typer.echo("Migration complete:")
typer.echo(f" Total issues in issues.yaml: {migration_results['total_issues']}")
typer.echo(f" Already migrated: {migration_results['already_migrated']}")
typer.echo(f" Newly migrated: {migration_results['newly_migrated']}")
typer.echo(f" Skipped (no match): {migration_results['skipped_no_match']}")
typer.echo(f" Skipped (not in GitHub): {migration_results['skipped_not_in_github']}")

if migration_results["errors"]:
typer.echo(f"\nMigration warnings ({len(migration_results['errors'])}):", err=True)
for error in migration_results["errors"]:
typer.echo(f" - {error}", err=True)

typer.echo("") # Blank line before main processing
# ═══════════════════════════════════════════════════════════════════════════

# Create catalog adapter
catalog_adapter = await GitHubKitAdapter.create(
repo=catalog_repo,
github_auth_type=github_auth_type,
github_pat_token=github_pat_token,
github_app_id=github_app_id,
github_app_private_key_path=github_app_private_key_path,
github_app_installation_id=github_app_installation_id,
github_api_url=github_api_url,
)

# Get catalog default branch
catalog_repo_info = await catalog_adapter.get_repository()
catalog_default_branch = catalog_repo_info.default_branch

typer.echo(f"Catalog repository: {catalog_repo} (default branch: {catalog_default_branch})")

# Process test requirements
return await process_test_requirements(
test_cases_dir=test_cases_dir,
base_directory=base_directory,
project_adapter=project_adapter,
project_default_branch=project_default_branch,
project_repo_url=project_repo_url,
catalog_adapter=catalog_adapter,
catalog_default_branch=catalog_default_branch,
catalog_repo_url=catalog_repo_url,
issue_template_path=issue_template,
issue_labels=parsed_labels,
)

results = asyncio.run(run_processing())

# Report results
typer.echo("\n--- Processing Results ---")
typer.echo(f"Total test cases: {results['total_test_cases']}")
typer.echo(f"Issues created: {results['issues_created']}")
typer.echo(f"Project PRs created: {results['project_prs_created']}")
typer.echo(f"Catalog PRs created: {results['catalog_prs_created']}")

if results["errors"]:
typer.echo(f"\nErrors ({len(results['errors'])}):", err=True)
for error in results["errors"]:
typer.echo(f" - {error}", err=True)
raise typer.Exit(1)

typer.echo("\nProcessing completed successfully!")


# --- Move process-issues under repo_app ---
@repo_app.command(name="process-issues")
def process_issues_cli(
Expand Down
Loading