superset-showtime 0.1.0__py3-none-any.whl → 0.2.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of superset-showtime might be problematic. Click here for more details.

showtime/cli.py CHANGED
@@ -18,12 +18,9 @@ from .core.github import GitHubError, GitHubInterface
18
18
  DEFAULT_GITHUB_ACTOR = "unknown"
19
19
 
20
20
 
21
- def _get_service_urls(pr_number: int, sha: str = None):
21
+ def _get_service_urls(show):
22
22
  """Get AWS Console URLs for a service"""
23
- if sha:
24
- service_name = f"pr-{pr_number}-{sha}-service"
25
- else:
26
- service_name = f"pr-{pr_number}-service"
23
+ service_name = show.ecs_service_name
27
24
 
28
25
  return {
29
26
  "logs": f"https://us-west-2.console.aws.amazon.com/ecs/v2/clusters/superset-ci/services/{service_name}/logs?region=us-west-2",
@@ -31,15 +28,54 @@ def _get_service_urls(pr_number: int, sha: str = None):
31
28
  }
32
29
 
33
30
 
34
- def _show_service_urls(pr_number: int, context: str = "deployment", sha: str = None):
31
+ def _show_service_urls(show, context: str = "deployment"):
35
32
  """Show helpful AWS Console URLs for monitoring service"""
36
- urls = _get_service_urls(pr_number, sha)
33
+ urls = _get_service_urls(show)
37
34
  console.print(f"\n🎪 [bold blue]Monitor {context} progress:[/bold blue]")
38
35
  console.print(f" 📝 Live Logs: {urls['logs']}")
39
36
  console.print(f" 📊 ECS Service: {urls['service']}")
40
37
  console.print("")
41
38
 
42
39
 
40
+ def _determine_sync_action(pr, pr_state: str, target_sha: str) -> str:
41
+ """Determine what action is needed based on PR state and labels"""
42
+
43
+ # 1. Closed PRs always need cleanup
44
+ if pr_state == "closed":
45
+ return "cleanup"
46
+
47
+ # 2. Check for explicit trigger labels
48
+ trigger_labels = [label for label in pr.labels if "showtime-trigger-" in label]
49
+
50
+ # 3. Check for freeze label (PR-level) - only if no explicit triggers
51
+ freeze_labels = [label for label in pr.labels if "showtime-freeze" in label]
52
+ if freeze_labels and not trigger_labels:
53
+ return "frozen_no_action" # Frozen and no explicit triggers to override
54
+
55
+ if trigger_labels:
56
+ # Explicit triggers take priority
57
+ for trigger in trigger_labels:
58
+ if "showtime-trigger-start" in trigger:
59
+ if pr.current_show:
60
+ if pr.current_show.needs_update(target_sha):
61
+ return "rolling_update" # New commit with existing env
62
+ else:
63
+ return "no_action" # Same commit, no change needed
64
+ else:
65
+ return "create_environment" # New environment
66
+ elif "showtime-trigger-stop" in trigger:
67
+ return "destroy_environment"
68
+
69
+ # 3. No explicit triggers - check for implicit sync needs
70
+ if pr.current_show:
71
+ if pr.current_show.needs_update(target_sha):
72
+ return "auto_sync" # Auto-update on new commits
73
+ else:
74
+ return "no_action" # Everything in sync
75
+ else:
76
+ return "no_action" # No environment, no triggers
77
+
78
+
43
79
  def _schedule_blue_cleanup(pr_number: int, blue_services: list):
44
80
  """Schedule cleanup of blue services after successful green deployment"""
45
81
  import threading
@@ -96,10 +132,10 @@ app = typer.Typer(
96
132
  help="""🎪 Apache Superset ephemeral environment management
97
133
 
98
134
  [bold]GitHub Label Workflow:[/bold]
99
- 1. Add [green]🎪 trigger-start[/green] label to PR → Creates environment
135
+ 1. Add [green]🎪 ⚡ showtime-trigger-start[/green] label to PR → Creates environment
100
136
  2. Watch state labels: [blue]🎪 abc123f 🚦 building[/blue] → [green]🎪 abc123f 🚦 running[/green]
101
- 3. Add [yellow]🎪 conf-enable-ALERTS[/yellow] → Enables feature flags
102
- 4. Add [red]🎪 trigger-stop[/red] label → Destroys environment
137
+ 3. Add [orange]🎪 🧊 showtime-freeze[/orange] → Freezes environment from auto-sync
138
+ 4. Add [red]🎪 🛑 showtime-trigger-stop[/red] label → Destroys environment
103
139
 
104
140
  [bold]Reading State Labels:[/bold]
105
141
  • [green]🎪 abc123f 🚦 running[/green] - Environment status
@@ -111,13 +147,22 @@ app = typer.Typer(
111
147
  [dim]CLI commands work with existing environments or dry-run new ones.[/dim]""",
112
148
  rich_markup_mode="rich",
113
149
  )
