superset-showtime 0.2.8__py3-none-any.whl → 0.3.1__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 CHANGED
@@ -4,7 +4,7 @@
4
4
  Circus tent emoji state tracking for Apache Superset ephemeral environments.
5
5
  """
6
6
 
7
- __version__ = "0.2.8"
7
+ __version__ = "0.3.1"
8
8
  __author__ = "Maxime Beauchemin"
9
9
  __email__ = "maximebeauchemin@gmail.com"
10
10
 
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,247 @@ 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
+ console.print("🎪 Streaming Docker build output...")
334
+
335
+ # Stream output in real-time for better user experience
336
+ process = subprocess.Popen(
337
+ cmd,
338
+ stdout=subprocess.PIPE,
339
+ stderr=subprocess.STDOUT,
340
+ text=True,
341
+ bufsize=1,
342
+ universal_newlines=True,
343
+ )
344
+
345
+ # Stream output line by line
346
+ for line in process.stdout:
347
+ console.print(f"🐳 {line.rstrip()}")
348
+
349
+ # Wait for completion with timeout
350
+ try:
351
+ return_code = process.wait(timeout=3600) # 60 min timeout
352
+ except subprocess.TimeoutExpired:
353
+ process.kill()
354
+ console.print("🎪 ❌ Docker build timed out after 60 minutes")
355
+ return False
356
+
357
+ if return_code == 0:
358
+ console.print(f"🎪 ✅ Docker build succeeded: {tag}")
359
+ return True
360
+ else:
361
+ console.print(f"🎪 ❌ Docker build failed with exit code: {return_code}")
362
+ return False
363
+ except Exception as e:
364
+ console.print(f"🎪 ❌ Docker build error: {e}")
365
+ return False
366
+
367
+
368
+ def _set_state_internal(
369
+ state: str,
370
+ pr_number: int,
371
+ show: Show,
372
+ github: GitHubInterface,
373
+ dry_run_github: bool = False,
374
+ error_msg: Optional[str] = None,
375
+ ) -> None:
376
+ """Internal helper to set state and handle comments/labels
377
+
378
+ Used by sync and other commands to set final state transitions
379
+ """
380
+ console.print(f"🎪 Setting state to '{state}' for PR #{pr_number} SHA {show.sha}")
381
+
382
+ # Update show state
383
+ show.status = state
384
+
385
+ # Handle state-specific logic
386
+ comment_text = None
387
+
388
+ if state == "building":
389
+ comment_text = building_comment(show)
390
+ console.print("🎪 Posting building comment...")
391
+
392
+ elif state == "running":
393
+ comment_text = success_comment(show)
394
+ console.print("🎪 Posting success comment...")
395
+
396
+ elif state == "failed":
397
+ error_message = error_msg or "Build or deployment failed"
398
+ comment_text = failure_comment(show, error_message)
399
+ console.print("🎪 Posting failure comment...")
400
+
401
+ elif state in ["built", "deploying"]:
402
+ console.print(f"🎪 Silent state change to '{state}' - no comment posted")
403
+
404
+ # Post comment if needed
405
+ if comment_text and not dry_run_github:
406
+ github.post_comment(pr_number, comment_text)
407
+ console.print("🎪 ✅ Comment posted!")
408
+ elif comment_text:
409
+ console.print("🎪 [bold yellow]DRY-RUN-GITHUB[/bold yellow] - Would post comment")
410
+
411
+ # Set state labels
412
+ state_labels = show.to_circus_labels()
413
+
414
+ if not dry_run_github:
415
+ github.remove_circus_labels(pr_number)
416
+ for label in state_labels:
417
+ github.add_label(pr_number, label)
418
+ console.print("🎪 ✅ Labels updated!")
419
+ else:
420
+ console.print("🎪 [bold yellow]DRY-RUN-GITHUB[/bold yellow] - Would update labels")
421
+
422
+
179
423
  @app.command()
180
424
  def version():
181
425
  """Show version information"""
@@ -184,6 +428,74 @@ def version():
184
428
  console.print(f"🎪 Superset Showtime v{__version__}")
185
429
 
186
430
 
431
+ @app.command()
432
+ def set_state(
433
+ state: str = typer.Argument(
434
+ ..., help="State to set (building, built, deploying, running, failed)"
435
+ ),
436
+ pr_number: int = typer.Argument(..., help="PR number to update"),
437
+ sha: Optional[str] = typer.Option(None, "--sha", help="Specific commit SHA (default: latest)"),
438
+ error_msg: Optional[str] = typer.Option(
439
+ None, "--error-msg", help="Error message for failed state"
440
+ ),
441
+ dry_run_github: bool = typer.Option(
442
+ False, "--dry-run-github", help="Skip GitHub operations, show what would happen"
443
+ ),
444
+ ):
445
+ """🎪 Set environment state (generic state transition command)
446
+
447
+ States:
448
+ • building - Docker image is being built (posts comment)
449
+ • built - Docker build complete, ready for deployment (silent)
450
+ • deploying - AWS deployment in progress (silent)
451
+ • running - Environment is live and ready (posts success comment)
452
+ • failed - Build or deployment failed (posts error comment)
453
+ """
454
+ from datetime import datetime
455
+
456
+ try:
457
+ github = GitHubInterface()
458
+
459
+ # Get SHA - use provided SHA or default to latest
460
+ if sha:
461
+ target_sha = sha
462
+ console.print(f"🎪 Using specified SHA: {target_sha[:7]}")
463
+ else:
464
+ target_sha = github.get_latest_commit_sha(pr_number)
465
+ console.print(f"🎪 Using latest SHA: {target_sha[:7]}")
466
+
467
+ # Validate state
468
+ valid_states = ["building", "built", "deploying", "running", "failed"]
469
+ if state not in valid_states:
470
+ console.print(f"❌ Invalid state: {state}. Must be one of: {', '.join(valid_states)}")
471
+ raise typer.Exit(1)
472
+
473
+ # Get GitHub actor
474
+ github_actor = _get_github_actor()
475
+
476
+ # Create or update show object
477
+ show = Show(
478
+ pr_number=pr_number,
479
+ sha=short_sha(target_sha),
480
+ status=state,
481
+ created_at=datetime.utcnow().strftime("%Y-%m-%dT%H-%M"),
482
+ ttl="24h",
483
+ requested_by=github_actor,
484
+ )
485
+
486
+ # Use internal helper to set state
487
+ _set_state_internal(state, pr_number, show, github, dry_run_github, error_msg)
488
+
489
+ console.print(f"🎪 [bold green]State successfully set to '{state}'[/bold green]")
490
+
491
+ except GitHubError as e:
492
+ console.print(f"❌ GitHub error: {e}")
493
+ raise typer.Exit(1) from e
494
+ except Exception as e:
495
+ console.print(f"❌ Error: {e}")
496
+ raise typer.Exit(1) from e
497
+
498
+
187
499
  @app.command()
188
500
  def start(
189
501
  pr_number: int = typer.Argument(..., help="PR number to create environment for"),
@@ -648,15 +960,15 @@ def sync(
648
960
  check_only: bool = typer.Option(
649
961
  False, "--check-only", help="Check what actions are needed without executing"
650
962
  ),
651
- deploy: bool = typer.Option(
652
- False, "--deploy", help="Execute deployment actions (assumes build is complete)"
653
- ),
654
963
  dry_run_aws: bool = typer.Option(
655
964
  False, "--dry-run-aws", help="Skip AWS operations, use mock data"
656
965
  ),
657
966
  dry_run_github: bool = typer.Option(
658
967
  False, "--dry-run-github", help="Skip GitHub label operations"
659
968
  ),
969
+ dry_run_docker: bool = typer.Option(
970
+ False, "--dry-run-docker", help="Skip Docker build, use mock success"
971
+ ),
660
972
  aws_sleep: int = typer.Option(
661
973
  0, "--aws-sleep", help="Seconds to sleep during AWS operations (for testing)"
662
974
  ),
@@ -685,18 +997,99 @@ def sync(
685
997
  action_needed = _determine_sync_action(pr, pr_state, target_sha)
686
998
 
687
999
  if check_only:
688
- # Output structured results for GitHub Actions
689
- console.print(f"action_needed={action_needed}")
690
-
691
- # Build needed for new environments and updates (SHA changes)
1000
+ # Output simple results for GitHub Actions
692
1001
  build_needed = action_needed in ["create_environment", "rolling_update", "auto_sync"]
693
- console.print(f"build_needed={str(build_needed).lower()}")
1002
+ sync_needed = action_needed != "no_action"
694
1003
 
695
- # Deploy needed for everything except no_action
696
- deploy_needed = action_needed != "no_action"
697
- console.print(f"deploy_needed={str(deploy_needed).lower()}")
1004
+ console.print(f"build_needed={str(build_needed).lower()}")
1005
+ console.print(f"sync_needed={str(sync_needed).lower()}")
1006
+ console.print(f"pr_number={pr_number}")
1007
+ console.print(f"target_sha={target_sha}")
698
1008
  return
699
1009
 
1010
+ # Default behavior: execute the sync (directive command)
1011
+ # Use --check-only to override this and do read-only analysis
1012
+ if not check_only:
1013
+ console.print(
1014
+ f"🎪 [bold blue]Syncing PR #{pr_number}[/bold blue] (SHA: {target_sha[:7]})"
1015
+ )
1016
+ console.print(f"🎪 Action needed: {action_needed}")
1017
+
1018
+ # For trigger-based actions, use atomic claim to prevent race conditions
1019
+ if action_needed in ["create_environment", "rolling_update", "destroy_environment"]:
1020
+ if not _atomic_claim_environment(pr_number, target_sha, github, dry_run_github):
1021
+ console.print("🎪 Unable to claim environment - exiting")
1022
+ return
1023
+ console.print("🎪 ✅ Environment claimed - proceeding with work")
1024
+
1025
+ # Execute based on determined action
1026
+ if action_needed == "cleanup":
1027
+ console.print("🎪 PR is closed - cleaning up environment")
1028
+ if pr.current_show:
1029
+ _handle_stop_trigger(pr_number, github, dry_run_aws, dry_run_github)
1030
+ else:
1031
+ console.print("🎪 No environment to clean up")
1032
+ return
1033
+
1034
+ elif action_needed in ["create_environment", "rolling_update"]:
1035
+ # These require Docker build + deployment
1036
+ console.print(f"🎪 Starting {action_needed} workflow")
1037
+
1038
+ # Post building comment (atomic claim already set building state)
1039
+ if action_needed == "create_environment":
1040
+ console.print("🎪 Posting building comment...")
1041
+ elif action_needed == "rolling_update":
1042
+ console.print("🎪 Posting rolling update comment...")
1043
+
1044
+ # Build Docker image
1045
+ build_success = _build_docker_image(pr_number, target_sha, dry_run_docker)
1046
+ if not build_success:
1047
+ _set_state_internal(
1048
+ "failed",
1049
+ pr_number,
1050
+ Show(
1051
+ pr_number=pr_number,
1052
+ sha=short_sha(target_sha),
1053
+ status="failed",
1054
+ requested_by=_get_github_actor(),
1055
+ ),
1056
+ github,
1057
+ dry_run_github,
1058
+ "Docker build failed",
1059
+ )
1060
+ return
1061
+
1062
+ # Continue with AWS deployment (reuse existing logic)
1063
+ _handle_start_trigger(
1064
+ pr_number,
1065
+ github,
1066
+ dry_run_aws,
1067
+ dry_run_github,
1068
+ aws_sleep,
1069
+ docker_tag,
1070
+ force=True,
1071
+ )
1072
+ return
1073
+
1074
+ elif action_needed == "destroy_environment":
1075
+ console.print("🎪 Destroying environment")
1076
+ _handle_stop_trigger(pr_number, github, dry_run_aws, dry_run_github)
1077
+ return
1078
+
1079
+ elif action_needed == "auto_sync":
1080
+ console.print("🎪 Auto-sync on new commit")
1081
+ # This also requires build + deployment
1082
+ build_success = _build_docker_image(pr_number, target_sha, dry_run_docker)
1083
+ if build_success:
1084
+ _handle_sync_trigger(pr_number, github, dry_run_aws, dry_run_github, aws_sleep)
1085
+ else:
1086
+ console.print("🎪 ❌ Auto-sync failed due to build failure")
1087
+ return
1088
+
1089
+ else:
1090
+ console.print(f"🎪 No action needed ({action_needed})")
1091
+ return
1092
+
700
1093
  console.print(
701
1094
  f"🎪 [bold blue]Syncing PR #{pr_number}[/bold blue] (state: {pr_state}, SHA: {target_sha[:7]})"
702
1095
  )
@@ -1238,57 +1631,18 @@ def _handle_start_trigger(
1238
1631
  )
1239
1632
  _schedule_blue_cleanup(pr_number, blue_services)
1240
1633
 
1241
- # Update to running state with new SHA
1242
- show.status = "running"
1634
+ # Update show with deployment result
1243
1635
  show.ip = result.ip
1244
1636
 
1245
- # Traffic switching happens here - update GitHub labels atomically
1246
- running_labels = show.to_circus_labels()
1247
- console.print("\n🎪 Setting running state labels (traffic switch):")
1248
- for label in running_labels:
1249
- console.print(f" + {label}")
1250
-
1251
- if not dry_run_github:
1252
- console.print("🎪 Executing traffic switch via GitHub labels...")
1253
- # Remove existing circus labels first
1254
- github.remove_circus_labels(pr_number)
1255
- # Add new running labels - this switches traffic atomically
1256
- for label in running_labels:
1257
- github.add_label(pr_number, label)
1258
- console.print("🎪 ✅ Labels updated to running state!")
1259
-
1260
- # Post success comment with real IP
1261
- # Update show with real IP for comment
1262
- show.ip = result.ip
1263
- show.status = "running"
1264
- success_comment_text = success_comment(show, feature_count=len(feature_flags))
1265
-
1266
- github.post_comment(pr_number, success_comment_text)
1637
+ # Use internal helper to set running state (posts success comment)
1638
+ console.print("\n🎪 Traffic switching to running state:")
1639
+ _set_state_internal("running", pr_number, show, github, dry_run_github)
1267
1640
 
1268
1641
  else:
1269
1642
  console.print(f"🎪 [bold red]❌ AWS deployment failed:[/bold red] {result.error}")
1270
1643
 
1271
- # Update to failed state
1272
- show.status = "failed"
1273
- failed_labels = show.to_circus_labels()
1274
-
1275
- if not dry_run_github:
1276
- console.print("🎪 Setting failed state labels...")
1277
- github.remove_circus_labels(pr_number)
1278
- for label in failed_labels:
1279
- github.add_label(pr_number, label)
1280
-
1281
- # Post failure comment
1282
- failure_comment = f"""🎪 @{github_actor} Environment creation failed.
1283
-
1284
- **Error:** {result.error}
1285
- **Environment:** `{show.sha}`
1286
-
1287
- Please check the logs and try again.
1288
-
1289
- {_get_showtime_footer()}"""
1290
-
1291
- github.post_comment(pr_number, failure_comment)
1644
+ # Use internal helper to set failed state (posts failure comment)
1645
+ _set_state_internal("failed", pr_number, show, github, dry_run_github, result.error)
1292
1646
 
1293
1647
  except Exception as e:
1294
1648
  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"""
