Skip to content

Commit 3bfd59e

Browse files
authored
Merge pull request #1042 from DataRecce/feature/drc-2264-metadata-delete-in-recce-cloud-cli
feat: Add metadata delete in recce cloud cli
2 parents 511c780 + 5681f9a commit 3bfd59e

File tree

9 files changed

+667
-0
lines changed

9 files changed

+667
-0
lines changed

recce_cloud/api/base.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,22 @@ def get_session_download_urls(
130130
- catalog_url: Presigned URL for catalog.json download
131131
"""
132132
pass
133+
134+
@abstractmethod
135+
def delete_session(
136+
self,
137+
cr_number: Optional[int] = None,
138+
session_type: Optional[str] = None,
139+
) -> Dict:
140+
"""
141+
Delete a session.
142+
143+
Args:
144+
cr_number: Change request number (PR/MR number) for CR sessions
145+
session_type: Session type ("cr", "prod") - "prod" deletes base session
146+
147+
Returns:
148+
Dictionary containing:
149+
- session_id: Session ID of the deleted session
150+
"""
151+
pass

recce_cloud/api/client.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,41 @@ def update_session(self, org_id: str, project_id: str, session_id: str, adapter_
189189
)
190190
return response.json()
191191

192+
def delete_session(self, session_id: str) -> bool:
193+
"""
194+
Delete a session by ID.
195+
196+
This uses the user interactive endpoint DELETE /sessions/{session_id}.
197+
If the session is a base session, it will be automatically unbound first.
198+
199+
Args:
200+
session_id: Session ID to delete
201+
202+
Returns:
203+
True if deletion was successful
204+
205+
Raises:
206+
RecceCloudException: If the request fails
207+
"""
208+
api_url = f"{self.base_url_v2}/sessions/{session_id}"
209+
response = self._request("DELETE", api_url)
210+
if response.status_code == 204:
211+
return True
212+
if response.status_code == 403:
213+
raise RecceCloudException(
214+
reason=response.json().get("detail", "Permission denied"),
215+
status_code=response.status_code,
216+
)
217+
if response.status_code == 404:
218+
raise RecceCloudException(
219+
reason="Session not found",
220+
status_code=response.status_code,
221+
)
222+
raise RecceCloudException(
223+
reason=response.text,
224+
status_code=response.status_code,
225+
)
226+
192227

193228
class ReportClient:
194229
"""Client for fetching reports from Recce Cloud API."""

recce_cloud/api/github.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,32 @@ def get_session_download_urls(
104104
params["pr_number"] = cr_number
105105

106106
return self._make_request("GET", url, params=params)
107+
108+
def delete_session(
109+
self,
110+
cr_number: Optional[int] = None,
111+
session_type: Optional[str] = None,
112+
) -> Dict:
113+
"""
114+
Delete a GitHub PR/base session.
115+
116+
Args:
117+
cr_number: PR number for pull request sessions
118+
session_type: Session type ("cr", "prod") - "prod" deletes base session
119+
120+
Returns:
121+
Dictionary containing session_id of deleted session
122+
"""
123+
url = f"{self.api_host}/api/v2/github/{self.repository}/session"
124+
125+
# Build query parameters
126+
params = {}
127+
128+
# For prod session, set base=true
129+
if session_type == "prod":
130+
params["base"] = "true"
131+
# For CR session, include pr_number
132+
elif session_type == "cr" and cr_number is not None:
133+
params["pr_number"] = cr_number
134+
135+
return self._make_request("DELETE", url, params=params)

recce_cloud/api/gitlab.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,32 @@ def get_session_download_urls(
109109
params["mr_iid"] = cr_number
110110

111111
return self._make_request("GET", url, params=params)
112+
113+
def delete_session(
114+
self,
115+
cr_number: Optional[int] = None,
116+
session_type: Optional[str] = None,
117+
) -> Dict:
118+
"""
119+
Delete a GitLab MR/base session.
120+
121+
Args:
122+
cr_number: MR IID for merge request sessions
123+
session_type: Session type ("cr", "prod") - "prod" deletes base session
124+
125+
Returns:
126+
Dictionary containing session_id of deleted session
127+
"""
128+
url = f"{self.api_host}/api/v2/gitlab/{self.project_path}/session"
129+
130+
# Build query parameters
131+
params = {}
132+
133+
# For prod session, set base=true
134+
if session_type == "prod":
135+
params["base"] = "true"
136+
# For CR session, include mr_iid
137+
elif session_type == "cr" and cr_number is not None:
138+
params["mr_iid"] = cr_number
139+
140+
return self._make_request("DELETE", url, params=params)

recce_cloud/cli.py

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
from recce_cloud import __version__
1616
from recce_cloud.artifact import get_adapter_type, verify_artifacts_path
1717
from recce_cloud.ci_providers import CIDetector
18+
from recce_cloud.delete import (
19+
delete_existing_session,
20+
delete_with_platform_apis,
21+
)
1822
from recce_cloud.download import (
1923
download_from_existing_session,
2024
download_with_platform_apis,
@@ -432,6 +436,171 @@ def download(target_path, session_id, prod, dry_run, force):
432436
download_with_platform_apis(console, token, ci_info, target_path, force)
433437

434438

439+
@cloud_cli.command()
440+
@click.option(
441+
"--session-id",
442+
envvar="RECCE_SESSION_ID",
443+
help="Session ID to delete. Required for non-CI workflows.",
444+
)
445+
@click.option(
446+
"--dry-run",
447+
is_flag=True,
448+
help="Show what would be deleted without actually deleting",
449+
)
450+
@click.option(
451+
"--force",
452+
"-f",
453+
is_flag=True,
454+
help="Skip confirmation prompt",
455+
)
456+
def delete(session_id, dry_run, force):
457+
"""
458+
Delete a Recce Cloud session.
459+
460+
Note: Deleting production sessions is not supported. To update production,
461+
upload a new session to overwrite it. Contact us if you need to delete
462+
a production session.
463+
464+
\b
465+
Authentication (auto-detected):
466+
- RECCE_API_TOKEN (for --session-id workflow)
467+
- GITHUB_TOKEN (GitHub Actions)
468+
- CI_JOB_TOKEN (GitLab CI)
469+
470+
\b
471+
Common Examples:
472+
# Delete current PR/MR session (in CI)
473+
recce-cloud delete
474+
475+
# Delete a specific session by ID
476+
recce-cloud delete --session-id abc123
477+
478+
# Skip confirmation prompt
479+
recce-cloud delete --force
480+
"""
481+
console = Console()
482+
483+
# 1. Auto-detect CI environment information
484+
console.rule("CI Environment Detection", style="blue")
485+
try:
486+
ci_info = CIDetector.detect()
487+
488+
# Display detected CI information immediately
489+
if ci_info:
490+
info_table = []
491+
if ci_info.platform:
492+
info_table.append(f"[cyan]Platform:[/cyan] {ci_info.platform}")
493+
494+
if ci_info.repository:
495+
info_table.append(f"[cyan]Repository:[/cyan] {ci_info.repository}")
496+
497+
# Only show session type and CR info for platform workflow
498+
if not session_id:
499+
if ci_info.session_type:
500+
info_table.append(f"[cyan]Session Type:[/cyan] {ci_info.session_type}")
501+
502+
# Only show CR number and URL for CR sessions (not for prod)
503+
if ci_info.session_type == "cr" and ci_info.cr_number is not None:
504+
if ci_info.platform == "github-actions":
505+
info_table.append(f"[cyan]PR Number:[/cyan] {ci_info.cr_number}")
506+
elif ci_info.platform == "gitlab-ci":
507+
info_table.append(f"[cyan]MR Number:[/cyan] {ci_info.cr_number}")
508+
else:
509+
info_table.append(f"[cyan]CR Number:[/cyan] {ci_info.cr_number}")
510+
511+
# Only show CR URL for CR sessions
512+
if ci_info.session_type == "cr" and ci_info.cr_url:
513+
if ci_info.platform == "github-actions":
514+
info_table.append(f"[cyan]PR URL:[/cyan] {ci_info.cr_url}")
515+
elif ci_info.platform == "gitlab-ci":
516+
info_table.append(f"[cyan]MR URL:[/cyan] {ci_info.cr_url}")
517+
else:
518+
info_table.append(f"[cyan]CR URL:[/cyan] {ci_info.cr_url}")
519+
520+
for line in info_table:
521+
console.print(line)
522+
else:
523+
console.print("[yellow]No CI environment detected[/yellow]")
524+
except Exception as e:
525+
console.print(f"[yellow]Warning:[/yellow] Failed to detect CI environment: {e}")
526+
console.print("Continuing without CI metadata...")
527+
ci_info = None
528+
529+
# 2. Handle dry-run mode (before authentication or API calls)
530+
if dry_run:
531+
console.rule("Dry Run Summary", style="yellow")
532+
console.print("[yellow]Dry run mode enabled - no actual deletion will be performed[/yellow]")
533+
console.print()
534+
535+
# Display platform information if detected
536+
if ci_info and ci_info.platform:
537+
console.print("[cyan]Platform Information:[/cyan]")
538+
console.print(f" • Platform: {ci_info.platform}")
539+
if ci_info.repository:
540+
console.print(f" • Repository: {ci_info.repository}")
541+
if ci_info.session_type:
542+
console.print(f" • Session Type: {ci_info.session_type}")
543+
if ci_info.session_type == "cr" and ci_info.cr_number is not None:
544+
console.print(f" • CR Number: {ci_info.cr_number}")
545+
console.print()
546+
547+
# Display delete summary
548+
console.print("[cyan]Delete Workflow:[/cyan]")
549+
if session_id:
550+
console.print(" • Delete specific session by ID")
551+
console.print(f" • Session ID: {session_id}")
552+
else:
553+
console.print(" • Auto-detect and delete PR/MR session")
554+
555+
if ci_info and ci_info.platform in ["github-actions", "gitlab-ci"]:
556+
console.print(" • Platform-specific APIs will be used")
557+
else:
558+
console.print(" • [yellow]Warning: Platform not supported for auto-session discovery[/yellow]")
559+
560+
console.print()
561+
console.print("[green]✓[/green] Dry run completed successfully")
562+
sys.exit(0)
563+
564+
# 3. Confirmation prompt (unless --force is provided)
565+
if not force:
566+
console.print()
567+
if session_id:
568+
confirm_msg = f'Are you sure you want to delete session "{session_id}"?'
569+
else:
570+
confirm_msg = "Are you sure you want to delete the PR/MR session?"
571+
572+
if not click.confirm(confirm_msg):
573+
console.print("[yellow]Aborted[/yellow]")
574+
sys.exit(0)
575+
576+
# 4. Choose delete workflow based on whether session_id is provided
577+
if session_id:
578+
# Generic workflow: Delete from existing session using session ID
579+
# This workflow requires RECCE_API_TOKEN
580+
token = os.getenv("RECCE_API_TOKEN")
581+
if not token:
582+
console.print("[red]Error:[/red] No RECCE_API_TOKEN provided")
583+
console.print("Set RECCE_API_TOKEN environment variable for session-based delete")
584+
sys.exit(2)
585+
586+
delete_existing_session(console, token, session_id)
587+
else:
588+
# Platform-specific workflow: Use platform APIs to find and delete session
589+
# This workflow MUST use CI job tokens (CI_JOB_TOKEN or GITHUB_TOKEN)
590+
if not ci_info or not ci_info.access_token:
591+
console.print("[red]Error:[/red] Platform-specific delete requires CI environment")
592+
console.print("Either run in GitHub Actions/GitLab CI or provide --session-id for generic delete")
593+
sys.exit(2)
594+
595+
token = ci_info.access_token
596+
if ci_info.platform == "github-actions":
597+
console.print("[cyan]Info:[/cyan] Using GITHUB_TOKEN for platform-specific authentication")
598+
elif ci_info.platform == "gitlab-ci":
599+
console.print("[cyan]Info:[/cyan] Using CI_JOB_TOKEN for platform-specific authentication")
600+
601+
delete_with_platform_apis(console, token, ci_info, prod=False)
602+
603+
435604
@cloud_cli.command()
436605
@click.option(
437606
"--repo",

0 commit comments

Comments
 (0)