150
+
114
151
  console = Console()
115
152
 
116
153
 
154
+ @app.command()
155
+ def version():
156
+ """Show version information"""
157
+ from . import __version__
158
+
159
+ console.print(f"🎪 Superset Showtime v{__version__}")
160
+
161
+
117
162
  @app.command()
118
163
  def start(
119
164
  pr_number: int = typer.Argument(..., help="PR number to create environment for"),
120
- sha: Optional[str] = typer.Option(None, help="Specific commit SHA (default: latest)"),
165
+ sha: Optional[str] = typer.Option(None, "--sha", help="Specific commit SHA (default: latest)"),
121
166
  ttl: Optional[str] = typer.Option("24h", help="Time to live (24h, 48h, 1w, close)"),
122
167
  size: Optional[str] = typer.Option("standard", help="Environment size (standard, large)"),
123
168
  dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be done"),
@@ -125,14 +170,23 @@ def start(
125
170
  False, "--dry-run-aws", help="Skip AWS operations, use mock data"
126
171
  ),
127
172
  aws_sleep: int = typer.Option(0, "--aws-sleep", help="Seconds to sleep during AWS operations"),
173
+ image_tag: Optional[str] = typer.Option(
174
+ None, "--image-tag", help="Override ECR image tag (e.g., pr-34764-ci)"
175
+ ),
176
+ force: bool = typer.Option(
177
+ False, "--force", help="Force re-deployment by deleting existing service"
178
+ ),
128
179
  ):
129
180
  """Create ephemeral environment for PR"""
130
181
  try:
131
182
  github = GitHubInterface()
132
183
 
133
- # Get latest SHA if not provided
184
+ # Get SHA - use provided SHA or default to latest
134
185
  if not sha:
135
186
  sha = github.get_latest_commit_sha(pr_number)
187
+ console.print(f"🎪 Using latest SHA: {sha[:7]}")
188
+ else:
189
+ console.print(f"🎪 Using specified SHA: {sha[:7]}")
136
190
 
137
191
  if dry_run:
138
192
  console.print("🎪 [bold yellow]DRY RUN[/bold yellow] - Would create environment:")
