superset-showtime 0.1.0__py3-none-any.whl → 0.2.2__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/__init__.py +1 -3
- showtime/cli.py +302 -173
- showtime/core/aws.py +73 -24
- showtime/core/circus.py +42 -58
- showtime/core/emojis.py +0 -10
- showtime/core/github.py +43 -1
- showtime/core/label_colors.py +105 -0
- showtime/data/ecs-task-definition.json +4 -0
- {superset_showtime-0.1.0.dist-info → superset_showtime-0.2.2.dist-info}/METADATA +97 -96
- superset_showtime-0.2.2.dist-info/RECORD +16 -0
- showtime/core/config.py +0 -152
- superset_showtime-0.1.0.dist-info/RECORD +0 -16
- {superset_showtime-0.1.0.dist-info → superset_showtime-0.2.2.dist-info}/WHEEL +0 -0
- {superset_showtime-0.1.0.dist-info → superset_showtime-0.2.2.dist-info}/entry_points.txt +0 -0
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(
|
|
21
|
+
def _get_service_urls(show):
|
|
22
22
|
"""Get AWS Console URLs for a service"""
|
|
23
|
-
|
|
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(
|
|
31
|
+
def _show_service_urls(show, context: str = "deployment"):
|
|
35
32
|
"""Show helpful AWS Console URLs for monitoring service"""
|
|
36
|
-
urls = _get_service_urls(
|
|
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 [
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
-
#
|
|
445
|
-
console.print("[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
|
-
|
|
452
|
-
"
|
|
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
|
-
" •
|
|
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]
|
|
516
|
-
console.print(" • Add label: [
|
|
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
|
|
564
|
-
|
|
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
|
|
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
|
-
"""🎪
|
|
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
|
-
#
|
|
602
|
-
|
|
603
|
-
|
|
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
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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
|
-
|
|
653
|
+
# Determine what actions are needed
|
|
654
|
+
action_needed = _determine_sync_action(pr, pr_state, target_sha)
|
|
613
655
|
|
|
614
|
-
|
|
615
|
-
|
|
656
|
+
if check_only:
|
|
657
|
+
# Output structured results for GitHub Actions
|
|
658
|
+
console.print(f"action_needed={action_needed}")
|
|
616
659
|
|
|
617
|
-
#
|
|
618
|
-
|
|
619
|
-
|
|
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"🎪
|
|
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
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
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(
|
|
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
|
-
**
|
|
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
|
-
|
|
1112
|
-
results.append(
|
|
1113
|
-
console.print(
|
|
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 =
|
|
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()
|