|
7 | 7 | import os |
8 | 8 | import subprocess |
9 | 9 | import sys |
| 10 | +from typing import Optional |
10 | 11 |
|
11 | 12 | import click |
12 | 13 | from rich.console import Console |
|
15 | 16 | from recce_cloud import __version__ |
16 | 17 | from recce_cloud.artifact import get_adapter_type, verify_artifacts_path |
17 | 18 | from recce_cloud.ci_providers import CIDetector |
| 19 | +from recce_cloud.commands.diagnostics import doctor |
18 | 20 | from recce_cloud.delete import ( |
19 | 21 | delete_existing_session, |
20 | 22 | delete_with_platform_apis, |
@@ -49,6 +51,10 @@ def cloud_cli(): |
49 | 51 | pass |
50 | 52 |
|
51 | 53 |
|
| 54 | +# Register commands from command modules |
| 55 | +cloud_cli.add_command(doctor) |
| 56 | + |
| 57 | + |
52 | 58 | @cloud_cli.command() |
53 | 59 | def version(): |
54 | 60 | """Show the version of recce-cloud.""" |
@@ -359,10 +365,89 @@ def init(org, project, status, clear): |
359 | 365 | console.print(f"[red]Error:[/red] Failed to fetch data from Recce Cloud: {e}") |
360 | 366 | sys.exit(1) |
361 | 367 | except Exception as e: |
362 | | - console.print(f"[red]Error:[/red] {e}") |
| 368 | + logger.debug("Unexpected error during init: %s", e, exc_info=True) |
| 369 | + console.print(f"[red]Error:[/red] An unexpected error occurred: {e}") |
| 370 | + console.print(" Try running 'recce-cloud login' again or check your network connection.") |
363 | 371 | sys.exit(1) |
364 | 372 |
|
365 | 373 |
|
| 374 | +def _get_production_session_id(console: Console, token: str) -> Optional[str]: |
| 375 | + """ |
| 376 | + Fetch the production session ID from Recce Cloud. |
| 377 | +
|
| 378 | + Returns the session ID if found, None otherwise (with error message printed). |
| 379 | + """ |
| 380 | + from recce_cloud.api.client import RecceCloudClient |
| 381 | + from recce_cloud.api.exceptions import RecceCloudException |
| 382 | + from recce_cloud.config.project_config import get_project_binding |
| 383 | + |
| 384 | + # Get project binding |
| 385 | + binding = get_project_binding() |
| 386 | + if not binding: |
| 387 | + # Check environment variables as fallback |
| 388 | + env_org = os.environ.get("RECCE_ORG") |
| 389 | + env_project = os.environ.get("RECCE_PROJECT") |
| 390 | + if env_org and env_project: |
| 391 | + binding = {"org": env_org, "project": env_project} |
| 392 | + else: |
| 393 | + console.print("[red]Error:[/red] No project binding found") |
| 394 | + console.print("Run 'recce-cloud init' to bind this directory to a project") |
| 395 | + return None |
| 396 | + |
| 397 | + org_slug = binding.get("org") |
| 398 | + project_slug = binding.get("project") |
| 399 | + |
| 400 | + try: |
| 401 | + client = RecceCloudClient(token) |
| 402 | + |
| 403 | + # Get org and project IDs |
| 404 | + org_info = client.get_organization(org_slug) |
| 405 | + if not org_info: |
| 406 | + console.print(f"[red]Error:[/red] Organization '{org_slug}' not found") |
| 407 | + return None |
| 408 | + org_id = org_info.get("id") |
| 409 | + if not org_id: |
| 410 | + console.print(f"[red]Error:[/red] Organization '{org_slug}' response missing ID") |
| 411 | + return None |
| 412 | + |
| 413 | + project_info = client.get_project(org_id, project_slug) |
| 414 | + if not project_info: |
| 415 | + console.print(f"[red]Error:[/red] Project '{project_slug}' not found") |
| 416 | + return None |
| 417 | + project_id = project_info.get("id") |
| 418 | + if not project_id: |
| 419 | + console.print(f"[red]Error:[/red] Project '{project_slug}' response missing ID") |
| 420 | + return None |
| 421 | + |
| 422 | + # List sessions and find production session |
| 423 | + sessions = client.list_sessions(org_id, project_id) |
| 424 | + for session in sessions: |
| 425 | + if session.get("is_base"): |
| 426 | + session_id = session.get("id") |
| 427 | + if not session_id: |
| 428 | + console.print("[red]Error:[/red] Production session found but has no ID") |
| 429 | + return None |
| 430 | + session_name = session.get("name") or "(unnamed)" |
| 431 | + session_id_display = session_id[:8] if len(session_id) >= 8 else session_id |
| 432 | + console.print( |
| 433 | + f"[cyan]Info:[/cyan] Found production session '{session_name}' (ID: {session_id_display}...)" |
| 434 | + ) |
| 435 | + return session_id |
| 436 | + |
| 437 | + console.print("[red]Error:[/red] No production session found") |
| 438 | + console.print("Create a production session first using 'recce-cloud upload --type prod' or via CI pipeline") |
| 439 | + return None |
| 440 | + |
| 441 | + except RecceCloudException as e: |
| 442 | + console.print(f"[red]Error:[/red] Failed to fetch sessions: {e}") |
| 443 | + return None |
| 444 | + except Exception as e: |
| 445 | + logger.debug("Unexpected error in _get_production_session_id: %s", e, exc_info=True) |
| 446 | + console.print(f"[red]Error:[/red] Unexpected error: {e}") |
| 447 | + console.print(" Check your network connection and try again.") |
| 448 | + return None |
| 449 | + |
| 450 | + |
366 | 451 | @cloud_cli.command() |
367 | 452 | @click.option( |
368 | 453 | "--target-path", |
@@ -593,22 +678,41 @@ def upload(target_path, session_id, session_name, skip_confirmation, cr, session |
593 | 678 | skip_confirmation=skip_confirmation, |
594 | 679 | ) |
595 | 680 | else: |
596 | | - # Platform-specific workflow: Use platform APIs to create session and upload |
| 681 | + # GitHub Action or GitLab CI/CD workflow: Use platform APIs to create session and upload |
597 | 682 | # This workflow MUST use CI job tokens (CI_JOB_TOKEN or GITHUB_TOKEN) |
598 | 683 | if not ci_info or not ci_info.access_token: |
599 | | - console.print("[red]Error:[/red] Platform-specific upload requires CI environment") |
600 | | - console.print( |
601 | | - "Either run in GitHub Actions/GitLab CI or provide --session-id/--session-name for generic upload" |
602 | | - ) |
603 | | - sys.exit(2) |
604 | | - |
605 | | - token = ci_info.access_token |
606 | | - if ci_info.platform == "github-actions": |
607 | | - console.print("[cyan]Info:[/cyan] Using GITHUB_TOKEN for platform-specific authentication") |
608 | | - elif ci_info.platform == "gitlab-ci": |
609 | | - console.print("[cyan]Info:[/cyan] Using CI_JOB_TOKEN for platform-specific authentication") |
| 684 | + # If --type prod is specified outside CI, fetch the production session and upload to it |
| 685 | + if session_type == "prod": |
| 686 | + from recce_cloud.auth.profile import get_api_token |
| 687 | + |
| 688 | + token = os.getenv("RECCE_API_TOKEN") or get_api_token() |
| 689 | + if not token: |
| 690 | + console.print("[red]Error:[/red] No RECCE_API_TOKEN provided and not logged in") |
| 691 | + console.print("Either set RECCE_API_TOKEN environment variable or run 'recce-cloud login' first") |
| 692 | + sys.exit(2) |
| 693 | + |
| 694 | + # Fetch the production session ID |
| 695 | + prod_session_id = _get_production_session_id(console, token) |
| 696 | + if not prod_session_id: |
| 697 | + sys.exit(2) |
| 698 | + |
| 699 | + upload_to_existing_session( |
| 700 | + console, token, prod_session_id, manifest_path, catalog_path, adapter_type, target_path |
| 701 | + ) |
| 702 | + else: |
| 703 | + console.print("[red]Error:[/red] Platform-specific upload requires CI environment") |
| 704 | + console.print( |
| 705 | + "Either run in GitHub Actions/GitLab CI or provide --session-id/--session-name for generic upload" |
| 706 | + ) |
| 707 | + sys.exit(2) |
| 708 | + else: |
| 709 | + token = ci_info.access_token |
| 710 | + if ci_info.platform == "github-actions": |
| 711 | + console.print("[cyan]Info:[/cyan] Using GITHUB_TOKEN for platform-specific authentication") |
| 712 | + elif ci_info.platform == "gitlab-ci": |
| 713 | + console.print("[cyan]Info:[/cyan] Using CI_JOB_TOKEN for platform-specific authentication") |
610 | 714 |
|
611 | | - upload_with_platform_apis(console, token, ci_info, manifest_path, catalog_path, adapter_type, target_path) |
| 715 | + upload_with_platform_apis(console, token, ci_info, manifest_path, catalog_path, adapter_type, target_path) |
612 | 716 |
|
613 | 717 |
|
614 | 718 | @cloud_cli.command(name="list") |
@@ -682,16 +786,24 @@ def list_sessions_cmd(session_type, output_json): |
682 | 786 | if not org_info: |
683 | 787 | console.print(f"[red]Error:[/red] Organization '{org}' not found or you don't have access") |
684 | 788 | sys.exit(2) |
685 | | - org_id = org_info["id"] |
| 789 | + org_id = org_info.get("id") |
| 790 | + if not org_id: |
| 791 | + console.print(f"[red]Error:[/red] Organization '{org}' response missing ID") |
| 792 | + sys.exit(2) |
686 | 793 |
|
687 | 794 | project_info = client.get_project(org_id, project) |
688 | 795 | if not project_info: |
689 | 796 | console.print(f"[red]Error:[/red] Project '{project}' not found in organization '{org}'") |
690 | 797 | sys.exit(2) |
691 | | - project_id = project_info["id"] |
| 798 | + project_id = project_info.get("id") |
| 799 | + if not project_id: |
| 800 | + console.print(f"[red]Error:[/red] Project '{project}' response missing ID") |
| 801 | + sys.exit(2) |
692 | 802 |
|
693 | 803 | except Exception as e: |
| 804 | + logger.debug("Failed to initialize client for list_sessions: %s", e, exc_info=True) |
694 | 805 | console.print(f"[red]Error:[/red] Failed to initialize: {e}") |
| 806 | + console.print(" Check your authentication and network connection.") |
695 | 807 | sys.exit(2) |
696 | 808 |
|
697 | 809 | # Helper to derive session type from fields: |
|
0 commit comments