@@ -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.8
3
+ Version: 0.3.1
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
- ## 📊 For Maintainers (CLI Operations)
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
- Showtime is designed to be called by Superset's GitHub Actions workflows:
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
- **Install CLI for debugging/testing:**
265
- ```bash
266
- pip install superset-showtime
267
- export GITHUB_TOKEN=your_personal_access_token
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
- **Manual operations:**
265
+ **Commands used:**
271
266
  ```bash
272
- showtime list # Monitor all active environments
273
- showtime status 1234 # Debug specific environment
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
- ### For Repository Integration (GitHub Actions)
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
- > **Primary Interface**: Use GitHub labels in PR interface. CLI is mainly for maintainers debugging or developing Showtime itself.
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
- showtime list # Monitor all environments
297
- showtime status 1234 # Debug specific environment
298
- showtime labels # Complete label reference
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=1wse0Q42I1-_VwF1AYaIDYvkqx4eC32omodFrMeStlE,420
1
+ showtime/__init__.py,sha256=4dJqKn56kFArrqJiEHyLI6UB0dekT9A_ycSW9f-0f30,420
2
2
  showtime/__main__.py,sha256=EVaDaTX69yIhCzChg99vqvFSCN4ELstEt7Mpb9FMZX8,109
3
- showtime/cli.py,sha256=gd8OIjdQov7ei8A9A2X8bdreOeWJVhc5FFhAN9u_spQ,62913
3
+ showtime/cli.py,sha256=xOyOY55bC1f26uPxDjDsET7lkILZkFQ2x_o-lWDD9eQ,75862
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=dtYEU1YnmaE9oWhPwrPiMof1GF5tzckvcDrc9iJ9VpQ,9037
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=iKd3AS0uvTnCtR-2jPtEq_efuThC8HUzEAMmQt5c3y4,7187
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.8.dist-info/METADATA,sha256=fMZkdKW3pgcxx-AXRP5rmTpmOBLEXtVcSaLr2L5A7xg,14635
15
- superset_showtime-0.2.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
- superset_showtime-0.2.8.dist-info/entry_points.txt,sha256=rDW7oZ57mqyBUS4N_3_R7bZNGVHB-104jwmY-hHC_ck,85
17
- superset_showtime-0.2.8.dist-info/RECORD,,
14
+ superset_showtime-0.3.1.dist-info/METADATA,sha256=ruPEhhznTWhdPeGT0-Yx-bApJL9nETiKaVS7TKUdAeo,11616
15
+ superset_showtime-0.3.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
+ superset_showtime-0.3.1.dist-info/entry_points.txt,sha256=rDW7oZ57mqyBUS4N_3_R7bZNGVHB-104jwmY-hHC_ck,85
17
+ superset_showtime-0.3.1.dist-info/RECORD,,