superset-showtime 0.2.8__py3-none-any.whl → 0.2.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of superset-showtime might be problematic. Click here for more details.
- showtime/__init__.py +1 -1
- showtime/cli.py +395 -56
- showtime/core/circus.py +11 -1
- showtime/core/github_messages.py +11 -1
- {superset_showtime-0.2.8.dist-info → superset_showtime-0.2.9.dist-info}/METADATA +42 -115
- {superset_showtime-0.2.8.dist-info → superset_showtime-0.2.9.dist-info}/RECORD +8 -8
- {superset_showtime-0.2.8.dist-info → superset_showtime-0.2.9.dist-info}/WHEEL +0 -0
- {superset_showtime-0.2.8.dist-info → superset_showtime-0.2.9.dist-info}/entry_points.txt +0 -0
showtime/__init__.py
CHANGED
showtime/cli.py
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
Main command-line interface for Apache Superset circus tent environment management.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import subprocess
|
|
7
8
|
from typing import Optional
|
|
8
9
|
|
|
9
10
|
import typer
|
|
@@ -14,6 +15,8 @@ from .core.circus import PullRequest, Show, short_sha
|
|
|
14
15
|
from .core.emojis import STATUS_DISPLAY
|
|
15
16
|
from .core.github import GitHubError, GitHubInterface
|
|
16
17
|
from .core.github_messages import (
|
|
18
|
+
building_comment,
|
|
19
|
+
failure_comment,
|
|
17
20
|
get_aws_console_urls,
|
|
18
21
|
rolling_failure_comment,
|
|
19
22
|
rolling_start_comment,
|
|
@@ -176,6 +179,232 @@ def _get_showtime_footer() -> str:
|
|
|
176
179
|
return "{_get_showtime_footer()}"
|
|
177
180
|
|
|
178
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")
|
|
406
|
+
|
|
407
|
+
|
|
179
408
|
@app.command()
|
|
180
409
|
def version():
|
|
181
410
|
"""Show version information"""
|
|
@@ -184,6 +413,74 @@ def version():
|
|
|
184
413
|
console.print(f"🎪 Superset Showtime v{__version__}")
|
|
185
414
|
|
|
186
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
|
|
482
|
+
|
|
483
|
+
|
|
187
484
|
@app.command()
|
|
188
485
|
def start(
|
|
189
486
|
pr_number: int = typer.Argument(..., help="PR number to create environment for"),
|
|
@@ -648,15 +945,15 @@ def sync(
|
|
|
648
945
|
check_only: bool = typer.Option(
|
|
649
946
|
False, "--check-only", help="Check what actions are needed without executing"
|
|
650
947
|
),
|
|
651
|
-
deploy: bool = typer.Option(
|
|
652
|
-
False, "--deploy", help="Execute deployment actions (assumes build is complete)"
|
|
653
|
-
),
|
|
654
948
|
dry_run_aws: bool = typer.Option(
|
|
655
949
|
False, "--dry-run-aws", help="Skip AWS operations, use mock data"
|
|
656
950
|
),
|
|
657
951
|
dry_run_github: bool = typer.Option(
|
|
658
952
|
False, "--dry-run-github", help="Skip GitHub label operations"
|
|
659
953
|
),
|
|
954
|
+
dry_run_docker: bool = typer.Option(
|
|
955
|
+
False, "--dry-run-docker", help="Skip Docker build, use mock success"
|
|
956
|
+
),
|
|
660
957
|
aws_sleep: int = typer.Option(
|
|
661
958
|
0, "--aws-sleep", help="Seconds to sleep during AWS operations (for testing)"
|
|
662
959
|
),
|
|
@@ -685,18 +982,99 @@ def sync(
|
|
|
685
982
|
action_needed = _determine_sync_action(pr, pr_state, target_sha)
|
|
686
983
|
|
|
687
984
|
if check_only:
|
|
688
|
-
# Output
|
|
689
|
-
console.print(f"action_needed={action_needed}")
|
|
690
|
-
|
|
691
|
-
# Build needed for new environments and updates (SHA changes)
|
|
985
|
+
# Output simple results for GitHub Actions
|
|
692
986
|
build_needed = action_needed in ["create_environment", "rolling_update", "auto_sync"]
|
|
693
|
-
|
|
987
|
+
sync_needed = action_needed != "no_action"
|
|
694
988
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
console.print(f"
|
|
989
|
+
console.print(f"build_needed={str(build_needed).lower()}")
|
|
990
|
+
console.print(f"sync_needed={str(sync_needed).lower()}")
|
|
991
|
+
console.print(f"pr_number={pr_number}")
|
|
992
|
+
console.print(f"target_sha={target_sha}")
|
|
698
993
|
return
|
|
699
994
|
|
|
995
|
+
# Default behavior: execute the sync (directive command)
|
|
996
|
+
# Use --check-only to override this and do read-only analysis
|
|
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
|
|
1018
|
+
|
|
1019
|
+
elif action_needed in ["create_environment", "rolling_update"]:
|
|
1020
|
+
# These require Docker build + deployment
|
|
1021
|
+
console.print(f"🎪 Starting {action_needed} workflow")
|
|
1022
|
+
|
|
1023
|
+
# Post building comment (atomic claim already set building state)
|
|
1024
|
+
if action_needed == "create_environment":
|
|
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,
|
|
1056
|
+
)
|
|
1057
|
+
return
|
|
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)
|
|
1070
|
+
else:
|
|
1071
|
+
console.print("🎪 ❌ Auto-sync failed due to build failure")
|
|
1072
|
+
return
|
|
1073
|
+
|
|
1074
|
+
else:
|
|
1075
|
+
console.print(f"🎪 No action needed ({action_needed})")
|
|
1076
|
+
return
|
|
1077
|
+
|
|
700
1078
|
console.print(
|
|
701
1079
|
f"🎪 [bold blue]Syncing PR #{pr_number}[/bold blue] (state: {pr_state}, SHA: {target_sha[:7]})"
|
|
702
1080
|
)
|
|
@@ -1238,57 +1616,18 @@ def _handle_start_trigger(
|
|
|
1238
1616
|
)
|
|
1239
1617
|
_schedule_blue_cleanup(pr_number, blue_services)
|
|
1240
1618
|
|
|
1241
|
-
# Update
|
|
1242
|
-
show.status = "running"
|
|
1619
|
+
# Update show with deployment result
|
|
1243
1620
|
show.ip = result.ip
|
|
1244
1621
|
|
|
1245
|
-
#
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
for label in running_labels:
|
|
1249
|
-
console.print(f" + {label}")
|
|
1250
|
-
|
|
1251
|
-
if not dry_run_github:
|
|
1252
|
-
console.print("🎪 Executing traffic switch via GitHub labels...")
|
|
1253
|
-
# Remove existing circus labels first
|
|
1254
|
-
github.remove_circus_labels(pr_number)
|
|
1255
|
-
# Add new running labels - this switches traffic atomically
|
|
1256
|
-
for label in running_labels:
|
|
1257
|
-
github.add_label(pr_number, label)
|
|
1258
|
-
console.print("🎪 ✅ Labels updated to running state!")
|
|
1259
|
-
|
|
1260
|
-
# Post success comment with real IP
|
|
1261
|
-
# Update show with real IP for comment
|
|
1262
|
-
show.ip = result.ip
|
|
1263
|
-
show.status = "running"
|
|
1264
|
-
success_comment_text = success_comment(show, feature_count=len(feature_flags))
|
|
1265
|
-
|
|
1266
|
-
github.post_comment(pr_number, success_comment_text)
|
|
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)
|
|
1267
1625
|
|
|
1268
1626
|
else:
|
|
1269
1627
|
console.print(f"🎪 [bold red]❌ AWS deployment failed:[/bold red] {result.error}")
|
|
1270
1628
|
|
|
1271
|
-
#
|
|
1272
|
-
show
|
|
1273
|
-
failed_labels = show.to_circus_labels()
|
|
1274
|
-
|
|
1275
|
-
if not dry_run_github:
|
|
1276
|
-
console.print("🎪 Setting failed state labels...")
|
|
1277
|
-
github.remove_circus_labels(pr_number)
|
|
1278
|
-
for label in failed_labels:
|
|
1279
|
-
github.add_label(pr_number, label)
|
|
1280
|
-
|
|
1281
|
-
# Post failure comment
|
|
1282
|
-
failure_comment = f"""🎪 @{github_actor} Environment creation failed.
|
|
1283
|
-
|
|
1284
|
-
**Error:** {result.error}
|
|
1285
|
-
**Environment:** `{show.sha}`
|
|
1286
|
-
|
|
1287
|
-
Please check the logs and try again.
|
|
1288
|
-
|
|
1289
|
-
{_get_showtime_footer()}"""
|
|
1290
|
-
|
|
1291
|
-
github.post_comment(pr_number, failure_comment)
|
|
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)
|
|
1292
1631
|
|
|
1293
1632
|
except Exception as e:
|
|
1294
1633
|
console.print(f"🎪 [bold red]Start trigger failed:[/bold red] {e}")
|
showtime/core/circus.py
CHANGED
|
@@ -18,7 +18,7 @@ class Show:
|
|
|
18
18
|
|
|
19
19
|
pr_number: int
|
|
20
20
|
sha: str # 7-char commit SHA
|
|
21
|
-
status: str # building, running, updating, failed
|
|
21
|
+
status: str # building, built, deploying, running, updating, failed
|
|
22
22
|
ip: Optional[str] = None # Environment IP address
|
|
23
23
|
created_at: Optional[str] = None # ISO timestamp
|
|
24
24
|
ttl: str = "24h" # 24h, 48h, close, etc.
|
|
@@ -54,6 +54,16 @@ class Show:
|
|
|
54
54
|
"""Check if environment is currently building"""
|
|
55
55
|
return self.status == "building"
|
|
56
56
|
|
|
57
|
+
@property
|
|
58
|
+
def is_built(self) -> bool:
|
|
59
|
+
"""Check if environment is built (Docker complete, ready for deploy)"""
|
|
60
|
+
return self.status == "built"
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def is_deploying(self) -> bool:
|
|
64
|
+
"""Check if environment is currently deploying to AWS"""
|
|
65
|
+
return self.status == "deploying"
|
|
66
|
+
|
|
57
67
|
@property
|
|
58
68
|
def is_updating(self) -> bool:
|
|
59
69
|
"""Check if environment is currently updating"""
|
showtime/core/github_messages.py
CHANGED
|
@@ -96,8 +96,18 @@ def get_aws_console_urls(service_name: str) -> Dict[str, str]:
|
|
|
96
96
|
# Typed comment functions with clear parameter requirements
|
|
97
97
|
|
|
98
98
|
|
|
99
|
+
def building_comment(show: Show) -> str:
|
|
100
|
+
"""Create building start comment
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
show: Show object with SHA and other metadata
|
|
104
|
+
"""
|
|
105
|
+
links = _create_header_links(show.sha)
|
|
106
|
+
return f"🎪 {links['showtime_link']} is building environment on {links['gha_link']} for {links['commit_link']}"
|
|
107
|
+
|
|
108
|
+
|
|
99
109
|
def start_comment(show: Show) -> str:
|
|
100
|
-
"""Create environment start comment
|
|
110
|
+
"""Create environment start comment (DEPRECATED - use building_comment)
|
|
101
111
|
|
|
102
112
|
Args:
|
|
103
113
|
show: Show object with SHA and other metadata
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: superset-showtime
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.9
|
|
4
4
|
Summary: 🎪 Apache Superset ephemeral environment management with circus tent emoji state tracking
|
|
5
5
|
Project-URL: Homepage, https://github.com/apache/superset-showtime
|
|
6
6
|
Project-URL: Documentation, https://superset-showtime.readthedocs.io/
|
|
@@ -113,9 +113,34 @@ Superset Showtime is a CLI tool designed primarily for **GitHub Actions** to man
|
|
|
113
113
|
🎪 abc123f 🌐 52-1-2-3 # Available at http://52.1.2.3:8080
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
### 🔄 Showtime Workflow
|
|
117
|
+
|
|
118
|
+
```mermaid
|
|
119
|
+
flowchart TD
|
|
120
|
+
A[User adds 🎪 ⚡ trigger-start] --> B[GitHub Actions: sync]
|
|
121
|
+
B --> C{Current state?}
|
|
122
|
+
|
|
123
|
+
C -->|No environment| D[🔒 Claim: Remove trigger + Set building]
|
|
124
|
+
C -->|Running + new SHA| E[🔒 Claim: Remove trigger + Set building]
|
|
125
|
+
C -->|Already building| F[❌ Exit: Another job active]
|
|
126
|
+
C -->|No triggers| G[❌ Exit: Nothing to do]
|
|
127
|
+
|
|
128
|
+
D --> H[📋 State: building]
|
|
129
|
+
E --> H
|
|
130
|
+
H --> I[🐳 Docker build]
|
|
131
|
+
I -->|Success| J[📋 State: built]
|
|
132
|
+
I -->|Fail| K[📋 State: failed]
|
|
133
|
+
|
|
134
|
+
J --> L[📋 State: deploying]
|
|
135
|
+
L --> M[☁️ AWS Deploy]
|
|
136
|
+
M -->|Success| N[📋 State: running]
|
|
137
|
+
M -->|Fail| O[📋 State: failed]
|
|
138
|
+
|
|
139
|
+
N --> P[🎪 Environment ready!]
|
|
140
|
+
|
|
141
|
+
Q[User adds 🎪 🛑 trigger-stop] --> R[🧹 Cleanup AWS + Remove labels]
|
|
142
|
+
```
|
|
117
143
|
|
|
118
|
-
> **Note**: CLI is mainly for debugging or developing Showtime itself. Primary interface is GitHub labels above.
|
|
119
144
|
|
|
120
145
|
**Install CLI for debugging:**
|
|
121
146
|
```bash
|
|
@@ -229,129 +254,31 @@ You'll see:
|
|
|
229
254
|
|
|
230
255
|
### GitHub Actions Integration
|
|
231
256
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
```yaml
|
|
235
|
-
# .github/workflows/showtime.yml - Integrates with Superset's existing build workflows
|
|
236
|
-
on:
|
|
237
|
-
pull_request_target:
|
|
238
|
-
types: [labeled, unlabeled, synchronize]
|
|
239
|
-
|
|
240
|
-
jobs:
|
|
241
|
-
showtime-handler:
|
|
242
|
-
if: contains(github.event.label.name, '🎪')
|
|
243
|
-
steps:
|
|
244
|
-
- name: Install Showtime from PyPI
|
|
245
|
-
run: pip install superset-showtime
|
|
246
|
-
|
|
247
|
-
- name: Process circus triggers
|
|
248
|
-
run: python -m showtime handle-trigger ${{ github.event.pull_request.number }}
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
**Integration approach:**
|
|
252
|
-
- **Coordinates with Superset builds** - Uses existing container build workflows
|
|
253
|
-
- **Runs trusted code** (from PyPI, not PR code)
|
|
254
|
-
- **Simple orchestration logic** (install CLI and run commands)
|
|
255
|
-
- **Leverages existing infrastructure** - Same AWS resources and permissions
|
|
256
|
-
|
|
257
|
-
## 🛠️ Installation & Setup
|
|
258
|
-
|
|
259
|
-
### For Contributors (GitHub Labels Only)
|
|
260
|
-
No installation needed! Just use GitHub labels to trigger environments.
|
|
261
|
-
|
|
262
|
-
### For Maintainers (Manual CLI Operations)
|
|
257
|
+
**🎯 Live Workflow**: [showtime-trigger.yml](https://github.com/apache/superset/actions/workflows/showtime-trigger.yml)
|
|
263
258
|
|
|
264
|
-
**
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
259
|
+
**How it works:**
|
|
260
|
+
- Triggers on PR label changes, commits, and closures
|
|
261
|
+
- Installs `superset-showtime` from PyPI (trusted code, not PR code)
|
|
262
|
+
- Runs `showtime sync` to handle trigger processing and deployments
|
|
263
|
+
- Supports manual testing via `workflow_dispatch` with specific SHA override
|
|
269
264
|
|
|
270
|
-
**
|
|
265
|
+
**Commands used:**
|
|
271
266
|
```bash
|
|
272
|
-
showtime
|
|
273
|
-
showtime
|
|
274
|
-
showtime labels # Reference complete label system
|
|
267
|
+
showtime sync PR_NUMBER --check-only # Determine build_needed + target_sha
|
|
268
|
+
showtime sync PR_NUMBER --sha SHA # Execute atomic claim + build + deploy
|
|
275
269
|
```
|
|
276
270
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
**1. Install GitHub workflows:**
|
|
280
|
-
Copy `workflows-reference/showtime-trigger.yml` and `workflows-reference/showtime-cleanup.yml` to Superset's `.github/workflows/`.
|
|
281
|
-
|
|
282
|
-
**2. Configure secrets (already exist in Superset):**
|
|
283
|
-
- `AWS_ACCESS_KEY_ID`
|
|
284
|
-
- `AWS_SECRET_ACCESS_KEY`
|
|
285
|
-
- `GITHUB_TOKEN`
|
|
286
|
-
|
|
287
|
-
**3. Dependencies:**
|
|
288
|
-
Showtime coordinates with Superset's existing build infrastructure - no additional setup needed.
|
|
289
|
-
|
|
290
|
-
## 📊 CLI Reference (For Development/Debugging)
|
|
271
|
+
## 🛠️ CLI Usage
|
|
291
272
|
|
|
292
|
-
|
|
273
|
+
The CLI is primarily used by GitHub Actions, but available for debugging and advanced users:
|
|
293
274
|
|
|
294
|
-
### Debugging Commands
|
|
295
275
|
```bash
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
showtime
|
|
299
|
-
showtime test-lifecycle 1234 # Full workflow simulation
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
### Manual Operations (Advanced)
|
|
303
|
-
```bash
|
|
304
|
-
showtime start 1234 # Manually create environment
|
|
305
|
-
showtime start 1234 --sha abc123f # Create environment (specific SHA)
|
|
306
|
-
showtime stop 1234 # Manually delete environment
|
|
307
|
-
showtime sync 1234 # Force sync to desired state
|
|
308
|
-
showtime cleanup --respect-ttl # Manual cleanup
|
|
276
|
+
pip install superset-showtime
|
|
277
|
+
export GITHUB_TOKEN=your_token
|
|
278
|
+
showtime --help # See all available commands
|
|
309
279
|
```
|
|
310
280
|
|
|
311
|
-
### GitHub Actions Commands
|
|
312
|
-
```bash
|
|
313
|
-
showtime handle-trigger 1234 # Process trigger labels (called by GHA)
|
|
314
|
-
showtime cleanup --older-than 48h # Scheduled cleanup (called by GHA)
|
|
315
|
-
```
|
|
316
281
|
|
|
317
|
-
## 🎪 Benefits for Superset
|
|
318
|
-
|
|
319
|
-
### For Contributors
|
|
320
|
-
- **🎯 Simple workflow** - Just add/remove GitHub labels
|
|
321
|
-
- **👀 Visual feedback** - See environment status in PR labels
|
|
322
|
-
- **⚡ Automatic updates** - New commits update environments automatically
|
|
323
|
-
- **🔧 Configuration testing** - Test config changes through code commits
|
|
324
|
-
|
|
325
|
-
### For Maintainers
|
|
326
|
-
- **📊 Complete visibility** - `showtime list` shows all environments
|
|
327
|
-
- **🧹 Easy cleanup** - Automatic expired environment cleanup
|
|
328
|
-
- **🔍 Better debugging** - Clear state in labels, comprehensive CLI
|
|
329
|
-
- **💰 Cost savings** - No duplicate environments, proper cleanup
|
|
330
|
-
|
|
331
|
-
### For Operations
|
|
332
|
-
- **📝 Simpler workflows** - Replace complex GHA scripts with simple CLI calls
|
|
333
|
-
- **🔒 Same security model** - No new permissions needed
|
|
334
|
-
- **🎯 Deterministic** - Predictable AWS resource naming
|
|
335
|
-
- **🚨 Monitoring ready** - 48h maximum lifetime, scheduled cleanup
|
|
336
|
-
|
|
337
|
-
## 🏗️ Architecture
|
|
338
|
-
|
|
339
|
-
### State Management
|
|
340
|
-
All state lives in **GitHub labels** - no external databases needed:
|
|
341
|
-
- **Trigger labels** (`🎪 trigger-*`) - Commands that get processed and removed
|
|
342
|
-
- **State labels** (`🎪 🚦 *`) - Current environment status, managed by CLI
|
|
343
|
-
|
|
344
|
-
### AWS Resources
|
|
345
|
-
Deterministic naming enables reliable cleanup:
|
|
346
|
-
- **ECS Service:** `pr-{pr_number}-{sha}` (e.g., `pr-1234-abc123f`)
|
|
347
|
-
- **ECR Image:** `pr-{pr_number}-{sha}-ci` (e.g., `pr-1234-abc123f-ci`)
|
|
348
|
-
|
|
349
|
-
### Rolling Updates
|
|
350
|
-
Zero-downtime updates by running multiple environments:
|
|
351
|
-
1. Keep old environment serving traffic
|
|
352
|
-
2. Build new environment in parallel
|
|
353
|
-
3. Switch traffic when new environment is healthy
|
|
354
|
-
4. Clean up old environment
|
|
355
282
|
|
|
356
283
|
## 🤝 Contributing
|
|
357
284
|
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
showtime/__init__.py,sha256=
|
|
1
|
+
showtime/__init__.py,sha256=0p_J3NDgjtmHCR4Q_u9hM4sLnLdmAluCY3TKMLybL2w,420
|
|
2
2
|
showtime/__main__.py,sha256=EVaDaTX69yIhCzChg99vqvFSCN4ELstEt7Mpb9FMZX8,109
|
|
3
|
-
showtime/cli.py,sha256=
|
|
3
|
+
showtime/cli.py,sha256=ZBf9-LGfMzA1wzR46Qx4DZyd0Q2hoousYeBMduiUnT4,75469
|
|
4
4
|
showtime/commands/__init__.py,sha256=M2wn5hYgwNCryMjLT79ncobvK884r-xk3znkCmINN_0,28
|
|
5
5
|
showtime/commands/start.py,sha256=DPGbgvGPh7I60LK_VioDljUhdmhNFVjEy6BchFv1lCo,1026
|
|
6
6
|
showtime/core/__init__.py,sha256=54hbdFNGrzuNMBdraezfjT8Zi6g221pKlJ9mREnKwCw,34
|
|
7
7
|
showtime/core/aws.py,sha256=xWrje3MS3LpFeilH6jloWy_VgE9Wky_G5VkRCOB0ZNw,32535
|
|
8
|
-
showtime/core/circus.py,sha256=
|
|
8
|
+
showtime/core/circus.py,sha256=VHEWPpL8xLrrVm7tIF58QSDutA5gCS60gE3lZt3Lk5E,9378
|
|
9
9
|
showtime/core/emojis.py,sha256=MHEDuPIdfNiop4zbNLuviz3eY05QiftYSHHCVbkfKhw,2129
|
|
10
10
|
showtime/core/github.py,sha256=HWhM8_Yq4P-AHq0FV3UfrfQHUHXxkhn74vvc_9RguKA,9822
|
|
11
|
-
showtime/core/github_messages.py,sha256=
|
|
11
|
+
showtime/core/github_messages.py,sha256=6NI1c4375XHO64sNQIsQev-0rPLKVZp9Vw4-mIEPwCQ,7537
|
|
12
12
|
showtime/core/label_colors.py,sha256=efhbFnz_3nqEnEqmgyF6_hZbxtCu_fmb68BIIUpSsnk,3895
|
|
13
13
|
showtime/data/ecs-task-definition.json,sha256=0ZaE0FZ8IWduXd2RyscMhXeVgxyym6qtjH02CK9mXBI,2235
|
|
14
|
-
superset_showtime-0.2.
|
|
15
|
-
superset_showtime-0.2.
|
|
16
|
-
superset_showtime-0.2.
|
|
17
|
-
superset_showtime-0.2.
|
|
14
|
+
superset_showtime-0.2.9.dist-info/METADATA,sha256=PR2uktLbkO38HfmDuHzhmyJCgL0nq272qIozcP7mmKw,11616
|
|
15
|
+
superset_showtime-0.2.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
16
|
+
superset_showtime-0.2.9.dist-info/entry_points.txt,sha256=rDW7oZ57mqyBUS4N_3_R7bZNGVHB-104jwmY-hHC_ck,85
|
|
17
|
+
superset_showtime-0.2.9.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|