superset-showtime 0.2.8__py3-none-any.whl → 0.4.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 +3 -2
- showtime/cli.py +276 -1159
- showtime/core/aws.py +30 -26
- showtime/core/github.py +10 -6
- showtime/core/github_messages.py +16 -13
- showtime/core/pull_request.py +536 -0
- showtime/core/show.py +279 -0
- {superset_showtime-0.2.8.dist-info → superset_showtime-0.4.2.dist-info}/METADATA +56 -121
- superset_showtime-0.4.2.dist-info/RECORD +16 -0
- showtime/commands/__init__.py +0 -1
- showtime/commands/start.py +0 -40
- showtime/core/circus.py +0 -279
- superset_showtime-0.2.8.dist-info/RECORD +0 -17
- {superset_showtime-0.2.8.dist-info → superset_showtime-0.4.2.dist-info}/WHEEL +0 -0
- {superset_showtime-0.2.8.dist-info → superset_showtime-0.4.2.dist-info}/entry_points.txt +0 -0
showtime/cli.py
CHANGED
|
@@ -4,130 +4,36 @@
|
|
|
4
4
|
Main command-line interface for Apache Superset circus tent environment management.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from typing import Optional
|
|
7
|
+
from typing import Dict, Optional
|
|
8
8
|
|
|
9
9
|
import typer
|
|
10
10
|
from rich.console import Console
|
|
11
11
|
from rich.table import Table
|
|
12
12
|
|
|
13
|
-
from .core.circus import PullRequest, Show, short_sha
|
|
14
13
|
from .core.emojis import STATUS_DISPLAY
|
|
15
14
|
from .core.github import GitHubError, GitHubInterface
|
|
16
15
|
from .core.github_messages import (
|
|
17
16
|
get_aws_console_urls,
|
|
18
|
-
rolling_failure_comment,
|
|
19
|
-
rolling_start_comment,
|
|
20
|
-
rolling_success_comment,
|
|
21
|
-
start_comment,
|
|
22
|
-
success_comment,
|
|
23
17
|
)
|
|
18
|
+
from .core.pull_request import PullRequest
|
|
19
|
+
from .core.show import Show
|
|
24
20
|
|
|
25
21
|
# Constants
|
|
26
22
|
DEFAULT_GITHUB_ACTOR = "unknown"
|
|
27
23
|
|
|
28
24
|
|
|
29
|
-
def _get_service_urls(show):
|
|
25
|
+
def _get_service_urls(show: Show) -> Dict[str, str]:
|
|
30
26
|
"""Get AWS Console URLs for a service"""
|
|
31
27
|
return get_aws_console_urls(show.ecs_service_name)
|
|
32
28
|
|
|
33
29
|
|
|
34
|
-
def _show_service_urls(show, context: str = "deployment"):
|
|
30
|
+
def _show_service_urls(show: Show, context: str = "deployment") -> None:
|
|
35
31
|
"""Show helpful AWS Console URLs for monitoring service"""
|
|
36
32
|
urls = _get_service_urls(show)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def _determine_sync_action(pr, pr_state: str, target_sha: str) -> str:
|
|
44
|
-
"""Determine what action is needed based on PR state and labels"""
|
|
45
|
-
|
|
46
|
-
# 1. Closed PRs always need cleanup
|
|
47
|
-
if pr_state == "closed":
|
|
48
|
-
return "cleanup"
|
|
49
|
-
|
|
50
|
-
# 2. Check for explicit trigger labels
|
|
51
|
-
trigger_labels = [label for label in pr.labels if "showtime-trigger-" in label]
|
|
52
|
-
|
|
53
|
-
# 3. Check for freeze label (PR-level) - only if no explicit triggers
|
|
54
|
-
freeze_labels = [label for label in pr.labels if "showtime-freeze" in label]
|
|
55
|
-
if freeze_labels and not trigger_labels:
|
|
56
|
-
return "frozen_no_action" # Frozen and no explicit triggers to override
|
|
57
|
-
|
|
58
|
-
if trigger_labels:
|
|
59
|
-
# Explicit triggers take priority
|
|
60
|
-
for trigger in trigger_labels:
|
|
61
|
-
if "showtime-trigger-start" in trigger:
|
|
62
|
-
if pr.current_show:
|
|
63
|
-
if pr.current_show.needs_update(target_sha):
|
|
64
|
-
return "rolling_update" # New commit with existing env
|
|
65
|
-
else:
|
|
66
|
-
return "no_action" # Same commit, no change needed
|
|
67
|
-
else:
|
|
68
|
-
return "create_environment" # New environment
|
|
69
|
-
elif "showtime-trigger-stop" in trigger:
|
|
70
|
-
return "destroy_environment"
|
|
71
|
-
|
|
72
|
-
# 3. No explicit triggers - check for implicit sync needs
|
|
73
|
-
if pr.current_show:
|
|
74
|
-
if pr.current_show.needs_update(target_sha):
|
|
75
|
-
return "auto_sync" # Auto-update on new commits
|
|
76
|
-
else:
|
|
77
|
-
return "no_action" # Everything in sync
|
|
78
|
-
else:
|
|
79
|
-
return "no_action" # No environment, no triggers
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def _schedule_blue_cleanup(pr_number: int, blue_services: list):
|
|
83
|
-
"""Schedule cleanup of blue services after successful green deployment"""
|
|
84
|
-
import threading
|
|
85
|
-
import time
|
|
86
|
-
|
|
87
|
-
def cleanup_after_delay():
|
|
88
|
-
"""Background cleanup of blue services"""
|
|
89
|
-
try:
|
|
90
|
-
# Wait 5 minutes before cleanup
|
|
91
|
-
time.sleep(300) # 5 minutes
|
|
92
|
-
|
|
93
|
-
console.print(
|
|
94
|
-
f"\n🧹 [bold blue]Starting scheduled cleanup of blue services for PR #{pr_number}[/bold blue]"
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
from .core.aws import AWSInterface
|
|
98
|
-
|
|
99
|
-
aws = AWSInterface()
|
|
100
|
-
|
|
101
|
-
for blue_svc in blue_services:
|
|
102
|
-
service_name = blue_svc["service_name"]
|
|
103
|
-
console.print(f"🗑️ Cleaning up blue service: {service_name}")
|
|
104
|
-
|
|
105
|
-
try:
|
|
106
|
-
# Delete ECS service
|
|
107
|
-
if aws._delete_ecs_service(service_name):
|
|
108
|
-
# Delete ECR image
|
|
109
|
-
pr_match = service_name.split("-")
|
|
110
|
-
if len(pr_match) >= 2:
|
|
111
|
-
pr_num = pr_match[1]
|
|
112
|
-
image_tag = f"pr-{pr_num}-ci" # Legacy format for old services
|
|
113
|
-
aws._delete_ecr_image(image_tag)
|
|
114
|
-
|
|
115
|
-
console.print(f"✅ Cleaned up blue service: {service_name}")
|
|
116
|
-
else:
|
|
117
|
-
console.print(f"⚠️ Failed to clean up: {service_name}")
|
|
118
|
-
|
|
119
|
-
except Exception as e:
|
|
120
|
-
console.print(f"❌ Cleanup error for {service_name}: {e}")
|
|
121
|
-
|
|
122
|
-
console.print("🧹 ✅ Blue service cleanup completed")
|
|
123
|
-
|
|
124
|
-
except Exception as e:
|
|
125
|
-
console.print(f"❌ Background cleanup failed: {e}")
|
|
126
|
-
|
|
127
|
-
# Start cleanup in background thread
|
|
128
|
-
cleanup_thread = threading.Thread(target=cleanup_after_delay, daemon=True)
|
|
129
|
-
cleanup_thread.start()
|
|
130
|
-
console.print("🕐 Background cleanup scheduled")
|
|
33
|
+
p(f"\n🎪 [bold blue]Monitor {context} progress:[/bold blue]")
|
|
34
|
+
p(f"📝 Logs: {urls['logs']}")
|
|
35
|
+
p(f"📊 Service: {urls['service']}")
|
|
36
|
+
p("")
|
|
131
37
|
|
|
132
38
|
|
|
133
39
|
app = typer.Typer(
|
|
@@ -149,9 +55,11 @@ app = typer.Typer(
|
|
|
149
55
|
|
|
150
56
|
[dim]CLI commands work with existing environments or dry-run new ones.[/dim]""",
|
|
151
57
|
rich_markup_mode="rich",
|
|
58
|
+
no_args_is_help=True,
|
|
152
59
|
)
|
|
153
60
|
|
|
154
61
|
console = Console()
|
|
62
|
+
p = console.print # Shorthand for cleaner code
|
|
155
63
|
|
|
156
64
|
|
|
157
65
|
def _get_github_workflow_url() -> str:
|
|
@@ -173,15 +81,15 @@ def _get_github_actor() -> str:
|
|
|
173
81
|
|
|
174
82
|
def _get_showtime_footer() -> str:
|
|
175
83
|
"""Get consistent Showtime footer for PR comments"""
|
|
176
|
-
return "
|
|
84
|
+
return "🎪 *Managed by [Superset Showtime](https://github.com/your-org/superset-showtime)*"
|
|
177
85
|
|
|
178
86
|
|
|
179
87
|
@app.command()
|
|
180
|
-
def version():
|
|
88
|
+
def version() -> None:
|
|
181
89
|
"""Show version information"""
|
|
182
90
|
from . import __version__
|
|
183
91
|
|
|
184
|
-
|
|
92
|
+
p(f"🎪 Superset Showtime v{__version__}")
|
|
185
93
|
|
|
186
94
|
|
|
187
95
|
@app.command()
|
|
@@ -204,73 +112,77 @@ def start(
|
|
|
204
112
|
force: bool = typer.Option(
|
|
205
113
|
False, "--force", help="Force re-deployment by deleting existing service"
|
|
206
114
|
),
|
|
207
|
-
):
|
|
115
|
+
) -> None:
|
|
208
116
|
"""Create ephemeral environment for PR"""
|
|
209
117
|
try:
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
#
|
|
213
|
-
if not
|
|
214
|
-
|
|
215
|
-
|
|
118
|
+
pr = PullRequest.from_id(pr_number)
|
|
119
|
+
|
|
120
|
+
# Check if working environment already exists (unless force)
|
|
121
|
+
if pr.current_show and pr.current_show.status not in ["failed"] and not force:
|
|
122
|
+
p(f"🎪 [bold yellow]Environment already exists for PR #{pr_number}[/bold yellow]")
|
|
123
|
+
ip_info = f" at {pr.current_show.ip}" if pr.current_show.ip else ""
|
|
124
|
+
p(f"Current: {pr.current_show.sha}{ip_info} ({pr.current_show.status})")
|
|
125
|
+
p("Use 'showtime sync' to update or 'showtime stop' to clean up first")
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
# Handle failed environment replacement
|
|
129
|
+
if pr.current_show and pr.current_show.status == "failed":
|
|
130
|
+
p(f"🎪 [bold orange]Replacing failed environment for PR #{pr_number}[/bold orange]")
|
|
131
|
+
p(f"Failed: {pr.current_show.sha} at {pr.current_show.created_at}")
|
|
132
|
+
p("🔄 Creating new environment...")
|
|
133
|
+
elif pr.current_show:
|
|
134
|
+
p(f"🎪 [bold blue]Creating environment for PR #{pr_number}[/bold blue]")
|
|
216
135
|
else:
|
|
217
|
-
|
|
136
|
+
p(f"🎪 [bold green]Creating new environment for PR #{pr_number}[/bold green]")
|
|
218
137
|
|
|
219
138
|
if dry_run:
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
console.print(f" 🎪 ⌛ {ttl}")
|
|
139
|
+
from .core.pull_request import get_github
|
|
140
|
+
|
|
141
|
+
target_sha = sha or get_github().get_latest_commit_sha(pr_number)
|
|
142
|
+
p("🎪 [bold yellow]DRY RUN[/bold yellow] - Would create environment:")
|
|
143
|
+
p(f" PR: #{pr_number}")
|
|
144
|
+
p(f" SHA: {target_sha[:7]}")
|
|
145
|
+
p(f" AWS Service: pr-{pr_number}-{target_sha[:7]}")
|
|
146
|
+
p(f" TTL: {ttl}")
|
|
229
147
|
return
|
|
230
148
|
|
|
231
|
-
#
|
|
232
|
-
|
|
233
|
-
if pr.current_show:
|
|
234
|
-
console.print(
|
|
235
|
-
f"🎪 [bold yellow]Environment already exists for PR #{pr_number}[/bold yellow]"
|
|
236
|
-
)
|
|
237
|
-
console.print(f"Current: {pr.current_show.sha} at {pr.current_show.ip}")
|
|
238
|
-
console.print("Use 'showtime sync' to update or 'showtime stop' to clean up first")
|
|
239
|
-
return
|
|
149
|
+
# Use PullRequest method for all logic
|
|
150
|
+
result = pr.start_environment(sha=sha, dry_run_github=False, dry_run_aws=dry_run_aws)
|
|
240
151
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
152
|
+
if result.success:
|
|
153
|
+
if result.show:
|
|
154
|
+
p(f"🎪 ✅ Environment created: {result.show.sha}")
|
|
155
|
+
else:
|
|
156
|
+
p("🎪 ✅ Environment created")
|
|
157
|
+
else:
|
|
158
|
+
p(f"🎪 ❌ Failed to create environment: {result.error}")
|
|
159
|
+
raise typer.Exit(1)
|
|
246
160
|
|
|
247
161
|
except GitHubError as e:
|
|
248
|
-
|
|
162
|
+
p(f"❌ GitHub error: {e}")
|
|
163
|
+
raise typer.Exit(1) from e
|
|
249
164
|
except Exception as e:
|
|
250
|
-
|
|
165
|
+
p(f"❌ Error: {e}")
|
|
166
|
+
raise typer.Exit(1) from e
|
|
251
167
|
|
|
252
168
|
|
|
253
169
|
@app.command()
|
|
254
170
|
def status(
|
|
255
171
|
pr_number: int = typer.Argument(..., help="PR number to check status for"),
|
|
256
172
|
verbose: bool = typer.Option(False, "-v", "--verbose", help="Show detailed information"),
|
|
257
|
-
):
|
|
173
|
+
) -> None:
|
|
258
174
|
"""Show environment status for PR"""
|
|
259
175
|
try:
|
|
260
|
-
|
|
176
|
+
pr = PullRequest.from_id(pr_number)
|
|
261
177
|
|
|
262
|
-
|
|
178
|
+
# Use PullRequest method for data
|
|
179
|
+
status_data = pr.get_status()
|
|
263
180
|
|
|
264
|
-
if
|
|
265
|
-
|
|
181
|
+
if status_data["status"] == "no_environment":
|
|
182
|
+
p(f"🎪 No environment found for PR #{pr_number}")
|
|
266
183
|
return
|
|
267
184
|
|
|
268
|
-
|
|
269
|
-
if not show:
|
|
270
|
-
console.print(f"🎪 No active environment for PR #{pr_number}")
|
|
271
|
-
if pr.building_show:
|
|
272
|
-
console.print(f"🏗️ Building environment: {pr.building_show.sha}")
|
|
273
|
-
return
|
|
185
|
+
show_data = status_data["show"]
|
|
274
186
|
|
|
275
187
|
# Create status table
|
|
276
188
|
table = Table(title=f"🎪 Environment Status - PR #{pr_number}")
|
|
@@ -278,37 +190,43 @@ def status(
|
|
|
278
190
|
table.add_column("Value", style="white")
|
|
279
191
|
|
|
280
192
|
status_emoji = STATUS_DISPLAY
|
|
193
|
+
table.add_row(
|
|
194
|
+
"Status", f"{status_emoji.get(show_data['status'], '❓')} {show_data['status'].title()}"
|
|
195
|
+
)
|
|
196
|
+
table.add_row("Environment", f"`{show_data['sha']}`")
|
|
197
|
+
table.add_row("AWS Service", f"`{show_data['aws_service_name']}`")
|
|
281
198
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if show.ip:
|
|
287
|
-
table.add_row("URL", f"http://{show.ip}:8080")
|
|
199
|
+
if show_data["ip"]:
|
|
200
|
+
table.add_row("URL", f"http://{show_data['ip']}:8080")
|
|
201
|
+
if show_data["created_at"]:
|
|
202
|
+
table.add_row("Created", show_data["created_at"])
|
|
288
203
|
|
|
289
|
-
|
|
290
|
-
table.add_row("Created", show.created_at)
|
|
204
|
+
table.add_row("TTL", show_data["ttl"])
|
|
291
205
|
|
|
292
|
-
|
|
206
|
+
if show_data["requested_by"]:
|
|
207
|
+
table.add_row("Requested by", f"@{show_data['requested_by']}")
|
|
293
208
|
|
|
294
|
-
|
|
295
|
-
|
|
209
|
+
# Show active triggers
|
|
210
|
+
trigger_labels = [label for label in pr.labels if "showtime-trigger-" in label]
|
|
211
|
+
if trigger_labels:
|
|
212
|
+
trigger_display = ", ".join(trigger_labels)
|
|
213
|
+
table.add_row("Active Triggers", trigger_display)
|
|
296
214
|
|
|
297
215
|
if verbose:
|
|
298
216
|
table.add_row("All Labels", ", ".join(pr.circus_labels))
|
|
299
217
|
|
|
300
|
-
|
|
218
|
+
p(table)
|
|
301
219
|
|
|
302
220
|
# Show building environment if exists
|
|
303
|
-
if pr.building_show and pr.building_show !=
|
|
304
|
-
|
|
305
|
-
f"🏗️ [bold yellow]Building new environment:[/bold yellow] {pr.building_show.sha}"
|
|
306
|
-
)
|
|
221
|
+
if pr.building_show and pr.building_show.sha != show_data["sha"]:
|
|
222
|
+
p(f"🏗️ [bold yellow]Building new environment:[/bold yellow] {pr.building_show.sha}")
|
|
307
223
|
|
|
308
224
|
except GitHubError as e:
|
|
309
|
-
|
|
225
|
+
p(f"❌ GitHub error: {e}")
|
|
226
|
+
raise typer.Exit(1) from e
|
|
310
227
|
except Exception as e:
|
|
311
|
-
|
|
228
|
+
p(f"❌ Error: {e}")
|
|
229
|
+
raise typer.Exit(1) from e
|
|
312
230
|
|
|
313
231
|
|
|
314
232
|
@app.command()
|
|
@@ -320,114 +238,47 @@ def stop(
|
|
|
320
238
|
False, "--dry-run-aws", help="Skip AWS operations, use mock data"
|
|
321
239
|
),
|
|
322
240
|
aws_sleep: int = typer.Option(0, "--aws-sleep", help="Seconds to sleep during AWS operations"),
|
|
323
|
-
):
|
|
241
|
+
) -> None:
|
|
324
242
|
"""Delete environment for PR"""
|
|
325
243
|
try:
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
pr = PullRequest.from_id(pr_number, github)
|
|
244
|
+
pr = PullRequest.from_id(pr_number)
|
|
329
245
|
|
|
330
246
|
if not pr.current_show:
|
|
331
|
-
|
|
247
|
+
p(f"🎪 No active environment found for PR #{pr_number}")
|
|
332
248
|
return
|
|
333
249
|
|
|
334
250
|
show = pr.current_show
|
|
335
|
-
|
|
336
|
-
|
|
251
|
+
p(f"🎪 [bold yellow]Stopping environment for PR #{pr_number}...[/bold yellow]")
|
|
252
|
+
p(f"Environment: {show.sha} at {show.ip}")
|
|
337
253
|
|
|
338
254
|
if dry_run:
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
255
|
+
p("🎪 [bold yellow]DRY RUN[/bold yellow] - Would delete environment:")
|
|
256
|
+
p(f" AWS Service: {show.aws_service_name}")
|
|
257
|
+
p(f" ECR Image: {show.aws_image_tag}")
|
|
258
|
+
p(f" Circus Labels: {len(pr.circus_labels)} labels")
|
|
343
259
|
return
|
|
344
260
|
|
|
345
261
|
if not force:
|
|
346
262
|
confirm = typer.confirm(f"Delete environment {show.aws_service_name}?")
|
|
347
263
|
if not confirm:
|
|
348
|
-
|
|
264
|
+
p("🎪 Cancelled")
|
|
349
265
|
return
|
|
350
266
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
console.print(f" - ECS service: {show.aws_service_name}")
|
|
354
|
-
console.print(f" - ECR image: {show.aws_image_tag}")
|
|
355
|
-
if aws_sleep > 0:
|
|
356
|
-
import time
|
|
267
|
+
# Use PullRequest method for all logic
|
|
268
|
+
result = pr.stop_environment(dry_run_github=False, dry_run_aws=dry_run_aws)
|
|
357
269
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
console.print("🎪 [bold green]Mock AWS cleanup complete![/bold green]")
|
|
270
|
+
if result.success:
|
|
271
|
+
p("🎪 ✅ Environment stopped and cleaned up!")
|
|
361
272
|
else:
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
console.print("🎪 [bold blue]Starting AWS cleanup...[/bold blue]")
|
|
366
|
-
aws = AWSInterface()
|
|
367
|
-
|
|
368
|
-
try:
|
|
369
|
-
# Get current environment info
|
|
370
|
-
pr = PullRequest.from_id(pr_number, github)
|
|
371
|
-
|
|
372
|
-
if pr.current_show:
|
|
373
|
-
show = pr.current_show
|
|
374
|
-
|
|
375
|
-
# Show logs URL for monitoring cleanup
|
|
376
|
-
_show_service_urls(show, "cleanup")
|
|
377
|
-
console.print(f"🎪 Destroying environment: {show.aws_service_name}")
|
|
378
|
-
|
|
379
|
-
# Step 1: Check if ECS service exists and is active
|
|
380
|
-
service_name = show.ecs_service_name
|
|
381
|
-
console.print(f"🎪 Checking ECS service: {service_name}")
|
|
382
|
-
|
|
383
|
-
service_exists = aws._service_exists(service_name)
|
|
384
|
-
|
|
385
|
-
if service_exists:
|
|
386
|
-
console.print(f"🎪 Found active ECS service: {service_name}")
|
|
387
|
-
|
|
388
|
-
# Step 2: Delete ECS service
|
|
389
|
-
console.print("🎪 Deleting ECS service...")
|
|
390
|
-
success = aws._delete_ecs_service(service_name)
|
|
391
|
-
|
|
392
|
-
if success:
|
|
393
|
-
console.print("🎪 ✅ ECS service deleted successfully")
|
|
394
|
-
|
|
395
|
-
# Step 3: Delete ECR image tag
|
|
396
|
-
image_tag = f"pr-{pr_number}-ci" # Match GHA image tag format
|
|
397
|
-
console.print(f"🎪 Deleting ECR image tag: {image_tag}")
|
|
398
|
-
|
|
399
|
-
ecr_success = aws._delete_ecr_image(image_tag)
|
|
400
|
-
|
|
401
|
-
if ecr_success:
|
|
402
|
-
console.print("🎪 ✅ ECR image deleted successfully")
|
|
403
|
-
else:
|
|
404
|
-
console.print("🎪 ⚠️ ECR image deletion failed (may not exist)")
|
|
405
|
-
|
|
406
|
-
console.print(
|
|
407
|
-
"🎪 [bold green]✅ AWS cleanup completed successfully![/bold green]"
|
|
408
|
-
)
|
|
409
|
-
|
|
410
|
-
else:
|
|
411
|
-
console.print("🎪 [bold red]❌ ECS service deletion failed[/bold red]")
|
|
412
|
-
|
|
413
|
-
else:
|
|
414
|
-
console.print(f"🎪 No active ECS service found: {service_name}")
|
|
415
|
-
console.print("🎪 ✅ No AWS resources to clean up")
|
|
416
|
-
else:
|
|
417
|
-
console.print(f"🎪 No active environment found for PR #{pr_number}")
|
|
418
|
-
|
|
419
|
-
except Exception as e:
|
|
420
|
-
console.print(f"🎪 [bold red]❌ AWS cleanup failed:[/bold red] {e}")
|
|
421
|
-
|
|
422
|
-
# Remove circus labels
|
|
423
|
-
github.remove_circus_labels(pr_number)
|
|
424
|
-
|
|
425
|
-
console.print("🎪 [bold green]Environment stopped and labels cleaned up![/bold green]")
|
|
273
|
+
p(f"🎪 ❌ Failed to stop environment: {result.error}")
|
|
274
|
+
raise typer.Exit(1)
|
|
426
275
|
|
|
427
276
|
except GitHubError as e:
|
|
428
|
-
|
|
277
|
+
p(f"❌ GitHub error: {e}")
|
|
278
|
+
raise typer.Exit(1) from e
|
|
429
279
|
except Exception as e:
|
|
430
|
-
|
|
280
|
+
p(f"❌ Error: {e}")
|
|
281
|
+
raise typer.Exit(1) from e
|
|
431
282
|
|
|
432
283
|
|
|
433
284
|
@app.command()
|
|
@@ -436,37 +287,33 @@ def list(
|
|
|
436
287
|
None, "--status", help="Filter by status (running, building, etc.)"
|
|
437
288
|
),
|
|
438
289
|
user: Optional[str] = typer.Option(None, "--user", help="Filter by user"),
|
|
439
|
-
):
|
|
290
|
+
) -> None:
|
|
440
291
|
"""List all environments"""
|
|
441
292
|
try:
|
|
442
|
-
|
|
293
|
+
# Use PullRequest method for data collection
|
|
294
|
+
all_environments = PullRequest.list_all_environments()
|
|
443
295
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
if not pr_numbers:
|
|
448
|
-
console.print("🎪 No environments currently running")
|
|
296
|
+
if not all_environments:
|
|
297
|
+
p("🎪 No environments currently running")
|
|
449
298
|
return
|
|
450
299
|
|
|
451
|
-
#
|
|
452
|
-
|
|
453
|
-
for
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
if not all_shows:
|
|
300
|
+
# Apply filters
|
|
301
|
+
filtered_envs = []
|
|
302
|
+
for env in all_environments:
|
|
303
|
+
show_data = env["show"]
|
|
304
|
+
if status_filter and show_data["status"] != status_filter:
|
|
305
|
+
continue
|
|
306
|
+
if user and show_data["requested_by"] != user:
|
|
307
|
+
continue
|
|
308
|
+
filtered_envs.append(env)
|
|
309
|
+
|
|
310
|
+
if not filtered_envs:
|
|
464
311
|
filter_msg = ""
|
|
465
312
|
if status_filter:
|
|
466
313
|
filter_msg += f" with status '{status_filter}'"
|
|
467
314
|
if user:
|
|
468
315
|
filter_msg += f" by user '{user}'"
|
|
469
|
-
|
|
316
|
+
p(f"🎪 No environments found{filter_msg}")
|
|
470
317
|
return
|
|
471
318
|
|
|
472
319
|
# Create table with full terminal width
|
|
@@ -481,50 +328,56 @@ def list(
|
|
|
481
328
|
|
|
482
329
|
status_emoji = STATUS_DISPLAY
|
|
483
330
|
|
|
484
|
-
for
|
|
331
|
+
for env in sorted(filtered_envs, key=lambda e: e["pr_number"]):
|
|
332
|
+
show_data = env["show"]
|
|
333
|
+
pr_number = env["pr_number"]
|
|
485
334
|
# Make Superset URL clickable and show full URL
|
|
486
|
-
if
|
|
487
|
-
full_url = f"http://{
|
|
335
|
+
if show_data["ip"]:
|
|
336
|
+
full_url = f"http://{show_data['ip']}:8080"
|
|
488
337
|
superset_url = f"[link={full_url}]{full_url}[/link]"
|
|
489
338
|
else:
|
|
490
339
|
superset_url = "-"
|
|
491
340
|
|
|
492
341
|
# Get AWS service URLs - iTerm2 supports Rich clickable links
|
|
493
|
-
|
|
342
|
+
from .core.github_messages import get_aws_console_urls
|
|
343
|
+
|
|
344
|
+
aws_urls = get_aws_console_urls(show_data["aws_service_name"])
|
|
494
345
|
aws_logs_link = f"[link={aws_urls['logs']}]View[/link]"
|
|
495
346
|
|
|
496
347
|
# Make PR number clickable
|
|
497
|
-
pr_url = f"https://github.com/apache/superset/pull/{
|
|
498
|
-
clickable_pr = f"[link={pr_url}]{
|
|
348
|
+
pr_url = f"https://github.com/apache/superset/pull/{pr_number}"
|
|
349
|
+
clickable_pr = f"[link={pr_url}]{pr_number}[/link]"
|
|
499
350
|
|
|
500
351
|
table.add_row(
|
|
501
352
|
clickable_pr,
|
|
502
|
-
f"{status_emoji.get(
|
|
503
|
-
|
|
353
|
+
f"{status_emoji.get(show_data['status'], '❓')} {show_data['status']}",
|
|
354
|
+
show_data["sha"],
|
|
504
355
|
superset_url,
|
|
505
356
|
aws_logs_link,
|
|
506
|
-
|
|
507
|
-
f"@{
|
|
357
|
+
show_data["ttl"],
|
|
358
|
+
f"@{show_data['requested_by']}" if show_data["requested_by"] else "-",
|
|
508
359
|
)
|
|
509
360
|
|
|
510
|
-
|
|
361
|
+
p(table)
|
|
511
362
|
|
|
512
363
|
except GitHubError as e:
|
|
513
|
-
|
|
364
|
+
p(f"❌ GitHub error: {e}")
|
|
365
|
+
raise typer.Exit(1) from e
|
|
514
366
|
except Exception as e:
|
|
515
|
-
|
|
367
|
+
p(f"❌ Error: {e}")
|
|
368
|
+
raise typer.Exit(1) from e
|
|
516
369
|
|
|
517
370
|
|
|
518
371
|
@app.command()
|
|
519
|
-
def labels():
|
|
372
|
+
def labels() -> None:
|
|
520
373
|
"""🎪 Show complete circus tent label reference"""
|
|
521
374
|
from .core.label_colors import LABEL_DEFINITIONS
|
|
522
375
|
|
|
523
|
-
|
|
524
|
-
|
|
376
|
+
p("🎪 [bold blue]Circus Tent Label Reference[/bold blue]")
|
|
377
|
+
p()
|
|
525
378
|
|
|
526
379
|
# User Action Labels (from LABEL_DEFINITIONS)
|
|
527
|
-
|
|
380
|
+
p("[bold yellow]🎯 User Action Labels (Add these to GitHub PR):[/bold yellow]")
|
|
528
381
|
trigger_table = Table()
|
|
529
382
|
trigger_table.add_column("Label", style="green")
|
|
530
383
|
trigger_table.add_column("Description", style="dim")
|
|
@@ -532,11 +385,11 @@ def labels():
|
|
|
532
385
|
for label_name, definition in LABEL_DEFINITIONS.items():
|
|
533
386
|
trigger_table.add_row(f"`{label_name}`", definition["description"])
|
|
534
387
|
|
|
535
|
-
|
|
536
|
-
|
|
388
|
+
p(trigger_table)
|
|
389
|
+
p()
|
|
537
390
|
|
|
538
391
|
# State Labels
|
|
539
|
-
|
|
392
|
+
p("[bold cyan]📊 State Labels (Automatically managed):[/bold cyan]")
|
|
540
393
|
state_table = Table()
|
|
541
394
|
state_table.add_column("Label", style="cyan")
|
|
542
395
|
state_table.add_column("Meaning", style="white")
|
|
@@ -552,93 +405,43 @@ def labels():
|
|
|
552
405
|
state_table.add_row("🎪 {sha} ⌛ {ttl-policy}", "TTL policy", "🎪 abc123f ⌛ 24h")
|
|
553
406
|
state_table.add_row("🎪 {sha} 🤡 {username}", "Requested by", "🎪 abc123f 🤡 maxime")
|
|
554
407
|
|
|
555
|
-
|
|
556
|
-
|
|
408
|
+
p(state_table)
|
|
409
|
+
p()
|
|
557
410
|
|
|
558
411
|
# Workflow Examples
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
console.print("[bold]1. Create Environment:[/bold]")
|
|
563
|
-
console.print(" • Add label: [green]🎪 ⚡ showtime-trigger-start[/green]")
|
|
564
|
-
console.print(
|
|
565
|
-
" • Watch for: [blue]🎪 abc123f 🚦 building[/blue] → [green]🎪 abc123f 🚦 running[/green]"
|
|
566
|
-
)
|
|
567
|
-
console.print(
|
|
568
|
-
" • Get URL from: [cyan]🎪 abc123f 🌐 52.1.2.3:8080[/cyan] → http://52.1.2.3:8080"
|
|
569
|
-
)
|
|
570
|
-
console.print()
|
|
571
|
-
|
|
572
|
-
console.print("[bold]2. Freeze Environment (Optional):[/bold]")
|
|
573
|
-
console.print(" • Add label: [orange]🎪 🧊 showtime-freeze[/orange]")
|
|
574
|
-
console.print(" • Result: Environment won't auto-update on new commits")
|
|
575
|
-
console.print(" • Use case: Test specific SHA while continuing development")
|
|
576
|
-
console.print()
|
|
577
|
-
|
|
578
|
-
console.print("[bold]3. Update to New Commit (Automatic):[/bold]")
|
|
579
|
-
console.print(" • New commit pushed → Automatic blue-green rolling update")
|
|
580
|
-
console.print(
|
|
581
|
-
" • Watch for: [blue]🎪 abc123f 🚦 updating[/blue] → [green]🎪 def456a 🚦 running[/green]"
|
|
582
|
-
)
|
|
583
|
-
console.print(" • SHA changes: [cyan]🎪 🎯 abc123f[/cyan] → [cyan]🎪 🎯 def456a[/cyan]")
|
|
584
|
-
console.print()
|
|
585
|
-
|
|
586
|
-
console.print("[bold]4. Clean Up:[/bold]")
|
|
587
|
-
console.print(" • Add label: [red]🎪 🛑 showtime-trigger-stop[/red]")
|
|
588
|
-
console.print(" • Result: All 🎪 labels removed, AWS resources deleted")
|
|
589
|
-
console.print()
|
|
590
|
-
|
|
591
|
-
console.print("[bold]📊 Understanding State:[/bold]")
|
|
592
|
-
console.print("• [dim]TTL labels show policy (24h, 48h, close) not time remaining[/dim]")
|
|
593
|
-
console.print("• [dim]Use 'showtime status {pr-id}' to calculate actual time remaining[/dim]")
|
|
594
|
-
console.print("• [dim]Multiple SHA labels during updates (🎯 active, 🏗️ building)[/dim]")
|
|
595
|
-
console.print()
|
|
596
|
-
|
|
597
|
-
console.print("[dim]💡 Tip: Only maintainers with write access can add trigger labels[/dim]")
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
@app.command()
|
|
601
|
-
def test_lifecycle(
|
|
602
|
-
pr_number: int,
|
|
603
|
-
dry_run_aws: bool = typer.Option(
|
|
604
|
-
True, "--dry-run-aws/--real-aws", help="Use mock AWS operations"
|
|
605
|
-
),
|
|
606
|
-
dry_run_github: bool = typer.Option(
|
|
607
|
-
True, "--dry-run-github/--real-github", help="Use mock GitHub operations"
|
|
608
|
-
),
|
|
609
|
-
aws_sleep: int = typer.Option(10, "--aws-sleep", help="Seconds to sleep during AWS operations"),
|
|
610
|
-
):
|
|
611
|
-
"""🎪 Test full environment lifecycle with mock triggers"""
|
|
612
|
-
|
|
613
|
-
console.print(f"🎪 [bold blue]Testing full lifecycle for PR #{pr_number}[/bold blue]")
|
|
614
|
-
console.print(
|
|
615
|
-
f"AWS: {'DRY-RUN' if dry_run_aws else 'REAL'}, GitHub: {'DRY-RUN' if dry_run_github else 'REAL'}"
|
|
616
|
-
)
|
|
617
|
-
console.print()
|
|
618
|
-
|
|
619
|
-
try:
|
|
620
|
-
github = GitHubInterface()
|
|
412
|
+
p("[bold magenta]🎪 Complete Workflow Examples:[/bold magenta]")
|
|
413
|
+
p()
|
|
621
414
|
|
|
622
|
-
|
|
623
|
-
|
|
415
|
+
p("[bold]1. Create Environment:[/bold]")
|
|
416
|
+
p(" • Add label: [green]🎪 ⚡ showtime-trigger-start[/green]")
|
|
417
|
+
p(" • Watch for: [blue]🎪 abc123f 🚦 building[/blue] → [green]🎪 abc123f 🚦 running[/green]")
|
|
418
|
+
p(" • Get URL from: [cyan]🎪 abc123f 🌐 52.1.2.3:8080[/cyan] → http://52.1.2.3:8080")
|
|
419
|
+
p()
|
|
624
420
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
421
|
+
p("[bold]2. Freeze Environment (Optional):[/bold]")
|
|
422
|
+
p(" • Add label: [orange]🎪 🧊 showtime-freeze[/orange]")
|
|
423
|
+
p(" • Result: Environment won't auto-update on new commits")
|
|
424
|
+
p(" • Use case: Test specific SHA while continuing development")
|
|
425
|
+
p()
|
|
628
426
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
427
|
+
p("[bold]3. Update to New Commit (Automatic):[/bold]")
|
|
428
|
+
p(" • New commit pushed → Automatic blue-green rolling update")
|
|
429
|
+
p(" • Watch for: [blue]🎪 abc123f 🚦 updating[/blue] → [green]🎪 def456a 🚦 running[/green]")
|
|
430
|
+
p(" • SHA changes: [cyan]🎪 🎯 abc123f[/cyan] → [cyan]🎪 🎯 def456a[/cyan]")
|
|
431
|
+
p()
|
|
632
432
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
433
|
+
p("[bold]4. Clean Up:[/bold]")
|
|
434
|
+
p(" • Add label: [red]🎪 🛑 showtime-trigger-stop[/red]")
|
|
435
|
+
p(" • Result: All 🎪 labels removed, AWS resources deleted")
|
|
436
|
+
p()
|
|
636
437
|
|
|
637
|
-
|
|
638
|
-
|
|
438
|
+
p("[bold]📊 Understanding State:[/bold]")
|
|
439
|
+
p("• [dim]TTL labels show policy (24h, 48h, close) not time remaining[/dim]")
|
|
440
|
+
p("• [dim]Use 'showtime status {pr-id}' to calculate actual time remaining[/dim]")
|
|
441
|
+
p("• [dim]Multiple SHA labels during updates (🎯 active, 🏗️ building)[/dim]")
|
|
442
|
+
p()
|
|
639
443
|
|
|
640
|
-
|
|
641
|
-
console.print(f"🎪 [bold red]Lifecycle test failed:[/bold red] {e}")
|
|
444
|
+
p("[dim]💡 Tip: Only maintainers with write access can add trigger labels[/dim]")
|
|
642
445
|
|
|
643
446
|
|
|
644
447
|
@app.command()
|
|
@@ -648,158 +451,133 @@ def sync(
|
|
|
648
451
|
check_only: bool = typer.Option(
|
|
649
452
|
False, "--check-only", help="Check what actions are needed without executing"
|
|
650
453
|
),
|
|
651
|
-
deploy: bool = typer.Option(
|
|
652
|
-
False, "--deploy", help="Execute deployment actions (assumes build is complete)"
|
|
653
|
-
),
|
|
654
454
|
dry_run_aws: bool = typer.Option(
|
|
655
455
|
False, "--dry-run-aws", help="Skip AWS operations, use mock data"
|
|
656
456
|
),
|
|
657
457
|
dry_run_github: bool = typer.Option(
|
|
658
458
|
False, "--dry-run-github", help="Skip GitHub label operations"
|
|
659
459
|
),
|
|
460
|
+
dry_run_docker: bool = typer.Option(
|
|
461
|
+
False, "--dry-run-docker", help="Skip Docker build, use mock success"
|
|
462
|
+
),
|
|
660
463
|
aws_sleep: int = typer.Option(
|
|
661
464
|
0, "--aws-sleep", help="Seconds to sleep during AWS operations (for testing)"
|
|
662
465
|
),
|
|
663
466
|
docker_tag: Optional[str] = typer.Option(
|
|
664
467
|
None, "--docker-tag", help="Override Docker image tag (e.g., pr-34639-9a82c20-ci, latest)"
|
|
665
468
|
),
|
|
666
|
-
):
|
|
469
|
+
) -> None:
|
|
667
470
|
"""🎪 Intelligently sync PR to desired state (called by GitHub Actions)"""
|
|
668
471
|
try:
|
|
669
|
-
|
|
670
|
-
pr = PullRequest.from_id(pr_number
|
|
472
|
+
# Use singletons - no interface creation needed
|
|
473
|
+
pr = PullRequest.from_id(pr_number)
|
|
671
474
|
|
|
672
|
-
# Get
|
|
673
|
-
pr_data = github.get_pr_data(pr_number)
|
|
674
|
-
pr_state = pr_data.get("state", "open") # open, closed
|
|
675
|
-
|
|
676
|
-
# Get SHA - use provided SHA or default to latest
|
|
475
|
+
# Get target SHA - use provided SHA or default to latest
|
|
677
476
|
if sha:
|
|
678
477
|
target_sha = sha
|
|
679
|
-
|
|
478
|
+
p(f"🎪 Using specified SHA: {target_sha[:7]}")
|
|
680
479
|
else:
|
|
681
|
-
|
|
682
|
-
console.print(f"🎪 Using latest SHA: {target_sha[:7]}")
|
|
480
|
+
from .core.pull_request import get_github
|
|
683
481
|
|
|
684
|
-
|
|
685
|
-
|
|
482
|
+
target_sha = get_github().get_latest_commit_sha(pr_number)
|
|
483
|
+
p(f"🎪 Using latest SHA: {target_sha[:7]}")
|
|
686
484
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
console.print(f"action_needed={action_needed}")
|
|
485
|
+
# Get PR state for analysis
|
|
486
|
+
from .core.pull_request import get_github
|
|
690
487
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
console.print(f"build_needed={str(build_needed).lower()}")
|
|
488
|
+
pr_data = get_github().get_pr_data(pr_number)
|
|
489
|
+
pr_state = pr_data.get("state", "open")
|
|
694
490
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
491
|
+
if check_only:
|
|
492
|
+
# Analysis mode - just return what's needed
|
|
493
|
+
analysis_result = pr.analyze(target_sha, pr_state)
|
|
494
|
+
p(f"build_needed={str(analysis_result.build_needed).lower()}")
|
|
495
|
+
p(f"sync_needed={str(analysis_result.sync_needed).lower()}")
|
|
496
|
+
p(f"pr_number={pr_number}")
|
|
497
|
+
p(f"target_sha={target_sha}")
|
|
698
498
|
return
|
|
699
499
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
)
|
|
703
|
-
console.print(f"🎪 Action needed: {action_needed}")
|
|
500
|
+
# Execution mode - do the sync
|
|
501
|
+
p(f"🎪 [bold blue]Syncing PR #{pr_number}[/bold blue] (SHA: {target_sha[:7]})")
|
|
704
502
|
|
|
705
|
-
#
|
|
706
|
-
if
|
|
707
|
-
|
|
503
|
+
# Handle closed PRs specially
|
|
504
|
+
if pr_state == "closed":
|
|
505
|
+
p("🎪 PR is closed - cleaning up environment")
|
|
708
506
|
if pr.current_show:
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
# 2. Find explicit trigger labels
|
|
715
|
-
trigger_labels = [label for label in pr.labels if "showtime-trigger-" in label]
|
|
716
|
-
|
|
717
|
-
# 3. Handle explicit triggers first
|
|
718
|
-
if trigger_labels:
|
|
719
|
-
console.print(f"🎪 Processing {len(trigger_labels)} explicit trigger(s)")
|
|
720
|
-
|
|
721
|
-
for trigger in trigger_labels:
|
|
722
|
-
console.print(f"🎪 Processing: {trigger}")
|
|
723
|
-
|
|
724
|
-
# Remove trigger label immediately (atomic operation)
|
|
725
|
-
if not dry_run_github:
|
|
726
|
-
github.remove_label(pr_number, trigger)
|
|
507
|
+
stop_result = pr.stop_environment(
|
|
508
|
+
dry_run_github=dry_run_github, dry_run_aws=dry_run_aws
|
|
509
|
+
)
|
|
510
|
+
if stop_result.success:
|
|
511
|
+
p("🎪 ✅ Cleanup completed")
|
|
727
512
|
else:
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
# Process the trigger
|
|
733
|
-
if "showtime-trigger-start" in trigger:
|
|
734
|
-
_handle_start_trigger(
|
|
735
|
-
pr_number, github, dry_run_aws, dry_run_github, aws_sleep, docker_tag
|
|
736
|
-
)
|
|
737
|
-
elif "showtime-trigger-stop" in trigger:
|
|
738
|
-
_handle_stop_trigger(pr_number, github, dry_run_aws, dry_run_github)
|
|
739
|
-
|
|
740
|
-
console.print("🎪 All explicit triggers processed!")
|
|
513
|
+
p(f"🎪 ❌ Cleanup failed: {stop_result.error}")
|
|
514
|
+
else:
|
|
515
|
+
p("🎪 No environment to clean up")
|
|
741
516
|
return
|
|
742
517
|
|
|
743
|
-
#
|
|
744
|
-
|
|
518
|
+
# Regular sync for open PRs
|
|
519
|
+
result = pr.sync(
|
|
520
|
+
target_sha,
|
|
521
|
+
dry_run_github=dry_run_github,
|
|
522
|
+
dry_run_aws=dry_run_aws,
|
|
523
|
+
dry_run_docker=dry_run_docker,
|
|
524
|
+
)
|
|
745
525
|
|
|
746
|
-
if
|
|
747
|
-
|
|
748
|
-
if pr.current_show.needs_update(target_sha):
|
|
749
|
-
console.print(
|
|
750
|
-
f"🎪 Environment outdated ({pr.current_show.sha} → {target_sha[:7]}) - auto-syncing"
|
|
751
|
-
)
|
|
752
|
-
_handle_sync_trigger(pr_number, github, dry_run_aws, dry_run_github, aws_sleep)
|
|
753
|
-
else:
|
|
754
|
-
console.print(f"🎪 Environment is up to date ({pr.current_show.sha})")
|
|
526
|
+
if result.success:
|
|
527
|
+
p(f"🎪 ✅ Sync completed: {result.action_taken}")
|
|
755
528
|
else:
|
|
756
|
-
|
|
757
|
-
|
|
529
|
+
p(f"🎪 ❌ Sync failed: {result.error}")
|
|
530
|
+
raise typer.Exit(1)
|
|
758
531
|
|
|
532
|
+
except GitHubError as e:
|
|
533
|
+
p(f"❌ GitHub error: {e}")
|
|
534
|
+
raise typer.Exit(1) from e
|
|
759
535
|
except Exception as e:
|
|
760
|
-
|
|
536
|
+
p(f"❌ Error: {e}")
|
|
537
|
+
raise typer.Exit(1) from e
|
|
761
538
|
|
|
762
539
|
|
|
763
540
|
@app.command()
|
|
764
|
-
def handle_sync(pr_number: int):
|
|
541
|
+
def handle_sync(pr_number: int) -> None:
|
|
765
542
|
"""🎪 Handle new commit sync (called by GitHub Actions on PR synchronize)"""
|
|
766
543
|
try:
|
|
767
|
-
|
|
768
|
-
pr = PullRequest.from_id(pr_number, github)
|
|
544
|
+
pr = PullRequest.from_id(pr_number)
|
|
769
545
|
|
|
770
546
|
# Only sync if there's an active environment
|
|
771
547
|
if not pr.current_show:
|
|
772
|
-
|
|
548
|
+
p(f"🎪 No active environment for PR #{pr_number} - skipping sync")
|
|
773
549
|
return
|
|
774
550
|
|
|
775
551
|
# Get latest commit SHA
|
|
776
|
-
|
|
552
|
+
from .core.pull_request import get_github
|
|
553
|
+
|
|
554
|
+
latest_sha = get_github().get_latest_commit_sha(pr_number)
|
|
777
555
|
|
|
778
556
|
# Check if update is needed
|
|
779
557
|
if not pr.current_show.needs_update(latest_sha):
|
|
780
|
-
|
|
558
|
+
p(f"🎪 Environment already up to date for PR #{pr_number}")
|
|
781
559
|
return
|
|
782
560
|
|
|
783
|
-
|
|
561
|
+
p(f"🎪 Syncing PR #{pr_number} to commit {latest_sha[:7]}")
|
|
784
562
|
|
|
785
563
|
# TODO: Implement rolling update logic
|
|
786
|
-
|
|
564
|
+
p("🎪 [bold yellow]Sync logic not yet implemented[/bold yellow]")
|
|
787
565
|
|
|
788
566
|
except Exception as e:
|
|
789
|
-
|
|
567
|
+
p(f"🎪 [bold red]Error handling sync:[/bold red] {e}")
|
|
790
568
|
|
|
791
569
|
|
|
792
570
|
@app.command()
|
|
793
571
|
def setup_labels(
|
|
794
572
|
dry_run: bool = typer.Option(False, "--dry-run", help="Show what labels would be created"),
|
|
795
|
-
):
|
|
573
|
+
) -> None:
|
|
796
574
|
"""🎪 Set up GitHub label definitions with colors and descriptions"""
|
|
797
575
|
try:
|
|
798
576
|
from .core.label_colors import LABEL_DEFINITIONS
|
|
799
577
|
|
|
800
578
|
github = GitHubInterface()
|
|
801
579
|
|
|
802
|
-
|
|
580
|
+
p("🎪 [bold blue]Setting up circus tent label definitions...[/bold blue]")
|
|
803
581
|
|
|
804
582
|
created_count = 0
|
|
805
583
|
updated_count = 0
|
|
@@ -809,32 +587,32 @@ def setup_labels(
|
|
|
809
587
|
description = definition["description"]
|
|
810
588
|
|
|
811
589
|
if dry_run:
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
590
|
+
p(f"🏷️ Would create: [bold]{label_name}[/bold]")
|
|
591
|
+
p(f" Color: #{color}")
|
|
592
|
+
p(f" Description: {description}")
|
|
815
593
|
else:
|
|
816
594
|
try:
|
|
817
595
|
# Try to create or update the label
|
|
818
596
|
success = github.create_or_update_label(label_name, color, description)
|
|
819
597
|
if success:
|
|
820
598
|
created_count += 1
|
|
821
|
-
|
|
599
|
+
p(f"✅ Created: [bold]{label_name}[/bold]")
|
|
822
600
|
else:
|
|
823
601
|
updated_count += 1
|
|
824
|
-
|
|
602
|
+
p(f"🔄 Updated: [bold]{label_name}[/bold]")
|
|
825
603
|
except Exception as e:
|
|
826
|
-
|
|
604
|
+
p(f"❌ Failed to create {label_name}: {e}")
|
|
827
605
|
|
|
828
606
|
if not dry_run:
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
607
|
+
p("\n🎪 [bold green]Label setup complete![/bold green]")
|
|
608
|
+
p(f" 📊 Created: {created_count}")
|
|
609
|
+
p(f" 🔄 Updated: {updated_count}")
|
|
610
|
+
p(
|
|
833
611
|
"\n🎪 [dim]Note: Dynamic labels (with SHA) are created automatically during deployment[/dim]"
|
|
834
612
|
)
|
|
835
613
|
|
|
836
614
|
except Exception as e:
|
|
837
|
-
|
|
615
|
+
p(f"🎪 [bold red]Error setting up labels:[/bold red] {e}")
|
|
838
616
|
|
|
839
617
|
|
|
840
618
|
@app.command()
|
|
@@ -854,706 +632,45 @@ def cleanup(
|
|
|
854
632
|
"--cleanup-labels/--no-cleanup-labels",
|
|
855
633
|
help="Also cleanup SHA-based label definitions from repository",
|
|
856
634
|
),
|
|
857
|
-
):
|
|
635
|
+
) -> None:
|
|
858
636
|
"""🎪 Clean up orphaned or expired environments and labels"""
|
|
859
637
|
try:
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
# Step 1: Clean up expired AWS ECS services
|
|
863
|
-
console.print("🎪 [bold blue]Checking AWS ECS services for cleanup...[/bold blue]")
|
|
864
|
-
|
|
865
|
-
from .core.aws import AWSInterface
|
|
866
|
-
|
|
867
|
-
aws = AWSInterface()
|
|
868
|
-
|
|
869
|
-
try:
|
|
870
|
-
expired_services = aws.find_expired_services(older_than)
|
|
871
|
-
|
|
872
|
-
if expired_services:
|
|
873
|
-
console.print(f"🎪 Found {len(expired_services)} expired ECS services")
|
|
874
|
-
|
|
875
|
-
for service_info in expired_services:
|
|
876
|
-
service_name = service_info["service_name"]
|
|
877
|
-
pr_number = service_info["pr_number"]
|
|
878
|
-
age_hours = service_info["age_hours"]
|
|
879
|
-
|
|
880
|
-
if dry_run:
|
|
881
|
-
console.print(
|
|
882
|
-
f"🎪 [yellow]Would delete service {service_name} (PR #{pr_number}, {age_hours:.1f}h old)[/yellow]"
|
|
883
|
-
)
|
|
884
|
-
console.print(
|
|
885
|
-
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]"
|
|
886
|
-
)
|
|
887
|
-
else:
|
|
888
|
-
console.print(
|
|
889
|
-
f"🎪 Deleting expired service {service_name} (PR #{pr_number}, {age_hours:.1f}h old)"
|
|
890
|
-
)
|
|
891
|
-
# Create minimal Show object for URL generation
|
|
892
|
-
from .core.circus import Show
|
|
893
|
-
|
|
894
|
-
temp_show = Show(
|
|
895
|
-
pr_number=pr_number, sha=service_name.split("-")[2], status="cleanup"
|
|
896
|
-
)
|
|
897
|
-
_show_service_urls(temp_show, "cleanup")
|
|
898
|
-
|
|
899
|
-
# Delete ECS service
|
|
900
|
-
if aws._delete_ecs_service(service_name):
|
|
901
|
-
# Delete ECR image
|
|
902
|
-
image_tag = f"pr-{pr_number}-ci"
|
|
903
|
-
aws._delete_ecr_image(image_tag)
|
|
904
|
-
console.print(f"🎪 ✅ Cleaned up {service_name}")
|
|
905
|
-
else:
|
|
906
|
-
console.print(f"🎪 ❌ Failed to clean up {service_name}")
|
|
907
|
-
else:
|
|
908
|
-
console.print("🎪 [dim]No expired ECS services found[/dim]")
|
|
909
|
-
|
|
910
|
-
except Exception as e:
|
|
911
|
-
console.print(f"🎪 [bold red]AWS cleanup failed:[/bold red] {e}")
|
|
912
|
-
|
|
913
|
-
# Step 2: Find and clean up expired environments from PRs
|
|
914
|
-
if respect_ttl:
|
|
915
|
-
console.print("🎪 Finding environments expired based on individual TTL labels")
|
|
916
|
-
else:
|
|
917
|
-
console.print(f"🎪 Finding environments older than {older_than}")
|
|
918
|
-
prs_with_shows = github.find_prs_with_shows()
|
|
919
|
-
|
|
920
|
-
if not prs_with_shows:
|
|
921
|
-
console.print("🎪 [dim]No PRs with circus tent labels found[/dim]")
|
|
922
|
-
else:
|
|
923
|
-
console.print(f"🎪 Found {len(prs_with_shows)} PRs with shows")
|
|
924
|
-
|
|
925
|
-
import re
|
|
926
|
-
from datetime import datetime, timedelta
|
|
927
|
-
|
|
928
|
-
from .core.circus import PullRequest, get_effective_ttl, parse_ttl_days
|
|
929
|
-
|
|
930
|
-
# Parse max_age if provided (safety ceiling)
|
|
931
|
-
max_age_days = None
|
|
932
|
-
if max_age:
|
|
933
|
-
max_age_days = parse_ttl_days(max_age)
|
|
934
|
-
|
|
935
|
-
cleaned_prs = 0
|
|
936
|
-
for pr_number in prs_with_shows:
|
|
937
|
-
try:
|
|
938
|
-
pr = PullRequest.from_id(pr_number, github)
|
|
939
|
-
expired_shows = []
|
|
940
|
-
|
|
941
|
-
if respect_ttl:
|
|
942
|
-
# Use individual TTL labels
|
|
943
|
-
effective_ttl_days = get_effective_ttl(pr)
|
|
944
|
-
|
|
945
|
-
if effective_ttl_days is None:
|
|
946
|
-
# "never" label found - skip cleanup
|
|
947
|
-
console.print(
|
|
948
|
-
f"🎪 [blue]PR #{pr_number} marked as 'never expire' - skipping[/blue]"
|
|
949
|
-
)
|
|
950
|
-
continue
|
|
951
|
-
|
|
952
|
-
# Apply max_age ceiling if specified
|
|
953
|
-
if max_age_days and effective_ttl_days > max_age_days:
|
|
954
|
-
console.print(
|
|
955
|
-
f"🎪 [yellow]PR #{pr_number} TTL ({effective_ttl_days}d) exceeds max-age ({max_age_days}d)[/yellow]"
|
|
956
|
-
)
|
|
957
|
-
effective_ttl_days = max_age_days
|
|
958
|
-
|
|
959
|
-
cutoff_time = datetime.now() - timedelta(days=effective_ttl_days)
|
|
960
|
-
console.print(
|
|
961
|
-
f"🎪 PR #{pr_number} effective TTL: {effective_ttl_days} days"
|
|
962
|
-
)
|
|
963
|
-
|
|
964
|
-
else:
|
|
965
|
-
# Use global older_than parameter (current behavior)
|
|
966
|
-
time_match = re.match(r"(\d+)([hd])", older_than)
|
|
967
|
-
if not time_match:
|
|
968
|
-
console.print(
|
|
969
|
-
f"🎪 [bold red]Invalid time format:[/bold red] {older_than}"
|
|
970
|
-
)
|
|
971
|
-
return
|
|
972
|
-
|
|
973
|
-
hours = int(time_match.group(1))
|
|
974
|
-
if time_match.group(2) == "d":
|
|
975
|
-
hours *= 24
|
|
976
|
-
|
|
977
|
-
cutoff_time = datetime.now() - timedelta(hours=hours)
|
|
978
|
-
|
|
979
|
-
# Check all shows in the PR for expiration
|
|
980
|
-
for show in pr.shows:
|
|
981
|
-
if show.created_at:
|
|
982
|
-
try:
|
|
983
|
-
# Parse timestamp (format: 2024-01-15T14-30)
|
|
984
|
-
show_time = datetime.fromisoformat(
|
|
985
|
-
show.created_at.replace("-", ":")
|
|
986
|
-
)
|
|
987
|
-
if show_time < cutoff_time:
|
|
988
|
-
expired_shows.append(show)
|
|
989
|
-
except (ValueError, AttributeError):
|
|
990
|
-
# If we can't parse the timestamp, consider it expired
|
|
991
|
-
expired_shows.append(show)
|
|
992
|
-
|
|
993
|
-
if expired_shows:
|
|
994
|
-
if dry_run:
|
|
995
|
-
console.print(
|
|
996
|
-
f"🎪 [yellow]Would clean {len(expired_shows)} expired shows from PR #{pr_number}[/yellow]"
|
|
997
|
-
)
|
|
998
|
-
for show in expired_shows:
|
|
999
|
-
console.print(f" - SHA {show.sha} ({show.status})")
|
|
1000
|
-
else:
|
|
1001
|
-
console.print(
|
|
1002
|
-
f"🎪 Cleaning {len(expired_shows)} expired shows from PR #{pr_number}"
|
|
1003
|
-
)
|
|
1004
|
-
|
|
1005
|
-
# Remove circus labels for expired shows
|
|
1006
|
-
current_labels = github.get_circus_labels(pr_number)
|
|
1007
|
-
labels_to_keep = []
|
|
1008
|
-
|
|
1009
|
-
for label in current_labels:
|
|
1010
|
-
# Keep labels that don't belong to expired shows
|
|
1011
|
-
should_keep = True
|
|
1012
|
-
for expired_show in expired_shows:
|
|
1013
|
-
if expired_show.sha in label:
|
|
1014
|
-
should_keep = False
|
|
1015
|
-
break
|
|
1016
|
-
if should_keep:
|
|
1017
|
-
labels_to_keep.append(label)
|
|
1018
|
-
|
|
1019
|
-
# Update PR labels
|
|
1020
|
-
github.remove_circus_labels(pr_number)
|
|
1021
|
-
for label in labels_to_keep:
|
|
1022
|
-
github.add_label(pr_number, label)
|
|
1023
|
-
|
|
1024
|
-
cleaned_prs += 1
|
|
1025
|
-
|
|
1026
|
-
except Exception as e:
|
|
1027
|
-
console.print(f"🎪 [red]Error processing PR #{pr_number}:[/red] {e}")
|
|
1028
|
-
|
|
1029
|
-
if not dry_run and cleaned_prs > 0:
|
|
1030
|
-
console.print(f"🎪 [green]Cleaned up environments from {cleaned_prs} PRs[/green]")
|
|
1031
|
-
|
|
1032
|
-
# Step 2: Clean up SHA-based label definitions from repository
|
|
1033
|
-
if cleanup_labels:
|
|
1034
|
-
console.print("🎪 Finding SHA-based labels in repository")
|
|
1035
|
-
sha_labels = github.cleanup_sha_labels(dry_run=dry_run)
|
|
1036
|
-
|
|
1037
|
-
if sha_labels:
|
|
1038
|
-
if dry_run:
|
|
1039
|
-
console.print(
|
|
1040
|
-
f"🎪 [yellow]Would delete {len(sha_labels)} SHA-based label definitions:[/yellow]"
|
|
1041
|
-
)
|
|
1042
|
-
for label in sha_labels[:10]: # Show first 10
|
|
1043
|
-
console.print(f" - {label}")
|
|
1044
|
-
if len(sha_labels) > 10:
|
|
1045
|
-
console.print(f" ... and {len(sha_labels) - 10} more")
|
|
1046
|
-
else:
|
|
1047
|
-
console.print(
|
|
1048
|
-
f"🎪 [green]Deleted {len(sha_labels)} SHA-based label definitions[/green]"
|
|
1049
|
-
)
|
|
1050
|
-
else:
|
|
1051
|
-
console.print("🎪 [dim]No SHA-based labels found to clean[/dim]")
|
|
1052
|
-
|
|
1053
|
-
except Exception as e:
|
|
1054
|
-
console.print(f"🎪 [bold red]Error during cleanup:[/bold red] {e}")
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
# Helper functions for trigger processing
|
|
1058
|
-
def _handle_start_trigger(
|
|
1059
|
-
pr_number: int,
|
|
1060
|
-
github: GitHubInterface,
|
|
1061
|
-
dry_run_aws: bool = False,
|
|
1062
|
-
dry_run_github: bool = False,
|
|
1063
|
-
aws_sleep: int = 0,
|
|
1064
|
-
docker_tag_override: Optional[str] = None,
|
|
1065
|
-
force: bool = False,
|
|
1066
|
-
):
|
|
1067
|
-
"""Handle start trigger"""
|
|
1068
|
-
import time
|
|
1069
|
-
from datetime import datetime
|
|
1070
|
-
|
|
1071
|
-
console.print(f"🎪 Starting environment for PR #{pr_number}")
|
|
1072
|
-
|
|
1073
|
-
try:
|
|
1074
|
-
# Get latest SHA and GitHub actor
|
|
1075
|
-
latest_sha = github.get_latest_commit_sha(pr_number)
|
|
1076
|
-
github_actor = _get_github_actor()
|
|
1077
|
-
|
|
1078
|
-
# Create new show
|
|
1079
|
-
show = Show(
|
|
1080
|
-
pr_number=pr_number,
|
|
1081
|
-
sha=short_sha(latest_sha),
|
|
1082
|
-
status="building",
|
|
1083
|
-
created_at=datetime.utcnow().strftime("%Y-%m-%dT%H-%M"),
|
|
1084
|
-
ttl="24h",
|
|
1085
|
-
requested_by=github_actor,
|
|
1086
|
-
)
|
|
1087
|
-
|
|
1088
|
-
console.print(f"🎪 Creating environment {show.aws_service_name}")
|
|
1089
|
-
|
|
1090
|
-
# Post confirmation comment
|
|
1091
|
-
confirmation_comment = start_comment(show)
|
|
1092
|
-
|
|
1093
|
-
if not dry_run_github:
|
|
1094
|
-
github.post_comment(pr_number, confirmation_comment)
|
|
1095
|
-
|
|
1096
|
-
# Set building state labels
|
|
1097
|
-
building_labels = show.to_circus_labels()
|
|
1098
|
-
console.print("🎪 Setting building state labels:")
|
|
1099
|
-
for label in building_labels:
|
|
1100
|
-
console.print(f" + {label}")
|
|
1101
|
-
|
|
1102
|
-
# Set building labels
|
|
1103
|
-
if not dry_run_github:
|
|
1104
|
-
# Actually set the labels for real testing
|
|
1105
|
-
console.print("🎪 Setting labels on GitHub...")
|
|
1106
|
-
# Remove existing circus labels first
|
|
1107
|
-
github.remove_circus_labels(pr_number)
|
|
1108
|
-
# Add new labels one by one
|
|
1109
|
-
for label in building_labels:
|
|
1110
|
-
github.add_label(pr_number, label)
|
|
1111
|
-
console.print("🎪 ✅ Labels set on GitHub!")
|
|
1112
|
-
else:
|
|
1113
|
-
console.print("🎪 [bold yellow]DRY-RUN-GITHUB[/bold yellow] - Would set labels")
|
|
1114
|
-
|
|
1115
|
-
if dry_run_aws:
|
|
1116
|
-
console.print("🎪 [bold yellow]DRY-RUN-AWS[/bold yellow] - Skipping AWS operations")
|
|
1117
|
-
if aws_sleep > 0:
|
|
1118
|
-
console.print(f"🎪 Sleeping {aws_sleep}s to simulate AWS build time...")
|
|
1119
|
-
time.sleep(aws_sleep)
|
|
1120
|
-
|
|
1121
|
-
# Mock successful deployment
|
|
1122
|
-
mock_ip = "52.1.2.3"
|
|
1123
|
-
console.print(
|
|
1124
|
-
f"🎪 [bold green]Mock AWS deployment successful![/bold green] IP: {mock_ip}"
|
|
1125
|
-
)
|
|
1126
|
-
|
|
1127
|
-
# Update to running state
|
|
1128
|
-
show.status = "running"
|
|
1129
|
-
show.ip = mock_ip
|
|
1130
|
-
|
|
1131
|
-
running_labels = show.to_circus_labels()
|
|
1132
|
-
console.print("🎪 Setting running state labels:")
|
|
1133
|
-
for label in running_labels:
|
|
1134
|
-
console.print(f" + {label}")
|
|
1135
|
-
|
|
1136
|
-
# Set running labels
|
|
1137
|
-
if not dry_run_github:
|
|
1138
|
-
console.print("🎪 Updating to running state...")
|
|
1139
|
-
# Remove existing circus labels first
|
|
1140
|
-
github.remove_circus_labels(pr_number)
|
|
1141
|
-
# Add new running labels
|
|
1142
|
-
for label in running_labels:
|
|
1143
|
-
github.add_label(pr_number, label)
|
|
1144
|
-
console.print("🎪 ✅ Labels updated to running state!")
|
|
1145
|
-
else:
|
|
1146
|
-
console.print(
|
|
1147
|
-
"🎪 [bold yellow]DRY-RUN-GITHUB[/bold yellow] - Would update to running state"
|
|
1148
|
-
)
|
|
1149
|
-
|
|
1150
|
-
# Post success comment (only in dry-run-aws mode since we have mock IP)
|
|
1151
|
-
# Create mock show with IP for success comment
|
|
1152
|
-
mock_show = Show(
|
|
1153
|
-
pr_number=show.pr_number,
|
|
1154
|
-
sha=show.sha,
|
|
1155
|
-
status="running",
|
|
1156
|
-
ip=mock_ip,
|
|
1157
|
-
ttl=show.ttl,
|
|
1158
|
-
requested_by=show.requested_by,
|
|
1159
|
-
)
|
|
1160
|
-
success_comment_text = success_comment(mock_show)
|
|
1161
|
-
|
|
1162
|
-
if not dry_run_github:
|
|
1163
|
-
github.post_comment(pr_number, success_comment_text)
|
|
1164
|
-
|
|
1165
|
-
else:
|
|
1166
|
-
# Real AWS operations
|
|
1167
|
-
from .core.aws import AWSInterface, EnvironmentResult
|
|
1168
|
-
|
|
1169
|
-
console.print("🎪 [bold blue]Starting AWS deployment...[/bold blue]")
|
|
1170
|
-
aws = AWSInterface()
|
|
1171
|
-
|
|
1172
|
-
# Show logs URL immediately for monitoring
|
|
1173
|
-
_show_service_urls(show, "deployment")
|
|
1174
|
-
|
|
1175
|
-
# Parse feature flags from PR description (replicate GHA feature flag logic)
|
|
1176
|
-
feature_flags = _extract_feature_flags_from_pr(pr_number, github)
|
|
1177
|
-
|
|
1178
|
-
# Create environment (synchronous, matches GHA wait behavior)
|
|
1179
|
-
result: EnvironmentResult = aws.create_environment(
|
|
1180
|
-
pr_number=pr_number,
|
|
1181
|
-
sha=latest_sha,
|
|
1182
|
-
github_user=github_actor,
|
|
1183
|
-
feature_flags=feature_flags,
|
|
1184
|
-
image_tag_override=docker_tag_override,
|
|
1185
|
-
force=force,
|
|
1186
|
-
)
|
|
1187
|
-
|
|
1188
|
-
if result.success:
|
|
1189
|
-
console.print(
|
|
1190
|
-
f"🎪 [bold green]✅ Green service deployed successfully![/bold green] IP: {result.ip}"
|
|
1191
|
-
)
|
|
1192
|
-
|
|
1193
|
-
# Show helpful links for the new service
|
|
1194
|
-
console.print("\n🎪 [bold blue]Useful Links:[/bold blue]")
|
|
1195
|
-
console.print(f" 🌐 Environment: http://{result.ip}:8080")
|
|
1196
|
-
|
|
1197
|
-
# Use centralized URL generation
|
|
1198
|
-
urls = _get_service_urls(show)
|
|
1199
|
-
console.print(f" 📊 ECS Service: {urls['service']}")
|
|
1200
|
-
console.print(f" 📝 Service Logs: {urls['logs']}")
|
|
1201
|
-
console.print(f" 🏥 Health Checks: {urls['health']}")
|
|
1202
|
-
console.print(
|
|
1203
|
-
f" 🔍 GitHub PR: https://github.com/apache/superset/pull/{pr_number}"
|
|
1204
|
-
)
|
|
1205
|
-
console.print(
|
|
1206
|
-
"\n🎪 [dim]Note: Superset takes 2-3 minutes to initialize after container starts[/dim]"
|
|
1207
|
-
)
|
|
1208
|
-
|
|
1209
|
-
# Blue-Green Traffic Switch: Update GitHub labels to point to new service
|
|
1210
|
-
console.print(
|
|
1211
|
-
f"\n🎪 [bold blue]Switching traffic to green service {latest_sha[:7]}...[/bold blue]"
|
|
1212
|
-
)
|
|
1213
|
-
|
|
1214
|
-
# Check for existing services to show blue-green transition
|
|
1215
|
-
from .core.aws import AWSInterface
|
|
1216
|
-
|
|
1217
|
-
aws = AWSInterface()
|
|
1218
|
-
existing_services = aws._find_pr_services(pr_number)
|
|
1219
|
-
|
|
1220
|
-
if len(existing_services) > 1:
|
|
1221
|
-
console.print("🔄 Blue-Green Deployment:")
|
|
1222
|
-
blue_services = []
|
|
1223
|
-
for svc in existing_services:
|
|
1224
|
-
if svc["sha"] == latest_sha[:7]:
|
|
1225
|
-
console.print(
|
|
1226
|
-
f" 🟢 Green: {svc['service_name']} (NEW - receiving traffic)"
|
|
1227
|
-
)
|
|
1228
|
-
else:
|
|
1229
|
-
console.print(
|
|
1230
|
-
f" 🔵 Blue: {svc['service_name']} (OLD - will be cleaned up in 5 minutes)"
|
|
1231
|
-
)
|
|
1232
|
-
blue_services.append(svc)
|
|
1233
|
-
|
|
1234
|
-
# Schedule cleanup of blue services
|
|
1235
|
-
if blue_services:
|
|
1236
|
-
console.print(
|
|
1237
|
-
f"\n🧹 Scheduling cleanup of {len(blue_services)} blue service(s) in 5 minutes..."
|
|
1238
|
-
)
|
|
1239
|
-
_schedule_blue_cleanup(pr_number, blue_services)
|
|
1240
|
-
|
|
1241
|
-
# Update to running state with new SHA
|
|
1242
|
-
show.status = "running"
|
|
1243
|
-
show.ip = result.ip
|
|
1244
|
-
|
|
1245
|
-
# Traffic switching happens here - update GitHub labels atomically
|
|
1246
|
-
running_labels = show.to_circus_labels()
|
|
1247
|
-
console.print("\n🎪 Setting running state labels (traffic switch):")
|
|
1248
|
-
for label in running_labels:
|
|
1249
|
-
console.print(f" + {label}")
|
|
1250
|
-
|
|
1251
|
-
if not dry_run_github:
|
|
1252
|
-
console.print("🎪 Executing traffic switch via GitHub labels...")
|
|
1253
|
-
# Remove existing circus labels first
|
|
1254
|
-
github.remove_circus_labels(pr_number)
|
|
1255
|
-
# Add new running labels - this switches traffic atomically
|
|
1256
|
-
for label in running_labels:
|
|
1257
|
-
github.add_label(pr_number, label)
|
|
1258
|
-
console.print("🎪 ✅ Labels updated to running state!")
|
|
1259
|
-
|
|
1260
|
-
# Post success comment with real IP
|
|
1261
|
-
# Update show with real IP for comment
|
|
1262
|
-
show.ip = result.ip
|
|
1263
|
-
show.status = "running"
|
|
1264
|
-
success_comment_text = success_comment(show, feature_count=len(feature_flags))
|
|
1265
|
-
|
|
1266
|
-
github.post_comment(pr_number, success_comment_text)
|
|
1267
|
-
|
|
1268
|
-
else:
|
|
1269
|
-
console.print(f"🎪 [bold red]❌ AWS deployment failed:[/bold red] {result.error}")
|
|
1270
|
-
|
|
1271
|
-
# Update to failed state
|
|
1272
|
-
show.status = "failed"
|
|
1273
|
-
failed_labels = show.to_circus_labels()
|
|
1274
|
-
|
|
1275
|
-
if not dry_run_github:
|
|
1276
|
-
console.print("🎪 Setting failed state labels...")
|
|
1277
|
-
github.remove_circus_labels(pr_number)
|
|
1278
|
-
for label in failed_labels:
|
|
1279
|
-
github.add_label(pr_number, label)
|
|
1280
|
-
|
|
1281
|
-
# Post failure comment
|
|
1282
|
-
failure_comment = f"""🎪 @{github_actor} Environment creation failed.
|
|
1283
|
-
|
|
1284
|
-
**Error:** {result.error}
|
|
1285
|
-
**Environment:** `{show.sha}`
|
|
1286
|
-
|
|
1287
|
-
Please check the logs and try again.
|
|
1288
|
-
|
|
1289
|
-
{_get_showtime_footer()}"""
|
|
1290
|
-
|
|
1291
|
-
github.post_comment(pr_number, failure_comment)
|
|
1292
|
-
|
|
1293
|
-
except Exception as e:
|
|
1294
|
-
console.print(f"🎪 [bold red]Start trigger failed:[/bold red] {e}")
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
def _extract_feature_flags_from_pr(pr_number: int, github: GitHubInterface) -> list:
|
|
1298
|
-
"""Extract feature flags from PR description (replicate GHA eval-feature-flags step)"""
|
|
1299
|
-
import re
|
|
1300
|
-
|
|
1301
|
-
try:
|
|
1302
|
-
# Get PR description
|
|
1303
|
-
pr_data = github.get_pr_data(pr_number)
|
|
1304
|
-
description = pr_data.get("body") or ""
|
|
1305
|
-
|
|
1306
|
-
# Replicate exact GHA regex pattern: FEATURE_(\w+)=(\w+)
|
|
1307
|
-
pattern = r"FEATURE_(\w+)=(\w+)"
|
|
1308
|
-
results = []
|
|
1309
|
-
|
|
1310
|
-
for match in re.finditer(pattern, description):
|
|
1311
|
-
feature_config = {"name": f"SUPERSET_FEATURE_{match.group(1)}", "value": match.group(2)}
|
|
1312
|
-
results.append(feature_config)
|
|
1313
|
-
console.print(
|
|
1314
|
-
f"🎪 Found feature flag: {feature_config['name']}={feature_config['value']}"
|
|
1315
|
-
)
|
|
1316
|
-
|
|
1317
|
-
return results
|
|
1318
|
-
|
|
1319
|
-
except Exception as e:
|
|
1320
|
-
console.print(f"🎪 Warning: Could not extract feature flags: {e}")
|
|
1321
|
-
return []
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
def _handle_stop_trigger(
|
|
1325
|
-
pr_number: int, github: GitHubInterface, dry_run_aws: bool = False, dry_run_github: bool = False
|
|
1326
|
-
):
|
|
1327
|
-
"""Handle stop trigger"""
|
|
1328
|
-
|
|
1329
|
-
console.print(f"🎪 Stopping environment for PR #{pr_number}")
|
|
1330
|
-
|
|
1331
|
-
try:
|
|
1332
|
-
pr = PullRequest.from_id(pr_number, github)
|
|
638
|
+
# Parse older_than to hours
|
|
639
|
+
import re
|
|
1333
640
|
|
|
1334
|
-
|
|
1335
|
-
|
|
641
|
+
time_match = re.match(r"(\d+)([hd])", older_than)
|
|
642
|
+
if not time_match:
|
|
643
|
+
p(f"❌ Invalid time format: {older_than}")
|
|
1336
644
|
return
|
|
1337
645
|
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
if dry_run_aws:
|
|
1342
|
-
console.print("🎪 [bold yellow]DRY-RUN-AWS[/bold yellow] - Would delete AWS resources")
|
|
1343
|
-
console.print(f" - ECS service: {show.aws_service_name}")
|
|
1344
|
-
console.print(f" - ECR image: {show.aws_image_tag}")
|
|
1345
|
-
else:
|
|
1346
|
-
# Real AWS cleanup (replicate ephemeral-env-pr-close.yml logic)
|
|
1347
|
-
from .core.aws import AWSInterface
|
|
1348
|
-
|
|
1349
|
-
console.print("🎪 [bold blue]Starting AWS cleanup...[/bold blue]")
|
|
1350
|
-
aws = AWSInterface()
|
|
1351
|
-
|
|
1352
|
-
try:
|
|
1353
|
-
# Show logs URL for monitoring cleanup
|
|
1354
|
-
_show_service_urls(show, "cleanup")
|
|
1355
|
-
|
|
1356
|
-
# Step 1: Check if ECS service exists and is active (replicate GHA describe-services)
|
|
1357
|
-
service_name = show.ecs_service_name
|
|
1358
|
-
console.print(f"🎪 Checking ECS service: {service_name}")
|
|
1359
|
-
|
|
1360
|
-
service_exists = aws._service_exists(service_name)
|
|
646
|
+
max_age_hours = int(time_match.group(1))
|
|
647
|
+
if time_match.group(2) == "d":
|
|
648
|
+
max_age_hours *= 24
|
|
1361
649
|
|
|
1362
|
-
|
|
1363
|
-
console.print(f"🎪 Found active ECS service: {service_name}")
|
|
650
|
+
p(f"🎪 [bold blue]Cleaning environments older than {max_age_hours}h...[/bold blue]")
|
|
1364
651
|
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
if success:
|
|
1370
|
-
console.print("🎪 ✅ ECS service deleted successfully")
|
|
1371
|
-
|
|
1372
|
-
# Step 3: Delete ECR image tag (replicate GHA batch-delete-image)
|
|
1373
|
-
image_tag = f"pr-{pr_number}-ci" # Match GHA image tag format
|
|
1374
|
-
console.print(f"🎪 Deleting ECR image tag: {image_tag}")
|
|
1375
|
-
|
|
1376
|
-
ecr_success = aws._delete_ecr_image(image_tag)
|
|
1377
|
-
|
|
1378
|
-
if ecr_success:
|
|
1379
|
-
console.print("🎪 ✅ ECR image deleted successfully")
|
|
1380
|
-
else:
|
|
1381
|
-
console.print("🎪 ⚠️ ECR image deletion failed (may not exist)")
|
|
1382
|
-
|
|
1383
|
-
console.print(
|
|
1384
|
-
"🎪 [bold green]✅ AWS cleanup completed successfully![/bold green]"
|
|
1385
|
-
)
|
|
1386
|
-
|
|
1387
|
-
else:
|
|
1388
|
-
console.print("🎪 [bold red]❌ ECS service deletion failed[/bold red]")
|
|
1389
|
-
|
|
1390
|
-
else:
|
|
1391
|
-
console.print(f"🎪 No active ECS service found: {service_name}")
|
|
1392
|
-
console.print("🎪 ✅ No AWS resources to clean up")
|
|
1393
|
-
|
|
1394
|
-
except Exception as e:
|
|
1395
|
-
console.print(f"🎪 [bold red]❌ AWS cleanup failed:[/bold red] {e}")
|
|
1396
|
-
|
|
1397
|
-
# Remove all circus labels for this PR
|
|
1398
|
-
console.print(f"🎪 Removing all circus labels for PR #{pr_number}")
|
|
1399
|
-
if not dry_run_github:
|
|
1400
|
-
github.remove_circus_labels(pr_number)
|
|
1401
|
-
|
|
1402
|
-
# Post cleanup comment
|
|
1403
|
-
github_actor = _get_github_actor()
|
|
1404
|
-
cleanup_comment = f"""🎪 @{github_actor} Environment `{show.sha}` cleaned up
|
|
1405
|
-
|
|
1406
|
-
**AWS Resources:** ECS service and ECR image deleted
|
|
1407
|
-
**Cost Impact:** No further charges
|
|
1408
|
-
|
|
1409
|
-
Add `🎪 trigger-start` to create a new environment.
|
|
1410
|
-
|
|
1411
|
-
{_get_showtime_footer()}"""
|
|
1412
|
-
|
|
1413
|
-
if not dry_run_github:
|
|
1414
|
-
github.post_comment(pr_number, cleanup_comment)
|
|
1415
|
-
|
|
1416
|
-
console.print("🎪 [bold green]Environment stopped![/bold green]")
|
|
1417
|
-
|
|
1418
|
-
except Exception as e:
|
|
1419
|
-
console.print(f"🎪 [bold red]Stop trigger failed:[/bold red] {e}")
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
def _handle_sync_trigger(
|
|
1423
|
-
pr_number: int,
|
|
1424
|
-
github: GitHubInterface,
|
|
1425
|
-
dry_run_aws: bool = False,
|
|
1426
|
-
dry_run_github: bool = False,
|
|
1427
|
-
aws_sleep: int = 0,
|
|
1428
|
-
):
|
|
1429
|
-
"""Handle sync trigger"""
|
|
1430
|
-
import time
|
|
1431
|
-
from datetime import datetime
|
|
1432
|
-
|
|
1433
|
-
console.print(f"🎪 Syncing environment for PR #{pr_number}")
|
|
1434
|
-
|
|
1435
|
-
try:
|
|
1436
|
-
pr = PullRequest.from_id(pr_number, github)
|
|
1437
|
-
|
|
1438
|
-
if not pr.current_show:
|
|
1439
|
-
console.print(f"🎪 No active environment for PR #{pr_number}")
|
|
1440
|
-
return
|
|
1441
|
-
|
|
1442
|
-
latest_sha = github.get_latest_commit_sha(pr_number)
|
|
1443
|
-
|
|
1444
|
-
if not pr.current_show.needs_update(latest_sha):
|
|
1445
|
-
console.print(f"🎪 Environment already up to date: {pr.current_show.sha}")
|
|
652
|
+
# Get all PRs with environments
|
|
653
|
+
pr_numbers = PullRequest.find_all_with_environments()
|
|
654
|
+
if not pr_numbers:
|
|
655
|
+
p("🎪 No environments found to clean")
|
|
1446
656
|
return
|
|
1447
657
|
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
sha=latest_sha[:7],
|
|
1454
|
-
status="building",
|
|
1455
|
-
created_at=datetime.utcnow().strftime("%Y-%m-%dT%H-%M"),
|
|
1456
|
-
ttl=pr.current_show.ttl,
|
|
1457
|
-
requested_by=pr.current_show.requested_by,
|
|
1458
|
-
)
|
|
1459
|
-
|
|
1460
|
-
console.print(f"🎪 Building new environment: {new_show.aws_service_name}")
|
|
1461
|
-
|
|
1462
|
-
if dry_run_aws:
|
|
1463
|
-
console.print("🎪 [bold yellow]DRY-RUN-AWS[/bold yellow] - Mocking rolling update")
|
|
1464
|
-
if aws_sleep > 0:
|
|
1465
|
-
console.print(f"🎪 Sleeping {aws_sleep}s to simulate build + deploy...")
|
|
1466
|
-
time.sleep(aws_sleep)
|
|
1467
|
-
|
|
1468
|
-
# Mock successful update
|
|
1469
|
-
new_show.status = "running"
|
|
1470
|
-
new_show.ip = "52.4.5.6" # New mock IP
|
|
1471
|
-
|
|
1472
|
-
console.print("🎪 [bold green]Mock rolling update complete![/bold green]")
|
|
1473
|
-
console.print(f"🎪 Traffic switched to {new_show.sha} at {new_show.ip}")
|
|
1474
|
-
|
|
1475
|
-
# Post rolling update success comment
|
|
1476
|
-
update_comment = f"""🎪 Environment updated: {pr.current_show.sha} → `{new_show.sha}`
|
|
1477
|
-
|
|
1478
|
-
**New Environment:** http://{new_show.ip}:8080
|
|
1479
|
-
**Update:** Zero-downtime rolling deployment
|
|
1480
|
-
**Old Environment:** Automatically cleaned up
|
|
1481
|
-
|
|
1482
|
-
Your latest changes are now live.
|
|
1483
|
-
|
|
1484
|
-
{_get_showtime_footer()}"""
|
|
1485
|
-
|
|
1486
|
-
if not dry_run_github:
|
|
1487
|
-
github.post_comment(pr_number, update_comment)
|
|
658
|
+
cleaned_count = 0
|
|
659
|
+
for pr_number in pr_numbers:
|
|
660
|
+
pr = PullRequest.from_id(pr_number)
|
|
661
|
+
if pr.stop_if_expired(max_age_hours, dry_run):
|
|
662
|
+
cleaned_count += 1
|
|
1488
663
|
|
|
664
|
+
if cleaned_count > 0:
|
|
665
|
+
p(f"🎪 ✅ Cleaned up {cleaned_count} expired environments")
|
|
1489
666
|
else:
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
from .core.aws import AWSInterface, EnvironmentResult
|
|
1493
|
-
|
|
1494
|
-
console.print("🎪 [bold blue]Starting real rolling update...[/bold blue]")
|
|
1495
|
-
|
|
1496
|
-
# Post rolling update start comment
|
|
1497
|
-
start_comment_text = rolling_start_comment(pr.current_show, latest_sha)
|
|
1498
|
-
|
|
1499
|
-
if not dry_run_github:
|
|
1500
|
-
github.post_comment(pr_number, start_comment_text)
|
|
1501
|
-
|
|
1502
|
-
aws = AWSInterface()
|
|
1503
|
-
|
|
1504
|
-
# Get feature flags from PR description
|
|
1505
|
-
feature_flags = _extract_feature_flags_from_pr(pr_number, github)
|
|
1506
|
-
github_actor = _get_github_actor()
|
|
1507
|
-
|
|
1508
|
-
# Use blue-green deployment (create_environment handles existing services)
|
|
1509
|
-
result: EnvironmentResult = aws.create_environment(
|
|
1510
|
-
pr_number=pr_number,
|
|
1511
|
-
sha=latest_sha,
|
|
1512
|
-
github_user=github_actor,
|
|
1513
|
-
feature_flags=feature_flags,
|
|
1514
|
-
force=False, # Don't force - let blue-green handle it
|
|
1515
|
-
)
|
|
1516
|
-
|
|
1517
|
-
if result.success:
|
|
1518
|
-
console.print(
|
|
1519
|
-
f"🎪 [bold green]✅ Rolling update complete![/bold green] New IP: {result.ip}"
|
|
1520
|
-
)
|
|
1521
|
-
|
|
1522
|
-
# Update labels to point to new service
|
|
1523
|
-
pr.refresh_labels(github)
|
|
1524
|
-
new_show = pr.get_show_by_sha(latest_sha)
|
|
1525
|
-
if new_show:
|
|
1526
|
-
new_show.status = "running"
|
|
1527
|
-
new_show.ip = result.ip
|
|
1528
|
-
|
|
1529
|
-
# Update GitHub labels
|
|
1530
|
-
github.remove_circus_labels(pr_number)
|
|
1531
|
-
for label in new_show.to_circus_labels():
|
|
1532
|
-
github.add_label(pr_number, label)
|
|
1533
|
-
|
|
1534
|
-
console.print("🎪 ✅ Labels updated to point to new environment")
|
|
1535
|
-
|
|
1536
|
-
# Post rolling update success comment
|
|
1537
|
-
success_comment_text = rolling_success_comment(pr.current_show, new_show)
|
|
1538
|
-
|
|
1539
|
-
if not dry_run_github:
|
|
1540
|
-
github.post_comment(pr_number, success_comment_text)
|
|
1541
|
-
else:
|
|
1542
|
-
console.print(f"🎪 [bold red]❌ Rolling update failed:[/bold red] {result.error}")
|
|
1543
|
-
|
|
1544
|
-
# Post rolling update failure comment
|
|
1545
|
-
failure_comment_text = rolling_failure_comment(
|
|
1546
|
-
pr.current_show, latest_sha, result.error
|
|
1547
|
-
)
|
|
1548
|
-
|
|
1549
|
-
if not dry_run_github:
|
|
1550
|
-
github.post_comment(pr_number, failure_comment_text)
|
|
667
|
+
p("🎪 No expired environments found")
|
|
1551
668
|
|
|
1552
669
|
except Exception as e:
|
|
1553
|
-
|
|
670
|
+
p(f"❌ Cleanup failed: {e}")
|
|
1554
671
|
|
|
1555
672
|
|
|
1556
|
-
def main():
|
|
673
|
+
def main() -> None:
|
|
1557
674
|
"""Main entry point for the CLI"""
|
|
1558
675
|
app()
|
|
1559
676
|
|