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