superset-showtime 0.3.3__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of superset-showtime might be problematic. Click here for more details.
- showtime/__init__.py +3 -2
- showtime/cli.py +268 -1523
- showtime/core/aws.py +30 -26
- showtime/core/github.py +10 -6
- showtime/core/github_messages.py +4 -4
- showtime/core/pull_request.py +534 -0
- showtime/core/show.py +279 -0
- {superset_showtime-0.3.3.dist-info → superset_showtime-0.4.0.dist-info}/METADATA +17 -9
- superset_showtime-0.4.0.dist-info/RECORD +16 -0
- showtime/commands/__init__.py +0 -1
- showtime/commands/start.py +0 -40
- showtime/core/circus.py +0 -289
- superset_showtime-0.3.3.dist-info/RECORD +0 -17
- {superset_showtime-0.3.3.dist-info → superset_showtime-0.4.0.dist-info}/WHEEL +0 -0
- {superset_showtime-0.3.3.dist-info → superset_showtime-0.4.0.dist-info}/entry_points.txt +0 -0
showtime/cli.py
CHANGED
|
@@ -4,133 +4,36 @@
|
|
|
4
4
|
Main command-line interface for Apache Superset circus tent environment management.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import
|
|
8
|
-
from typing import Optional
|
|
7
|
+
from typing import Dict, Optional
|
|
9
8
|
|
|
10
9
|
import typer
|
|
11
10
|
from rich.console import Console
|
|
12
11
|
from rich.table import Table
|
|
13
12
|
|
|
14
|
-
from .core.circus import PullRequest, Show, short_sha
|
|
15
13
|
from .core.emojis import STATUS_DISPLAY
|
|
16
14
|
from .core.github import GitHubError, GitHubInterface
|
|
17
15
|
from .core.github_messages import (
|
|
18
|
-
building_comment,
|
|
19
|
-
failure_comment,
|
|
20
16
|
get_aws_console_urls,
|
|
21
|
-
rolling_failure_comment,
|
|
22
|
-
rolling_start_comment,
|
|
23
|
-
rolling_success_comment,
|
|
24
|
-
start_comment,
|
|
25
|
-
success_comment,
|
|
26
17
|
)
|
|
18
|
+
from .core.pull_request import PullRequest
|
|
19
|
+
from .core.show import Show
|
|
27
20
|
|
|
28
21
|
# Constants
|
|
29
22
|
DEFAULT_GITHUB_ACTOR = "unknown"
|
|
30
23
|
|
|
31
24
|
|
|
32
|
-
def _get_service_urls(show):
|
|
25
|
+
def _get_service_urls(show: Show) -> Dict[str, str]:
|
|
33
26
|
"""Get AWS Console URLs for a service"""
|
|
34
27
|
return get_aws_console_urls(show.ecs_service_name)
|
|
35
28
|
|
|
36
29
|
|
|
37
|
-
def _show_service_urls(show, context: str = "deployment"):
|
|
30
|
+
def _show_service_urls(show: Show, context: str = "deployment") -> None:
|
|
38
31
|
"""Show helpful AWS Console URLs for monitoring service"""
|
|
39
32
|
urls = _get_service_urls(show)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def _determine_sync_action(pr, pr_state: str, target_sha: str) -> str:
|
|
47
|
-
"""Determine what action is needed based on PR state and labels"""
|
|
48
|
-
|
|
49
|
-
# 1. Closed PRs always need cleanup
|
|
50
|
-
if pr_state == "closed":
|
|
51
|
-
return "cleanup"
|
|
52
|
-
|
|
53
|
-
# 2. Check for explicit trigger labels
|
|
54
|
-
trigger_labels = [label for label in pr.labels if "showtime-trigger-" in label]
|
|
55
|
-
|
|
56
|
-
# 3. Check for freeze label (PR-level) - only if no explicit triggers
|
|
57
|
-
freeze_labels = [label for label in pr.labels if "showtime-freeze" in label]
|
|
58
|
-
if freeze_labels and not trigger_labels:
|
|
59
|
-
return "frozen_no_action" # Frozen and no explicit triggers to override
|
|
60
|
-
|
|
61
|
-
if trigger_labels:
|
|
62
|
-
# Explicit triggers take priority
|
|
63
|
-
for trigger in trigger_labels:
|
|
64
|
-
if "showtime-trigger-start" in trigger:
|
|
65
|
-
if pr.current_show:
|
|
66
|
-
if pr.current_show.needs_update(target_sha):
|
|
67
|
-
return "rolling_update" # New commit with existing env
|
|
68
|
-
else:
|
|
69
|
-
return "no_action" # Same commit, no change needed
|
|
70
|
-
else:
|
|
71
|
-
return "create_environment" # New environment
|
|
72
|
-
elif "showtime-trigger-stop" in trigger:
|
|
73
|
-
return "destroy_environment"
|
|
74
|
-
|
|
75
|
-
# 3. No explicit triggers - check for implicit sync needs
|
|
76
|
-
if pr.current_show:
|
|
77
|
-
if pr.current_show.needs_update(target_sha):
|
|
78
|
-
return "auto_sync" # Auto-update on new commits
|
|
79
|
-
else:
|
|
80
|
-
return "no_action" # Everything in sync
|
|
81
|
-
else:
|
|
82
|
-
return "no_action" # No environment, no triggers
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def _schedule_blue_cleanup(pr_number: int, blue_services: list):
|
|
86
|
-
"""Schedule cleanup of blue services after successful green deployment"""
|
|
87
|
-
import threading
|
|
88
|
-
import time
|
|
89
|
-
|
|
90
|
-
def cleanup_after_delay():
|
|
91
|
-
"""Background cleanup of blue services"""
|
|
92
|
-
try:
|
|
93
|
-
# Wait 5 minutes before cleanup
|
|
94
|
-
time.sleep(300) # 5 minutes
|
|
95
|
-
|
|
96
|
-
console.print(
|
|
97
|
-
f"\n🧹 [bold blue]Starting scheduled cleanup of blue services for PR #{pr_number}[/bold blue]"
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
from .core.aws import AWSInterface
|
|
101
|
-
|
|
102
|
-
aws = AWSInterface()
|
|
103
|
-
|
|
104
|
-
for blue_svc in blue_services:
|
|
105
|
-
service_name = blue_svc["service_name"]
|
|
106
|
-
console.print(f"🗑️ Cleaning up blue service: {service_name}")
|
|
107
|
-
|
|
108
|
-
try:
|
|
109
|
-
# Delete ECS service
|
|
110
|
-
if aws._delete_ecs_service(service_name):
|
|
111
|
-
# Delete ECR image
|
|
112
|
-
pr_match = service_name.split("-")
|
|
113
|
-
if len(pr_match) >= 2:
|
|
114
|
-
pr_num = pr_match[1]
|
|
115
|
-
image_tag = f"pr-{pr_num}-ci" # Legacy format for old services
|
|
116
|
-
aws._delete_ecr_image(image_tag)
|
|
117
|
-
|
|
118
|
-
console.print(f"✅ Cleaned up blue service: {service_name}")
|
|
119
|
-
else:
|
|
120
|
-
console.print(f"⚠️ Failed to clean up: {service_name}")
|
|
121
|
-
|
|
122
|
-
except Exception as e:
|
|
123
|
-
console.print(f"❌ Cleanup error for {service_name}: {e}")
|
|
124
|
-
|
|
125
|
-
console.print("🧹 ✅ Blue service cleanup completed")
|
|
126
|
-
|
|
127
|
-
except Exception as e:
|
|
128
|
-
console.print(f"❌ Background cleanup failed: {e}")
|
|
129
|
-
|
|
130
|
-
# Start cleanup in background thread
|
|
131
|
-
cleanup_thread = threading.Thread(target=cleanup_after_delay, daemon=True)
|
|
132
|
-
cleanup_thread.start()
|
|
133
|
-
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("")
|
|
134
37
|
|
|
135
38
|
|
|
136
39
|
app = typer.Typer(
|
|
@@ -152,9 +55,11 @@ app = typer.Typer(
|
|
|
152
55
|
|
|
153
56
|
[dim]CLI commands work with existing environments or dry-run new ones.[/dim]""",
|
|
154
57
|
rich_markup_mode="rich",
|
|
58
|
+
no_args_is_help=True,
|
|
155
59
|
)
|
|
156
60
|
|
|
157
61
|
console = Console()
|
|
62
|
+
p = console.print # Shorthand for cleaner code
|
|
158
63
|
|
|
159
64
|
|
|
160
65
|
def _get_github_workflow_url() -> str:
|
|
@@ -176,336 +81,15 @@ def _get_github_actor() -> str:
|
|
|
176
81
|
|
|
177
82
|
def _get_showtime_footer() -> str:
|
|
178
83
|
"""Get consistent Showtime footer for PR comments"""
|
|
179
|
-
return "
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
def _validate_non_active_state(pr: PullRequest) -> bool:
|
|
183
|
-
"""Check if PR is in a state where new work can begin
|
|
184
|
-
|
|
185
|
-
Args:
|
|
186
|
-
pr: PullRequest object with current state
|
|
187
|
-
|
|
188
|
-
Returns:
|
|
189
|
-
True if safe to start new work, False if another job is already active
|
|
190
|
-
"""
|
|
191
|
-
if pr.current_show:
|
|
192
|
-
active_states = ["building", "built", "deploying", "running", "updating"]
|
|
193
|
-
if pr.current_show.status in active_states:
|
|
194
|
-
return False # Already active
|
|
195
|
-
return True # Safe to proceed
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
def _atomic_claim_environment(
|
|
199
|
-
pr_number: int, target_sha: str, github: GitHubInterface, dry_run: bool = False
|
|
200
|
-
) -> bool:
|
|
201
|
-
"""Atomically claim environment for this job using compare-and-swap pattern
|
|
202
|
-
|
|
203
|
-
Args:
|
|
204
|
-
pr_number: PR number to claim
|
|
205
|
-
target_sha: Target commit SHA
|
|
206
|
-
github: GitHub interface for label operations
|
|
207
|
-
dry_run: If True, simulate operations
|
|
208
|
-
|
|
209
|
-
Returns:
|
|
210
|
-
True if successfully claimed, False if another job already active or no triggers
|
|
211
|
-
"""
|
|
212
|
-
from datetime import datetime
|
|
213
|
-
|
|
214
|
-
try:
|
|
215
|
-
# 1. CHECK: Load current PR state
|
|
216
|
-
pr = PullRequest.from_id(pr_number, github)
|
|
217
|
-
|
|
218
|
-
# 2. VALIDATE: Ensure non-active state (compare part of compare-and-swap)
|
|
219
|
-
if not _validate_non_active_state(pr):
|
|
220
|
-
current_state = pr.current_show.status if pr.current_show else "unknown"
|
|
221
|
-
console.print(
|
|
222
|
-
f"🎪 Environment already active (state: {current_state}) - another job is running"
|
|
223
|
-
)
|
|
224
|
-
return False
|
|
225
|
-
|
|
226
|
-
# 3. FIND TRIGGERS: Must have triggers to claim
|
|
227
|
-
trigger_labels = [label for label in pr.labels if "showtime-trigger-" in label]
|
|
228
|
-
if not trigger_labels:
|
|
229
|
-
console.print("🎪 No trigger labels found - nothing to claim")
|
|
230
|
-
return False
|
|
231
|
-
|
|
232
|
-
# 4. VALIDATE TRIGGER-SPECIFIC STATE REQUIREMENTS
|
|
233
|
-
for trigger_label in trigger_labels:
|
|
234
|
-
if "showtime-trigger-start" in trigger_label:
|
|
235
|
-
# Start trigger: should NOT already be building/running
|
|
236
|
-
if pr.current_show and pr.current_show.status in [
|
|
237
|
-
"building",
|
|
238
|
-
"built",
|
|
239
|
-
"deploying",
|
|
240
|
-
"running",
|
|
241
|
-
]:
|
|
242
|
-
console.print(
|
|
243
|
-
f"🎪 Start trigger invalid - environment already {pr.current_show.status}"
|
|
244
|
-
)
|
|
245
|
-
return False
|
|
246
|
-
elif "showtime-trigger-stop" in trigger_label:
|
|
247
|
-
# Stop trigger: should HAVE an active environment
|
|
248
|
-
if not pr.current_show or pr.current_show.status in ["failed"]:
|
|
249
|
-
console.print("🎪 Stop trigger invalid - no active environment to stop")
|
|
250
|
-
return False
|
|
251
|
-
|
|
252
|
-
console.print(f"🎪 Claiming environment for PR #{pr_number} SHA {target_sha[:7]}")
|
|
253
|
-
console.print(f"🎪 Found {len(trigger_labels)} valid trigger(s) to process")
|
|
254
|
-
|
|
255
|
-
if dry_run:
|
|
256
|
-
console.print(
|
|
257
|
-
"🎪 [bold yellow]DRY-RUN[/bold yellow] - Would atomically claim environment"
|
|
258
|
-
)
|
|
259
|
-
return True
|
|
260
|
-
|
|
261
|
-
# 4. ATOMIC SWAP: Remove triggers + Set building state (swap part)
|
|
262
|
-
console.print("🎪 Executing atomic claim (remove triggers + set building)...")
|
|
263
|
-
|
|
264
|
-
# Remove all trigger labels first
|
|
265
|
-
for trigger_label in trigger_labels:
|
|
266
|
-
console.print(f" 🗑️ Removing trigger: {trigger_label}")
|
|
267
|
-
github.remove_label(pr_number, trigger_label)
|
|
268
|
-
|
|
269
|
-
# Immediately set building state to claim the environment
|
|
270
|
-
building_show = Show(
|
|
271
|
-
pr_number=pr_number,
|
|
272
|
-
sha=short_sha(target_sha),
|
|
273
|
-
status="building",
|
|
274
|
-
created_at=datetime.utcnow().strftime("%Y-%m-%dT%H-%M"),
|
|
275
|
-
ttl="24h",
|
|
276
|
-
requested_by=_get_github_actor(),
|
|
277
|
-
)
|
|
278
|
-
|
|
279
|
-
# Clear any stale state and set building labels atomically
|
|
280
|
-
github.remove_circus_labels(pr_number)
|
|
281
|
-
for label in building_show.to_circus_labels():
|
|
282
|
-
github.add_label(pr_number, label)
|
|
283
|
-
|
|
284
|
-
console.print("🎪 ✅ Environment claimed successfully")
|
|
285
|
-
return True
|
|
286
|
-
|
|
287
|
-
except Exception as e:
|
|
288
|
-
console.print(f"🎪 ❌ Failed to claim environment: {e}")
|
|
289
|
-
return False
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
def _build_docker_image(pr_number: int, sha: str, dry_run: bool = False) -> bool:
|
|
293
|
-
"""Build Docker image directly without supersetbot dependency
|
|
294
|
-
|
|
295
|
-
Args:
|
|
296
|
-
pr_number: PR number for tagging
|
|
297
|
-
sha: Full commit SHA
|
|
298
|
-
dry_run: If True, print command but don't execute
|
|
299
|
-
|
|
300
|
-
Returns:
|
|
301
|
-
True if build succeeded, False if failed
|
|
302
|
-
"""
|
|
303
|
-
tag = f"apache/superset:pr-{pr_number}-{short_sha(sha)}-ci"
|
|
304
|
-
|
|
305
|
-
cmd = [
|
|
306
|
-
"docker",
|
|
307
|
-
"buildx",
|
|
308
|
-
"build",
|
|
309
|
-
"--push",
|
|
310
|
-
"--load",
|
|
311
|
-
"--platform",
|
|
312
|
-
"linux/amd64",
|
|
313
|
-
"--target",
|
|
314
|
-
"ci",
|
|
315
|
-
"--cache-from",
|
|
316
|
-
"type=registry,ref=apache/superset-cache:3.10-slim-bookworm",
|
|
317
|
-
"--cache-to",
|
|
318
|
-
"type=registry,mode=max,ref=apache/superset-cache:3.10-slim-bookworm",
|
|
319
|
-
"--build-arg",
|
|
320
|
-
"INCLUDE_CHROMIUM=false",
|
|
321
|
-
"--build-arg",
|
|
322
|
-
"LOAD_EXAMPLES_DUCKDB=true",
|
|
323
|
-
"-t",
|
|
324
|
-
tag,
|
|
325
|
-
".",
|
|
326
|
-
]
|
|
327
|
-
|
|
328
|
-
console.print(f"🐳 Building Docker image: {tag}")
|
|
329
|
-
if dry_run:
|
|
330
|
-
console.print(f"🎪 [bold yellow]DRY-RUN[/bold yellow] - Would run: {' '.join(cmd)}")
|
|
331
|
-
return True
|
|
332
|
-
|
|
333
|
-
try:
|
|
334
|
-
console.print(f"🎪 Running: {' '.join(cmd)}")
|
|
335
|
-
console.print("🎪 Streaming Docker build output...")
|
|
336
|
-
|
|
337
|
-
# Stream output in real-time for better user experience
|
|
338
|
-
import time
|
|
339
|
-
|
|
340
|
-
process = subprocess.Popen(
|
|
341
|
-
cmd,
|
|
342
|
-
stdout=subprocess.PIPE,
|
|
343
|
-
stderr=subprocess.STDOUT,
|
|
344
|
-
text=True,
|
|
345
|
-
bufsize=1,
|
|
346
|
-
universal_newlines=True,
|
|
347
|
-
)
|
|
348
|
-
|
|
349
|
-
# Stream output line by line with progress monitoring
|
|
350
|
-
last_output_time = time.time()
|
|
351
|
-
progress_timeout = 1800 # 30 min without output = stuck
|
|
352
|
-
|
|
353
|
-
for line in process.stdout:
|
|
354
|
-
console.print(f"🐳 {line.rstrip()}")
|
|
355
|
-
last_output_time = time.time() # Reset timeout on any output
|
|
356
|
-
|
|
357
|
-
# Check if process finished or we need to wait
|
|
358
|
-
if process.poll() is None:
|
|
359
|
-
# Process still running, wait with timeout
|
|
360
|
-
try:
|
|
361
|
-
return_code = process.wait(timeout=300) # 5 min final wait
|
|
362
|
-
except subprocess.TimeoutExpired:
|
|
363
|
-
process.kill()
|
|
364
|
-
console.print("🎪 ❌ Docker build hung - no progress for 30+ minutes")
|
|
365
|
-
return False
|
|
366
|
-
else:
|
|
367
|
-
return_code = process.returncode
|
|
368
|
-
|
|
369
|
-
if return_code == 0:
|
|
370
|
-
console.print(f"🎪 ✅ Docker build succeeded: {tag}")
|
|
371
|
-
return True
|
|
372
|
-
else:
|
|
373
|
-
console.print(f"🎪 ❌ Docker build failed with exit code: {return_code}")
|
|
374
|
-
return False
|
|
375
|
-
except Exception as e:
|
|
376
|
-
console.print(f"🎪 ❌ Docker build error: {e}")
|
|
377
|
-
return False
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
def _set_state_internal(
|
|
381
|
-
state: str,
|
|
382
|
-
pr_number: int,
|
|
383
|
-
show: Show,
|
|
384
|
-
github: GitHubInterface,
|
|
385
|
-
dry_run_github: bool = False,
|
|
386
|
-
error_msg: Optional[str] = None,
|
|
387
|
-
) -> None:
|
|
388
|
-
"""Internal helper to set state and handle comments/labels
|
|
389
|
-
|
|
390
|
-
Used by sync and other commands to set final state transitions
|
|
391
|
-
"""
|
|
392
|
-
console.print(f"🎪 Setting state to '{state}' for PR #{pr_number} SHA {show.sha}")
|
|
393
|
-
|
|
394
|
-
# Update show state
|
|
395
|
-
show.status = state
|
|
396
|
-
|
|
397
|
-
# Handle state-specific logic
|
|
398
|
-
comment_text = None
|
|
399
|
-
|
|
400
|
-
if state == "building":
|
|
401
|
-
comment_text = building_comment(show)
|
|
402
|
-
console.print("🎪 Posting building comment...")
|
|
403
|
-
|
|
404
|
-
elif state == "running":
|
|
405
|
-
comment_text = success_comment(show)
|
|
406
|
-
console.print("🎪 Posting success comment...")
|
|
407
|
-
|
|
408
|
-
elif state == "failed":
|
|
409
|
-
error_message = error_msg or "Build or deployment failed"
|
|
410
|
-
comment_text = failure_comment(show, error_message)
|
|
411
|
-
console.print("🎪 Posting failure comment...")
|
|
412
|
-
|
|
413
|
-
elif state in ["built", "deploying"]:
|
|
414
|
-
console.print(f"🎪 Silent state change to '{state}' - no comment posted")
|
|
415
|
-
|
|
416
|
-
# Post comment if needed
|
|
417
|
-
if comment_text and not dry_run_github:
|
|
418
|
-
github.post_comment(pr_number, comment_text)
|
|
419
|
-
console.print("🎪 ✅ Comment posted!")
|
|
420
|
-
elif comment_text:
|
|
421
|
-
console.print("🎪 [bold yellow]DRY-RUN-GITHUB[/bold yellow] - Would post comment")
|
|
422
|
-
|
|
423
|
-
# Set state labels
|
|
424
|
-
state_labels = show.to_circus_labels()
|
|
425
|
-
|
|
426
|
-
if not dry_run_github:
|
|
427
|
-
github.remove_circus_labels(pr_number)
|
|
428
|
-
for label in state_labels:
|
|
429
|
-
github.add_label(pr_number, label)
|
|
430
|
-
console.print("🎪 ✅ Labels updated!")
|
|
431
|
-
else:
|
|
432
|
-
console.print("🎪 [bold yellow]DRY-RUN-GITHUB[/bold yellow] - Would update labels")
|
|
84
|
+
return "🎪 *Managed by [Superset Showtime](https://github.com/your-org/superset-showtime)*"
|
|
433
85
|
|
|
434
86
|
|
|
435
87
|
@app.command()
|
|
436
|
-
def version():
|
|
88
|
+
def version() -> None:
|
|
437
89
|
"""Show version information"""
|
|
438
90
|
from . import __version__
|
|
439
91
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
@app.command()
|
|
444
|
-
def set_state(
|
|
445
|
-
state: str = typer.Argument(
|
|
446
|
-
..., help="State to set (building, built, deploying, running, failed)"
|
|
447
|
-
),
|
|
448
|
-
pr_number: int = typer.Argument(..., help="PR number to update"),
|
|
449
|
-
sha: Optional[str] = typer.Option(None, "--sha", help="Specific commit SHA (default: latest)"),
|
|
450
|
-
error_msg: Optional[str] = typer.Option(
|
|
451
|
-
None, "--error-msg", help="Error message for failed state"
|
|
452
|
-
),
|
|
453
|
-
dry_run_github: bool = typer.Option(
|
|
454
|
-
False, "--dry-run-github", help="Skip GitHub operations, show what would happen"
|
|
455
|
-
),
|
|
456
|
-
):
|
|
457
|
-
"""🎪 Set environment state (generic state transition command)
|
|
458
|
-
|
|
459
|
-
States:
|
|
460
|
-
• building - Docker image is being built (posts comment)
|
|
461
|
-
• built - Docker build complete, ready for deployment (silent)
|
|
462
|
-
• deploying - AWS deployment in progress (silent)
|
|
463
|
-
• running - Environment is live and ready (posts success comment)
|
|
464
|
-
• failed - Build or deployment failed (posts error comment)
|
|
465
|
-
"""
|
|
466
|
-
from datetime import datetime
|
|
467
|
-
|
|
468
|
-
try:
|
|
469
|
-
github = GitHubInterface()
|
|
470
|
-
|
|
471
|
-
# Get SHA - use provided SHA or default to latest
|
|
472
|
-
if sha:
|
|
473
|
-
target_sha = sha
|
|
474
|
-
console.print(f"🎪 Using specified SHA: {target_sha[:7]}")
|
|
475
|
-
else:
|
|
476
|
-
target_sha = github.get_latest_commit_sha(pr_number)
|
|
477
|
-
console.print(f"🎪 Using latest SHA: {target_sha[:7]}")
|
|
478
|
-
|
|
479
|
-
# Validate state
|
|
480
|
-
valid_states = ["building", "built", "deploying", "running", "failed"]
|
|
481
|
-
if state not in valid_states:
|
|
482
|
-
console.print(f"❌ Invalid state: {state}. Must be one of: {', '.join(valid_states)}")
|
|
483
|
-
raise typer.Exit(1)
|
|
484
|
-
|
|
485
|
-
# Get GitHub actor
|
|
486
|
-
github_actor = _get_github_actor()
|
|
487
|
-
|
|
488
|
-
# Create or update show object
|
|
489
|
-
show = Show(
|
|
490
|
-
pr_number=pr_number,
|
|
491
|
-
sha=short_sha(target_sha),
|
|
492
|
-
status=state,
|
|
493
|
-
created_at=datetime.utcnow().strftime("%Y-%m-%dT%H-%M"),
|
|
494
|
-
ttl="24h",
|
|
495
|
-
requested_by=github_actor,
|
|
496
|
-
)
|
|
497
|
-
|
|
498
|
-
# Use internal helper to set state
|
|
499
|
-
_set_state_internal(state, pr_number, show, github, dry_run_github, error_msg)
|
|
500
|
-
|
|
501
|
-
console.print(f"🎪 [bold green]State successfully set to '{state}'[/bold green]")
|
|
502
|
-
|
|
503
|
-
except GitHubError as e:
|
|
504
|
-
console.print(f"❌ GitHub error: {e}")
|
|
505
|
-
raise typer.Exit(1) from e
|
|
506
|
-
except Exception as e:
|
|
507
|
-
console.print(f"❌ Error: {e}")
|
|
508
|
-
raise typer.Exit(1) from e
|
|
92
|
+
p(f"🎪 Superset Showtime v{__version__}")
|
|
509
93
|
|
|
510
94
|
|
|
511
95
|
@app.command()
|
|
@@ -528,73 +112,77 @@ def start(
|
|
|
528
112
|
force: bool = typer.Option(
|
|
529
113
|
False, "--force", help="Force re-deployment by deleting existing service"
|
|
530
114
|
),
|
|
531
|
-
):
|
|
115
|
+
) -> None:
|
|
532
116
|
"""Create ephemeral environment for PR"""
|
|
533
117
|
try:
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
#
|
|
537
|
-
if not
|
|
538
|
-
|
|
539
|
-
|
|
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]")
|
|
540
135
|
else:
|
|
541
|
-
|
|
136
|
+
p(f"🎪 [bold green]Creating new environment for PR #{pr_number}[/bold green]")
|
|
542
137
|
|
|
543
138
|
if dry_run:
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
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}")
|
|
553
147
|
return
|
|
554
148
|
|
|
555
|
-
#
|
|
556
|
-
|
|
557
|
-
if pr.current_show:
|
|
558
|
-
console.print(
|
|
559
|
-
f"🎪 [bold yellow]Environment already exists for PR #{pr_number}[/bold yellow]"
|
|
560
|
-
)
|
|
561
|
-
console.print(f"Current: {pr.current_show.sha} at {pr.current_show.ip}")
|
|
562
|
-
console.print("Use 'showtime sync' to update or 'showtime stop' to clean up first")
|
|
563
|
-
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)
|
|
564
151
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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)
|
|
570
160
|
|
|
571
161
|
except GitHubError as e:
|
|
572
|
-
|
|
162
|
+
p(f"❌ GitHub error: {e}")
|
|
163
|
+
raise typer.Exit(1) from e
|
|
573
164
|
except Exception as e:
|
|
574
|
-
|
|
165
|
+
p(f"❌ Error: {e}")
|
|
166
|
+
raise typer.Exit(1) from e
|
|
575
167
|
|
|
576
168
|
|
|
577
169
|
@app.command()
|
|
578
170
|
def status(
|
|
579
171
|
pr_number: int = typer.Argument(..., help="PR number to check status for"),
|
|
580
172
|
verbose: bool = typer.Option(False, "-v", "--verbose", help="Show detailed information"),
|
|
581
|
-
):
|
|
173
|
+
) -> None:
|
|
582
174
|
"""Show environment status for PR"""
|
|
583
175
|
try:
|
|
584
|
-
|
|
176
|
+
pr = PullRequest.from_id(pr_number)
|
|
585
177
|
|
|
586
|
-
|
|
178
|
+
# Use PullRequest method for data
|
|
179
|
+
status_data = pr.get_status()
|
|
587
180
|
|
|
588
|
-
if
|
|
589
|
-
|
|
181
|
+
if status_data["status"] == "no_environment":
|
|
182
|
+
p(f"🎪 No environment found for PR #{pr_number}")
|
|
590
183
|
return
|
|
591
184
|
|
|
592
|
-
|
|
593
|
-
if not show:
|
|
594
|
-
console.print(f"🎪 No active environment for PR #{pr_number}")
|
|
595
|
-
if pr.building_show:
|
|
596
|
-
console.print(f"🏗️ Building environment: {pr.building_show.sha}")
|
|
597
|
-
return
|
|
185
|
+
show_data = status_data["show"]
|
|
598
186
|
|
|
599
187
|
# Create status table
|
|
600
188
|
table = Table(title=f"🎪 Environment Status - PR #{pr_number}")
|
|
@@ -602,37 +190,37 @@ def status(
|
|
|
602
190
|
table.add_column("Value", style="white")
|
|
603
191
|
|
|
604
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']}`")
|
|
605
198
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
if show.ip:
|
|
611
|
-
table.add_row("URL", f"http://{show.ip}:8080")
|
|
612
|
-
|
|
613
|
-
if show.created_at:
|
|
614
|
-
table.add_row("Created", show.created_at)
|
|
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"])
|
|
615
203
|
|
|
616
|
-
table.add_row("TTL",
|
|
204
|
+
table.add_row("TTL", show_data["ttl"])
|
|
617
205
|
|
|
618
|
-
if
|
|
619
|
-
table.add_row("Requested by", f"@{
|
|
206
|
+
if show_data["requested_by"]:
|
|
207
|
+
table.add_row("Requested by", f"@{show_data['requested_by']}")
|
|
620
208
|
|
|
621
209
|
if verbose:
|
|
622
210
|
table.add_row("All Labels", ", ".join(pr.circus_labels))
|
|
623
211
|
|
|
624
|
-
|
|
212
|
+
p(table)
|
|
625
213
|
|
|
626
214
|
# Show building environment if exists
|
|
627
|
-
if pr.building_show and pr.building_show !=
|
|
628
|
-
|
|
629
|
-
f"🏗️ [bold yellow]Building new environment:[/bold yellow] {pr.building_show.sha}"
|
|
630
|
-
)
|
|
215
|
+
if pr.building_show and pr.building_show.sha != show_data["sha"]:
|
|
216
|
+
p(f"🏗️ [bold yellow]Building new environment:[/bold yellow] {pr.building_show.sha}")
|
|
631
217
|
|
|
632
218
|
except GitHubError as e:
|
|
633
|
-
|
|
219
|
+
p(f"❌ GitHub error: {e}")
|
|
220
|
+
raise typer.Exit(1) from e
|
|
634
221
|
except Exception as e:
|
|
635
|
-
|
|
222
|
+
p(f"❌ Error: {e}")
|
|
223
|
+
raise typer.Exit(1) from e
|
|
636
224
|
|
|
637
225
|
|
|
638
226
|
@app.command()
|
|
@@ -644,114 +232,47 @@ def stop(
|
|
|
644
232
|
False, "--dry-run-aws", help="Skip AWS operations, use mock data"
|
|
645
233
|
),
|
|
646
234
|
aws_sleep: int = typer.Option(0, "--aws-sleep", help="Seconds to sleep during AWS operations"),
|
|
647
|
-
):
|
|
235
|
+
) -> None:
|
|
648
236
|
"""Delete environment for PR"""
|
|
649
237
|
try:
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
pr = PullRequest.from_id(pr_number, github)
|
|
238
|
+
pr = PullRequest.from_id(pr_number)
|
|
653
239
|
|
|
654
240
|
if not pr.current_show:
|
|
655
|
-
|
|
241
|
+
p(f"🎪 No active environment found for PR #{pr_number}")
|
|
656
242
|
return
|
|
657
243
|
|
|
658
244
|
show = pr.current_show
|
|
659
|
-
|
|
660
|
-
|
|
245
|
+
p(f"🎪 [bold yellow]Stopping environment for PR #{pr_number}...[/bold yellow]")
|
|
246
|
+
p(f"Environment: {show.sha} at {show.ip}")
|
|
661
247
|
|
|
662
248
|
if dry_run:
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
249
|
+
p("🎪 [bold yellow]DRY RUN[/bold yellow] - Would delete environment:")
|
|
250
|
+
p(f" AWS Service: {show.aws_service_name}")
|
|
251
|
+
p(f" ECR Image: {show.aws_image_tag}")
|
|
252
|
+
p(f" Circus Labels: {len(pr.circus_labels)} labels")
|
|
667
253
|
return
|
|
668
254
|
|
|
669
255
|
if not force:
|
|
670
256
|
confirm = typer.confirm(f"Delete environment {show.aws_service_name}?")
|
|
671
257
|
if not confirm:
|
|
672
|
-
|
|
258
|
+
p("🎪 Cancelled")
|
|
673
259
|
return
|
|
674
260
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
console.print(f" - ECS service: {show.aws_service_name}")
|
|
678
|
-
console.print(f" - ECR image: {show.aws_image_tag}")
|
|
679
|
-
if aws_sleep > 0:
|
|
680
|
-
import time
|
|
261
|
+
# Use PullRequest method for all logic
|
|
262
|
+
result = pr.stop_environment(dry_run_github=False, dry_run_aws=dry_run_aws)
|
|
681
263
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
console.print("🎪 [bold green]Mock AWS cleanup complete![/bold green]")
|
|
264
|
+
if result.success:
|
|
265
|
+
p("🎪 ✅ Environment stopped and cleaned up!")
|
|
685
266
|
else:
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
console.print("🎪 [bold blue]Starting AWS cleanup...[/bold blue]")
|
|
690
|
-
aws = AWSInterface()
|
|
691
|
-
|
|
692
|
-
try:
|
|
693
|
-
# Get current environment info
|
|
694
|
-
pr = PullRequest.from_id(pr_number, github)
|
|
695
|
-
|
|
696
|
-
if pr.current_show:
|
|
697
|
-
show = pr.current_show
|
|
698
|
-
|
|
699
|
-
# Show logs URL for monitoring cleanup
|
|
700
|
-
_show_service_urls(show, "cleanup")
|
|
701
|
-
console.print(f"🎪 Destroying environment: {show.aws_service_name}")
|
|
702
|
-
|
|
703
|
-
# Step 1: Check if ECS service exists and is active
|
|
704
|
-
service_name = show.ecs_service_name
|
|
705
|
-
console.print(f"🎪 Checking ECS service: {service_name}")
|
|
706
|
-
|
|
707
|
-
service_exists = aws._service_exists(service_name)
|
|
708
|
-
|
|
709
|
-
if service_exists:
|
|
710
|
-
console.print(f"🎪 Found active ECS service: {service_name}")
|
|
711
|
-
|
|
712
|
-
# Step 2: Delete ECS service
|
|
713
|
-
console.print("🎪 Deleting ECS service...")
|
|
714
|
-
success = aws._delete_ecs_service(service_name)
|
|
715
|
-
|
|
716
|
-
if success:
|
|
717
|
-
console.print("🎪 ✅ ECS service deleted successfully")
|
|
718
|
-
|
|
719
|
-
# Step 3: Delete ECR image tag
|
|
720
|
-
image_tag = f"pr-{pr_number}-ci" # Match GHA image tag format
|
|
721
|
-
console.print(f"🎪 Deleting ECR image tag: {image_tag}")
|
|
722
|
-
|
|
723
|
-
ecr_success = aws._delete_ecr_image(image_tag)
|
|
724
|
-
|
|
725
|
-
if ecr_success:
|
|
726
|
-
console.print("🎪 ✅ ECR image deleted successfully")
|
|
727
|
-
else:
|
|
728
|
-
console.print("🎪 ⚠️ ECR image deletion failed (may not exist)")
|
|
729
|
-
|
|
730
|
-
console.print(
|
|
731
|
-
"🎪 [bold green]✅ AWS cleanup completed successfully![/bold green]"
|
|
732
|
-
)
|
|
733
|
-
|
|
734
|
-
else:
|
|
735
|
-
console.print("🎪 [bold red]❌ ECS service deletion failed[/bold red]")
|
|
736
|
-
|
|
737
|
-
else:
|
|
738
|
-
console.print(f"🎪 No active ECS service found: {service_name}")
|
|
739
|
-
console.print("🎪 ✅ No AWS resources to clean up")
|
|
740
|
-
else:
|
|
741
|
-
console.print(f"🎪 No active environment found for PR #{pr_number}")
|
|
742
|
-
|
|
743
|
-
except Exception as e:
|
|
744
|
-
console.print(f"🎪 [bold red]❌ AWS cleanup failed:[/bold red] {e}")
|
|
745
|
-
|
|
746
|
-
# Remove circus labels
|
|
747
|
-
github.remove_circus_labels(pr_number)
|
|
748
|
-
|
|
749
|
-
console.print("🎪 [bold green]Environment stopped and labels cleaned up![/bold green]")
|
|
267
|
+
p(f"🎪 ❌ Failed to stop environment: {result.error}")
|
|
268
|
+
raise typer.Exit(1)
|
|
750
269
|
|
|
751
270
|
except GitHubError as e:
|
|
752
|
-
|
|
271
|
+
p(f"❌ GitHub error: {e}")
|
|
272
|
+
raise typer.Exit(1) from e
|
|
753
273
|
except Exception as e:
|
|
754
|
-
|
|
274
|
+
p(f"❌ Error: {e}")
|
|
275
|
+
raise typer.Exit(1) from e
|
|
755
276
|
|
|
756
277
|
|
|
757
278
|
@app.command()
|
|
@@ -760,37 +281,33 @@ def list(
|
|
|
760
281
|
None, "--status", help="Filter by status (running, building, etc.)"
|
|
761
282
|
),
|
|
762
283
|
user: Optional[str] = typer.Option(None, "--user", help="Filter by user"),
|
|
763
|
-
):
|
|
284
|
+
) -> None:
|
|
764
285
|
"""List all environments"""
|
|
765
286
|
try:
|
|
766
|
-
|
|
287
|
+
# Use PullRequest method for data collection
|
|
288
|
+
all_environments = PullRequest.list_all_environments()
|
|
767
289
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
if not pr_numbers:
|
|
772
|
-
console.print("🎪 No environments currently running")
|
|
290
|
+
if not all_environments:
|
|
291
|
+
p("🎪 No environments currently running")
|
|
773
292
|
return
|
|
774
293
|
|
|
775
|
-
#
|
|
776
|
-
|
|
777
|
-
for
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
if not all_shows:
|
|
294
|
+
# Apply filters
|
|
295
|
+
filtered_envs = []
|
|
296
|
+
for env in all_environments:
|
|
297
|
+
show_data = env["show"]
|
|
298
|
+
if status_filter and show_data["status"] != status_filter:
|
|
299
|
+
continue
|
|
300
|
+
if user and show_data["requested_by"] != user:
|
|
301
|
+
continue
|
|
302
|
+
filtered_envs.append(env)
|
|
303
|
+
|
|
304
|
+
if not filtered_envs:
|
|
788
305
|
filter_msg = ""
|
|
789
306
|
if status_filter:
|
|
790
307
|
filter_msg += f" with status '{status_filter}'"
|
|
791
308
|
if user:
|
|
792
309
|
filter_msg += f" by user '{user}'"
|
|
793
|
-
|
|
310
|
+
p(f"🎪 No environments found{filter_msg}")
|
|
794
311
|
return
|
|
795
312
|
|
|
796
313
|
# Create table with full terminal width
|
|
@@ -805,50 +322,56 @@ def list(
|
|
|
805
322
|
|
|
806
323
|
status_emoji = STATUS_DISPLAY
|
|
807
324
|
|
|
808
|
-
for
|
|
325
|
+
for env in sorted(filtered_envs, key=lambda e: e["pr_number"]):
|
|
326
|
+
show_data = env["show"]
|
|
327
|
+
pr_number = env["pr_number"]
|
|
809
328
|
# Make Superset URL clickable and show full URL
|
|
810
|
-
if
|
|
811
|
-
full_url = f"http://{
|
|
329
|
+
if show_data["ip"]:
|
|
330
|
+
full_url = f"http://{show_data['ip']}:8080"
|
|
812
331
|
superset_url = f"[link={full_url}]{full_url}[/link]"
|
|
813
332
|
else:
|
|
814
333
|
superset_url = "-"
|
|
815
334
|
|
|
816
335
|
# Get AWS service URLs - iTerm2 supports Rich clickable links
|
|
817
|
-
|
|
336
|
+
from .core.github_messages import get_aws_console_urls
|
|
337
|
+
|
|
338
|
+
aws_urls = get_aws_console_urls(show_data["aws_service_name"])
|
|
818
339
|
aws_logs_link = f"[link={aws_urls['logs']}]View[/link]"
|
|
819
340
|
|
|
820
341
|
# Make PR number clickable
|
|
821
|
-
pr_url = f"https://github.com/apache/superset/pull/{
|
|
822
|
-
clickable_pr = f"[link={pr_url}]{
|
|
342
|
+
pr_url = f"https://github.com/apache/superset/pull/{pr_number}"
|
|
343
|
+
clickable_pr = f"[link={pr_url}]{pr_number}[/link]"
|
|
823
344
|
|
|
824
345
|
table.add_row(
|
|
825
346
|
clickable_pr,
|
|
826
|
-
f"{status_emoji.get(
|
|
827
|
-
|
|
347
|
+
f"{status_emoji.get(show_data['status'], '❓')} {show_data['status']}",
|
|
348
|
+
show_data["sha"],
|
|
828
349
|
superset_url,
|
|
829
350
|
aws_logs_link,
|
|
830
|
-
|
|
831
|
-
f"@{
|
|
351
|
+
show_data["ttl"],
|
|
352
|
+
f"@{show_data['requested_by']}" if show_data["requested_by"] else "-",
|
|
832
353
|
)
|
|
833
354
|
|
|
834
|
-
|
|
355
|
+
p(table)
|
|
835
356
|
|
|
836
357
|
except GitHubError as e:
|
|
837
|
-
|
|
358
|
+
p(f"❌ GitHub error: {e}")
|
|
359
|
+
raise typer.Exit(1) from e
|
|
838
360
|
except Exception as e:
|
|
839
|
-
|
|
361
|
+
p(f"❌ Error: {e}")
|
|
362
|
+
raise typer.Exit(1) from e
|
|
840
363
|
|
|
841
364
|
|
|
842
365
|
@app.command()
|
|
843
|
-
def labels():
|
|
366
|
+
def labels() -> None:
|
|
844
367
|
"""🎪 Show complete circus tent label reference"""
|
|
845
368
|
from .core.label_colors import LABEL_DEFINITIONS
|
|
846
369
|
|
|
847
|
-
|
|
848
|
-
|
|
370
|
+
p("🎪 [bold blue]Circus Tent Label Reference[/bold blue]")
|
|
371
|
+
p()
|
|
849
372
|
|
|
850
373
|
# User Action Labels (from LABEL_DEFINITIONS)
|
|
851
|
-
|
|
374
|
+
p("[bold yellow]🎯 User Action Labels (Add these to GitHub PR):[/bold yellow]")
|
|
852
375
|
trigger_table = Table()
|
|
853
376
|
trigger_table.add_column("Label", style="green")
|
|
854
377
|
trigger_table.add_column("Description", style="dim")
|
|
@@ -856,11 +379,11 @@ def labels():
|
|
|
856
379
|
for label_name, definition in LABEL_DEFINITIONS.items():
|
|
857
380
|
trigger_table.add_row(f"`{label_name}`", definition["description"])
|
|
858
381
|
|
|
859
|
-
|
|
860
|
-
|
|
382
|
+
p(trigger_table)
|
|
383
|
+
p()
|
|
861
384
|
|
|
862
385
|
# State Labels
|
|
863
|
-
|
|
386
|
+
p("[bold cyan]📊 State Labels (Automatically managed):[/bold cyan]")
|
|
864
387
|
state_table = Table()
|
|
865
388
|
state_table.add_column("Label", style="cyan")
|
|
866
389
|
state_table.add_column("Meaning", style="white")
|
|
@@ -876,93 +399,43 @@ def labels():
|
|
|
876
399
|
state_table.add_row("🎪 {sha} ⌛ {ttl-policy}", "TTL policy", "🎪 abc123f ⌛ 24h")
|
|
877
400
|
state_table.add_row("🎪 {sha} 🤡 {username}", "Requested by", "🎪 abc123f 🤡 maxime")
|
|
878
401
|
|
|
879
|
-
|
|
880
|
-
|
|
402
|
+
p(state_table)
|
|
403
|
+
p()
|
|
881
404
|
|
|
882
405
|
# Workflow Examples
|
|
883
|
-
|
|
884
|
-
|
|
406
|
+
p("[bold magenta]🎪 Complete Workflow Examples:[/bold magenta]")
|
|
407
|
+
p()
|
|
885
408
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
)
|
|
891
|
-
console.print(
|
|
892
|
-
" • Get URL from: [cyan]🎪 abc123f 🌐 52.1.2.3:8080[/cyan] → http://52.1.2.3:8080"
|
|
893
|
-
)
|
|
894
|
-
console.print()
|
|
895
|
-
|
|
896
|
-
console.print("[bold]2. Freeze Environment (Optional):[/bold]")
|
|
897
|
-
console.print(" • Add label: [orange]🎪 🧊 showtime-freeze[/orange]")
|
|
898
|
-
console.print(" • Result: Environment won't auto-update on new commits")
|
|
899
|
-
console.print(" • Use case: Test specific SHA while continuing development")
|
|
900
|
-
console.print()
|
|
901
|
-
|
|
902
|
-
console.print("[bold]3. Update to New Commit (Automatic):[/bold]")
|
|
903
|
-
console.print(" • New commit pushed → Automatic blue-green rolling update")
|
|
904
|
-
console.print(
|
|
905
|
-
" • Watch for: [blue]🎪 abc123f 🚦 updating[/blue] → [green]🎪 def456a 🚦 running[/green]"
|
|
906
|
-
)
|
|
907
|
-
console.print(" • SHA changes: [cyan]🎪 🎯 abc123f[/cyan] → [cyan]🎪 🎯 def456a[/cyan]")
|
|
908
|
-
console.print()
|
|
409
|
+
p("[bold]1. Create Environment:[/bold]")
|
|
410
|
+
p(" • Add label: [green]🎪 ⚡ showtime-trigger-start[/green]")
|
|
411
|
+
p(" • Watch for: [blue]🎪 abc123f 🚦 building[/blue] → [green]🎪 abc123f 🚦 running[/green]")
|
|
412
|
+
p(" • Get URL from: [cyan]🎪 abc123f 🌐 52.1.2.3:8080[/cyan] → http://52.1.2.3:8080")
|
|
413
|
+
p()
|
|
909
414
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
415
|
+
p("[bold]2. Freeze Environment (Optional):[/bold]")
|
|
416
|
+
p(" • Add label: [orange]🎪 🧊 showtime-freeze[/orange]")
|
|
417
|
+
p(" • Result: Environment won't auto-update on new commits")
|
|
418
|
+
p(" • Use case: Test specific SHA while continuing development")
|
|
419
|
+
p()
|
|
914
420
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
421
|
+
p("[bold]3. Update to New Commit (Automatic):[/bold]")
|
|
422
|
+
p(" • New commit pushed → Automatic blue-green rolling update")
|
|
423
|
+
p(" • Watch for: [blue]🎪 abc123f 🚦 updating[/blue] → [green]🎪 def456a 🚦 running[/green]")
|
|
424
|
+
p(" • SHA changes: [cyan]🎪 🎯 abc123f[/cyan] → [cyan]🎪 🎯 def456a[/cyan]")
|
|
425
|
+
p()
|
|
920
426
|
|
|
921
|
-
|
|
427
|
+
p("[bold]4. Clean Up:[/bold]")
|
|
428
|
+
p(" • Add label: [red]🎪 🛑 showtime-trigger-stop[/red]")
|
|
429
|
+
p(" • Result: All 🎪 labels removed, AWS resources deleted")
|
|
430
|
+
p()
|
|
922
431
|
|
|
432
|
+
p("[bold]📊 Understanding State:[/bold]")
|
|
433
|
+
p("• [dim]TTL labels show policy (24h, 48h, close) not time remaining[/dim]")
|
|
434
|
+
p("• [dim]Use 'showtime status {pr-id}' to calculate actual time remaining[/dim]")
|
|
435
|
+
p("• [dim]Multiple SHA labels during updates (🎯 active, 🏗️ building)[/dim]")
|
|
436
|
+
p()
|
|
923
437
|
|
|
924
|
-
|
|
925
|
-
def test_lifecycle(
|
|
926
|
-
pr_number: int,
|
|
927
|
-
dry_run_aws: bool = typer.Option(
|
|
928
|
-
True, "--dry-run-aws/--real-aws", help="Use mock AWS operations"
|
|
929
|
-
),
|
|
930
|
-
dry_run_github: bool = typer.Option(
|
|
931
|
-
True, "--dry-run-github/--real-github", help="Use mock GitHub operations"
|
|
932
|
-
),
|
|
933
|
-
aws_sleep: int = typer.Option(10, "--aws-sleep", help="Seconds to sleep during AWS operations"),
|
|
934
|
-
):
|
|
935
|
-
"""🎪 Test full environment lifecycle with mock triggers"""
|
|
936
|
-
|
|
937
|
-
console.print(f"🎪 [bold blue]Testing full lifecycle for PR #{pr_number}[/bold blue]")
|
|
938
|
-
console.print(
|
|
939
|
-
f"AWS: {'DRY-RUN' if dry_run_aws else 'REAL'}, GitHub: {'DRY-RUN' if dry_run_github else 'REAL'}"
|
|
940
|
-
)
|
|
941
|
-
console.print()
|
|
942
|
-
|
|
943
|
-
try:
|
|
944
|
-
github = GitHubInterface()
|
|
945
|
-
|
|
946
|
-
console.print("🎪 [bold]Step 1: Simulate trigger-start[/bold]")
|
|
947
|
-
_handle_start_trigger(pr_number, github, dry_run_aws, dry_run_github, aws_sleep, None)
|
|
948
|
-
|
|
949
|
-
console.print()
|
|
950
|
-
console.print("🎪 [bold]Step 2: Simulate config update[/bold]")
|
|
951
|
-
console.print("🎪 [dim]Config changes now done via code commits, not labels[/dim]")
|
|
952
|
-
|
|
953
|
-
console.print()
|
|
954
|
-
console.print("🎪 [bold]Step 3: Simulate trigger-sync (new commit)[/bold]")
|
|
955
|
-
_handle_sync_trigger(pr_number, github, dry_run_aws, dry_run_github, aws_sleep)
|
|
956
|
-
|
|
957
|
-
console.print()
|
|
958
|
-
console.print("🎪 [bold]Step 4: Simulate trigger-stop[/bold]")
|
|
959
|
-
_handle_stop_trigger(pr_number, github, dry_run_aws, dry_run_github)
|
|
960
|
-
|
|
961
|
-
console.print()
|
|
962
|
-
console.print("🎪 [bold green]Full lifecycle test complete![/bold green]")
|
|
963
|
-
|
|
964
|
-
except Exception as e:
|
|
965
|
-
console.print(f"🎪 [bold red]Lifecycle test failed:[/bold red] {e}")
|
|
438
|
+
p("[dim]💡 Tip: Only maintainers with write access can add trigger labels[/dim]")
|
|
966
439
|
|
|
967
440
|
|
|
968
441
|
@app.command()
|
|
@@ -987,224 +460,118 @@ def sync(
|
|
|
987
460
|
docker_tag: Optional[str] = typer.Option(
|
|
988
461
|
None, "--docker-tag", help="Override Docker image tag (e.g., pr-34639-9a82c20-ci, latest)"
|
|
989
462
|
),
|
|
990
|
-
):
|
|
463
|
+
) -> None:
|
|
991
464
|
"""🎪 Intelligently sync PR to desired state (called by GitHub Actions)"""
|
|
992
465
|
try:
|
|
993
|
-
|
|
994
|
-
pr = PullRequest.from_id(pr_number
|
|
995
|
-
|
|
996
|
-
# Get PR metadata for state-based decisions
|
|
997
|
-
pr_data = github.get_pr_data(pr_number)
|
|
998
|
-
pr_state = pr_data.get("state", "open") # open, closed
|
|
466
|
+
# Use singletons - no interface creation needed
|
|
467
|
+
pr = PullRequest.from_id(pr_number)
|
|
999
468
|
|
|
1000
|
-
# Get SHA - use provided SHA or default to latest
|
|
469
|
+
# Get target SHA - use provided SHA or default to latest
|
|
1001
470
|
if sha:
|
|
1002
471
|
target_sha = sha
|
|
1003
|
-
|
|
472
|
+
p(f"🎪 Using specified SHA: {target_sha[:7]}")
|
|
1004
473
|
else:
|
|
1005
|
-
|
|
1006
|
-
|
|
474
|
+
from .core.pull_request import get_github
|
|
475
|
+
|
|
476
|
+
target_sha = get_github().get_latest_commit_sha(pr_number)
|
|
477
|
+
p(f"🎪 Using latest SHA: {target_sha[:7]}")
|
|
478
|
+
|
|
479
|
+
# Get PR state for analysis
|
|
480
|
+
from .core.pull_request import get_github
|
|
1007
481
|
|
|
1008
|
-
|
|
1009
|
-
|
|
482
|
+
pr_data = get_github().get_pr_data(pr_number)
|
|
483
|
+
pr_state = pr_data.get("state", "open")
|
|
1010
484
|
|
|
1011
485
|
if check_only:
|
|
1012
|
-
#
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
console.print(f"pr_number={pr_number}")
|
|
1019
|
-
console.print(f"target_sha={target_sha}")
|
|
486
|
+
# Analysis mode - just return what's needed
|
|
487
|
+
analysis_result = pr.analyze(target_sha, pr_state)
|
|
488
|
+
p(f"build_needed={str(analysis_result.build_needed).lower()}")
|
|
489
|
+
p(f"sync_needed={str(analysis_result.sync_needed).lower()}")
|
|
490
|
+
p(f"pr_number={pr_number}")
|
|
491
|
+
p(f"target_sha={target_sha}")
|
|
1020
492
|
return
|
|
1021
493
|
|
|
1022
|
-
#
|
|
1023
|
-
|
|
1024
|
-
if not check_only:
|
|
1025
|
-
console.print(
|
|
1026
|
-
f"🎪 [bold blue]Syncing PR #{pr_number}[/bold blue] (SHA: {target_sha[:7]})"
|
|
1027
|
-
)
|
|
1028
|
-
console.print(f"🎪 Action needed: {action_needed}")
|
|
1029
|
-
|
|
1030
|
-
# For trigger-based actions, use atomic claim to prevent race conditions
|
|
1031
|
-
if action_needed in ["create_environment", "rolling_update", "destroy_environment"]:
|
|
1032
|
-
if not _atomic_claim_environment(pr_number, target_sha, github, dry_run_github):
|
|
1033
|
-
console.print("🎪 Unable to claim environment - exiting")
|
|
1034
|
-
return
|
|
1035
|
-
console.print("🎪 ✅ Environment claimed - proceeding with work")
|
|
1036
|
-
|
|
1037
|
-
# Execute based on determined action
|
|
1038
|
-
if action_needed == "cleanup":
|
|
1039
|
-
console.print("🎪 PR is closed - cleaning up environment")
|
|
1040
|
-
if pr.current_show:
|
|
1041
|
-
_handle_stop_trigger(pr_number, github, dry_run_aws, dry_run_github)
|
|
1042
|
-
else:
|
|
1043
|
-
console.print("🎪 No environment to clean up")
|
|
1044
|
-
return
|
|
494
|
+
# Execution mode - do the sync
|
|
495
|
+
p(f"🎪 [bold blue]Syncing PR #{pr_number}[/bold blue] (SHA: {target_sha[:7]})")
|
|
1045
496
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
console.print("🎪 Posting building comment...")
|
|
1053
|
-
elif action_needed == "rolling_update":
|
|
1054
|
-
console.print("🎪 Posting rolling update comment...")
|
|
1055
|
-
|
|
1056
|
-
# Build Docker image
|
|
1057
|
-
build_success = _build_docker_image(pr_number, target_sha, dry_run_docker)
|
|
1058
|
-
if not build_success:
|
|
1059
|
-
_set_state_internal(
|
|
1060
|
-
"failed",
|
|
1061
|
-
pr_number,
|
|
1062
|
-
Show(
|
|
1063
|
-
pr_number=pr_number,
|
|
1064
|
-
sha=short_sha(target_sha),
|
|
1065
|
-
status="failed",
|
|
1066
|
-
requested_by=_get_github_actor(),
|
|
1067
|
-
),
|
|
1068
|
-
github,
|
|
1069
|
-
dry_run_github,
|
|
1070
|
-
"Docker build failed",
|
|
1071
|
-
)
|
|
1072
|
-
return
|
|
1073
|
-
|
|
1074
|
-
# Continue with AWS deployment (reuse existing logic)
|
|
1075
|
-
_handle_start_trigger(
|
|
1076
|
-
pr_number,
|
|
1077
|
-
github,
|
|
1078
|
-
dry_run_aws,
|
|
1079
|
-
dry_run_github,
|
|
1080
|
-
aws_sleep,
|
|
1081
|
-
docker_tag,
|
|
1082
|
-
force=True,
|
|
497
|
+
# Handle closed PRs specially
|
|
498
|
+
if pr_state == "closed":
|
|
499
|
+
p("🎪 PR is closed - cleaning up environment")
|
|
500
|
+
if pr.current_show:
|
|
501
|
+
stop_result = pr.stop_environment(
|
|
502
|
+
dry_run_github=dry_run_github, dry_run_aws=dry_run_aws
|
|
1083
503
|
)
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
elif action_needed == "destroy_environment":
|
|
1087
|
-
console.print("🎪 Destroying environment")
|
|
1088
|
-
_handle_stop_trigger(pr_number, github, dry_run_aws, dry_run_github)
|
|
1089
|
-
return
|
|
1090
|
-
|
|
1091
|
-
elif action_needed == "auto_sync":
|
|
1092
|
-
console.print("🎪 Auto-sync on new commit")
|
|
1093
|
-
# This also requires build + deployment
|
|
1094
|
-
build_success = _build_docker_image(pr_number, target_sha, dry_run_docker)
|
|
1095
|
-
if build_success:
|
|
1096
|
-
_handle_sync_trigger(pr_number, github, dry_run_aws, dry_run_github, aws_sleep)
|
|
504
|
+
if stop_result.success:
|
|
505
|
+
p("🎪 ✅ Cleanup completed")
|
|
1097
506
|
else:
|
|
1098
|
-
|
|
1099
|
-
return
|
|
1100
|
-
|
|
1101
|
-
else:
|
|
1102
|
-
console.print(f"🎪 No action needed ({action_needed})")
|
|
1103
|
-
return
|
|
1104
|
-
|
|
1105
|
-
console.print(
|
|
1106
|
-
f"🎪 [bold blue]Syncing PR #{pr_number}[/bold blue] (state: {pr_state}, SHA: {target_sha[:7]})"
|
|
1107
|
-
)
|
|
1108
|
-
console.print(f"🎪 Action needed: {action_needed}")
|
|
1109
|
-
|
|
1110
|
-
# Execute the determined action
|
|
1111
|
-
if action_needed == "cleanup":
|
|
1112
|
-
console.print("🎪 PR is closed - cleaning up environment")
|
|
1113
|
-
if pr.current_show:
|
|
1114
|
-
_handle_stop_trigger(pr_number, github, dry_run_aws, dry_run_github)
|
|
507
|
+
p(f"🎪 ❌ Cleanup failed: {stop_result.error}")
|
|
1115
508
|
else:
|
|
1116
|
-
|
|
509
|
+
p("🎪 No environment to clean up")
|
|
1117
510
|
return
|
|
1118
511
|
|
|
1119
|
-
#
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
for trigger in trigger_labels:
|
|
1127
|
-
console.print(f"🎪 Processing: {trigger}")
|
|
1128
|
-
|
|
1129
|
-
# Remove trigger label immediately (atomic operation)
|
|
1130
|
-
if not dry_run_github:
|
|
1131
|
-
github.remove_label(pr_number, trigger)
|
|
1132
|
-
else:
|
|
1133
|
-
console.print(
|
|
1134
|
-
f"🎪 [bold yellow]DRY-RUN-GITHUB[/bold yellow] - Would remove: {trigger}"
|
|
1135
|
-
)
|
|
1136
|
-
|
|
1137
|
-
# Process the trigger
|
|
1138
|
-
if "showtime-trigger-start" in trigger:
|
|
1139
|
-
_handle_start_trigger(
|
|
1140
|
-
pr_number, github, dry_run_aws, dry_run_github, aws_sleep, docker_tag
|
|
1141
|
-
)
|
|
1142
|
-
elif "showtime-trigger-stop" in trigger:
|
|
1143
|
-
_handle_stop_trigger(pr_number, github, dry_run_aws, dry_run_github)
|
|
1144
|
-
|
|
1145
|
-
console.print("🎪 All explicit triggers processed!")
|
|
1146
|
-
return
|
|
1147
|
-
|
|
1148
|
-
# 4. No explicit triggers - check for implicit sync needs
|
|
1149
|
-
console.print("🎪 No explicit triggers found - checking for implicit sync needs")
|
|
512
|
+
# Regular sync for open PRs
|
|
513
|
+
result = pr.sync(
|
|
514
|
+
target_sha,
|
|
515
|
+
dry_run_github=dry_run_github,
|
|
516
|
+
dry_run_aws=dry_run_aws,
|
|
517
|
+
dry_run_docker=dry_run_docker,
|
|
518
|
+
)
|
|
1150
519
|
|
|
1151
|
-
if
|
|
1152
|
-
|
|
1153
|
-
if pr.current_show.needs_update(target_sha):
|
|
1154
|
-
console.print(
|
|
1155
|
-
f"🎪 Environment outdated ({pr.current_show.sha} → {target_sha[:7]}) - auto-syncing"
|
|
1156
|
-
)
|
|
1157
|
-
_handle_sync_trigger(pr_number, github, dry_run_aws, dry_run_github, aws_sleep)
|
|
1158
|
-
else:
|
|
1159
|
-
console.print(f"🎪 Environment is up to date ({pr.current_show.sha})")
|
|
520
|
+
if result.success:
|
|
521
|
+
p(f"🎪 ✅ Sync completed: {result.action_taken}")
|
|
1160
522
|
else:
|
|
1161
|
-
|
|
1162
|
-
|
|
523
|
+
p(f"🎪 ❌ Sync failed: {result.error}")
|
|
524
|
+
raise typer.Exit(1)
|
|
1163
525
|
|
|
526
|
+
except GitHubError as e:
|
|
527
|
+
p(f"❌ GitHub error: {e}")
|
|
528
|
+
raise typer.Exit(1) from e
|
|
1164
529
|
except Exception as e:
|
|
1165
|
-
|
|
530
|
+
p(f"❌ Error: {e}")
|
|
531
|
+
raise typer.Exit(1) from e
|
|
1166
532
|
|
|
1167
533
|
|
|
1168
534
|
@app.command()
|
|
1169
|
-
def handle_sync(pr_number: int):
|
|
535
|
+
def handle_sync(pr_number: int) -> None:
|
|
1170
536
|
"""🎪 Handle new commit sync (called by GitHub Actions on PR synchronize)"""
|
|
1171
537
|
try:
|
|
1172
|
-
|
|
1173
|
-
pr = PullRequest.from_id(pr_number, github)
|
|
538
|
+
pr = PullRequest.from_id(pr_number)
|
|
1174
539
|
|
|
1175
540
|
# Only sync if there's an active environment
|
|
1176
541
|
if not pr.current_show:
|
|
1177
|
-
|
|
542
|
+
p(f"🎪 No active environment for PR #{pr_number} - skipping sync")
|
|
1178
543
|
return
|
|
1179
544
|
|
|
1180
545
|
# Get latest commit SHA
|
|
1181
|
-
|
|
546
|
+
from .core.pull_request import get_github
|
|
547
|
+
|
|
548
|
+
latest_sha = get_github().get_latest_commit_sha(pr_number)
|
|
1182
549
|
|
|
1183
550
|
# Check if update is needed
|
|
1184
551
|
if not pr.current_show.needs_update(latest_sha):
|
|
1185
|
-
|
|
552
|
+
p(f"🎪 Environment already up to date for PR #{pr_number}")
|
|
1186
553
|
return
|
|
1187
554
|
|
|
1188
|
-
|
|
555
|
+
p(f"🎪 Syncing PR #{pr_number} to commit {latest_sha[:7]}")
|
|
1189
556
|
|
|
1190
557
|
# TODO: Implement rolling update logic
|
|
1191
|
-
|
|
558
|
+
p("🎪 [bold yellow]Sync logic not yet implemented[/bold yellow]")
|
|
1192
559
|
|
|
1193
560
|
except Exception as e:
|
|
1194
|
-
|
|
561
|
+
p(f"🎪 [bold red]Error handling sync:[/bold red] {e}")
|
|
1195
562
|
|
|
1196
563
|
|
|
1197
564
|
@app.command()
|
|
1198
565
|
def setup_labels(
|
|
1199
566
|
dry_run: bool = typer.Option(False, "--dry-run", help="Show what labels would be created"),
|
|
1200
|
-
):
|
|
567
|
+
) -> None:
|
|
1201
568
|
"""🎪 Set up GitHub label definitions with colors and descriptions"""
|
|
1202
569
|
try:
|
|
1203
570
|
from .core.label_colors import LABEL_DEFINITIONS
|
|
1204
571
|
|
|
1205
572
|
github = GitHubInterface()
|
|
1206
573
|
|
|
1207
|
-
|
|
574
|
+
p("🎪 [bold blue]Setting up circus tent label definitions...[/bold blue]")
|
|
1208
575
|
|
|
1209
576
|
created_count = 0
|
|
1210
577
|
updated_count = 0
|
|
@@ -1214,32 +581,32 @@ def setup_labels(
|
|
|
1214
581
|
description = definition["description"]
|
|
1215
582
|
|
|
1216
583
|
if dry_run:
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
584
|
+
p(f"🏷️ Would create: [bold]{label_name}[/bold]")
|
|
585
|
+
p(f" Color: #{color}")
|
|
586
|
+
p(f" Description: {description}")
|
|
1220
587
|
else:
|
|
1221
588
|
try:
|
|
1222
589
|
# Try to create or update the label
|
|
1223
590
|
success = github.create_or_update_label(label_name, color, description)
|
|
1224
591
|
if success:
|
|
1225
592
|
created_count += 1
|
|
1226
|
-
|
|
593
|
+
p(f"✅ Created: [bold]{label_name}[/bold]")
|
|
1227
594
|
else:
|
|
1228
595
|
updated_count += 1
|
|
1229
|
-
|
|
596
|
+
p(f"🔄 Updated: [bold]{label_name}[/bold]")
|
|
1230
597
|
except Exception as e:
|
|
1231
|
-
|
|
598
|
+
p(f"❌ Failed to create {label_name}: {e}")
|
|
1232
599
|
|
|
1233
600
|
if not dry_run:
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
601
|
+
p("\n🎪 [bold green]Label setup complete![/bold green]")
|
|
602
|
+
p(f" 📊 Created: {created_count}")
|
|
603
|
+
p(f" 🔄 Updated: {updated_count}")
|
|
604
|
+
p(
|
|
1238
605
|
"\n🎪 [dim]Note: Dynamic labels (with SHA) are created automatically during deployment[/dim]"
|
|
1239
606
|
)
|
|
1240
607
|
|
|
1241
608
|
except Exception as e:
|
|
1242
|
-
|
|
609
|
+
p(f"🎪 [bold red]Error setting up labels:[/bold red] {e}")
|
|
1243
610
|
|
|
1244
611
|
|
|
1245
612
|
@app.command()
|
|
@@ -1259,667 +626,45 @@ def cleanup(
|
|
|
1259
626
|
"--cleanup-labels/--no-cleanup-labels",
|
|
1260
627
|
help="Also cleanup SHA-based label definitions from repository",
|
|
1261
628
|
),
|
|
1262
|
-
):
|
|
629
|
+
) -> None:
|
|
1263
630
|
"""🎪 Clean up orphaned or expired environments and labels"""
|
|
1264
631
|
try:
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
# Step 1: Clean up expired AWS ECS services
|
|
1268
|
-
console.print("🎪 [bold blue]Checking AWS ECS services for cleanup...[/bold blue]")
|
|
1269
|
-
|
|
1270
|
-
from .core.aws import AWSInterface
|
|
1271
|
-
|
|
1272
|
-
aws = AWSInterface()
|
|
1273
|
-
|
|
1274
|
-
try:
|
|
1275
|
-
expired_services = aws.find_expired_services(older_than)
|
|
1276
|
-
|
|
1277
|
-
if expired_services:
|
|
1278
|
-
console.print(f"🎪 Found {len(expired_services)} expired ECS services")
|
|
1279
|
-
|
|
1280
|
-
for service_info in expired_services:
|
|
1281
|
-
service_name = service_info["service_name"]
|
|
1282
|
-
pr_number = service_info["pr_number"]
|
|
1283
|
-
age_hours = service_info["age_hours"]
|
|
1284
|
-
|
|
1285
|
-
if dry_run:
|
|
1286
|
-
console.print(
|
|
1287
|
-
f"🎪 [yellow]Would delete service {service_name} (PR #{pr_number}, {age_hours:.1f}h old)[/yellow]"
|
|
1288
|
-
)
|
|
1289
|
-
console.print(
|
|
1290
|
-
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]"
|
|
1291
|
-
)
|
|
1292
|
-
else:
|
|
1293
|
-
console.print(
|
|
1294
|
-
f"🎪 Deleting expired service {service_name} (PR #{pr_number}, {age_hours:.1f}h old)"
|
|
1295
|
-
)
|
|
1296
|
-
# Create minimal Show object for URL generation
|
|
1297
|
-
from .core.circus import Show
|
|
1298
|
-
|
|
1299
|
-
temp_show = Show(
|
|
1300
|
-
pr_number=pr_number, sha=service_name.split("-")[2], status="cleanup"
|
|
1301
|
-
)
|
|
1302
|
-
_show_service_urls(temp_show, "cleanup")
|
|
1303
|
-
|
|
1304
|
-
# Delete ECS service
|
|
1305
|
-
if aws._delete_ecs_service(service_name):
|
|
1306
|
-
# Delete ECR image
|
|
1307
|
-
image_tag = f"pr-{pr_number}-ci"
|
|
1308
|
-
aws._delete_ecr_image(image_tag)
|
|
1309
|
-
console.print(f"🎪 ✅ Cleaned up {service_name}")
|
|
1310
|
-
else:
|
|
1311
|
-
console.print(f"🎪 ❌ Failed to clean up {service_name}")
|
|
1312
|
-
else:
|
|
1313
|
-
console.print("🎪 [dim]No expired ECS services found[/dim]")
|
|
1314
|
-
|
|
1315
|
-
except Exception as e:
|
|
1316
|
-
console.print(f"🎪 [bold red]AWS cleanup failed:[/bold red] {e}")
|
|
1317
|
-
|
|
1318
|
-
# Step 2: Find and clean up expired environments from PRs
|
|
1319
|
-
if respect_ttl:
|
|
1320
|
-
console.print("🎪 Finding environments expired based on individual TTL labels")
|
|
1321
|
-
else:
|
|
1322
|
-
console.print(f"🎪 Finding environments older than {older_than}")
|
|
1323
|
-
prs_with_shows = github.find_prs_with_shows()
|
|
1324
|
-
|
|
1325
|
-
if not prs_with_shows:
|
|
1326
|
-
console.print("🎪 [dim]No PRs with circus tent labels found[/dim]")
|
|
1327
|
-
else:
|
|
1328
|
-
console.print(f"🎪 Found {len(prs_with_shows)} PRs with shows")
|
|
1329
|
-
|
|
1330
|
-
import re
|
|
1331
|
-
from datetime import datetime, timedelta
|
|
1332
|
-
|
|
1333
|
-
from .core.circus import PullRequest, get_effective_ttl, parse_ttl_days
|
|
1334
|
-
|
|
1335
|
-
# Parse max_age if provided (safety ceiling)
|
|
1336
|
-
max_age_days = None
|
|
1337
|
-
if max_age:
|
|
1338
|
-
max_age_days = parse_ttl_days(max_age)
|
|
1339
|
-
|
|
1340
|
-
cleaned_prs = 0
|
|
1341
|
-
for pr_number in prs_with_shows:
|
|
1342
|
-
try:
|
|
1343
|
-
pr = PullRequest.from_id(pr_number, github)
|
|
1344
|
-
expired_shows = []
|
|
1345
|
-
|
|
1346
|
-
if respect_ttl:
|
|
1347
|
-
# Use individual TTL labels
|
|
1348
|
-
effective_ttl_days = get_effective_ttl(pr)
|
|
1349
|
-
|
|
1350
|
-
if effective_ttl_days is None:
|
|
1351
|
-
# "never" label found - skip cleanup
|
|
1352
|
-
console.print(
|
|
1353
|
-
f"🎪 [blue]PR #{pr_number} marked as 'never expire' - skipping[/blue]"
|
|
1354
|
-
)
|
|
1355
|
-
continue
|
|
1356
|
-
|
|
1357
|
-
# Apply max_age ceiling if specified
|
|
1358
|
-
if max_age_days and effective_ttl_days > max_age_days:
|
|
1359
|
-
console.print(
|
|
1360
|
-
f"🎪 [yellow]PR #{pr_number} TTL ({effective_ttl_days}d) exceeds max-age ({max_age_days}d)[/yellow]"
|
|
1361
|
-
)
|
|
1362
|
-
effective_ttl_days = max_age_days
|
|
1363
|
-
|
|
1364
|
-
cutoff_time = datetime.now() - timedelta(days=effective_ttl_days)
|
|
1365
|
-
console.print(
|
|
1366
|
-
f"🎪 PR #{pr_number} effective TTL: {effective_ttl_days} days"
|
|
1367
|
-
)
|
|
1368
|
-
|
|
1369
|
-
else:
|
|
1370
|
-
# Use global older_than parameter (current behavior)
|
|
1371
|
-
time_match = re.match(r"(\d+)([hd])", older_than)
|
|
1372
|
-
if not time_match:
|
|
1373
|
-
console.print(
|
|
1374
|
-
f"🎪 [bold red]Invalid time format:[/bold red] {older_than}"
|
|
1375
|
-
)
|
|
1376
|
-
return
|
|
1377
|
-
|
|
1378
|
-
hours = int(time_match.group(1))
|
|
1379
|
-
if time_match.group(2) == "d":
|
|
1380
|
-
hours *= 24
|
|
1381
|
-
|
|
1382
|
-
cutoff_time = datetime.now() - timedelta(hours=hours)
|
|
1383
|
-
|
|
1384
|
-
# Check all shows in the PR for expiration
|
|
1385
|
-
for show in pr.shows:
|
|
1386
|
-
if show.created_at:
|
|
1387
|
-
try:
|
|
1388
|
-
# Parse timestamp (format: 2024-01-15T14-30)
|
|
1389
|
-
show_time = datetime.fromisoformat(
|
|
1390
|
-
show.created_at.replace("-", ":")
|
|
1391
|
-
)
|
|
1392
|
-
if show_time < cutoff_time:
|
|
1393
|
-
expired_shows.append(show)
|
|
1394
|
-
except (ValueError, AttributeError):
|
|
1395
|
-
# If we can't parse the timestamp, consider it expired
|
|
1396
|
-
expired_shows.append(show)
|
|
1397
|
-
|
|
1398
|
-
if expired_shows:
|
|
1399
|
-
if dry_run:
|
|
1400
|
-
console.print(
|
|
1401
|
-
f"🎪 [yellow]Would clean {len(expired_shows)} expired shows from PR #{pr_number}[/yellow]"
|
|
1402
|
-
)
|
|
1403
|
-
for show in expired_shows:
|
|
1404
|
-
console.print(f" - SHA {show.sha} ({show.status})")
|
|
1405
|
-
else:
|
|
1406
|
-
console.print(
|
|
1407
|
-
f"🎪 Cleaning {len(expired_shows)} expired shows from PR #{pr_number}"
|
|
1408
|
-
)
|
|
1409
|
-
|
|
1410
|
-
# Remove circus labels for expired shows
|
|
1411
|
-
current_labels = github.get_circus_labels(pr_number)
|
|
1412
|
-
labels_to_keep = []
|
|
1413
|
-
|
|
1414
|
-
for label in current_labels:
|
|
1415
|
-
# Keep labels that don't belong to expired shows
|
|
1416
|
-
should_keep = True
|
|
1417
|
-
for expired_show in expired_shows:
|
|
1418
|
-
if expired_show.sha in label:
|
|
1419
|
-
should_keep = False
|
|
1420
|
-
break
|
|
1421
|
-
if should_keep:
|
|
1422
|
-
labels_to_keep.append(label)
|
|
1423
|
-
|
|
1424
|
-
# Update PR labels
|
|
1425
|
-
github.remove_circus_labels(pr_number)
|
|
1426
|
-
for label in labels_to_keep:
|
|
1427
|
-
github.add_label(pr_number, label)
|
|
1428
|
-
|
|
1429
|
-
cleaned_prs += 1
|
|
1430
|
-
|
|
1431
|
-
except Exception as e:
|
|
1432
|
-
console.print(f"🎪 [red]Error processing PR #{pr_number}:[/red] {e}")
|
|
1433
|
-
|
|
1434
|
-
if not dry_run and cleaned_prs > 0:
|
|
1435
|
-
console.print(f"🎪 [green]Cleaned up environments from {cleaned_prs} PRs[/green]")
|
|
1436
|
-
|
|
1437
|
-
# Step 2: Clean up SHA-based label definitions from repository
|
|
1438
|
-
if cleanup_labels:
|
|
1439
|
-
console.print("🎪 Finding SHA-based labels in repository")
|
|
1440
|
-
sha_labels = github.cleanup_sha_labels(dry_run=dry_run)
|
|
1441
|
-
|
|
1442
|
-
if sha_labels:
|
|
1443
|
-
if dry_run:
|
|
1444
|
-
console.print(
|
|
1445
|
-
f"🎪 [yellow]Would delete {len(sha_labels)} SHA-based label definitions:[/yellow]"
|
|
1446
|
-
)
|
|
1447
|
-
for label in sha_labels[:10]: # Show first 10
|
|
1448
|
-
console.print(f" - {label}")
|
|
1449
|
-
if len(sha_labels) > 10:
|
|
1450
|
-
console.print(f" ... and {len(sha_labels) - 10} more")
|
|
1451
|
-
else:
|
|
1452
|
-
console.print(
|
|
1453
|
-
f"🎪 [green]Deleted {len(sha_labels)} SHA-based label definitions[/green]"
|
|
1454
|
-
)
|
|
1455
|
-
else:
|
|
1456
|
-
console.print("🎪 [dim]No SHA-based labels found to clean[/dim]")
|
|
1457
|
-
|
|
1458
|
-
except Exception as e:
|
|
1459
|
-
console.print(f"🎪 [bold red]Error during cleanup:[/bold red] {e}")
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
# Helper functions for trigger processing
|
|
1463
|
-
def _handle_start_trigger(
|
|
1464
|
-
pr_number: int,
|
|
1465
|
-
github: GitHubInterface,
|
|
1466
|
-
dry_run_aws: bool = False,
|
|
1467
|
-
dry_run_github: bool = False,
|
|
1468
|
-
aws_sleep: int = 0,
|
|
1469
|
-
docker_tag_override: Optional[str] = None,
|
|
1470
|
-
force: bool = False,
|
|
1471
|
-
):
|
|
1472
|
-
"""Handle start trigger"""
|
|
1473
|
-
import time
|
|
1474
|
-
from datetime import datetime
|
|
1475
|
-
|
|
1476
|
-
console.print(f"🎪 Starting environment for PR #{pr_number}")
|
|
1477
|
-
|
|
1478
|
-
try:
|
|
1479
|
-
# Get latest SHA and GitHub actor
|
|
1480
|
-
latest_sha = github.get_latest_commit_sha(pr_number)
|
|
1481
|
-
github_actor = _get_github_actor()
|
|
1482
|
-
|
|
1483
|
-
# Create new show
|
|
1484
|
-
show = Show(
|
|
1485
|
-
pr_number=pr_number,
|
|
1486
|
-
sha=short_sha(latest_sha),
|
|
1487
|
-
status="building",
|
|
1488
|
-
created_at=datetime.utcnow().strftime("%Y-%m-%dT%H-%M"),
|
|
1489
|
-
ttl="24h",
|
|
1490
|
-
requested_by=github_actor,
|
|
1491
|
-
)
|
|
1492
|
-
|
|
1493
|
-
console.print(f"🎪 Creating environment {show.aws_service_name}")
|
|
1494
|
-
|
|
1495
|
-
# Post confirmation comment
|
|
1496
|
-
confirmation_comment = start_comment(show)
|
|
1497
|
-
|
|
1498
|
-
if not dry_run_github:
|
|
1499
|
-
github.post_comment(pr_number, confirmation_comment)
|
|
1500
|
-
|
|
1501
|
-
# Set building state labels
|
|
1502
|
-
building_labels = show.to_circus_labels()
|
|
1503
|
-
console.print("🎪 Setting building state labels:")
|
|
1504
|
-
for label in building_labels:
|
|
1505
|
-
console.print(f" + {label}")
|
|
1506
|
-
|
|
1507
|
-
# Set building labels
|
|
1508
|
-
if not dry_run_github:
|
|
1509
|
-
# Actually set the labels for real testing
|
|
1510
|
-
console.print("🎪 Setting labels on GitHub...")
|
|
1511
|
-
# Remove existing circus labels first
|
|
1512
|
-
github.remove_circus_labels(pr_number)
|
|
1513
|
-
# Add new labels one by one
|
|
1514
|
-
for label in building_labels:
|
|
1515
|
-
github.add_label(pr_number, label)
|
|
1516
|
-
console.print("🎪 ✅ Labels set on GitHub!")
|
|
1517
|
-
else:
|
|
1518
|
-
console.print("🎪 [bold yellow]DRY-RUN-GITHUB[/bold yellow] - Would set labels")
|
|
1519
|
-
|
|
1520
|
-
if dry_run_aws:
|
|
1521
|
-
console.print("🎪 [bold yellow]DRY-RUN-AWS[/bold yellow] - Skipping AWS operations")
|
|
1522
|
-
if aws_sleep > 0:
|
|
1523
|
-
console.print(f"🎪 Sleeping {aws_sleep}s to simulate AWS build time...")
|
|
1524
|
-
time.sleep(aws_sleep)
|
|
1525
|
-
|
|
1526
|
-
# Mock successful deployment
|
|
1527
|
-
mock_ip = "52.1.2.3"
|
|
1528
|
-
console.print(
|
|
1529
|
-
f"🎪 [bold green]Mock AWS deployment successful![/bold green] IP: {mock_ip}"
|
|
1530
|
-
)
|
|
1531
|
-
|
|
1532
|
-
# Update to running state
|
|
1533
|
-
show.status = "running"
|
|
1534
|
-
show.ip = mock_ip
|
|
1535
|
-
|
|
1536
|
-
running_labels = show.to_circus_labels()
|
|
1537
|
-
console.print("🎪 Setting running state labels:")
|
|
1538
|
-
for label in running_labels:
|
|
1539
|
-
console.print(f" + {label}")
|
|
1540
|
-
|
|
1541
|
-
# Set running labels
|
|
1542
|
-
if not dry_run_github:
|
|
1543
|
-
console.print("🎪 Updating to running state...")
|
|
1544
|
-
# Remove existing circus labels first
|
|
1545
|
-
github.remove_circus_labels(pr_number)
|
|
1546
|
-
# Add new running labels
|
|
1547
|
-
for label in running_labels:
|
|
1548
|
-
github.add_label(pr_number, label)
|
|
1549
|
-
console.print("🎪 ✅ Labels updated to running state!")
|
|
1550
|
-
else:
|
|
1551
|
-
console.print(
|
|
1552
|
-
"🎪 [bold yellow]DRY-RUN-GITHUB[/bold yellow] - Would update to running state"
|
|
1553
|
-
)
|
|
1554
|
-
|
|
1555
|
-
# Post success comment (only in dry-run-aws mode since we have mock IP)
|
|
1556
|
-
# Create mock show with IP for success comment
|
|
1557
|
-
mock_show = Show(
|
|
1558
|
-
pr_number=show.pr_number,
|
|
1559
|
-
sha=show.sha,
|
|
1560
|
-
status="running",
|
|
1561
|
-
ip=mock_ip,
|
|
1562
|
-
ttl=show.ttl,
|
|
1563
|
-
requested_by=show.requested_by,
|
|
1564
|
-
)
|
|
1565
|
-
success_comment_text = success_comment(mock_show)
|
|
1566
|
-
|
|
1567
|
-
if not dry_run_github:
|
|
1568
|
-
github.post_comment(pr_number, success_comment_text)
|
|
1569
|
-
|
|
1570
|
-
else:
|
|
1571
|
-
# Real AWS operations
|
|
1572
|
-
from .core.aws import AWSInterface, EnvironmentResult
|
|
1573
|
-
|
|
1574
|
-
console.print("🎪 [bold blue]Starting AWS deployment...[/bold blue]")
|
|
1575
|
-
aws = AWSInterface()
|
|
1576
|
-
|
|
1577
|
-
# Show logs URL immediately for monitoring
|
|
1578
|
-
_show_service_urls(show, "deployment")
|
|
1579
|
-
|
|
1580
|
-
# Parse feature flags from PR description (replicate GHA feature flag logic)
|
|
1581
|
-
feature_flags = _extract_feature_flags_from_pr(pr_number, github)
|
|
1582
|
-
|
|
1583
|
-
# Create environment (synchronous, matches GHA wait behavior)
|
|
1584
|
-
result: EnvironmentResult = aws.create_environment(
|
|
1585
|
-
pr_number=pr_number,
|
|
1586
|
-
sha=latest_sha,
|
|
1587
|
-
github_user=github_actor,
|
|
1588
|
-
feature_flags=feature_flags,
|
|
1589
|
-
image_tag_override=docker_tag_override,
|
|
1590
|
-
force=force,
|
|
1591
|
-
)
|
|
1592
|
-
|
|
1593
|
-
if result.success:
|
|
1594
|
-
console.print(
|
|
1595
|
-
f"🎪 [bold green]✅ Green service deployed successfully![/bold green] IP: {result.ip}"
|
|
1596
|
-
)
|
|
1597
|
-
|
|
1598
|
-
# Show helpful links for the new service
|
|
1599
|
-
console.print("\n🎪 [bold blue]Useful Links:[/bold blue]")
|
|
1600
|
-
console.print(f" 🌐 Environment: http://{result.ip}:8080")
|
|
1601
|
-
|
|
1602
|
-
# Use centralized URL generation
|
|
1603
|
-
urls = _get_service_urls(show)
|
|
1604
|
-
console.print(f" 📊 ECS Service: {urls['service']}")
|
|
1605
|
-
console.print(f" 📝 Service Logs: {urls['logs']}")
|
|
1606
|
-
console.print(f" 🏥 Health Checks: {urls['health']}")
|
|
1607
|
-
console.print(
|
|
1608
|
-
f" 🔍 GitHub PR: https://github.com/apache/superset/pull/{pr_number}"
|
|
1609
|
-
)
|
|
1610
|
-
console.print(
|
|
1611
|
-
"\n🎪 [dim]Note: Superset takes 2-3 minutes to initialize after container starts[/dim]"
|
|
1612
|
-
)
|
|
1613
|
-
|
|
1614
|
-
# Blue-Green Traffic Switch: Update GitHub labels to point to new service
|
|
1615
|
-
console.print(
|
|
1616
|
-
f"\n🎪 [bold blue]Switching traffic to green service {latest_sha[:7]}...[/bold blue]"
|
|
1617
|
-
)
|
|
1618
|
-
|
|
1619
|
-
# Check for existing services to show blue-green transition
|
|
1620
|
-
from .core.aws import AWSInterface
|
|
1621
|
-
|
|
1622
|
-
aws = AWSInterface()
|
|
1623
|
-
existing_services = aws._find_pr_services(pr_number)
|
|
1624
|
-
|
|
1625
|
-
if len(existing_services) > 1:
|
|
1626
|
-
console.print("🔄 Blue-Green Deployment:")
|
|
1627
|
-
blue_services = []
|
|
1628
|
-
for svc in existing_services:
|
|
1629
|
-
if svc["sha"] == latest_sha[:7]:
|
|
1630
|
-
console.print(
|
|
1631
|
-
f" 🟢 Green: {svc['service_name']} (NEW - receiving traffic)"
|
|
1632
|
-
)
|
|
1633
|
-
else:
|
|
1634
|
-
console.print(
|
|
1635
|
-
f" 🔵 Blue: {svc['service_name']} (OLD - will be cleaned up in 5 minutes)"
|
|
1636
|
-
)
|
|
1637
|
-
blue_services.append(svc)
|
|
1638
|
-
|
|
1639
|
-
# Schedule cleanup of blue services
|
|
1640
|
-
if blue_services:
|
|
1641
|
-
console.print(
|
|
1642
|
-
f"\n🧹 Scheduling cleanup of {len(blue_services)} blue service(s) in 5 minutes..."
|
|
1643
|
-
)
|
|
1644
|
-
_schedule_blue_cleanup(pr_number, blue_services)
|
|
1645
|
-
|
|
1646
|
-
# Update show with deployment result
|
|
1647
|
-
show.ip = result.ip
|
|
1648
|
-
|
|
1649
|
-
# Use internal helper to set running state (posts success comment)
|
|
1650
|
-
console.print("\n🎪 Traffic switching to running state:")
|
|
1651
|
-
_set_state_internal("running", pr_number, show, github, dry_run_github)
|
|
1652
|
-
|
|
1653
|
-
else:
|
|
1654
|
-
console.print(f"🎪 [bold red]❌ AWS deployment failed:[/bold red] {result.error}")
|
|
1655
|
-
|
|
1656
|
-
# Use internal helper to set failed state (posts failure comment)
|
|
1657
|
-
_set_state_internal("failed", pr_number, show, github, dry_run_github, result.error)
|
|
1658
|
-
|
|
1659
|
-
except Exception as e:
|
|
1660
|
-
console.print(f"🎪 [bold red]Start trigger failed:[/bold red] {e}")
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
def _extract_feature_flags_from_pr(pr_number: int, github: GitHubInterface) -> list:
|
|
1664
|
-
"""Extract feature flags from PR description (replicate GHA eval-feature-flags step)"""
|
|
1665
|
-
import re
|
|
1666
|
-
|
|
1667
|
-
try:
|
|
1668
|
-
# Get PR description
|
|
1669
|
-
pr_data = github.get_pr_data(pr_number)
|
|
1670
|
-
description = pr_data.get("body") or ""
|
|
1671
|
-
|
|
1672
|
-
# Replicate exact GHA regex pattern: FEATURE_(\w+)=(\w+)
|
|
1673
|
-
pattern = r"FEATURE_(\w+)=(\w+)"
|
|
1674
|
-
results = []
|
|
1675
|
-
|
|
1676
|
-
for match in re.finditer(pattern, description):
|
|
1677
|
-
feature_config = {"name": f"SUPERSET_FEATURE_{match.group(1)}", "value": match.group(2)}
|
|
1678
|
-
results.append(feature_config)
|
|
1679
|
-
console.print(
|
|
1680
|
-
f"🎪 Found feature flag: {feature_config['name']}={feature_config['value']}"
|
|
1681
|
-
)
|
|
1682
|
-
|
|
1683
|
-
return results
|
|
1684
|
-
|
|
1685
|
-
except Exception as e:
|
|
1686
|
-
console.print(f"🎪 Warning: Could not extract feature flags: {e}")
|
|
1687
|
-
return []
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
def _handle_stop_trigger(
|
|
1691
|
-
pr_number: int, github: GitHubInterface, dry_run_aws: bool = False, dry_run_github: bool = False
|
|
1692
|
-
):
|
|
1693
|
-
"""Handle stop trigger"""
|
|
1694
|
-
|
|
1695
|
-
console.print(f"🎪 Stopping environment for PR #{pr_number}")
|
|
1696
|
-
|
|
1697
|
-
try:
|
|
1698
|
-
pr = PullRequest.from_id(pr_number, github)
|
|
632
|
+
# Parse older_than to hours
|
|
633
|
+
import re
|
|
1699
634
|
|
|
1700
|
-
|
|
1701
|
-
|
|
635
|
+
time_match = re.match(r"(\d+)([hd])", older_than)
|
|
636
|
+
if not time_match:
|
|
637
|
+
p(f"❌ Invalid time format: {older_than}")
|
|
1702
638
|
return
|
|
1703
639
|
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
if dry_run_aws:
|
|
1708
|
-
console.print("🎪 [bold yellow]DRY-RUN-AWS[/bold yellow] - Would delete AWS resources")
|
|
1709
|
-
console.print(f" - ECS service: {show.aws_service_name}")
|
|
1710
|
-
console.print(f" - ECR image: {show.aws_image_tag}")
|
|
1711
|
-
else:
|
|
1712
|
-
# Real AWS cleanup (replicate ephemeral-env-pr-close.yml logic)
|
|
1713
|
-
from .core.aws import AWSInterface
|
|
1714
|
-
|
|
1715
|
-
console.print("🎪 [bold blue]Starting AWS cleanup...[/bold blue]")
|
|
1716
|
-
aws = AWSInterface()
|
|
1717
|
-
|
|
1718
|
-
try:
|
|
1719
|
-
# Show logs URL for monitoring cleanup
|
|
1720
|
-
_show_service_urls(show, "cleanup")
|
|
1721
|
-
|
|
1722
|
-
# Step 1: Check if ECS service exists and is active (replicate GHA describe-services)
|
|
1723
|
-
service_name = show.ecs_service_name
|
|
1724
|
-
console.print(f"🎪 Checking ECS service: {service_name}")
|
|
1725
|
-
|
|
1726
|
-
service_exists = aws._service_exists(service_name)
|
|
1727
|
-
|
|
1728
|
-
if service_exists:
|
|
1729
|
-
console.print(f"🎪 Found active ECS service: {service_name}")
|
|
1730
|
-
|
|
1731
|
-
# Step 2: Delete ECS service (replicate GHA delete-service)
|
|
1732
|
-
console.print("🎪 Deleting ECS service...")
|
|
1733
|
-
success = aws._delete_ecs_service(service_name)
|
|
1734
|
-
|
|
1735
|
-
if success:
|
|
1736
|
-
console.print("🎪 ✅ ECS service deleted successfully")
|
|
1737
|
-
|
|
1738
|
-
# Step 3: Delete ECR image tag (replicate GHA batch-delete-image)
|
|
1739
|
-
image_tag = f"pr-{pr_number}-ci" # Match GHA image tag format
|
|
1740
|
-
console.print(f"🎪 Deleting ECR image tag: {image_tag}")
|
|
1741
|
-
|
|
1742
|
-
ecr_success = aws._delete_ecr_image(image_tag)
|
|
1743
|
-
|
|
1744
|
-
if ecr_success:
|
|
1745
|
-
console.print("🎪 ✅ ECR image deleted successfully")
|
|
1746
|
-
else:
|
|
1747
|
-
console.print("🎪 ⚠️ ECR image deletion failed (may not exist)")
|
|
1748
|
-
|
|
1749
|
-
console.print(
|
|
1750
|
-
"🎪 [bold green]✅ AWS cleanup completed successfully![/bold green]"
|
|
1751
|
-
)
|
|
1752
|
-
|
|
1753
|
-
else:
|
|
1754
|
-
console.print("🎪 [bold red]❌ ECS service deletion failed[/bold red]")
|
|
1755
|
-
|
|
1756
|
-
else:
|
|
1757
|
-
console.print(f"🎪 No active ECS service found: {service_name}")
|
|
1758
|
-
console.print("🎪 ✅ No AWS resources to clean up")
|
|
1759
|
-
|
|
1760
|
-
except Exception as e:
|
|
1761
|
-
console.print(f"🎪 [bold red]❌ AWS cleanup failed:[/bold red] {e}")
|
|
1762
|
-
|
|
1763
|
-
# Remove all circus labels for this PR
|
|
1764
|
-
console.print(f"🎪 Removing all circus labels for PR #{pr_number}")
|
|
1765
|
-
if not dry_run_github:
|
|
1766
|
-
github.remove_circus_labels(pr_number)
|
|
1767
|
-
|
|
1768
|
-
# Post cleanup comment
|
|
1769
|
-
github_actor = _get_github_actor()
|
|
1770
|
-
cleanup_comment = f"""🎪 @{github_actor} Environment `{show.sha}` cleaned up
|
|
1771
|
-
|
|
1772
|
-
**AWS Resources:** ECS service and ECR image deleted
|
|
1773
|
-
**Cost Impact:** No further charges
|
|
1774
|
-
|
|
1775
|
-
Add `🎪 trigger-start` to create a new environment.
|
|
1776
|
-
|
|
1777
|
-
{_get_showtime_footer()}"""
|
|
1778
|
-
|
|
1779
|
-
if not dry_run_github:
|
|
1780
|
-
github.post_comment(pr_number, cleanup_comment)
|
|
1781
|
-
|
|
1782
|
-
console.print("🎪 [bold green]Environment stopped![/bold green]")
|
|
1783
|
-
|
|
1784
|
-
except Exception as e:
|
|
1785
|
-
console.print(f"🎪 [bold red]Stop trigger failed:[/bold red] {e}")
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
def _handle_sync_trigger(
|
|
1789
|
-
pr_number: int,
|
|
1790
|
-
github: GitHubInterface,
|
|
1791
|
-
dry_run_aws: bool = False,
|
|
1792
|
-
dry_run_github: bool = False,
|
|
1793
|
-
aws_sleep: int = 0,
|
|
1794
|
-
):
|
|
1795
|
-
"""Handle sync trigger"""
|
|
1796
|
-
import time
|
|
1797
|
-
from datetime import datetime
|
|
1798
|
-
|
|
1799
|
-
console.print(f"🎪 Syncing environment for PR #{pr_number}")
|
|
640
|
+
max_age_hours = int(time_match.group(1))
|
|
641
|
+
if time_match.group(2) == "d":
|
|
642
|
+
max_age_hours *= 24
|
|
1800
643
|
|
|
1801
|
-
|
|
1802
|
-
pr = PullRequest.from_id(pr_number, github)
|
|
644
|
+
p(f"🎪 [bold blue]Cleaning environments older than {max_age_hours}h...[/bold blue]")
|
|
1803
645
|
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
latest_sha = github.get_latest_commit_sha(pr_number)
|
|
1809
|
-
|
|
1810
|
-
if not pr.current_show.needs_update(latest_sha):
|
|
1811
|
-
console.print(f"🎪 Environment already up to date: {pr.current_show.sha}")
|
|
646
|
+
# Get all PRs with environments
|
|
647
|
+
pr_numbers = PullRequest.find_all_with_environments()
|
|
648
|
+
if not pr_numbers:
|
|
649
|
+
p("🎪 No environments found to clean")
|
|
1812
650
|
return
|
|
1813
651
|
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
sha=latest_sha[:7],
|
|
1820
|
-
status="building",
|
|
1821
|
-
created_at=datetime.utcnow().strftime("%Y-%m-%dT%H-%M"),
|
|
1822
|
-
ttl=pr.current_show.ttl,
|
|
1823
|
-
requested_by=pr.current_show.requested_by,
|
|
1824
|
-
)
|
|
1825
|
-
|
|
1826
|
-
console.print(f"🎪 Building new environment: {new_show.aws_service_name}")
|
|
1827
|
-
|
|
1828
|
-
if dry_run_aws:
|
|
1829
|
-
console.print("🎪 [bold yellow]DRY-RUN-AWS[/bold yellow] - Mocking rolling update")
|
|
1830
|
-
if aws_sleep > 0:
|
|
1831
|
-
console.print(f"🎪 Sleeping {aws_sleep}s to simulate build + deploy...")
|
|
1832
|
-
time.sleep(aws_sleep)
|
|
1833
|
-
|
|
1834
|
-
# Mock successful update
|
|
1835
|
-
new_show.status = "running"
|
|
1836
|
-
new_show.ip = "52.4.5.6" # New mock IP
|
|
1837
|
-
|
|
1838
|
-
console.print("🎪 [bold green]Mock rolling update complete![/bold green]")
|
|
1839
|
-
console.print(f"🎪 Traffic switched to {new_show.sha} at {new_show.ip}")
|
|
1840
|
-
|
|
1841
|
-
# Post rolling update success comment
|
|
1842
|
-
update_comment = f"""🎪 Environment updated: {pr.current_show.sha} → `{new_show.sha}`
|
|
1843
|
-
|
|
1844
|
-
**New Environment:** http://{new_show.ip}:8080
|
|
1845
|
-
**Update:** Zero-downtime rolling deployment
|
|
1846
|
-
**Old Environment:** Automatically cleaned up
|
|
1847
|
-
|
|
1848
|
-
Your latest changes are now live.
|
|
1849
|
-
|
|
1850
|
-
{_get_showtime_footer()}"""
|
|
1851
|
-
|
|
1852
|
-
if not dry_run_github:
|
|
1853
|
-
github.post_comment(pr_number, update_comment)
|
|
652
|
+
cleaned_count = 0
|
|
653
|
+
for pr_number in pr_numbers:
|
|
654
|
+
pr = PullRequest.from_id(pr_number)
|
|
655
|
+
if pr.stop_if_expired(max_age_hours, dry_run):
|
|
656
|
+
cleaned_count += 1
|
|
1854
657
|
|
|
658
|
+
if cleaned_count > 0:
|
|
659
|
+
p(f"🎪 ✅ Cleaned up {cleaned_count} expired environments")
|
|
1855
660
|
else:
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
from .core.aws import AWSInterface, EnvironmentResult
|
|
1859
|
-
|
|
1860
|
-
console.print("🎪 [bold blue]Starting real rolling update...[/bold blue]")
|
|
1861
|
-
|
|
1862
|
-
# Post rolling update start comment
|
|
1863
|
-
start_comment_text = rolling_start_comment(pr.current_show, latest_sha)
|
|
1864
|
-
|
|
1865
|
-
if not dry_run_github:
|
|
1866
|
-
github.post_comment(pr_number, start_comment_text)
|
|
1867
|
-
|
|
1868
|
-
aws = AWSInterface()
|
|
1869
|
-
|
|
1870
|
-
# Get feature flags from PR description
|
|
1871
|
-
feature_flags = _extract_feature_flags_from_pr(pr_number, github)
|
|
1872
|
-
github_actor = _get_github_actor()
|
|
1873
|
-
|
|
1874
|
-
# Use blue-green deployment (create_environment handles existing services)
|
|
1875
|
-
result: EnvironmentResult = aws.create_environment(
|
|
1876
|
-
pr_number=pr_number,
|
|
1877
|
-
sha=latest_sha,
|
|
1878
|
-
github_user=github_actor,
|
|
1879
|
-
feature_flags=feature_flags,
|
|
1880
|
-
force=False, # Don't force - let blue-green handle it
|
|
1881
|
-
)
|
|
1882
|
-
|
|
1883
|
-
if result.success:
|
|
1884
|
-
console.print(
|
|
1885
|
-
f"🎪 [bold green]✅ Rolling update complete![/bold green] New IP: {result.ip}"
|
|
1886
|
-
)
|
|
1887
|
-
|
|
1888
|
-
# Update labels to point to new service
|
|
1889
|
-
pr.refresh_labels(github)
|
|
1890
|
-
new_show = pr.get_show_by_sha(latest_sha)
|
|
1891
|
-
if new_show:
|
|
1892
|
-
new_show.status = "running"
|
|
1893
|
-
new_show.ip = result.ip
|
|
1894
|
-
|
|
1895
|
-
# Update GitHub labels
|
|
1896
|
-
github.remove_circus_labels(pr_number)
|
|
1897
|
-
for label in new_show.to_circus_labels():
|
|
1898
|
-
github.add_label(pr_number, label)
|
|
1899
|
-
|
|
1900
|
-
console.print("🎪 ✅ Labels updated to point to new environment")
|
|
1901
|
-
|
|
1902
|
-
# Post rolling update success comment
|
|
1903
|
-
success_comment_text = rolling_success_comment(pr.current_show, new_show)
|
|
1904
|
-
|
|
1905
|
-
if not dry_run_github:
|
|
1906
|
-
github.post_comment(pr_number, success_comment_text)
|
|
1907
|
-
else:
|
|
1908
|
-
console.print(f"🎪 [bold red]❌ Rolling update failed:[/bold red] {result.error}")
|
|
1909
|
-
|
|
1910
|
-
# Post rolling update failure comment
|
|
1911
|
-
failure_comment_text = rolling_failure_comment(
|
|
1912
|
-
pr.current_show, latest_sha, result.error
|
|
1913
|
-
)
|
|
1914
|
-
|
|
1915
|
-
if not dry_run_github:
|
|
1916
|
-
github.post_comment(pr_number, failure_comment_text)
|
|
661
|
+
p("🎪 No expired environments found")
|
|
1917
662
|
|
|
1918
663
|
except Exception as e:
|
|
1919
|
-
|
|
664
|
+
p(f"❌ Cleanup failed: {e}")
|
|
1920
665
|
|
|
1921
666
|
|
|
1922
|
-
def main():
|
|
667
|
+
def main() -> None:
|
|
1923
668
|
"""Main entry point for the CLI"""
|
|
1924
669
|
app()
|
|
1925
670
|
|