@@ -158,7 +212,9 @@ def start(
158
212
 
159
213
  # Create environment using trigger handler logic
160
214
  console.print(f"🎪 [bold blue]Creating environment for PR #{pr_number}...[/bold blue]")
161
- _handle_start_trigger(pr_number, github, dry_run_aws, (dry_run or False), aws_sleep)
215
+ _handle_start_trigger(
216
+ pr_number, github, dry_run_aws, (dry_run or False), aws_sleep, image_tag, force
217
+ )
162
218
 
163
219
  except GitHubError as e:
164
220
  console.print(f"🎪 [bold red]GitHub error:[/bold red] {e.message}")
@@ -210,9 +266,6 @@ def status(
210
266
  if show.requested_by:
211
267
  table.add_row("Requested by", f"@{show.requested_by}")
212
268
 
213
- if show.config != "standard":
214
- table.add_row("Configuration", show.config)
215
-
216
269
  if verbose:
217
270
  table.add_row("All Labels", ", ".join(pr.circus_labels))
218
271
 
@@ -284,19 +337,19 @@ def stop(
284
337
  console.print("🎪 [bold blue]Starting AWS cleanup...[/bold blue]")
285
338
  aws = AWSInterface()
286
339
 
287
- # Show logs URL for monitoring cleanup
288
- _show_service_urls(pr_number, "cleanup")
289
-
290
340
  try:
291
341
  # Get current environment info
292
342
  pr = PullRequest.from_id(pr_number, github)
293
343
 
294
344
  if pr.current_show:
295
345
  show = pr.current_show
346
+
347
+ # Show logs URL for monitoring cleanup
348
+ _show_service_urls(show, "cleanup")
296
349
  console.print(f"🎪 Destroying environment: {show.aws_service_name}")
297
350
 
298
351
  # Step 1: Check if ECS service exists and is active
299
- service_name = f"pr-{pr_number}-service" # Match GHA service naming
352
+ service_name = show.ecs_service_name
300
353
  console.print(f"🎪 Checking ECS service: {service_name}")
301
354
 
302
355
  service_exists = aws._service_exists(service_name)
@@ -409,7 +462,7 @@ def list(
409
462
  superset_url = "-"
410
463
 
411
464
  # Get AWS service URLs - iTerm2 supports Rich clickable links
412
- aws_urls = _get_service_urls(show.pr_number, show.sha)
465
+ aws_urls = _get_service_urls(show)
413
466
  aws_logs_link = f"[link={aws_urls['logs']}]View[/link]"
414
467
 
415
468
  # Make PR number clickable
@@ -437,34 +490,19 @@ def list(
437
490
  @app.command()
438
491
  def labels():
439
492
  """🎪 Show complete circus tent label reference"""
493
+ from .core.label_colors import LABEL_DEFINITIONS
440
494
 
441
495
  console.print("🎪 [bold blue]Circus Tent Label Reference[/bold blue]")
442
496
  console.print()
443
497
 
444
- # Trigger Labels
445
- console.print("[bold yellow]🎯 Trigger Labels (Add these to GitHub PR):[/bold yellow]")
498
+ # User Action Labels (from LABEL_DEFINITIONS)
499
+ console.print("[bold yellow]🎯 User Action Labels (Add these to GitHub PR):[/bold yellow]")
446
500
  trigger_table = Table()
447
501
  trigger_table.add_column("Label", style="green")
448
- trigger_table.add_column("Action", style="white")
449
502
  trigger_table.add_column("Description", style="dim")
450
503
 
451
- trigger_table.add_row(
452
- "🎪 trigger-start", "Create environment", "Builds and deploys ephemeral environment"
453
- )
454
- trigger_table.add_row(
455
- "🎪 trigger-stop", "Destroy environment", "Cleans up AWS resources and removes labels"
456
- )
457
- trigger_table.add_row(
458
- "🎪 trigger-sync", "Update environment", "Updates to latest commit with zero downtime"
459
- )
460
- trigger_table.add_row(
461
- "🎪 conf-enable-ALERTS", "Enable feature flag", "Enables SUPERSET_FEATURE_ALERTS=True"
462
- )
463
- trigger_table.add_row(
464
- "🎪 conf-disable-DASHBOARD_RBAC",
465
- "Disable feature flag",
466
- "Disables SUPERSET_FEATURE_DASHBOARD_RBAC=False",
467
- )
504
+ for label_name, definition in LABEL_DEFINITIONS.items():
505
+ trigger_table.add_row(f"`{label_name}`", definition["description"])
468
506
 
469
507
  console.print(trigger_table)
470
508
  console.print()
@@ -485,7 +523,6 @@ def labels():
485
523
  state_table.add_row("🎪 {sha} 🌐 {ip-with-dashes}", "Environment IP", "🎪 abc123f 🌐 52-1-2-3")
486
524
  state_table.add_row("🎪 {sha} ⌛ {ttl-policy}", "TTL policy", "🎪 abc123f ⌛ 24h")
487
525
  state_table.add_row("🎪 {sha} 🤡 {username}", "Requested by", "🎪 abc123f 🤡 maxime")
488
- state_table.add_row("🎪 {sha} ⚙️ {config-list}", "Feature flags", "🎪 abc123f ⚙️ alerts,debug")
489
526
 
490
527
  console.print(state_table)
491
528
  console.print()
@@ -495,25 +532,23 @@ def labels():
495
532
  console.print()
496
533
 
497
534
  console.print("[bold]1. Create Environment:[/bold]")
498
- console.print(" • Add label: [green]🎪 trigger-start[/green]")
535
+ console.print(" • Add label: [green]🎪 ⚡ showtime-trigger-start[/green]")
499
536
  console.print(
500
537
  " • Watch for: [blue]🎪 abc123f 🚦 building[/blue] → [green]🎪 abc123f 🚦 running[/green]"
501
538
  )
502
- console.print(" • Get URL from: [cyan]🎪 abc123f 🌐 52-1-2-3[/cyan] → http://52.1.2.3:8080")
503
- console.print()
504
-
505
- console.print("[bold]2. Enable Feature Flag:[/bold]")
506
- console.print(" • Add label: [yellow]🎪 conf-enable-ALERTS[/yellow]")
507
- console.print(
508
- " • Watch for: [blue]🎪 abc123f 🚦 configuring[/blue] → [green]🎪 abc123f 🚦 running[/green]"
509
- )
510
539
  console.print(
511
- " • Config updates: [cyan]🎪 abc123f ⚙️ standard[/cyan] → [cyan]🎪 abc123f ⚙️ alerts[/cyan]"
540
+ " • Get URL from: [cyan]🎪 abc123f 🌐 52.1.2.3:8080[/cyan] → http://52.1.2.3:8080"
512
541
  )
513
542
  console.print()
514
543
 
515
- console.print("[bold]3. Update to New Commit:[/bold]")
516
- console.print(" • Add label: [green]🎪 trigger-sync[/green]")
544
+ console.print("[bold]2. Freeze Environment (Optional):[/bold]")
545
+ console.print(" • Add label: [orange]🎪 🧊 showtime-freeze[/orange]")
546
+ console.print(" • Result: Environment won't auto-update on new commits")
547
+ console.print(" • Use case: Test specific SHA while continuing development")
548
+ console.print()
549
+
550
+ console.print("[bold]3. Update to New Commit (Automatic):[/bold]")
551
+ console.print(" • New commit pushed → Automatic blue-green rolling update")
517
552
  console.print(
518
553
  " • Watch for: [blue]🎪 abc123f 🚦 updating[/blue] → [green]🎪 def456a 🚦 running[/green]"
519
554
  )
@@ -521,7 +556,7 @@ def labels():
521
556
  console.print()
522
557
 
523
558
  console.print("[bold]4. Clean Up:[/bold]")
524
- console.print(" • Add label: [red]🎪 trigger-stop[/red]")
559
+ console.print(" • Add label: [red]🎪 🛑 showtime-trigger-stop[/red]")
525
560
  console.print(" • Result: All 🎪 labels removed, AWS resources deleted")
526
561
  console.print()
527
562
 
@@ -560,10 +595,8 @@ def test_lifecycle(
560
595
  _handle_start_trigger(pr_number, github, dry_run_aws, dry_run_github, aws_sleep)
561
596
 
562
597
  console.print()
563
- console.print("🎪 [bold]Step 2: Simulate conf-enable-ALERTS[/bold]")
564
- _handle_config_trigger(
565
- pr_number, "🎪 conf-enable-ALERTS", github, dry_run_aws, dry_run_github
566
- )
598
+ console.print("🎪 [bold]Step 2: Simulate config update[/bold]")
599
+ console.print("🎪 [dim]Config changes now done via code commits, not labels[/dim]")
567
600
 
568
601
  console.print()
569
602
  console.print("🎪 [bold]Step 3: Simulate trigger-sync (new commit)[/bold]")
@@ -581,8 +614,15 @@ def test_lifecycle(
581
614
 
582
615
 
583
616
  @app.command()
584
- def handle_trigger(
617
+ def sync(
585
618
  pr_number: int,
619
+ sha: Optional[str] = typer.Option(None, "--sha", help="Specific commit SHA (default: latest)"),
620
+ check_only: bool = typer.Option(
621
+ False, "--check-only", help="Check what actions are needed without executing"
622
+ ),
623
+ deploy: bool = typer.Option(
624
+ False, "--deploy", help="Execute deployment actions (assumes build is complete)"
625
+ ),
586
626
  dry_run_aws: bool = typer.Option(
587
627
  False, "--dry-run-aws", help="Skip AWS operations, use mock data"
588
628
  ),
@@ -593,46 +633,95 @@ def handle_trigger(
593
633
  0, "--aws-sleep", help="Seconds to sleep during AWS operations (for testing)"
594
634
  ),
595
635
  ):
596
- """🎪 Process trigger labels (called by GitHub Actions)"""
636
+ """🎪 Intelligently sync PR to desired state (called by GitHub Actions)"""
597
637
  try:
598
638
  github = GitHubInterface()
599
639
  pr = PullRequest.from_id(pr_number, github)
600
640
 
601
- # Find trigger labels
602
- trigger_labels = [
603
- label
604
- for label in pr.labels
605
- if label.startswith("🎪 trigger-") or label.startswith("🎪 conf-")
606
- ]
641
+ # Get PR metadata for state-based decisions
642
+ pr_data = github.get_pr_data(pr_number)
643
+ pr_state = pr_data.get("state", "open") # open, closed
607
644
 
608
- if not trigger_labels:
609
- console.print(f"🎪 No trigger labels found for PR #{pr_number}")
610
- return
645
+ # Get SHA - use provided SHA or default to latest
646
+ if sha:
647
+ target_sha = sha
648
+ console.print(f"🎪 Using specified SHA: {target_sha[:7]}")
649
+ else:
650
+ target_sha = github.get_latest_commit_sha(pr_number)
651
+ console.print(f"🎪 Using latest SHA: {target_sha[:7]}")
611
652
 
612
- console.print(f"🎪 Processing {len(trigger_labels)} trigger(s) for PR #{pr_number}")
653
+ # Determine what actions are needed
654
+ action_needed = _determine_sync_action(pr, pr_state, target_sha)
613
655
 
614
- for trigger in trigger_labels:
615
- console.print(f"🎪 Processing: {trigger}")
656
+ if check_only:
657
+ # Output structured results for GitHub Actions
658
+ console.print(f"action_needed={action_needed}")
616
659
 
617
- # Remove trigger label immediately (atomic operation)
618
- if not dry_run_github:
619
- github.remove_label(pr_number, trigger)
660
+ # Build needed for new environments and updates (SHA changes)
661
+ build_needed = action_needed in ["create_environment", "rolling_update", "auto_sync"]
662
+ console.print(f"build_needed={str(build_needed).lower()}")
663
+
664
+ # Deploy needed for everything except no_action
665
+ deploy_needed = action_needed != "no_action"
666
+ console.print(f"deploy_needed={str(deploy_needed).lower()}")
667
+ return
668
+
669
+ console.print(
670
+ f"🎪 [bold blue]Syncing PR #{pr_number}[/bold blue] (state: {pr_state}, SHA: {target_sha[:7]})"
671
+ )
672
+ console.print(f"🎪 Action needed: {action_needed}")
673
+
674
+ # Execute the determined action
675
+ if action_needed == "cleanup":
676
+ console.print("🎪 PR is closed - cleaning up environment")
677
+ if pr.current_show:
678
+ _handle_stop_trigger(pr_number, github, dry_run_aws, dry_run_github)
620
679
  else:
680
+ console.print("🎪 No environment to clean up")
681
+ return
682
+
683
+ # 2. Find explicit trigger labels
684
+ trigger_labels = [label for label in pr.labels if "showtime-trigger-" in label]
685
+
686
+ # 3. Handle explicit triggers first
687
+ if trigger_labels:
688
+ console.print(f"🎪 Processing {len(trigger_labels)} explicit trigger(s)")
689
+
690
+ for trigger in trigger_labels:
691
+ console.print(f"🎪 Processing: {trigger}")
692
+
693
+ # Remove trigger label immediately (atomic operation)
694
+ if not dry_run_github:
695
+ github.remove_label(pr_number, trigger)
696
+ else:
697
+ console.print(
698
+ f"🎪 [bold yellow]DRY-RUN-GITHUB[/bold yellow] - Would remove: {trigger}"
699
+ )
700
+
701
+ # Process the trigger
702
+ if "showtime-trigger-start" in trigger:
703
+ _handle_start_trigger(pr_number, github, dry_run_aws, dry_run_github, aws_sleep)
704
+ elif "showtime-trigger-stop" in trigger:
705
+ _handle_stop_trigger(pr_number, github, dry_run_aws, dry_run_github)
706
+
707
+ console.print("🎪 All explicit triggers processed!")
708
+ return
709
+
710
+ # 4. No explicit triggers - check for implicit sync needs
711
+ console.print("🎪 No explicit triggers found - checking for implicit sync needs")
712
+
713
+ if pr.current_show:
714
+ # Environment exists - check if it needs updating
715
+ if pr.current_show.needs_update(target_sha):
621
716
  console.print(
622
- f"🎪 [bold yellow]DRY-RUN-GITHUB[/bold yellow] - Would remove: {trigger}"
717
+ f"🎪 Environment outdated ({pr.current_show.sha} → {target_sha[:7]}) - auto-syncing"
623
718
  )
624
-
625
- # Process the trigger
626
- if trigger == "🎪 trigger-start":
627
- _handle_start_trigger(pr_number, github, dry_run_aws, dry_run_github, aws_sleep)
628
- elif trigger == "🎪 trigger-stop":
629
- _handle_stop_trigger(pr_number, github, dry_run_aws, dry_run_github)
630
- elif trigger == "🎪 trigger-sync":
631
719
  _handle_sync_trigger(pr_number, github, dry_run_aws, dry_run_github, aws_sleep)
632
- elif trigger.startswith("🎪 conf-"):
633
- _handle_config_trigger(pr_number, trigger, github, dry_run_aws, dry_run_github)
634
-
635
- console.print("🎪 All triggers processed!")
720
+ else:
721
+ console.print(f"🎪 Environment is up to date ({pr.current_show.sha})")
722
+ else:
723
+ console.print(f"🎪 No environment exists for PR #{pr_number} - no action needed")
724
+ console.print("🎪 💡 Add '🎪 trigger-start' label to create an environment")
636
725
 
637
726
  except Exception as e:
638
727
  console.print(f"🎪 [bold red]Error processing triggers:[/bold red] {e}")
@@ -667,11 +756,65 @@ def handle_sync(pr_number: int):
667
756
  console.print(f"🎪 [bold red]Error handling sync:[/bold red] {e}")
668
757
 
669
758
 
759
+ @app.command()
760
+ def setup_labels(
761
+ dry_run: bool = typer.Option(False, "--dry-run", help="Show what labels would be created"),
762
+ ):
763
+ """🎪 Set up GitHub label definitions with colors and descriptions"""
764
+ try:
765
+ from .core.label_colors import LABEL_DEFINITIONS
766
+
767
+ github = GitHubInterface()
768
+
769
+ console.print("🎪 [bold blue]Setting up circus tent label definitions...[/bold blue]")
770
+
771
+ created_count = 0
772
+ updated_count = 0
773
+
774
+ for label_name, definition in LABEL_DEFINITIONS.items():
775
+ color = definition["color"]
776
+ description = definition["description"]
777
+
778
+ if dry_run:
779
+ console.print(f"🏷️ Would create: [bold]{label_name}[/bold]")
780
+ console.print(f" Color: #{color}")
781
+ console.print(f" Description: {description}")
782
+ else:
783
+ try:
784
+ # Try to create or update the label
785
+ success = github.create_or_update_label(label_name, color, description)
786
+ if success:
787
+ created_count += 1
788
+ console.print(f"✅ Created: [bold]{label_name}[/bold]")
789
+ else:
790
+ updated_count += 1
791
+ console.print(f"🔄 Updated: [bold]{label_name}[/bold]")
792
+ except Exception as e:
793
+ console.print(f"❌ Failed to create {label_name}: {e}")
794
+
795
+ if not dry_run:
796
+ console.print("\n🎪 [bold green]Label setup complete![/bold green]")
797
+ console.print(f" 📊 Created: {created_count}")
798
+ console.print(f" 🔄 Updated: {updated_count}")
799
+ console.print(
800
+ "\n🎪 [dim]Note: Dynamic labels (with SHA) are created automatically during deployment[/dim]"
801
+ )
802
+
803
+ except Exception as e:
804
+ console.print(f"🎪 [bold red]Error setting up labels:[/bold red] {e}")
805
+
806
+
670
807
  @app.command()
671
808
  def cleanup(
672
809
  dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be cleaned"),
673
810
  older_than: str = typer.Option(
674
- "48h", "--older-than", help="Clean environments older than this"
811
+ "48h", "--older-than", help="Clean environments older than this (ignored if --respect-ttl)"
812
+ ),
813
+ respect_ttl: bool = typer.Option(
814
+ False, "--respect-ttl", help="Use individual TTL labels instead of global --older-than"
815
+ ),
816
+ max_age: Optional[str] = typer.Option(
817
+ None, "--max-age", help="Maximum age limit when using --respect-ttl (e.g., 7d)"
675
818
  ),
676
819
  cleanup_labels: bool = typer.Option(
677
820
  True,
@@ -712,7 +855,13 @@ def cleanup(
712
855
  console.print(
713
856
  f"🎪 Deleting expired service {service_name} (PR #{pr_number}, {age_hours:.1f}h old)"
714
857
  )
715
- _show_service_urls(pr_number, "cleanup")
858
+ # Create minimal Show object for URL generation
859
+ from .core.circus import Show
860
+
861
+ temp_show = Show(
862
+ pr_number=pr_number, sha=service_name.split("-")[2], status="cleanup"
863
+ )
864
+ _show_service_urls(temp_show, "cleanup")
716
865
 
717
866
  # Delete ECS service
718
867
  if aws._delete_ecs_service(service_name):
@@ -729,7 +878,10 @@ def cleanup(
729
878
  console.print(f"🎪 [bold red]AWS cleanup failed:[/bold red] {e}")
730
879
 
731
880
  # Step 2: Find and clean up expired environments from PRs
732
- console.print(f"🎪 Finding environments older than {older_than}")
881
+ if respect_ttl:
882
+ console.print("🎪 Finding environments expired based on individual TTL labels")
883
+ else:
884
+ console.print(f"🎪 Finding environments older than {older_than}")
733
885
  prs_with_shows = github.find_prs_with_shows()
734
886
 
735
887
  if not prs_with_shows:
@@ -740,19 +892,12 @@ def cleanup(
740
892
  import re
741
893
  from datetime import datetime, timedelta
742
894
 
743
- from .core.circus import PullRequest
744
-
745
- # Parse the older_than parameter (e.g., "48h", "7d")
746
- time_match = re.match(r"(\d+)([hd])", older_than)
747
- if not time_match:
748
- console.print(f"🎪 [bold red]Invalid time format:[/bold red] {older_than}")
749
- return
750
-
751
- hours = int(time_match.group(1))
752
- if time_match.group(2) == "d":
753
- hours *= 24
895
+ from .core.circus import PullRequest, get_effective_ttl, parse_ttl_days
754
896
 
755
- cutoff_time = datetime.now() - timedelta(hours=hours)
897
+ # Parse max_age if provided (safety ceiling)
898
+ max_age_days = None
899
+ if max_age:
900
+ max_age_days = parse_ttl_days(max_age)
756
901
 
757
902
  cleaned_prs = 0
758
903
  for pr_number in prs_with_shows:
@@ -760,6 +905,44 @@ def cleanup(
760
905
  pr = PullRequest.from_id(pr_number, github)
761
906
  expired_shows = []
762
907
 
908
+ if respect_ttl:
909
+ # Use individual TTL labels
910
+ effective_ttl_days = get_effective_ttl(pr)
911
+
912
+ if effective_ttl_days is None:
913
+ # "never" label found - skip cleanup
914
+ console.print(
915
+ f"🎪 [blue]PR #{pr_number} marked as 'never expire' - skipping[/blue]"
916
+ )
917
+ continue
918
+
919
+ # Apply max_age ceiling if specified
920
+ if max_age_days and effective_ttl_days > max_age_days:
921
+ console.print(
922
+ f"🎪 [yellow]PR #{pr_number} TTL ({effective_ttl_days}d) exceeds max-age ({max_age_days}d)[/yellow]"
923
+ )
924
+ effective_ttl_days = max_age_days
925
+
926
+ cutoff_time = datetime.now() - timedelta(days=effective_ttl_days)
927
+ console.print(
928
+ f"🎪 PR #{pr_number} effective TTL: {effective_ttl_days} days"
929
+ )
930
+
931
+ else:
932
+ # Use global older_than parameter (current behavior)
933
+ time_match = re.match(r"(\d+)([hd])", older_than)
934
+ if not time_match:
935
+ console.print(
936
+ f"🎪 [bold red]Invalid time format:[/bold red] {older_than}"
937
+ )
938
+ return
939
+
940
+ hours = int(time_match.group(1))
941
+ if time_match.group(2) == "d":
942
+ hours *= 24
943
+
944
+ cutoff_time = datetime.now() - timedelta(hours=hours)
945
+
763
946
  # Check all shows in the PR for expiration
764
947
  for show in pr.shows:
765
948
  if show.created_at:
@@ -845,6 +1028,8 @@ def _handle_start_trigger(
845
1028
  dry_run_aws: bool = False,
846
1029
  dry_run_github: bool = False,
847
1030
  aws_sleep: int = 0,
1031
+ image_tag_override: Optional[str] = None,
1032
+ force: bool = False,
848
1033
  ):
849
1034
  """Handle start trigger"""
850
1035
  import os
@@ -883,7 +1068,6 @@ def _handle_start_trigger(
883
1068
  created_at=datetime.utcnow().strftime("%Y-%m-%dT%H-%M"),
884
1069
  ttl="24h",
885
1070
  requested_by=github_actor,
886
- config="standard",
887
1071
  )
888
1072
 
889
1073
  console.print(f"🎪 Creating environment {show.aws_service_name}")
@@ -949,7 +1133,7 @@ def _handle_start_trigger(
949
1133
  **Credentials:** admin / admin
950
1134
  **TTL:** {show.ttl} (auto-cleanup)
951
1135
 
952
- **Feature flags:** Add `🎪 conf-enable-ALERTS` labels to configure
1136
+ **Configuration:** Modify feature flags in your PR code for new SHA
953
1137
  **Updates:** Environment updates automatically on new commits
954
1138
 
955
1139
  *Powered by [Superset Showtime](https://github.com/mistercrunch/superset-showtime)*"""
@@ -965,7 +1149,7 @@ def _handle_start_trigger(
965
1149
  aws = AWSInterface()
966
1150
 
967
1151
  # Show logs URL immediately for monitoring
968
- _show_service_urls(pr_number, "deployment", latest_sha[:7])
1152
+ _show_service_urls(show, "deployment")
969
1153
 
970
1154
  # Parse feature flags from PR description (replicate GHA feature flag logic)
971
1155
  feature_flags = _extract_feature_flags_from_pr(pr_number, github)
@@ -976,6 +1160,8 @@ def _handle_start_trigger(
976
1160
  sha=latest_sha,
977
1161
  github_user=github_actor,
978
1162
  feature_flags=feature_flags,
1163
+ image_tag_override=image_tag_override,
1164
+ force=force,
979
1165
  )
980
1166
 
981
1167
  if result.success:
@@ -1058,7 +1244,7 @@ def _handle_start_trigger(
1058
1244
  **TTL:** {show.ttl} (auto-cleanup)
1059
1245
  **Feature flags:** {len(feature_flags)} enabled
1060
1246
 
1061
- **Feature flags:** Add `🎪 conf-enable-ALERTS` labels to configure
1247
+ **Configuration:** Modify feature flags in your PR code for new SHA
1062
1248
  **Updates:** Environment updates automatically on new commits
1063
1249
 
1064
1250
  *Powered by [Superset Showtime](https://github.com/mistercrunch/superset-showtime)*"""
@@ -1108,9 +1294,11 @@ def _extract_feature_flags_from_pr(pr_number: int, github: GitHubInterface) -> l
1108
1294
  results = []
1109
1295
 
1110
1296
  for match in re.finditer(pattern, description):
1111
- config = {"name": f"SUPERSET_FEATURE_{match.group(1)}", "value": match.group(2)}
1112
- results.append(config)
1113
- console.print(f"🎪 Found feature flag: {config['name']}={config['value']}")
1297
+ feature_config = {"name": f"SUPERSET_FEATURE_{match.group(1)}", "value": match.group(2)}
1298
+ results.append(feature_config)
1299
+ console.print(
1300
+ f"🎪 Found feature flag: {feature_config['name']}={feature_config['value']}"
1301
+ )
1114
1302
 
1115
1303
  return results
1116
1304
 
@@ -1148,12 +1336,12 @@ def _handle_stop_trigger(
1148
1336
  console.print("🎪 [bold blue]Starting AWS cleanup...[/bold blue]")
1149
1337
  aws = AWSInterface()
1150
1338
 
1151
- # Show logs URL for monitoring cleanup
1152
- _show_service_urls(pr_number, "cleanup")
1153
-
1154
1339
  try:
1340
+ # Show logs URL for monitoring cleanup
1341
+ _show_service_urls(show, "cleanup")
1342
+
1155
1343
  # Step 1: Check if ECS service exists and is active (replicate GHA describe-services)
1156
- service_name = f"pr-{pr_number}-service" # Match GHA service naming
1344
+ service_name = show.ecs_service_name
1157
1345
  console.print(f"🎪 Checking ECS service: {service_name}")
1158
1346
 
1159
1347
  service_exists = aws._service_exists(service_name)
@@ -1254,7 +1442,6 @@ def _handle_sync_trigger(
1254
1442
  created_at=datetime.utcnow().strftime("%Y-%m-%dT%H-%M"),
1255
1443
  ttl=pr.current_show.ttl,
1256
1444
  requested_by=pr.current_show.requested_by,
1257
- config=pr.current_show.config,
1258
1445
  )
1259
1446
 
1260
1447
  console.print(f"🎪 Building new environment: {new_show.aws_service_name}")
@@ -1273,9 +1460,6 @@ def _handle_sync_trigger(
1273
1460
  console.print(f"🎪 Traffic switched to {new_show.sha} at {new_show.ip}")
1274
1461
 
1275
1462
  # Post rolling update success comment
1276
- import os
1277
-
1278
- github_actor = os.getenv("GITHUB_ACTOR", DEFAULT_GITHUB_ACTOR)
1279
1463
  update_comment = f"""🎪 Environment updated: {pr.current_show.sha} → `{new_show.sha}`
1280
1464
 
1281
1465
  **New Environment:** http://{new_show.ip}:8080
@@ -1297,61 +1481,6 @@ Your latest changes are now live.
1297
1481
  console.print(f"🎪 [bold red]Sync trigger failed:[/bold red] {e}")
1298
1482
 
1299
1483
 
1300
- def _handle_config_trigger(
1301
- pr_number: int,
1302
- trigger: str,
1303
- github: GitHubInterface,
1304
- dry_run_aws: bool = False,
1305
- dry_run_github: bool = False,
1306
- ):
1307
- """Handle configuration trigger"""
1308
- from .core.circus import merge_config, parse_configuration_command
1309
-
1310
- console.print(f"🎪 Configuring environment for PR #{pr_number}: {trigger}")
1311
-
1312
- try:
1313
- command = parse_configuration_command(trigger)
1314
- if not command:
1315
- console.print(f"🎪 [bold red]Invalid config trigger:[/bold red] {trigger}")
1316
- return
1317
-
1318
- pr = PullRequest.from_id(pr_number, github)
1319
-
1320
- if not pr.current_show:
1321
- console.print(f"🎪 No active environment for PR #{pr_number}")
1322
- return
1323
-
1324
- show = pr.current_show
1325
- console.print(f"🎪 Applying config: {command} to {show.aws_service_name}")
1326
-
1327
- # Update configuration
1328
- new_config = merge_config(show.config, command)
1329
- console.print(f"🎪 Config: {show.config} → {new_config}")
1330
-
1331
- if dry_run_aws:
1332
- console.print("🎪 [bold yellow]DRY-RUN-AWS[/bold yellow] - Would update feature flags")
1333
- console.print(f" Command: {command}")
1334
- console.print(f" New config: {new_config}")
1335
- else:
1336
- # TODO: Real feature flag update
1337
- console.print(
1338
- "🎪 [bold yellow]Real feature flag update not yet implemented[/bold yellow]"
1339
- )
1340
-
1341
- # Update config in labels
1342
- show.config = new_config
1343
- updated_labels = show.to_circus_labels()
1344
- console.print("🎪 Updating config labels")
1345
-
1346
- # TODO: Actually update labels
1347
- # github.set_labels(pr_number, updated_labels)
1348
-
1349
- console.print("🎪 [bold green]Configuration updated![/bold green]")
1350
-
1351
- except Exception as e:
1352
- console.print(f"🎪 [bold red]Config trigger failed:[/bold red] {e}")
1353
-
1354
-
1355
1484
  def main():
1356
1485
  """Main entry point for the CLI"""
1357
1486
  app()