superset-showtime 0.1.0__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 ADDED
@@ -0,0 +1,1361 @@
1
+ """
2
+ ๐ŸŽช Superset Showtime CLI
3
+
4
+ Main command-line interface for Apache Superset circus tent environment management.
5
+ """
6
+
7
+ from typing import Optional
8
+
9
+ import typer
10
+ from rich.console import Console
11
+ from rich.table import Table
12
+
13
+ from .core.circus import PullRequest, Show
14
+ from .core.emojis import STATUS_DISPLAY
15
+ from .core.github import GitHubError, GitHubInterface
16
+
17
+ # Constants
18
+ DEFAULT_GITHUB_ACTOR = "unknown"
19
+
20
+
21
+ def _get_service_urls(pr_number: int, sha: str = None):
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"
27
+
28
+ return {
29
+ "logs": f"https://us-west-2.console.aws.amazon.com/ecs/v2/clusters/superset-ci/services/{service_name}/logs?region=us-west-2",
30
+ "service": f"https://us-west-2.console.aws.amazon.com/ecs/v2/clusters/superset-ci/services/{service_name}",
31
+ }
32
+
33
+
34
+ def _show_service_urls(pr_number: int, context: str = "deployment", sha: str = None):
35
+ """Show helpful AWS Console URLs for monitoring service"""
36
+ urls = _get_service_urls(pr_number, sha)
37
+ console.print(f"\n๐ŸŽช [bold blue]Monitor {context} progress:[/bold blue]")
38
+ console.print(f" ๐Ÿ“ Live Logs: {urls['logs']}")
39
+ console.print(f" ๐Ÿ“Š ECS Service: {urls['service']}")
40
+ console.print("")
41
+
42
+
43
+ def _schedule_blue_cleanup(pr_number: int, blue_services: list):
44
+ """Schedule cleanup of blue services after successful green deployment"""
45
+ import threading
46
+ import time
47
+
48
+ def cleanup_after_delay():
49
+ """Background cleanup of blue services"""
50
+ try:
51
+ # Wait 5 minutes before cleanup
52
+ time.sleep(300) # 5 minutes
53
+
54
+ console.print(
55
+ f"\n๐Ÿงน [bold blue]Starting scheduled cleanup of blue services for PR #{pr_number}[/bold blue]"
56
+ )
57
+
58
+ from .core.aws import AWSInterface
59
+
60
+ aws = AWSInterface()
61
+
62
+ for blue_svc in blue_services:
63
+ service_name = blue_svc["service_name"]
64
+ console.print(f"๐Ÿ—‘๏ธ Cleaning up blue service: {service_name}")
65
+
66
+ try:
67
+ # Delete ECS service
68
+ if aws._delete_ecs_service(service_name):
69
+ # Delete ECR image
70
+ pr_match = service_name.split("-")
71
+ if len(pr_match) >= 2:
72
+ pr_num = pr_match[1]
73
+ image_tag = f"pr-{pr_num}-ci" # Legacy format for old services
74
+ aws._delete_ecr_image(image_tag)
75
+
76
+ console.print(f"โœ… Cleaned up blue service: {service_name}")
77
+ else:
78
+ console.print(f"โš ๏ธ Failed to clean up: {service_name}")
79
+
80
+ except Exception as e:
81
+ console.print(f"โŒ Cleanup error for {service_name}: {e}")
82
+
83
+ console.print("๐Ÿงน โœ… Blue service cleanup completed")
84
+
85
+ except Exception as e:
86
+ console.print(f"โŒ Background cleanup failed: {e}")
87
+
88
+ # Start cleanup in background thread
89
+ cleanup_thread = threading.Thread(target=cleanup_after_delay, daemon=True)
90
+ cleanup_thread.start()
91
+ console.print("๐Ÿ• Background cleanup scheduled")
92
+
93
+
94
+ app = typer.Typer(
95
+ name="showtime",
96
+ help="""๐ŸŽช Apache Superset ephemeral environment management
97
+
98
+ [bold]GitHub Label Workflow:[/bold]
99
+ 1. Add [green]๐ŸŽช trigger-start[/green] label to PR โ†’ Creates environment
100
+ 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
103
+
104
+ [bold]Reading State Labels:[/bold]
105
+ โ€ข [green]๐ŸŽช abc123f ๐Ÿšฆ running[/green] - Environment status
106
+ โ€ข [blue]๐ŸŽช ๐ŸŽฏ abc123f[/blue] - Active environment pointer
107
+ โ€ข [cyan]๐ŸŽช abc123f ๐ŸŒ 52-1-2-3[/cyan] - Environment IP (http://52.1.2.3:8080)
108
+ โ€ข [yellow]๐ŸŽช abc123f โŒ› 24h[/yellow] - TTL policy
109
+ โ€ข [magenta]๐ŸŽช abc123f ๐Ÿคก maxime[/magenta] - Who requested (clown!)
110
+
111
+ [dim]CLI commands work with existing environments or dry-run new ones.[/dim]""",
112
+ rich_markup_mode="rich",
113
+ )
114
+ console = Console()
115
+
116
+
117
+ @app.command()
118
+ def start(
119
+ 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)"),
121
+ ttl: Optional[str] = typer.Option("24h", help="Time to live (24h, 48h, 1w, close)"),
122
+ size: Optional[str] = typer.Option("standard", help="Environment size (standard, large)"),
123
+ dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be done"),
124
+ dry_run_aws: bool = typer.Option(
125
+ False, "--dry-run-aws", help="Skip AWS operations, use mock data"
126
+ ),
127
+ aws_sleep: int = typer.Option(0, "--aws-sleep", help="Seconds to sleep during AWS operations"),
128
+ ):
129
+ """Create ephemeral environment for PR"""
130
+ try:
131
+ github = GitHubInterface()
132
+
133
+ # Get latest SHA if not provided
134
+ if not sha:
135
+ sha = github.get_latest_commit_sha(pr_number)
136
+
137
+ if dry_run:
138
+ console.print("๐ŸŽช [bold yellow]DRY RUN[/bold yellow] - Would create environment:")
139
+ console.print(f" PR: #{pr_number}")
140
+ console.print(f" SHA: {sha[:7]}")
141
+ console.print(f" AWS Service: pr-{pr_number}-{sha[:7]}")
142
+ console.print(f" TTL: {ttl}")
143
+ console.print(" Labels to add:")
144
+ console.print(" ๐ŸŽช ๐Ÿšฆ building")
145
+ console.print(f" ๐ŸŽช ๐ŸŽฏ {sha[:7]}")
146
+ console.print(f" ๐ŸŽช โŒ› {ttl}")
147
+ return
148
+
149
+ # Check if environment already exists
150
+ pr = PullRequest.from_id(pr_number, github)
151
+ if pr.current_show:
152
+ console.print(
153
+ f"๐ŸŽช [bold yellow]Environment already exists for PR #{pr_number}[/bold yellow]"
154
+ )
155
+ console.print(f"Current: {pr.current_show.sha} at {pr.current_show.ip}")
156
+ console.print("Use 'showtime sync' to update or 'showtime stop' to clean up first")
157
+ return
158
+
159
+ # Create environment using trigger handler logic
160
+ 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)
162
+
163
+ except GitHubError as e:
164
+ console.print(f"๐ŸŽช [bold red]GitHub error:[/bold red] {e.message}")
165
+ except Exception as e:
166
+ console.print(f"๐ŸŽช [bold red]Error:[/bold red] {e}")
167
+
168
+
169
+ @app.command()
170
+ def status(
171
+ pr_number: int = typer.Argument(..., help="PR number to check status for"),
172
+ verbose: bool = typer.Option(False, "-v", "--verbose", help="Show detailed information"),
173
+ ):
174
+ """Show environment status for PR"""
175
+ try:
176
+ github = GitHubInterface()
177
+
178
+ pr = PullRequest.from_id(pr_number, github)
179
+
180
+ if not pr.has_shows():
181
+ console.print(f"๐ŸŽช No environment found for PR #{pr_number}")
182
+ return
183
+
184
+ show = pr.current_show
185
+ if not show:
186
+ console.print(f"๐ŸŽช No active environment for PR #{pr_number}")
187
+ if pr.building_show:
188
+ console.print(f"๐Ÿ—๏ธ Building environment: {pr.building_show.sha}")
189
+ return
190
+
191
+ # Create status table
192
+ table = Table(title=f"๐ŸŽช Environment Status - PR #{pr_number}")
193
+ table.add_column("Property", style="cyan")
194
+ table.add_column("Value", style="white")
195
+
196
+ status_emoji = STATUS_DISPLAY
197
+
198
+ table.add_row("Status", f"{status_emoji.get(show.status, 'โ“')} {show.status.title()}")
199
+ table.add_row("Environment", f"`{show.sha}`")
200
+ table.add_row("AWS Service", f"`{show.aws_service_name}`")
201
+
202
+ if show.ip:
203
+ table.add_row("URL", f"http://{show.ip}:8080")
204
+
205
+ if show.created_at:
206
+ table.add_row("Created", show.created_at)
207
+
208
+ table.add_row("TTL", show.ttl)
209
+
210
+ if show.requested_by:
211
+ table.add_row("Requested by", f"@{show.requested_by}")
212
+
213
+ if show.config != "standard":
214
+ table.add_row("Configuration", show.config)
215
+
216
+ if verbose:
217
+ table.add_row("All Labels", ", ".join(pr.circus_labels))
218
+
219
+ console.print(table)
220
+
221
+ # Show building environment if exists
222
+ if pr.building_show and pr.building_show != show:
223
+ console.print(
224
+ f"๐Ÿ—๏ธ [bold yellow]Building new environment:[/bold yellow] {pr.building_show.sha}"
225
+ )
226
+
227
+ except GitHubError as e:
228
+ console.print(f"๐ŸŽช [bold red]GitHub error:[/bold red] {e.message}")
229
+ except Exception as e:
230
+ console.print(f"๐ŸŽช [bold red]Error:[/bold red] {e}")
231
+
232
+
233
+ @app.command()
234
+ def stop(
235
+ pr_number: int = typer.Argument(..., help="PR number to stop environment for"),
236
+ force: bool = typer.Option(False, "--force", help="Force cleanup even if errors occur"),
237
+ dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be done"),
238
+ dry_run_aws: bool = typer.Option(
239
+ False, "--dry-run-aws", help="Skip AWS operations, use mock data"
240
+ ),
241
+ aws_sleep: int = typer.Option(0, "--aws-sleep", help="Seconds to sleep during AWS operations"),
242
+ ):
243
+ """Delete environment for PR"""
244
+ try:
245
+ github = GitHubInterface()
246
+
247
+ pr = PullRequest.from_id(pr_number, github)
248
+
249
+ if not pr.current_show:
250
+ console.print(f"๐ŸŽช No active environment found for PR #{pr_number}")
251
+ return
252
+
253
+ show = pr.current_show
254
+ console.print(f"๐ŸŽช [bold yellow]Stopping environment for PR #{pr_number}...[/bold yellow]")
255
+ console.print(f"Environment: {show.sha} at {show.ip}")
256
+
257
+ if dry_run:
258
+ console.print("๐ŸŽช [bold yellow]DRY RUN[/bold yellow] - Would delete environment:")
259
+ console.print(f" AWS Service: {show.aws_service_name}")
260
+ console.print(f" ECR Image: {show.aws_image_tag}")
261
+ console.print(f" Circus Labels: {len(pr.circus_labels)} labels")
262
+ return
263
+
264
+ if not force:
265
+ confirm = typer.confirm(f"Delete environment {show.aws_service_name}?")
266
+ if not confirm:
267
+ console.print("๐ŸŽช Cancelled")
268
+ return
269
+
270
+ if dry_run_aws:
271
+ console.print("๐ŸŽช [bold yellow]DRY-RUN-AWS[/bold yellow] - Would delete AWS resources:")
272
+ console.print(f" - ECS service: {show.aws_service_name}")
273
+ console.print(f" - ECR image: {show.aws_image_tag}")
274
+ if aws_sleep > 0:
275
+ import time
276
+
277
+ console.print(f"๐ŸŽช Sleeping {aws_sleep}s to simulate AWS cleanup...")
278
+ time.sleep(aws_sleep)
279
+ console.print("๐ŸŽช [bold green]Mock AWS cleanup complete![/bold green]")
280
+ else:
281
+ # Real AWS cleanup
282
+ from .core.aws import AWSInterface
283
+
284
+ console.print("๐ŸŽช [bold blue]Starting AWS cleanup...[/bold blue]")
285
+ aws = AWSInterface()
286
+
287
+ # Show logs URL for monitoring cleanup
288
+ _show_service_urls(pr_number, "cleanup")
289
+
290
+ try:
291
+ # Get current environment info
292
+ pr = PullRequest.from_id(pr_number, github)
293
+
294
+ if pr.current_show:
295
+ show = pr.current_show
296
+ console.print(f"๐ŸŽช Destroying environment: {show.aws_service_name}")
297
+
298
+ # Step 1: Check if ECS service exists and is active
299
+ service_name = f"pr-{pr_number}-service" # Match GHA service naming
300
+ console.print(f"๐ŸŽช Checking ECS service: {service_name}")
301
+
302
+ service_exists = aws._service_exists(service_name)
303
+
304
+ if service_exists:
305
+ console.print(f"๐ŸŽช Found active ECS service: {service_name}")
306
+
307
+ # Step 2: Delete ECS service
308
+ console.print("๐ŸŽช Deleting ECS service...")
309
+ success = aws._delete_ecs_service(service_name)
310
+
311
+ if success:
312
+ console.print("๐ŸŽช โœ… ECS service deleted successfully")
313
+
314
+ # Step 3: Delete ECR image tag
315
+ image_tag = f"pr-{pr_number}-ci" # Match GHA image tag format
316
+ console.print(f"๐ŸŽช Deleting ECR image tag: {image_tag}")
317
+
318
+ ecr_success = aws._delete_ecr_image(image_tag)
319
+
320
+ if ecr_success:
321
+ console.print("๐ŸŽช โœ… ECR image deleted successfully")
322
+ else:
323
+ console.print("๐ŸŽช โš ๏ธ ECR image deletion failed (may not exist)")
324
+
325
+ console.print(
326
+ "๐ŸŽช [bold green]โœ… AWS cleanup completed successfully![/bold green]"
327
+ )
328
+
329
+ else:
330
+ console.print("๐ŸŽช [bold red]โŒ ECS service deletion failed[/bold red]")
331
+
332
+ else:
333
+ console.print(f"๐ŸŽช No active ECS service found: {service_name}")
334
+ console.print("๐ŸŽช โœ… No AWS resources to clean up")
335
+ else:
336
+ console.print(f"๐ŸŽช No active environment found for PR #{pr_number}")
337
+
338
+ except Exception as e:
339
+ console.print(f"๐ŸŽช [bold red]โŒ AWS cleanup failed:[/bold red] {e}")
340
+
341
+ # Remove circus labels
342
+ github.remove_circus_labels(pr_number)
343
+
344
+ console.print("๐ŸŽช [bold green]Environment stopped and labels cleaned up![/bold green]")
345
+
346
+ except GitHubError as e:
347
+ console.print(f"๐ŸŽช [bold red]GitHub error:[/bold red] {e.message}")
348
+ except Exception as e:
349
+ console.print(f"๐ŸŽช [bold red]Error:[/bold red] {e}")
350
+
351
+
352
+ @app.command()
353
+ def list(
354
+ status_filter: Optional[str] = typer.Option(
355
+ None, "--status", help="Filter by status (running, building, etc.)"
356
+ ),
357
+ user: Optional[str] = typer.Option(None, "--user", help="Filter by user"),
358
+ ):
359
+ """List all environments"""
360
+ try:
361
+ github = GitHubInterface()
362
+
363
+ # Find all PRs with circus tent labels
364
+ pr_numbers = github.find_prs_with_shows()
365
+
366
+ if not pr_numbers:
367
+ console.print("๐ŸŽช No environments currently running")
368
+ return
369
+
370
+ # Collect all shows
371
+ all_shows = []
372
+ for pr_number in pr_numbers:
373
+ pr = PullRequest.from_id(pr_number, github)
374
+ for show in pr.shows:
375
+ # Apply filters
376
+ if status_filter and show.status != status_filter:
377
+ continue
378
+ if user and show.requested_by != user:
379
+ continue
380
+ all_shows.append(show)
381
+
382
+ if not all_shows:
383
+ filter_msg = ""
384
+ if status_filter:
385
+ filter_msg += f" with status '{status_filter}'"
386
+ if user:
387
+ filter_msg += f" by user '{user}'"
388
+ console.print(f"๐ŸŽช No environments found{filter_msg}")
389
+ return
390
+
391
+ # Create table with full terminal width
392
+ table = Table(title="๐ŸŽช Environment List", expand=True)
393
+ table.add_column("PR", style="cyan", min_width=6)
394
+ table.add_column("Status", style="white", min_width=12)
395
+ table.add_column("SHA", style="green", min_width=11)
396
+ table.add_column("Superset URL", style="blue", min_width=25)
397
+ table.add_column("AWS Logs", style="dim blue", min_width=15)
398
+ table.add_column("TTL", style="yellow", min_width=6)
399
+ table.add_column("User", style="magenta", min_width=10)
400
+
401
+ status_emoji = STATUS_DISPLAY
402
+
403
+ for show in sorted(all_shows, key=lambda s: s.pr_number):
404
+ # Make Superset URL clickable and show full URL
405
+ if show.ip:
406
+ full_url = f"http://{show.ip}:8080"
407
+ superset_url = f"[link={full_url}]{full_url}[/link]"
408
+ else:
409
+ superset_url = "-"
410
+
411
+ # Get AWS service URLs - iTerm2 supports Rich clickable links
412
+ aws_urls = _get_service_urls(show.pr_number, show.sha)
413
+ aws_logs_link = f"[link={aws_urls['logs']}]View[/link]"
414
+
415
+ # Make PR number clickable
416
+ pr_url = f"https://github.com/apache/superset/pull/{show.pr_number}"
417
+ clickable_pr = f"[link={pr_url}]{show.pr_number}[/link]"
418
+
419
+ table.add_row(
420
+ clickable_pr,
421
+ f"{status_emoji.get(show.status, 'โ“')} {show.status}",
422
+ show.sha,
423
+ superset_url,
424
+ aws_logs_link,
425
+ show.ttl,
426
+ f"@{show.requested_by}" if show.requested_by else "-",
427
+ )
428
+
429
+ console.print(table)
430
+
431
+ except GitHubError as e:
432
+ console.print(f"๐ŸŽช [bold red]GitHub error:[/bold red] {e.message}")
433
+ except Exception as e:
434
+ console.print(f"๐ŸŽช [bold red]Error:[/bold red] {e}")
435
+
436
+
437
+ @app.command()
438
+ def labels():
439
+ """๐ŸŽช Show complete circus tent label reference"""
440
+
441
+ console.print("๐ŸŽช [bold blue]Circus Tent Label Reference[/bold blue]")
442
+ console.print()
443
+
444
+ # Trigger Labels
445
+ console.print("[bold yellow]๐ŸŽฏ Trigger Labels (Add these to GitHub PR):[/bold yellow]")
446
+ trigger_table = Table()
447
+ trigger_table.add_column("Label", style="green")
448
+ trigger_table.add_column("Action", style="white")
449
+ trigger_table.add_column("Description", style="dim")
450
+
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
+ )
468
+
469
+ console.print(trigger_table)
470
+ console.print()
471
+
472
+ # State Labels
473
+ console.print("[bold cyan]๐Ÿ“Š State Labels (Automatically managed):[/bold cyan]")
474
+ state_table = Table()
475
+ state_table.add_column("Label", style="cyan")
476
+ state_table.add_column("Meaning", style="white")
477
+ state_table.add_column("Example", style="dim")
478
+
479
+ state_table.add_row("๐ŸŽช {sha} ๐Ÿšฆ {status}", "Environment status", "๐ŸŽช abc123f ๐Ÿšฆ running")
480
+ state_table.add_row("๐ŸŽช ๐ŸŽฏ {sha}", "Active environment pointer", "๐ŸŽช ๐ŸŽฏ abc123f")
481
+ state_table.add_row("๐ŸŽช ๐Ÿ—๏ธ {sha}", "Building environment pointer", "๐ŸŽช ๐Ÿ—๏ธ def456a")
482
+ state_table.add_row(
483
+ "๐ŸŽช {sha} ๐Ÿ“… {timestamp}", "Creation timestamp", "๐ŸŽช abc123f ๐Ÿ“… 2024-01-15T14-30"
484
+ )
485
+ state_table.add_row("๐ŸŽช {sha} ๐ŸŒ {ip-with-dashes}", "Environment IP", "๐ŸŽช abc123f ๐ŸŒ 52-1-2-3")
486
+ state_table.add_row("๐ŸŽช {sha} โŒ› {ttl-policy}", "TTL policy", "๐ŸŽช abc123f โŒ› 24h")
487
+ state_table.add_row("๐ŸŽช {sha} ๐Ÿคก {username}", "Requested by", "๐ŸŽช abc123f ๐Ÿคก maxime")
488
+ state_table.add_row("๐ŸŽช {sha} โš™๏ธ {config-list}", "Feature flags", "๐ŸŽช abc123f โš™๏ธ alerts,debug")
489
+
490
+ console.print(state_table)
491
+ console.print()
492
+
493
+ # Workflow Examples
494
+ console.print("[bold magenta]๐ŸŽช Complete Workflow Examples:[/bold magenta]")
495
+ console.print()
496
+
497
+ console.print("[bold]1. Create Environment:[/bold]")
498
+ console.print(" โ€ข Add label: [green]๐ŸŽช trigger-start[/green]")
499
+ console.print(
500
+ " โ€ข Watch for: [blue]๐ŸŽช abc123f ๐Ÿšฆ building[/blue] โ†’ [green]๐ŸŽช abc123f ๐Ÿšฆ running[/green]"
501
+ )
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
+ console.print(
511
+ " โ€ข Config updates: [cyan]๐ŸŽช abc123f โš™๏ธ standard[/cyan] โ†’ [cyan]๐ŸŽช abc123f โš™๏ธ alerts[/cyan]"
512
+ )
513
+ console.print()
514
+
515
+ console.print("[bold]3. Update to New Commit:[/bold]")
516
+ console.print(" โ€ข Add label: [green]๐ŸŽช trigger-sync[/green]")
517
+ console.print(
518
+ " โ€ข Watch for: [blue]๐ŸŽช abc123f ๐Ÿšฆ updating[/blue] โ†’ [green]๐ŸŽช def456a ๐Ÿšฆ running[/green]"
519
+ )
520
+ console.print(" โ€ข SHA changes: [cyan]๐ŸŽช ๐ŸŽฏ abc123f[/cyan] โ†’ [cyan]๐ŸŽช ๐ŸŽฏ def456a[/cyan]")
521
+ console.print()
522
+
523
+ console.print("[bold]4. Clean Up:[/bold]")
524
+ console.print(" โ€ข Add label: [red]๐ŸŽช trigger-stop[/red]")
525
+ console.print(" โ€ข Result: All ๐ŸŽช labels removed, AWS resources deleted")
526
+ console.print()
527
+
528
+ console.print("[bold]๐Ÿ“Š Understanding State:[/bold]")
529
+ console.print("โ€ข [dim]TTL labels show policy (24h, 48h, close) not time remaining[/dim]")
530
+ console.print("โ€ข [dim]Use 'showtime status {pr-id}' to calculate actual time remaining[/dim]")
531
+ console.print("โ€ข [dim]Multiple SHA labels during updates (๐ŸŽฏ active, ๐Ÿ—๏ธ building)[/dim]")
532
+ console.print()
533
+
534
+ console.print("[dim]๐Ÿ’ก Tip: Only maintainers with write access can add trigger labels[/dim]")
535
+
536
+
537
+ @app.command()
538
+ def test_lifecycle(
539
+ pr_number: int,
540
+ dry_run_aws: bool = typer.Option(
541
+ True, "--dry-run-aws/--real-aws", help="Use mock AWS operations"
542
+ ),
543
+ dry_run_github: bool = typer.Option(
544
+ True, "--dry-run-github/--real-github", help="Use mock GitHub operations"
545
+ ),
546
+ aws_sleep: int = typer.Option(10, "--aws-sleep", help="Seconds to sleep during AWS operations"),
547
+ ):
548
+ """๐ŸŽช Test full environment lifecycle with mock triggers"""
549
+
550
+ console.print(f"๐ŸŽช [bold blue]Testing full lifecycle for PR #{pr_number}[/bold blue]")
551
+ console.print(
552
+ f"AWS: {'DRY-RUN' if dry_run_aws else 'REAL'}, GitHub: {'DRY-RUN' if dry_run_github else 'REAL'}"
553
+ )
554
+ console.print()
555
+
556
+ try:
557
+ github = GitHubInterface()
558
+
559
+ console.print("๐ŸŽช [bold]Step 1: Simulate trigger-start[/bold]")
560
+ _handle_start_trigger(pr_number, github, dry_run_aws, dry_run_github, aws_sleep)
561
+
562
+ 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
+ )
567
+
568
+ console.print()
569
+ console.print("๐ŸŽช [bold]Step 3: Simulate trigger-sync (new commit)[/bold]")
570
+ _handle_sync_trigger(pr_number, github, dry_run_aws, dry_run_github, aws_sleep)
571
+
572
+ console.print()
573
+ console.print("๐ŸŽช [bold]Step 4: Simulate trigger-stop[/bold]")
574
+ _handle_stop_trigger(pr_number, github, dry_run_aws, dry_run_github)
575
+
576
+ console.print()
577
+ console.print("๐ŸŽช [bold green]Full lifecycle test complete![/bold green]")
578
+
579
+ except Exception as e:
580
+ console.print(f"๐ŸŽช [bold red]Lifecycle test failed:[/bold red] {e}")
581
+
582
+
583
+ @app.command()
584
+ def handle_trigger(
585
+ pr_number: int,
586
+ dry_run_aws: bool = typer.Option(
587
+ False, "--dry-run-aws", help="Skip AWS operations, use mock data"
588
+ ),
589
+ dry_run_github: bool = typer.Option(
590
+ False, "--dry-run-github", help="Skip GitHub label operations"
591
+ ),
592
+ aws_sleep: int = typer.Option(
593
+ 0, "--aws-sleep", help="Seconds to sleep during AWS operations (for testing)"
594
+ ),
595
+ ):
596
+ """๐ŸŽช Process trigger labels (called by GitHub Actions)"""
597
+ try:
598
+ github = GitHubInterface()
599
+ pr = PullRequest.from_id(pr_number, github)
600
+
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
+ ]
607
+
608
+ if not trigger_labels:
609
+ console.print(f"๐ŸŽช No trigger labels found for PR #{pr_number}")
610
+ return
611
+
612
+ console.print(f"๐ŸŽช Processing {len(trigger_labels)} trigger(s) for PR #{pr_number}")
613
+
614
+ for trigger in trigger_labels:
615
+ console.print(f"๐ŸŽช Processing: {trigger}")
616
+
617
+ # Remove trigger label immediately (atomic operation)
618
+ if not dry_run_github:
619
+ github.remove_label(pr_number, trigger)
620
+ else:
621
+ console.print(
622
+ f"๐ŸŽช [bold yellow]DRY-RUN-GITHUB[/bold yellow] - Would remove: {trigger}"
623
+ )
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
+ _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!")
636
+
637
+ except Exception as e:
638
+ console.print(f"๐ŸŽช [bold red]Error processing triggers:[/bold red] {e}")
639
+
640
+
641
+ @app.command()
642
+ def handle_sync(pr_number: int):
643
+ """๐ŸŽช Handle new commit sync (called by GitHub Actions on PR synchronize)"""
644
+ try:
645
+ github = GitHubInterface()
646
+ pr = PullRequest.from_id(pr_number, github)
647
+
648
+ # Only sync if there's an active environment
649
+ if not pr.current_show:
650
+ console.print(f"๐ŸŽช No active environment for PR #{pr_number} - skipping sync")
651
+ return
652
+
653
+ # Get latest commit SHA
654
+ latest_sha = github.get_latest_commit_sha(pr_number)
655
+
656
+ # Check if update is needed
657
+ if not pr.current_show.needs_update(latest_sha):
658
+ console.print(f"๐ŸŽช Environment already up to date for PR #{pr_number}")
659
+ return
660
+
661
+ console.print(f"๐ŸŽช Syncing PR #{pr_number} to commit {latest_sha[:7]}")
662
+
663
+ # TODO: Implement rolling update logic
664
+ console.print("๐ŸŽช [bold yellow]Sync logic not yet implemented[/bold yellow]")
665
+
666
+ except Exception as e:
667
+ console.print(f"๐ŸŽช [bold red]Error handling sync:[/bold red] {e}")
668
+
669
+
670
+ @app.command()
671
+ def cleanup(
672
+ dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be cleaned"),
673
+ older_than: str = typer.Option(
674
+ "48h", "--older-than", help="Clean environments older than this"
675
+ ),
676
+ cleanup_labels: bool = typer.Option(
677
+ True,
678
+ "--cleanup-labels/--no-cleanup-labels",
679
+ help="Also cleanup SHA-based label definitions from repository",
680
+ ),
681
+ ):
682
+ """๐ŸŽช Clean up orphaned or expired environments and labels"""
683
+ try:
684
+ github = GitHubInterface()
685
+
686
+ # Step 1: Clean up expired AWS ECS services
687
+ console.print("๐ŸŽช [bold blue]Checking AWS ECS services for cleanup...[/bold blue]")
688
+
689
+ from .core.aws import AWSInterface
690
+
691
+ aws = AWSInterface()
692
+
693
+ try:
694
+ expired_services = aws.find_expired_services(older_than)
695
+
696
+ if expired_services:
697
+ console.print(f"๐ŸŽช Found {len(expired_services)} expired ECS services")
698
+
699
+ for service_info in expired_services:
700
+ service_name = service_info["service_name"]
701
+ pr_number = service_info["pr_number"]
702
+ age_hours = service_info["age_hours"]
703
+
704
+ if dry_run:
705
+ console.print(
706
+ f"๐ŸŽช [yellow]Would delete service {service_name} (PR #{pr_number}, {age_hours:.1f}h old)[/yellow]"
707
+ )
708
+ console.print(
709
+ f"๐ŸŽช [dim]Monitor at: https://us-west-2.console.aws.amazon.com/ecs/v2/clusters/superset-ci/services/{service_name}/logs?region=us-west-2[/dim]"
710
+ )
711
+ else:
712
+ console.print(
713
+ f"๐ŸŽช Deleting expired service {service_name} (PR #{pr_number}, {age_hours:.1f}h old)"
714
+ )
715
+ _show_service_urls(pr_number, "cleanup")
716
+
717
+ # Delete ECS service
718
+ if aws._delete_ecs_service(service_name):
719
+ # Delete ECR image
720
+ image_tag = f"pr-{pr_number}-ci"
721
+ aws._delete_ecr_image(image_tag)
722
+ console.print(f"๐ŸŽช โœ… Cleaned up {service_name}")
723
+ else:
724
+ console.print(f"๐ŸŽช โŒ Failed to clean up {service_name}")
725
+ else:
726
+ console.print("๐ŸŽช [dim]No expired ECS services found[/dim]")
727
+
728
+ except Exception as e:
729
+ console.print(f"๐ŸŽช [bold red]AWS cleanup failed:[/bold red] {e}")
730
+
731
+ # Step 2: Find and clean up expired environments from PRs
732
+ console.print(f"๐ŸŽช Finding environments older than {older_than}")
733
+ prs_with_shows = github.find_prs_with_shows()
734
+
735
+ if not prs_with_shows:
736
+ console.print("๐ŸŽช [dim]No PRs with circus tent labels found[/dim]")
737
+ else:
738
+ console.print(f"๐ŸŽช Found {len(prs_with_shows)} PRs with shows")
739
+
740
+ import re
741
+ from datetime import datetime, timedelta
742
+
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
754
+
755
+ cutoff_time = datetime.now() - timedelta(hours=hours)
756
+
757
+ cleaned_prs = 0
758
+ for pr_number in prs_with_shows:
759
+ try:
760
+ pr = PullRequest.from_id(pr_number, github)
761
+ expired_shows = []
762
+
763
+ # Check all shows in the PR for expiration
764
+ for show in pr.shows:
765
+ if show.created_at:
766
+ try:
767
+ # Parse timestamp (format: 2024-01-15T14-30)
768
+ show_time = datetime.fromisoformat(
769
+ show.created_at.replace("-", ":")
770
+ )
771
+ if show_time < cutoff_time:
772
+ expired_shows.append(show)
773
+ except (ValueError, AttributeError):
774
+ # If we can't parse the timestamp, consider it expired
775
+ expired_shows.append(show)
776
+
777
+ if expired_shows:
778
+ if dry_run:
779
+ console.print(
780
+ f"๐ŸŽช [yellow]Would clean {len(expired_shows)} expired shows from PR #{pr_number}[/yellow]"
781
+ )
782
+ for show in expired_shows:
783
+ console.print(f" - SHA {show.sha} ({show.status})")
784
+ else:
785
+ console.print(
786
+ f"๐ŸŽช Cleaning {len(expired_shows)} expired shows from PR #{pr_number}"
787
+ )
788
+
789
+ # Remove circus labels for expired shows
790
+ current_labels = github.get_circus_labels(pr_number)
791
+ labels_to_keep = []
792
+
793
+ for label in current_labels:
794
+ # Keep labels that don't belong to expired shows
795
+ should_keep = True
796
+ for expired_show in expired_shows:
797
+ if expired_show.sha in label:
798
+ should_keep = False
799
+ break
800
+ if should_keep:
801
+ labels_to_keep.append(label)
802
+
803
+ # Update PR labels
804
+ github.remove_circus_labels(pr_number)
805
+ for label in labels_to_keep:
806
+ github.add_label(pr_number, label)
807
+
808
+ cleaned_prs += 1
809
+
810
+ except Exception as e:
811
+ console.print(f"๐ŸŽช [red]Error processing PR #{pr_number}:[/red] {e}")
812
+
813
+ if not dry_run and cleaned_prs > 0:
814
+ console.print(f"๐ŸŽช [green]Cleaned up environments from {cleaned_prs} PRs[/green]")
815
+
816
+ # Step 2: Clean up SHA-based label definitions from repository
817
+ if cleanup_labels:
818
+ console.print("๐ŸŽช Finding SHA-based labels in repository")
819
+ sha_labels = github.cleanup_sha_labels(dry_run=dry_run)
820
+
821
+ if sha_labels:
822
+ if dry_run:
823
+ console.print(
824
+ f"๐ŸŽช [yellow]Would delete {len(sha_labels)} SHA-based label definitions:[/yellow]"
825
+ )
826
+ for label in sha_labels[:10]: # Show first 10
827
+ console.print(f" - {label}")
828
+ if len(sha_labels) > 10:
829
+ console.print(f" ... and {len(sha_labels) - 10} more")
830
+ else:
831
+ console.print(
832
+ f"๐ŸŽช [green]Deleted {len(sha_labels)} SHA-based label definitions[/green]"
833
+ )
834
+ else:
835
+ console.print("๐ŸŽช [dim]No SHA-based labels found to clean[/dim]")
836
+
837
+ except Exception as e:
838
+ console.print(f"๐ŸŽช [bold red]Error during cleanup:[/bold red] {e}")
839
+
840
+
841
+ # Helper functions for trigger processing
842
+ def _handle_start_trigger(
843
+ pr_number: int,
844
+ github: GitHubInterface,
845
+ dry_run_aws: bool = False,
846
+ dry_run_github: bool = False,
847
+ aws_sleep: int = 0,
848
+ ):
849
+ """Handle start trigger"""
850
+ import os
851
+ import time
852
+ from datetime import datetime
853
+
854
+ console.print(f"๐ŸŽช Starting environment for PR #{pr_number}")
855
+
856
+ try:
857
+ # Get latest SHA and GitHub actor
858
+ latest_sha = github.get_latest_commit_sha(pr_number)
859
+ github_actor = os.getenv("GITHUB_ACTOR", DEFAULT_GITHUB_ACTOR)
860
+
861
+ # Post confirmation comment
862
+ workflow_url = (
863
+ os.getenv("GITHUB_SERVER_URL", "https://github.com")
864
+ + f"/{os.getenv('GITHUB_REPOSITORY', 'repo')}/actions/runs/{os.getenv('GITHUB_RUN_ID', 'run')}"
865
+ )
866
+
867
+ confirmation_comment = f"""๐ŸŽช @{github_actor} Creating ephemeral environment for commit `{latest_sha[:7]}`
868
+
869
+ **Action:** [View workflow]({workflow_url})
870
+ **Environment:** `{latest_sha[:7]}`
871
+ **Powered by:** [Superset Showtime](https://github.com/mistercrunch/superset-showtime)
872
+
873
+ *Building and deploying... Watch the labels for progress updates.*"""
874
+
875
+ if not dry_run_github:
876
+ github.post_comment(pr_number, confirmation_comment)
877
+
878
+ # Create new show
879
+ show = Show(
880
+ pr_number=pr_number,
881
+ sha=latest_sha[:7],
882
+ status="building",
883
+ created_at=datetime.utcnow().strftime("%Y-%m-%dT%H-%M"),
884
+ ttl="24h",
885
+ requested_by=github_actor,
886
+ config="standard",
887
+ )
888
+
889
+ console.print(f"๐ŸŽช Creating environment {show.aws_service_name}")
890
+
891
+ # Set building state labels
892
+ building_labels = show.to_circus_labels()
893
+ console.print("๐ŸŽช Setting building state labels:")
894
+ for label in building_labels:
895
+ console.print(f" + {label}")
896
+
897
+ # Set building labels
898
+ if not dry_run_github:
899
+ # Actually set the labels for real testing
900
+ console.print("๐ŸŽช Setting labels on GitHub...")
901
+ # Remove existing circus labels first
902
+ github.remove_circus_labels(pr_number)
903
+ # Add new labels one by one
904
+ for label in building_labels:
905
+ github.add_label(pr_number, label)
906
+ console.print("๐ŸŽช โœ… Labels set on GitHub!")
907
+ else:
908
+ console.print("๐ŸŽช [bold yellow]DRY-RUN-GITHUB[/bold yellow] - Would set labels")
909
+
910
+ if dry_run_aws:
911
+ console.print("๐ŸŽช [bold yellow]DRY-RUN-AWS[/bold yellow] - Skipping AWS operations")
912
+ if aws_sleep > 0:
913
+ console.print(f"๐ŸŽช Sleeping {aws_sleep}s to simulate AWS build time...")
914
+ time.sleep(aws_sleep)
915
+
916
+ # Mock successful deployment
917
+ mock_ip = "52.1.2.3"
918
+ console.print(
919
+ f"๐ŸŽช [bold green]Mock AWS deployment successful![/bold green] IP: {mock_ip}"
920
+ )
921
+
922
+ # Update to running state
923
+ show.status = "running"
924
+ show.ip = mock_ip
925
+
926
+ running_labels = show.to_circus_labels()
927
+ console.print("๐ŸŽช Setting running state labels:")
928
+ for label in running_labels:
929
+ console.print(f" + {label}")
930
+
931
+ # Set running labels
932
+ if not dry_run_github:
933
+ console.print("๐ŸŽช Updating to running state...")
934
+ # Remove existing circus labels first
935
+ github.remove_circus_labels(pr_number)
936
+ # Add new running labels
937
+ for label in running_labels:
938
+ github.add_label(pr_number, label)
939
+ console.print("๐ŸŽช โœ… Labels updated to running state!")
940
+ else:
941
+ console.print(
942
+ "๐ŸŽช [bold yellow]DRY-RUN-GITHUB[/bold yellow] - Would update to running state"
943
+ )
944
+
945
+ # Post success comment (only in dry-run-aws mode since we have mock IP)
946
+ success_comment = f"""๐ŸŽช @{github_actor} Environment ready at **http://{mock_ip}:8080**
947
+
948
+ **Environment:** `{show.sha}`
949
+ **Credentials:** admin / admin
950
+ **TTL:** {show.ttl} (auto-cleanup)
951
+
952
+ **Feature flags:** Add `๐ŸŽช conf-enable-ALERTS` labels to configure
953
+ **Updates:** Environment updates automatically on new commits
954
+
955
+ *Powered by [Superset Showtime](https://github.com/mistercrunch/superset-showtime)*"""
956
+
957
+ if not dry_run_github:
958
+ github.post_comment(pr_number, success_comment)
959
+
960
+ else:
961
+ # Real AWS operations
962
+ from .core.aws import AWSInterface, EnvironmentResult
963
+
964
+ console.print("๐ŸŽช [bold blue]Starting AWS deployment...[/bold blue]")
965
+ aws = AWSInterface()
966
+
967
+ # Show logs URL immediately for monitoring
968
+ _show_service_urls(pr_number, "deployment", latest_sha[:7])
969
+
970
+ # Parse feature flags from PR description (replicate GHA feature flag logic)
971
+ feature_flags = _extract_feature_flags_from_pr(pr_number, github)
972
+
973
+ # Create environment (synchronous, matches GHA wait behavior)
974
+ result: EnvironmentResult = aws.create_environment(
975
+ pr_number=pr_number,
976
+ sha=latest_sha,
977
+ github_user=github_actor,
978
+ feature_flags=feature_flags,
979
+ )
980
+
981
+ if result.success:
982
+ console.print(
983
+ f"๐ŸŽช [bold green]โœ… Green service deployed successfully![/bold green] IP: {result.ip}"
984
+ )
985
+
986
+ # Show helpful links for the new service
987
+ console.print("\n๐ŸŽช [bold blue]Useful Links:[/bold blue]")
988
+ console.print(f" ๐ŸŒ Environment: http://{result.ip}:8080")
989
+ console.print(
990
+ f" ๐Ÿ“Š ECS Service: https://us-west-2.console.aws.amazon.com/ecs/v2/clusters/superset-ci/services/{result.service_name}"
991
+ )
992
+ console.print(
993
+ f" ๐Ÿ“ Service Logs: https://us-west-2.console.aws.amazon.com/ecs/v2/clusters/superset-ci/services/{result.service_name}/logs?region=us-west-2"
994
+ )
995
+ console.print(
996
+ f" ๐Ÿ” GitHub PR: https://github.com/apache/superset/pull/{pr_number}"
997
+ )
998
+ console.print(
999
+ "\n๐ŸŽช [dim]Note: Superset takes 2-3 minutes to initialize after container starts[/dim]"
1000
+ )
1001
+
1002
+ # Blue-Green Traffic Switch: Update GitHub labels to point to new service
1003
+ console.print(
1004
+ f"\n๐ŸŽช [bold blue]Switching traffic to green service {latest_sha[:7]}...[/bold blue]"
1005
+ )
1006
+
1007
+ # Check for existing services to show blue-green transition
1008
+ from .core.aws import AWSInterface
1009
+
1010
+ aws = AWSInterface()
1011
+ existing_services = aws._find_pr_services(pr_number)
1012
+
1013
+ if len(existing_services) > 1:
1014
+ console.print("๐Ÿ”„ Blue-Green Deployment:")
1015
+ blue_services = []
1016
+ for svc in existing_services:
1017
+ if svc["sha"] == latest_sha[:7]:
1018
+ console.print(
1019
+ f" ๐ŸŸข Green: {svc['service_name']} (NEW - receiving traffic)"
1020
+ )
1021
+ else:
1022
+ console.print(
1023
+ f" ๐Ÿ”ต Blue: {svc['service_name']} (OLD - will be cleaned up in 5 minutes)"
1024
+ )
1025
+ blue_services.append(svc)
1026
+
1027
+ # Schedule cleanup of blue services
1028
+ if blue_services:
1029
+ console.print(
1030
+ f"\n๐Ÿงน Scheduling cleanup of {len(blue_services)} blue service(s) in 5 minutes..."
1031
+ )
1032
+ _schedule_blue_cleanup(pr_number, blue_services)
1033
+
1034
+ # Update to running state with new SHA
1035
+ show.status = "running"
1036
+ show.ip = result.ip
1037
+
1038
+ # Traffic switching happens here - update GitHub labels atomically
1039
+ running_labels = show.to_circus_labels()
1040
+ console.print("\n๐ŸŽช Setting running state labels (traffic switch):")
1041
+ for label in running_labels:
1042
+ console.print(f" + {label}")
1043
+
1044
+ if not dry_run_github:
1045
+ console.print("๐ŸŽช Executing traffic switch via GitHub labels...")
1046
+ # Remove existing circus labels first
1047
+ github.remove_circus_labels(pr_number)
1048
+ # Add new running labels - this switches traffic atomically
1049
+ for label in running_labels:
1050
+ github.add_label(pr_number, label)
1051
+ console.print("๐ŸŽช โœ… Labels updated to running state!")
1052
+
1053
+ # Post success comment with real IP
1054
+ success_comment = f"""๐ŸŽช @{github_actor} Environment ready at **http://{result.ip}:8080**
1055
+
1056
+ **Environment:** `{show.sha}`
1057
+ **Credentials:** admin / admin
1058
+ **TTL:** {show.ttl} (auto-cleanup)
1059
+ **Feature flags:** {len(feature_flags)} enabled
1060
+
1061
+ **Feature flags:** Add `๐ŸŽช conf-enable-ALERTS` labels to configure
1062
+ **Updates:** Environment updates automatically on new commits
1063
+
1064
+ *Powered by [Superset Showtime](https://github.com/mistercrunch/superset-showtime)*"""
1065
+
1066
+ github.post_comment(pr_number, success_comment)
1067
+
1068
+ else:
1069
+ console.print(f"๐ŸŽช [bold red]โŒ AWS deployment failed:[/bold red] {result.error}")
1070
+
1071
+ # Update to failed state
1072
+ show.status = "failed"
1073
+ failed_labels = show.to_circus_labels()
1074
+
1075
+ if not dry_run_github:
1076
+ console.print("๐ŸŽช Setting failed state labels...")
1077
+ github.remove_circus_labels(pr_number)
1078
+ for label in failed_labels:
1079
+ github.add_label(pr_number, label)
1080
+
1081
+ # Post failure comment
1082
+ failure_comment = f"""๐ŸŽช @{github_actor} Environment creation failed.
1083
+
1084
+ **Error:** {result.error}
1085
+ **Environment:** `{show.sha}`
1086
+
1087
+ Please check the logs and try again.
1088
+
1089
+ *Powered by [Superset Showtime](https://github.com/mistercrunch/superset-showtime)*"""
1090
+
1091
+ github.post_comment(pr_number, failure_comment)
1092
+
1093
+ except Exception as e:
1094
+ console.print(f"๐ŸŽช [bold red]Start trigger failed:[/bold red] {e}")
1095
+
1096
+
1097
+ def _extract_feature_flags_from_pr(pr_number: int, github: GitHubInterface) -> list:
1098
+ """Extract feature flags from PR description (replicate GHA eval-feature-flags step)"""
1099
+ import re
1100
+
1101
+ try:
1102
+ # Get PR description
1103
+ pr_data = github.get_pr_data(pr_number)
1104
+ description = pr_data.get("body") or ""
1105
+
1106
+ # Replicate exact GHA regex pattern: FEATURE_(\w+)=(\w+)
1107
+ pattern = r"FEATURE_(\w+)=(\w+)"
1108
+ results = []
1109
+
1110
+ 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']}")
1114
+
1115
+ return results
1116
+
1117
+ except Exception as e:
1118
+ console.print(f"๐ŸŽช Warning: Could not extract feature flags: {e}")
1119
+ return []
1120
+
1121
+
1122
+ def _handle_stop_trigger(
1123
+ pr_number: int, github: GitHubInterface, dry_run_aws: bool = False, dry_run_github: bool = False
1124
+ ):
1125
+ """Handle stop trigger"""
1126
+ import os
1127
+
1128
+ console.print(f"๐ŸŽช Stopping environment for PR #{pr_number}")
1129
+
1130
+ try:
1131
+ pr = PullRequest.from_id(pr_number, github)
1132
+
1133
+ if not pr.current_show:
1134
+ console.print(f"๐ŸŽช No active environment found for PR #{pr_number}")
1135
+ return
1136
+
1137
+ show = pr.current_show
1138
+ console.print(f"๐ŸŽช Destroying environment: {show.aws_service_name}")
1139
+
1140
+ if dry_run_aws:
1141
+ console.print("๐ŸŽช [bold yellow]DRY-RUN-AWS[/bold yellow] - Would delete AWS resources")
1142
+ console.print(f" - ECS service: {show.aws_service_name}")
1143
+ console.print(f" - ECR image: {show.aws_image_tag}")
1144
+ else:
1145
+ # Real AWS cleanup (replicate ephemeral-env-pr-close.yml logic)
1146
+ from .core.aws import AWSInterface
1147
+
1148
+ console.print("๐ŸŽช [bold blue]Starting AWS cleanup...[/bold blue]")
1149
+ aws = AWSInterface()
1150
+
1151
+ # Show logs URL for monitoring cleanup
1152
+ _show_service_urls(pr_number, "cleanup")
1153
+
1154
+ try:
1155
+ # 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
1157
+ console.print(f"๐ŸŽช Checking ECS service: {service_name}")
1158
+
1159
+ service_exists = aws._service_exists(service_name)
1160
+
1161
+ if service_exists:
1162
+ console.print(f"๐ŸŽช Found active ECS service: {service_name}")
1163
+
1164
+ # Step 2: Delete ECS service (replicate GHA delete-service)
1165
+ console.print("๐ŸŽช Deleting ECS service...")
1166
+ success = aws._delete_ecs_service(service_name)
1167
+
1168
+ if success:
1169
+ console.print("๐ŸŽช โœ… ECS service deleted successfully")
1170
+
1171
+ # Step 3: Delete ECR image tag (replicate GHA batch-delete-image)
1172
+ image_tag = f"pr-{pr_number}-ci" # Match GHA image tag format
1173
+ console.print(f"๐ŸŽช Deleting ECR image tag: {image_tag}")
1174
+
1175
+ ecr_success = aws._delete_ecr_image(image_tag)
1176
+
1177
+ if ecr_success:
1178
+ console.print("๐ŸŽช โœ… ECR image deleted successfully")
1179
+ else:
1180
+ console.print("๐ŸŽช โš ๏ธ ECR image deletion failed (may not exist)")
1181
+
1182
+ console.print(
1183
+ "๐ŸŽช [bold green]โœ… AWS cleanup completed successfully![/bold green]"
1184
+ )
1185
+
1186
+ else:
1187
+ console.print("๐ŸŽช [bold red]โŒ ECS service deletion failed[/bold red]")
1188
+
1189
+ else:
1190
+ console.print(f"๐ŸŽช No active ECS service found: {service_name}")
1191
+ console.print("๐ŸŽช โœ… No AWS resources to clean up")
1192
+
1193
+ except Exception as e:
1194
+ console.print(f"๐ŸŽช [bold red]โŒ AWS cleanup failed:[/bold red] {e}")
1195
+
1196
+ # Remove all circus labels for this PR
1197
+ console.print(f"๐ŸŽช Removing all circus labels for PR #{pr_number}")
1198
+ if not dry_run_github:
1199
+ github.remove_circus_labels(pr_number)
1200
+
1201
+ # Post cleanup comment
1202
+ github_actor = os.getenv("GITHUB_ACTOR", DEFAULT_GITHUB_ACTOR)
1203
+ cleanup_comment = f"""๐ŸŽช @{github_actor} Environment `{show.sha}` cleaned up
1204
+
1205
+ **AWS Resources:** ECS service and ECR image deleted
1206
+ **Cost Impact:** No further charges
1207
+
1208
+ Add `๐ŸŽช trigger-start` to create a new environment.
1209
+
1210
+ *Powered by [Superset Showtime](https://github.com/mistercrunch/superset-showtime)*"""
1211
+
1212
+ if not dry_run_github:
1213
+ github.post_comment(pr_number, cleanup_comment)
1214
+
1215
+ console.print("๐ŸŽช [bold green]Environment stopped![/bold green]")
1216
+
1217
+ except Exception as e:
1218
+ console.print(f"๐ŸŽช [bold red]Stop trigger failed:[/bold red] {e}")
1219
+
1220
+
1221
+ def _handle_sync_trigger(
1222
+ pr_number: int,
1223
+ github: GitHubInterface,
1224
+ dry_run_aws: bool = False,
1225
+ dry_run_github: bool = False,
1226
+ aws_sleep: int = 0,
1227
+ ):
1228
+ """Handle sync trigger"""
1229
+ import time
1230
+ from datetime import datetime
1231
+
1232
+ console.print(f"๐ŸŽช Syncing environment for PR #{pr_number}")
1233
+
1234
+ try:
1235
+ pr = PullRequest.from_id(pr_number, github)
1236
+
1237
+ if not pr.current_show:
1238
+ console.print(f"๐ŸŽช No active environment for PR #{pr_number}")
1239
+ return
1240
+
1241
+ latest_sha = github.get_latest_commit_sha(pr_number)
1242
+
1243
+ if not pr.current_show.needs_update(latest_sha):
1244
+ console.print(f"๐ŸŽช Environment already up to date: {pr.current_show.sha}")
1245
+ return
1246
+
1247
+ console.print(f"๐ŸŽช Rolling update: {pr.current_show.sha} โ†’ {latest_sha[:7]}")
1248
+
1249
+ # Create new show for building
1250
+ new_show = Show(
1251
+ pr_number=pr_number,
1252
+ sha=latest_sha[:7],
1253
+ status="building",
1254
+ created_at=datetime.utcnow().strftime("%Y-%m-%dT%H-%M"),
1255
+ ttl=pr.current_show.ttl,
1256
+ requested_by=pr.current_show.requested_by,
1257
+ config=pr.current_show.config,
1258
+ )
1259
+
1260
+ console.print(f"๐ŸŽช Building new environment: {new_show.aws_service_name}")
1261
+
1262
+ if dry_run_aws:
1263
+ console.print("๐ŸŽช [bold yellow]DRY-RUN-AWS[/bold yellow] - Mocking rolling update")
1264
+ if aws_sleep > 0:
1265
+ console.print(f"๐ŸŽช Sleeping {aws_sleep}s to simulate build + deploy...")
1266
+ time.sleep(aws_sleep)
1267
+
1268
+ # Mock successful update
1269
+ new_show.status = "running"
1270
+ new_show.ip = "52.4.5.6" # New mock IP
1271
+
1272
+ console.print("๐ŸŽช [bold green]Mock rolling update complete![/bold green]")
1273
+ console.print(f"๐ŸŽช Traffic switched to {new_show.sha} at {new_show.ip}")
1274
+
1275
+ # Post rolling update success comment
1276
+ import os
1277
+
1278
+ github_actor = os.getenv("GITHUB_ACTOR", DEFAULT_GITHUB_ACTOR)
1279
+ update_comment = f"""๐ŸŽช Environment updated: {pr.current_show.sha} โ†’ `{new_show.sha}`
1280
+
1281
+ **New Environment:** http://{new_show.ip}:8080
1282
+ **Update:** Zero-downtime rolling deployment
1283
+ **Old Environment:** Automatically cleaned up
1284
+
1285
+ Your latest changes are now live.
1286
+
1287
+ *Powered by [Superset Showtime](https://github.com/mistercrunch/superset-showtime)*"""
1288
+
1289
+ if not dry_run_github:
1290
+ github.post_comment(pr_number, update_comment)
1291
+
1292
+ else:
1293
+ # TODO: Real rolling update
1294
+ console.print("๐ŸŽช [bold yellow]Real rolling update not yet implemented[/bold yellow]")
1295
+
1296
+ except Exception as e:
1297
+ console.print(f"๐ŸŽช [bold red]Sync trigger failed:[/bold red] {e}")
1298
+
1299
+
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
+ def main():
1356
+ """Main entry point for the CLI"""
1357
+ app()
1358
+
1359
+
1360
+ if __name__ == "__main__":
1361
+ main()