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