Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 19 additions & 0 deletions recce_cloud/api/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,22 @@ def get_session_download_urls(
- catalog_url: Presigned URL for catalog.json download
"""
pass

@abstractmethod
def delete_session(
self,
cr_number: Optional[int] = None,
session_type: Optional[str] = None,
) -> Dict:
"""
Delete a session.

Args:
cr_number: Change request number (PR/MR number) for CR sessions
session_type: Session type ("cr", "prod") - "prod" deletes base session

Returns:
Dictionary containing:
- session_id: Session ID of the deleted session
"""
pass
35 changes: 35 additions & 0 deletions recce_cloud/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,38 @@ def update_session(self, org_id: str, project_id: str, session_id: str, adapter_
status_code=response.status_code,
)
return response.json()

def delete_session(self, session_id: str) -> bool:
"""
Delete a session by ID.

This uses the user interactive endpoint DELETE /sessions/{session_id}.
If the session is a base session, it will be automatically unbound first.

Args:
session_id: Session ID to delete

Returns:
True if deletion was successful

Raises:
RecceCloudException: If the request fails
"""
api_url = f"{self.base_url_v2}/sessions/{session_id}"
response = self._request("DELETE", api_url)
if response.status_code == 204:
return True
if response.status_code == 403:
raise RecceCloudException(
reason=response.json().get("detail", "Permission denied"),
status_code=response.status_code,
)
if response.status_code == 404:
raise RecceCloudException(
reason="Session not found",
status_code=response.status_code,
)
raise RecceCloudException(
reason=response.text,
status_code=response.status_code,
)
29 changes: 29 additions & 0 deletions recce_cloud/api/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,32 @@ def get_session_download_urls(
params["pr_number"] = cr_number

return self._make_request("GET", url, params=params)

def delete_session(
self,
cr_number: Optional[int] = None,
session_type: Optional[str] = None,
) -> Dict:
"""
Delete a GitHub PR/base session.

Args:
cr_number: PR number for pull request sessions
session_type: Session type ("cr", "prod") - "prod" deletes base session

Returns:
Dictionary containing session_id of deleted session
"""
url = f"{self.api_host}/api/v2/github/{self.repository}/session"

# Build query parameters
params = {}

# For prod session, set base=true
if session_type == "prod":
params["base"] = "true"
# For CR session, include pr_number
elif session_type == "cr" and cr_number is not None:
params["pr_number"] = cr_number

return self._make_request("DELETE", url, params=params)
29 changes: 29 additions & 0 deletions recce_cloud/api/gitlab.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,32 @@ def get_session_download_urls(
params["mr_iid"] = cr_number

return self._make_request("GET", url, params=params)

def delete_session(
self,
cr_number: Optional[int] = None,
session_type: Optional[str] = None,
) -> Dict:
"""
Delete a GitLab MR/base session.

Args:
cr_number: MR IID for merge request sessions
session_type: Session type ("cr", "prod") - "prod" deletes base session

Returns:
Dictionary containing session_id of deleted session
"""
url = f"{self.api_host}/api/v2/gitlab/{self.project_path}/session"

# Build query parameters
params = {}

# For prod session, set base=true
if session_type == "prod":
params["base"] = "true"
# For CR session, include mr_iid
elif session_type == "cr" and cr_number is not None:
params["mr_iid"] = cr_number

return self._make_request("DELETE", url, params=params)
186 changes: 186 additions & 0 deletions recce_cloud/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
from recce_cloud import __version__
from recce_cloud.artifact import get_adapter_type, verify_artifacts_path
from recce_cloud.ci_providers import CIDetector
from recce_cloud.delete import (
delete_existing_session,
delete_with_platform_apis,
)
from recce_cloud.download import (
download_from_existing_session,
download_with_platform_apis,
Expand Down Expand Up @@ -430,5 +434,187 @@ def download(target_path, session_id, prod, dry_run, force):
download_with_platform_apis(console, token, ci_info, target_path, force)


@cloud_cli.command()
@click.option(
"--session-id",
envvar="RECCE_SESSION_ID",
help="Session ID to delete. Required for non-CI workflows.",
)
@click.option(
"--prod",
is_flag=True,
help="Delete production/base session instead of PR/MR session",
)
@click.option(
"--dry-run",
is_flag=True,
help="Show what would be deleted without actually deleting",
)
@click.option(
"--force",
"-f",
is_flag=True,
help="Skip confirmation prompt",
)
def delete(session_id, prod, dry_run, force):
"""
Delete a Recce Cloud session.

\b
Authentication (auto-detected):
- RECCE_API_TOKEN (for --session-id workflow)
- GITHUB_TOKEN (GitHub Actions)
- CI_JOB_TOKEN (GitLab CI)

\b
Common Examples:
# Delete current PR/MR session (in CI)
recce-cloud delete

# Delete project's production/base session
recce-cloud delete --prod

# Delete a specific session by ID
recce-cloud delete --session-id abc123

# Skip confirmation prompt
recce-cloud delete --force
"""
console = Console()

# Validate flag combinations
if session_id and prod:
console.print("[yellow]Warning:[/yellow] --prod is ignored when --session-id is provided")

# Determine session type from --prod flag
session_type = "prod" if prod else None

# 1. Auto-detect CI environment information
console.rule("CI Environment Detection", style="blue")
try:
ci_info = CIDetector.detect()
ci_info = CIDetector.apply_overrides(ci_info, session_type=session_type)

# Display detected CI information immediately
if ci_info:
info_table = []
if ci_info.platform:
info_table.append(f"[cyan]Platform:[/cyan] {ci_info.platform}")

if ci_info.repository:
info_table.append(f"[cyan]Repository:[/cyan] {ci_info.repository}")

# Only show session type and CR info for platform workflow
if not session_id:
if ci_info.session_type:
info_table.append(f"[cyan]Session Type:[/cyan] {ci_info.session_type}")

# Only show CR number and URL for CR sessions (not for prod)
if ci_info.session_type == "cr" and ci_info.cr_number is not None:
if ci_info.platform == "github-actions":
info_table.append(f"[cyan]PR Number:[/cyan] {ci_info.cr_number}")
elif ci_info.platform == "gitlab-ci":
info_table.append(f"[cyan]MR Number:[/cyan] {ci_info.cr_number}")
else:
info_table.append(f"[cyan]CR Number:[/cyan] {ci_info.cr_number}")

# Only show CR URL for CR sessions
if ci_info.session_type == "cr" and ci_info.cr_url:
if ci_info.platform == "github-actions":
info_table.append(f"[cyan]PR URL:[/cyan] {ci_info.cr_url}")
elif ci_info.platform == "gitlab-ci":
info_table.append(f"[cyan]MR URL:[/cyan] {ci_info.cr_url}")
else:
info_table.append(f"[cyan]CR URL:[/cyan] {ci_info.cr_url}")

for line in info_table:
console.print(line)
else:
console.print("[yellow]No CI environment detected[/yellow]")
except Exception as e:
console.print(f"[yellow]Warning:[/yellow] Failed to detect CI environment: {e}")
console.print("Continuing without CI metadata...")
ci_info = None

# 2. Handle dry-run mode (before authentication or API calls)
if dry_run:
console.rule("Dry Run Summary", style="yellow")
console.print("[yellow]Dry run mode enabled - no actual deletion will be performed[/yellow]")
console.print()

# Display platform information if detected
if ci_info and ci_info.platform:
console.print("[cyan]Platform Information:[/cyan]")
console.print(f" • Platform: {ci_info.platform}")
if ci_info.repository:
console.print(f" • Repository: {ci_info.repository}")
if ci_info.session_type:
console.print(f" • Session Type: {ci_info.session_type}")
if ci_info.session_type == "cr" and ci_info.cr_number is not None:
console.print(f" • CR Number: {ci_info.cr_number}")
console.print()

# Display delete summary
console.print("[cyan]Delete Workflow:[/cyan]")
if session_id:
console.print(" • Delete specific session by ID")
console.print(f" • Session ID: {session_id}")
else:
if prod:
console.print(" • Delete project's production/base session")
else:
console.print(" • Auto-detect and delete PR/MR session")

if ci_info and ci_info.platform in ["github-actions", "gitlab-ci"]:
console.print(" • Platform-specific APIs will be used")
else:
console.print(" • [yellow]Warning: Platform not supported for auto-session discovery[/yellow]")

console.print()
console.print("[green]✓[/green] Dry run completed successfully")
sys.exit(0)

# 3. Confirmation prompt (unless --force is provided)
if not force:
console.print()
if session_id:
confirm_msg = f'Are you sure you want to delete session "{session_id}"?'
elif prod:
confirm_msg = "Are you sure you want to delete the production/base session?"
else:
confirm_msg = "Are you sure you want to delete the PR/MR session?"

if not click.confirm(confirm_msg):
console.print("[yellow]Aborted[/yellow]")
sys.exit(0)

# 4. Choose delete workflow based on whether session_id is provided
if session_id:
# Generic workflow: Delete from existing session using session ID
# This workflow requires RECCE_API_TOKEN
token = os.getenv("RECCE_API_TOKEN")
if not token:
console.print("[red]Error:[/red] No RECCE_API_TOKEN provided")
console.print("Set RECCE_API_TOKEN environment variable for session-based delete")
sys.exit(2)

delete_existing_session(console, token, session_id)
else:
# Platform-specific workflow: Use platform APIs to find and delete session
# This workflow MUST use CI job tokens (CI_JOB_TOKEN or GITHUB_TOKEN)
if not ci_info or not ci_info.access_token:
console.print("[red]Error:[/red] Platform-specific delete requires CI environment")
console.print("Either run in GitHub Actions/GitLab CI or provide --session-id for generic delete")
sys.exit(2)

token = ci_info.access_token
if ci_info.platform == "github-actions":
console.print("[cyan]Info:[/cyan] Using GITHUB_TOKEN for platform-specific authentication")
elif ci_info.platform == "gitlab-ci":
console.print("[cyan]Info:[/cyan] Using CI_JOB_TOKEN for platform-specific authentication")

delete_with_platform_apis(console, token, ci_info, prod)


if __name__ == "__main__":
cloud_cli()
Loading