superset-showtime 0.5.12__py3-none-any.whl → 0.6.3__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.5.12"
7
+ __version__ = "0.6.3"
8
8
  __author__ = "Maxime Beauchemin"
9
9
  __email__ = "maximebeauchemin@gmail.com"
10
10
 
showtime/cli.py CHANGED
@@ -584,36 +584,6 @@ def sync(
584
584
  raise typer.Exit(1) from e
585
585
 
586
586
 
587
- @app.command()
588
- def handle_sync(pr_number: int) -> None:
589
- """🎪 Handle new commit sync (called by GitHub Actions on PR synchronize)"""
590
- try:
591
- pr = PullRequest.from_id(pr_number)
592
-
593
- # Only sync if there's an active environment
594
- if not pr.current_show:
595
- p(f"🎪 No active environment for PR #{pr_number} - skipping sync")
596
- return
597
-
598
- # Get latest commit SHA
599
- from .core.pull_request import get_github
600
-
601
- latest_sha = get_github().get_latest_commit_sha(pr_number)
602
-
603
- # Check if update is needed
604
- if not pr.current_show.needs_update(latest_sha):
605
- p(f"🎪 Environment already up to date for PR #{pr_number}")
606
- return
607
-
608
- p(f"🎪 Syncing PR #{pr_number} to commit {latest_sha[:7]}")
609
-
610
- # TODO: Implement rolling update logic
611
- p("🎪 [bold yellow]Sync logic not yet implemented[/bold yellow]")
612
-
613
- except Exception as e:
614
- p(f"🎪 [bold red]Error handling sync:[/bold red] {e}")
615
-
616
-
617
587
  @app.command()
618
588
  def setup_labels(
619
589
  dry_run: bool = typer.Option(False, "--dry-run", help="Show what labels would be created"),
@@ -662,107 +632,10 @@ def setup_labels(
662
632
  p(f"🎪 [bold red]Error setting up labels:[/bold red] {e}")
663
633
 
664
634
 
665
- @app.command()
666
- def aws_cleanup(
667
- dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be cleaned"),
668
- force: bool = typer.Option(False, "--force", help="Delete all showtime AWS resources"),
669
- ) -> None:
670
- """🧹 Clean up orphaned AWS resources without GitHub labels"""
671
- try:
672
- from .core.aws import AWSInterface
673
-
674
- aws = AWSInterface()
675
-
676
- p("🔍 [bold blue]Scanning for orphaned AWS resources...[/bold blue]")
677
-
678
- # 1. Get all GitHub PRs with circus labels
679
- github_services = set()
680
- try:
681
- all_pr_numbers = PullRequest.find_all_with_environments()
682
- p(f"📋 Found {len(all_pr_numbers)} PRs with circus labels:")
683
-
684
- for pr_number in all_pr_numbers:
685
- pr = PullRequest.from_id(pr_number)
686
- p(
687
- f" 🎪 PR #{pr_number}: {len(pr.shows)} shows, {len(pr.circus_labels)} circus labels"
688
- )
689
-
690
- for show in pr.shows:
691
- service_name = show.ecs_service_name
692
- github_services.add(service_name)
693
- p(f" 📝 Expected service: {service_name}")
694
-
695
- # Show labels for debugging
696
- if not pr.shows:
697
- p(f" ⚠️ No shows found, labels: {pr.circus_labels[:3]}...") # First 3 labels
698
-
699
- except Exception as e:
700
- p(f"⚠️ GitHub scan failed: {e}")
701
- github_services = set()
702
-
703
- # 2. Get all AWS ECS services matching showtime pattern
704
- p("\n☁️ [bold blue]Scanning AWS ECS services...[/bold blue]")
705
- try:
706
- aws_services = aws.find_showtime_services()
707
- p(f"🔍 Found {len(aws_services)} AWS services with pr-* pattern")
708
-
709
- for service in aws_services:
710
- p(f" ☁️ AWS: {service}")
711
- except Exception as e:
712
- p(f"❌ AWS scan failed: {e}")
713
- return
714
-
715
- # 3. Find orphaned services
716
- orphaned = [service for service in aws_services if service not in github_services]
717
-
718
- if not orphaned:
719
- p("\n✅ [bold green]No orphaned AWS resources found![/bold green]")
720
- return
721
-
722
- p(f"\n🚨 [bold red]Found {len(orphaned)} orphaned AWS resources:[/bold red]")
723
- for service in orphaned:
724
- p(f" 💰 {service} (consuming resources)")
725
-
726
- if dry_run:
727
- p(f"\n🎪 [bold yellow]DRY RUN[/bold yellow] - Would delete {len(orphaned)} services")
728
- return
729
-
730
- if not force:
731
- confirm = typer.confirm(f"Delete {len(orphaned)} orphaned AWS services?")
732
- if not confirm:
733
- p("🎪 Cancelled")
734
- return
735
-
736
- # 4. Delete orphaned resources
737
- deleted_count = 0
738
- for service in orphaned:
739
- p(f"🗑️ Deleting {service}...")
740
- try:
741
- # Extract PR number for delete_environment call
742
- pr_match = service.replace("pr-", "").replace("-service", "")
743
- parts = pr_match.split("-")
744
- if len(parts) >= 2:
745
- pr_number = int(parts[0])
746
- success = aws.delete_environment(service, pr_number)
747
- if success:
748
- p(f"✅ Deleted {service}")
749
- deleted_count += 1
750
- else:
751
- p(f"❌ Failed to delete {service}")
752
- else:
753
- p(f"❌ Invalid service name format: {service}")
754
- except Exception as e:
755
- p(f"❌ Error deleting {service}: {e}")
756
-
757
- p(f"\n🎪 ✅ Cleanup complete: deleted {deleted_count}/{len(orphaned)} services")
758
-
759
- except Exception as e:
760
- p(f"❌ AWS cleanup failed: {e}")
761
-
762
-
763
635
  @app.command()
764
636
  def cleanup(
765
637
  dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be cleaned"),
638
+ force: bool = typer.Option(False, "--force", help="Skip interactive prompts"),
766
639
  older_than: str = typer.Option(
767
640
  "48h", "--older-than", help="Clean environments older than this (ignored if --respect-ttl)"
768
641
  ),
@@ -777,6 +650,11 @@ def cleanup(
777
650
  "--cleanup-labels/--no-cleanup-labels",
778
651
  help="Also cleanup SHA-based label definitions from repository",
779
652
  ),
653
+ cleanup_aws_orphans: bool = typer.Option(
654
+ True,
655
+ "--cleanup-aws-orphans/--no-cleanup-aws-orphans",
656
+ help="Also cleanup orphaned AWS resources",
657
+ ),
780
658
  ) -> None:
781
659
  """🎪 Clean up orphaned or expired environments and labels"""
782
660
  try:
@@ -811,6 +689,106 @@ def cleanup(
811
689
  else:
812
690
  p("🎪 No expired environments found")
813
691
 
692
+ # Phase 2: AWS orphan cleanup
693
+ aws_cleaned_count = 0
694
+ if cleanup_aws_orphans:
695
+ from .core.aws import AWSInterface
696
+
697
+ p("\n☁️ [bold blue]Scanning for orphaned AWS resources...[/bold blue]")
698
+ aws = AWSInterface()
699
+
700
+ try:
701
+ # Get expected services from GitHub
702
+ github_services = set()
703
+ for pr_number in pr_numbers:
704
+ pr = PullRequest.from_id(pr_number)
705
+ for show in pr.shows:
706
+ github_services.add(show.ecs_service_name)
707
+
708
+ # Find AWS orphans
709
+ aws_services = aws.list_circus_environments()
710
+ aws_orphans = [
711
+ svc for svc in aws_services if svc.get("service_name") not in github_services
712
+ ]
713
+
714
+ if aws_orphans:
715
+ p(f"☁️ Found {len(aws_orphans)} orphaned AWS resources:")
716
+ for orphan in aws_orphans[:3]:
717
+ p(f" • {orphan['service_name']}")
718
+ if len(aws_orphans) > 3:
719
+ p(f" ... and {len(aws_orphans) - 3} more")
720
+
721
+ if not force and not dry_run:
722
+ if typer.confirm(f"Delete {len(aws_orphans)} orphaned AWS resources?"):
723
+ # Clean up AWS orphans
724
+ for orphan in aws_orphans:
725
+ if not dry_run:
726
+ aws.delete_service(orphan["service_name"])
727
+ aws_cleaned_count += 1
728
+ else:
729
+ p("❌ Skipping AWS orphan cleanup")
730
+ elif force or dry_run:
731
+ aws_cleaned_count = len(aws_orphans)
732
+ if not dry_run:
733
+ for orphan in aws_orphans:
734
+ aws.delete_service(orphan["service_name"])
735
+
736
+ if aws_cleaned_count > 0:
737
+ p(f"☁️ ✅ Cleaned up {aws_cleaned_count} orphaned AWS resources")
738
+ else:
739
+ p("☁️ No orphaned AWS resources found")
740
+
741
+ except Exception as e:
742
+ p(f"⚠️ AWS orphan scan failed: {e}")
743
+
744
+ # Phase 3: Repository label cleanup
745
+ label_cleaned_count = 0
746
+ if cleanup_labels:
747
+ from .core.pull_request import get_github
748
+
749
+ p("\n🏷️ [bold blue]Scanning for orphaned repository labels...[/bold blue]")
750
+ github = get_github()
751
+
752
+ try:
753
+ orphaned_labels = github.find_orphaned_labels(dry_run=True) # Preview
754
+
755
+ if orphaned_labels:
756
+ p(f"🏷️ Found {len(orphaned_labels)} orphaned repository labels:")
757
+ for label in orphaned_labels[:3]:
758
+ p(f" • {label}")
759
+ if len(orphaned_labels) > 3:
760
+ p(f" ... and {len(orphaned_labels) - 3} more")
761
+
762
+ if not force and not dry_run:
763
+ if typer.confirm(
764
+ f"Delete {len(orphaned_labels)} orphaned labels from repository?"
765
+ ):
766
+ deleted_labels = github.find_orphaned_labels(dry_run=False)
767
+ label_cleaned_count = len(deleted_labels)
768
+ else:
769
+ p("❌ Skipping repository label cleanup")
770
+ elif force or dry_run:
771
+ label_cleaned_count = len(orphaned_labels)
772
+ if not dry_run:
773
+ github.find_orphaned_labels(dry_run=False)
774
+
775
+ if label_cleaned_count > 0:
776
+ p(f"🏷️ ✅ Cleaned up {label_cleaned_count} orphaned repository labels")
777
+ else:
778
+ p("🏷️ No orphaned repository labels found")
779
+
780
+ except Exception as e:
781
+ p(f"⚠️ Repository label scan failed: {e}")
782
+
783
+ # Final summary
784
+ total_cleaned = cleaned_count + aws_cleaned_count + label_cleaned_count
785
+ if total_cleaned > 0:
786
+ p(
787
+ f"\n🎉 [bold green]Total cleanup: {cleaned_count} environments + {aws_cleaned_count} AWS orphans + {label_cleaned_count} labels[/bold green]"
788
+ )
789
+ else:
790
+ p("\n✨ [bold green]No cleanup needed - everything is clean![/bold green]")
791
+
814
792
  except Exception as e:
815
793
  p(f"❌ Cleanup failed: {e}")
816
794
 
showtime/core/emojis.py CHANGED
@@ -13,6 +13,7 @@ EMOJI_MEANINGS = {
13
13
  "🚦": "status", # Traffic light for environment status
14
14
  "🏗️": "building", # Construction for building environments
15
15
  "🎯": "active", # Target for currently active environment
16
+ "🔒": "blocked", # Lock for blocking all operations
16
17
  # Metadata
17
18
  "📅": "created_at", # Calendar for creation timestamp
18
19
  "🌐": "ip", # Globe for IP address
showtime/core/github.py CHANGED
@@ -143,9 +143,9 @@ class GitHubInterface:
143
143
  url = f"{self.base_url}/search/issues"
144
144
  # Search for PRs with any circus tent labels
145
145
  params = {
146
- "q": f"repo:{self.org}/{self.repo} is:pr 🎪",
146
+ "q": f"repo:{self.org}/{self.repo} is:pr is:open 🎪",
147
147
  "per_page": "100",
148
- } # Include closed PRs
148
+ } # Only open PRs - closed PRs should have cleaned up labels
149
149
 
150
150
  with httpx.Client() as client:
151
151
  response = client.get(url, headers=self.headers, params=params)
@@ -209,8 +209,8 @@ class GitHubInterface:
209
209
  all_labels = self.get_repository_labels()
210
210
  sha_labels = []
211
211
 
212
- # Find labels with SHA patterns (7+ hex chars after 🎪)
213
- sha_pattern = re.compile(r"^🎪 .* [a-f0-9]{7,}( .*)?$")
212
+ # Find labels with SHA patterns (7+ hex chars anywhere in label)
213
+ sha_pattern = re.compile(r"^🎪 .*[a-f0-9]{7,}.*$")
214
214
 
215
215
  for label in all_labels:
216
216
  if sha_pattern.match(label):
@@ -225,6 +225,69 @@ class GitHubInterface:
225
225
 
226
226
  return sha_labels
227
227
 
228
+ def find_orphaned_labels(self, dry_run: bool = False) -> List[str]:
229
+ """Find labels that exist in repository but aren't used on any PR"""
230
+ import re
231
+
232
+ print("🔍 Scanning repository labels...")
233
+
234
+ # 1. Get all repository labels with SHA patterns
235
+ all_repo_labels = self.get_repository_labels()
236
+ sha_pattern = re.compile(r"^🎪 .*[a-f0-9]{7,}.*$")
237
+ sha_repo_labels = {label for label in all_repo_labels if sha_pattern.match(label)}
238
+
239
+ print(f"📋 Found {len(sha_repo_labels)} SHA-containing labels in repository")
240
+
241
+ # 2. Get all labels actually used on PRs with circus labels
242
+ print("🔍 Scanning PRs with circus labels...")
243
+
244
+ # Import here to avoid circular import
245
+ import importlib
246
+
247
+ pull_request_module = importlib.import_module("showtime.core.pull_request")
248
+ PullRequest = pull_request_module.PullRequest
249
+
250
+ try:
251
+ pr_numbers = PullRequest.find_all_with_environments()
252
+ print(f"📋 Found {len(pr_numbers)} PRs with circus labels")
253
+
254
+ used_labels = set()
255
+ for pr_number in pr_numbers:
256
+ pr_labels = self.get_labels(pr_number)
257
+ circus_labels = {label for label in pr_labels if label.startswith("🎪 ")}
258
+ used_labels.update(circus_labels)
259
+
260
+ print(f"📋 Found {len(used_labels)} circus labels actually used on PRs")
261
+
262
+ # 3. Set difference to find orphaned labels
263
+ orphaned_labels = sha_repo_labels - used_labels
264
+
265
+ print(f"🗑️ Found {len(orphaned_labels)} truly orphaned labels")
266
+
267
+ # Debug: Show some examples if in dry run
268
+ if dry_run and orphaned_labels:
269
+ print("🔍 Examples of orphaned labels:")
270
+ for label in list(orphaned_labels)[:5]:
271
+ print(f" • {label}")
272
+ if dry_run and used_labels:
273
+ print("🔍 Examples of used labels:")
274
+ for label in list(used_labels)[:5]:
275
+ print(f" • {label}")
276
+
277
+ if not dry_run and orphaned_labels:
278
+ deleted_labels = []
279
+ for label in orphaned_labels:
280
+ if self.delete_repository_label(label):
281
+ deleted_labels.append(label)
282
+ return deleted_labels
283
+
284
+ return list(orphaned_labels)
285
+
286
+ except Exception as e:
287
+ print(f"⚠️ Error during orphan detection: {e}")
288
+ # Fallback to old pattern-based method
289
+ return self.cleanup_sha_labels(dry_run)
290
+
228
291
  def create_or_update_label(self, name: str, color: str, description: str) -> bool:
229
292
  """Create or update a label with color and description"""
230
293
  import urllib.parse
@@ -31,6 +31,10 @@ LABEL_DEFINITIONS = {
31
31
  "color": "FFE4B5", # Light orange
32
32
  "description": "Freeze PR - prevent auto-sync on new commits",
33
33
  },
34
+ "🎪 🔒 showtime-blocked": {
35
+ "color": "dc3545", # Red - blocking/danger
36
+ "description": "Block all Showtime operations - maintenance mode",
37
+ },
34
38
  }
35
39
 
36
40
  # Status-specific label patterns (generated dynamically)
@@ -90,20 +90,10 @@ class PullRequest:
90
90
 
91
91
  @property
92
92
  def building_show(self) -> Optional[Show]:
93
- """The currently building show (from 🏗️ label)"""
94
- building_sha = None
95
- for label in self.labels:
96
- if label.startswith("🎪 🏗️ "):
97
- building_sha = label.split(" ")[2]
98
- break
99
-
100
- if not building_sha:
101
- return None
102
-
93
+ """The currently building show (from building/deploying status)"""
103
94
  for show in self.shows:
104
- if show.sha == building_sha:
95
+ if show.status in ["building", "deploying"]:
105
96
  return show
106
-
107
97
  return None
108
98
 
109
99
  @property
@@ -185,6 +175,88 @@ class PullRequest:
185
175
  for label in circus_labels:
186
176
  self.remove_label(label)
187
177
 
178
+ def set_show_status(self, show: Show, new_status: str) -> None:
179
+ """Atomically update show status with thorough label cleanup"""
180
+ show.status = new_status
181
+
182
+ # 1. Refresh labels to get current GitHub state
183
+ self.refresh_labels()
184
+
185
+ # 2. Remove ALL existing status labels for this SHA (not just the "expected" one)
186
+ status_labels_to_remove = [
187
+ label for label in self.labels if label.startswith(f"🎪 {show.sha} 🚦 ")
188
+ ]
189
+
190
+ for label in status_labels_to_remove:
191
+ self.remove_label(label)
192
+
193
+ # 3. Add the new status label
194
+ new_status_label = f"🎪 {show.sha} 🚦 {new_status}"
195
+ self.add_label(new_status_label)
196
+
197
+ def set_active_show(self, show: Show) -> None:
198
+ """Atomically set this show as the active environment"""
199
+ from .emojis import CIRCUS_PREFIX, MEANING_TO_EMOJI
200
+
201
+ # 1. Refresh to get current state
202
+ self.refresh_labels()
203
+
204
+ # 2. Remove ALL existing active pointers (ensure only one)
205
+ active_emoji = MEANING_TO_EMOJI["active"] # Gets 🎯
206
+ active_prefix = f"{CIRCUS_PREFIX} {active_emoji} " # "🎪 🎯 "
207
+ active_pointers = [label for label in self.labels if label.startswith(active_prefix)]
208
+
209
+ for pointer in active_pointers:
210
+ self.remove_label(pointer)
211
+
212
+ # 3. Set this show as the new active one
213
+ active_pointer = f"{active_prefix}{show.sha}" # "🎪 🎯 abc123f"
214
+ self.add_label(active_pointer)
215
+
216
+ def _check_authorization(self) -> bool:
217
+ """Check if current GitHub actor is authorized for operations"""
218
+ import os
219
+
220
+ import httpx
221
+
222
+ # Only check in GitHub Actions context
223
+ if os.getenv("GITHUB_ACTIONS") != "true":
224
+ return True
225
+
226
+ actor = os.getenv("GITHUB_ACTOR")
227
+ if not actor:
228
+ return True # No actor info, allow operation
229
+
230
+ try:
231
+ # Use existing GitHubInterface for consistency
232
+ github = get_github()
233
+
234
+ # Check collaborator permissions
235
+ perm_url = f"{github.base_url}/repos/{github.org}/{github.repo}/collaborators/{actor}/permission"
236
+
237
+ with httpx.Client() as client:
238
+ response = client.get(perm_url, headers=github.headers)
239
+ if response.status_code == 404:
240
+ return False # Not a collaborator
241
+ response.raise_for_status()
242
+
243
+ data = response.json()
244
+ permission = data.get("permission", "none")
245
+
246
+ # Allow write and admin permissions only
247
+ authorized = permission in ["write", "admin"]
248
+
249
+ if not authorized:
250
+ print(f"🚨 Unauthorized actor {actor} (permission: {permission})")
251
+ # Set blocked label for security
252
+ self.add_label("🎪 🔒 showtime-blocked")
253
+
254
+ return authorized
255
+
256
+ except Exception as e:
257
+ print(f"⚠️ Authorization check failed: {e}")
258
+ return True # Fail open for non-security operations
259
+
188
260
  def analyze(self, target_sha: str, pr_state: str = "open") -> AnalysisResult:
189
261
  """Analyze what actions are needed (read-only, for --check-only)
190
262
 
@@ -208,7 +280,7 @@ class PullRequest:
208
280
  build_needed = action_needed in ["create_environment", "rolling_update", "auto_sync"]
209
281
 
210
282
  # Determine if sync execution is needed
211
- sync_needed = action_needed != "no_action"
283
+ sync_needed = action_needed not in ["no_action", "blocked"]
212
284
 
213
285
  return AnalysisResult(
214
286
  action_needed=action_needed,
@@ -244,8 +316,21 @@ class PullRequest:
244
316
  # 1. Determine what action is needed
245
317
  action_needed = self._determine_action(target_sha)
246
318
 
247
- # 2. Atomic claim for environment changes (PR-level lock)
248
- if action_needed in ["create_environment", "rolling_update", "auto_sync"]:
319
+ # 2. Check for blocked state (fast bailout)
320
+ if action_needed == "blocked":
321
+ return SyncResult(
322
+ success=False,
323
+ action_taken="blocked",
324
+ error="🔒 Showtime operations are blocked for this PR. Remove '🎪 🔒 showtime-blocked' label to re-enable.",
325
+ )
326
+
327
+ # 3. Atomic claim for environment changes (PR-level lock)
328
+ if action_needed in [
329
+ "create_environment",
330
+ "rolling_update",
331
+ "auto_sync",
332
+ "destroy_environment",
333
+ ]:
249
334
  print(f"🔒 Claiming environment for {action_needed}...")
250
335
  if not self._atomic_claim(target_sha, action_needed, dry_run_github):
251
336
  print("❌ Claim failed - another job is active")
@@ -266,14 +351,14 @@ class PullRequest:
266
351
  # Phase 1: Docker build
267
352
  print("🐳 Building Docker image...")
268
353
  show.build_docker(dry_run_docker)
269
- show.status = "built"
270
354
  print("✅ Docker build completed")
271
- self._update_show_labels(show, dry_run_github)
272
355
 
273
356
  # Phase 2: AWS deployment
274
357
  print("☁️ Deploying to AWS ECS...")
358
+ self.set_show_status(show, "deploying")
275
359
  show.deploy_aws(dry_run_aws)
276
- show.status = "running"
360
+ self.set_show_status(show, "running")
361
+ self.set_active_show(show)
277
362
  print(f"✅ Deployment completed - environment running at {show.ip}:8080")
278
363
  self._update_show_labels(show, dry_run_github)
279
364
 
@@ -303,14 +388,14 @@ class PullRequest:
303
388
  # Phase 1: Docker build
304
389
  print("🐳 Building updated Docker image...")
305
390
  new_show.build_docker(dry_run_docker)
306
- new_show.status = "built"
307
391
  print("✅ Docker build completed")
308
- self._update_show_labels(new_show, dry_run_github)
309
392
 
310
393
  # Phase 2: Blue-green deployment
311
394
  print("☁️ Deploying updated environment...")
395
+ self.set_show_status(new_show, "deploying")
312
396
  new_show.deploy_aws(dry_run_aws)
313
- new_show.status = "running"
397
+ self.set_show_status(new_show, "running")
398
+ self.set_active_show(new_show)
314
399
  print(f"✅ Rolling update completed - new environment at {new_show.ip}:8080")
315
400
  self._update_show_labels(new_show, dry_run_github)
316
401
 
@@ -406,7 +491,7 @@ class PullRequest:
406
491
  if any(label == f"🎪 🎯 {show.sha}" for label in pr.labels):
407
492
  show_type = "active"
408
493
  # Check for building pointer
409
- elif any(label == f"🎪 🏗️ {show.sha}" for label in pr.labels):
494
+ elif show.status in ["building", "deploying"]:
410
495
  show_type = "building"
411
496
  # No pointer = orphaned
412
497
 
@@ -433,6 +518,14 @@ class PullRequest:
433
518
  # CRITICAL: Get fresh labels before any decisions
434
519
  self.refresh_labels()
435
520
 
521
+ # Check for blocked state first (fast bailout)
522
+ if "🎪 🔒 showtime-blocked" in self.labels:
523
+ return "blocked"
524
+
525
+ # Check authorization (security layer)
526
+ if not self._check_authorization():
527
+ return "blocked"
528
+
436
529
  target_sha_short = target_sha[:7] # Ensure we're working with short SHA
437
530
 
438
531
  # Get the specific show for the target SHA
@@ -455,10 +548,14 @@ class PullRequest:
455
548
  elif "showtime-trigger-stop" in trigger:
456
549
  return "destroy_environment"
457
550
 
458
- # No explicit triggers - check target SHA state
551
+ # No explicit triggers - only auto-create if there's ANY previous environment
459
552
  if not target_show:
460
- # Target SHA doesn't exist - create it
461
- return "create_environment"
553
+ # Target SHA doesn't exist - only create if there's any previous environment
554
+ if self.shows: # Any previous environment exists
555
+ return "create_environment"
556
+ else:
557
+ # No previous environments - don't auto-create without explicit trigger
558
+ return "no_action"
462
559
  elif target_show.status == "failed":
463
560
  # Target SHA failed - rebuild it
464
561
  return "create_environment"
@@ -515,7 +612,6 @@ class PullRequest:
515
612
  for label in new_labels:
516
613
  try:
517
614
  self.add_label(label)
518
- print(f" ✅ Added: {label}")
519
615
  except Exception as e:
520
616
  print(f" ❌ Failed to add {label}: {e}")
521
617
  raise
@@ -646,7 +742,6 @@ class PullRequest:
646
742
  and (
647
743
  label.startswith(f"🎪 {show.sha} ") # SHA-first format: 🎪 abc123f 📅 ...
648
744
  or label.startswith(f"🎪 🎯 {show.sha}") # Pointer format: 🎪 🎯 abc123f
649
- or label.startswith(f"🎪 🏗️ {show.sha}") # Building pointer: 🎪 🏗️ abc123f
650
745
  )
651
746
  }
652
747
  desired_labels = set(show.to_circus_labels())
@@ -704,9 +799,7 @@ class PullRequest:
704
799
  existing_labels = [
705
800
  label
706
801
  for label in self.labels
707
- if label.startswith(f"🎪 {show.sha} ")
708
- or label == f"🎪 🎯 {show.sha}"
709
- or label == f"🎪 🏗️ {show.sha}"
802
+ if label.startswith(f"🎪 {show.sha} ") or label == f"🎪 🎯 {show.sha}"
710
803
  ]
711
804
  print(f"🏷️ Removing existing labels for {show.sha}: {existing_labels}")
712
805
  for label in existing_labels:
showtime/core/show.py CHANGED
@@ -96,21 +96,24 @@ class Show:
96
96
 
97
97
  def to_circus_labels(self) -> List[str]:
98
98
  """Convert show state to circus tent emoji labels (per-SHA format)"""
99
+ from .emojis import CIRCUS_PREFIX, MEANING_TO_EMOJI
100
+
99
101
  if not self.created_at:
100
102
  self.created_at = datetime.utcnow().strftime("%Y-%m-%dT%H-%M")
101
103
 
102
104
  labels = [
103
- f"🎪 {self.sha} 🚦 {self.status}", # SHA-first status
104
- f"🎪 🎯 {self.sha}", # Active pointer (no value)
105
- f"🎪 {self.sha} 📅 {self.created_at}", # SHA-first timestamp
106
- f"🎪 {self.sha} ⌛ {self.ttl}", # SHA-first TTL
105
+ f"{CIRCUS_PREFIX} {self.sha} {MEANING_TO_EMOJI['status']} {self.status}", # SHA-first status
106
+ f"{CIRCUS_PREFIX} {self.sha} {MEANING_TO_EMOJI['created_at']} {self.created_at}", # SHA-first timestamp
107
+ f"{CIRCUS_PREFIX} {self.sha} {MEANING_TO_EMOJI['ttl']} {self.ttl}", # SHA-first TTL
107
108
  ]
108
109
 
109
110
  if self.ip:
110
- labels.append(f"🎪 {self.sha} 🌐 {self.ip}:8080")
111
+ labels.append(f"{CIRCUS_PREFIX} {self.sha} {MEANING_TO_EMOJI['ip']} {self.ip}:8080")
111
112
 
112
113
  if self.requested_by:
113
- labels.append(f"🎪 {self.sha} 🤡 {self.requested_by}")
114
+ labels.append(
115
+ f"{CIRCUS_PREFIX} {self.sha} {MEANING_TO_EMOJI['requested_by']} {self.requested_by}"
116
+ )
114
117
 
115
118
  return labels
116
119
 
@@ -158,7 +161,6 @@ class Show:
158
161
  def _build_docker_image(self) -> None:
159
162
  """Build Docker image for this environment"""
160
163
  import os
161
- import platform
162
164
  import subprocess
163
165
 
164
166
  tag = f"apache/superset:pr-{self.pr_number}-{self.sha}-ci"
@@ -187,19 +189,23 @@ class Show:
187
189
  # Add caching based on environment
188
190
  if is_ci:
189
191
  # Full registry caching in CI (Docker driver supports it)
190
- cmd.extend([
191
- "--cache-from",
192
- "type=registry,ref=apache/superset-cache:showtime",
193
- "--cache-to",
194
- "type=registry,mode=max,ref=apache/superset-cache:showtime",
195
- ])
192
+ cmd.extend(
193
+ [
194
+ "--cache-from",
195
+ "type=registry,ref=apache/superset-cache:showtime",
196
+ "--cache-to",
197
+ "type=registry,mode=max,ref=apache/superset-cache:showtime",
198
+ ]
199
+ )
196
200
  print("🐳 CI environment: Using full registry caching")
197
201
  else:
198
202
  # Local build: cache-from only (no cache export)
199
- cmd.extend([
200
- "--cache-from",
201
- "type=registry,ref=apache/superset-cache:showtime",
202
- ])
203
+ cmd.extend(
204
+ [
205
+ "--cache-from",
206
+ "type=registry,ref=apache/superset-cache:showtime",
207
+ ]
208
+ )
203
209
  print("🐳 Local environment: Using cache-from only (no export)")
204
210
 
205
211
  # Add --load only when explicitly requested for local testing
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: superset-showtime
3
- Version: 0.5.12
3
+ Version: 0.6.3
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/
@@ -0,0 +1,17 @@
1
+ showtime/__init__.py,sha256=IDSoi3ERNMkK-VnFVyhCFJdw5YdzsIIUFwHUXKrSSDw,448
2
+ showtime/__main__.py,sha256=EVaDaTX69yIhCzChg99vqvFSCN4ELstEt7Mpb9FMZX8,109
3
+ showtime/cli.py,sha256=TLv9NaqPyewKJi9uCTZKWBijGelqunmsoSo7cyKajV4,31640
4
+ showtime/core/__init__.py,sha256=54hbdFNGrzuNMBdraezfjT8Zi6g221pKlJ9mREnKwCw,34
5
+ showtime/core/aws.py,sha256=uTjJAvEBQMyTccS93WZeNPhfeKQhJgOQQ0BJdnQjvCU,35007
6
+ showtime/core/emojis.py,sha256=arK0N5Q5FLkvOkci-lacb3WS56LTvY8NjYRqt_lhP9s,2188
7
+ showtime/core/git_validation.py,sha256=3dmSGpMDplDAmKWHUyoUEPgt3__8oTuBZxbfuhocT00,6831
8
+ showtime/core/github.py,sha256=mSOqRLy2KMDhWUS37V2gJ-CQdeBpEqunBRKL10v5hxU,12268
9
+ showtime/core/github_messages.py,sha256=MfgwCukrEsWWesMsuL8saciDgP4nS-gijzu8DXr-Alg,7450
10
+ showtime/core/label_colors.py,sha256=gSe7EIMl4YjWkIgKHUvuaRSwgEB_B-NYQBxFFlF8Z3s,4065
11
+ showtime/core/pull_request.py,sha256=v_1hi-UNOckIv1-C7JQWKyLgVna_6vflMZoRAymPxgE,32355
12
+ showtime/core/show.py,sha256=sOgZvGXwdcNDsidF1F_XwPXlSeTb8-Zeqhqb8w1pqAM,9973
13
+ showtime/data/ecs-task-definition.json,sha256=d-NLkIhvr4C6AnwDfDIwUTx-6KFMH9wRkt6pVCbqZY4,2365
14
+ superset_showtime-0.6.3.dist-info/METADATA,sha256=mXRuwlkq93XYCc5EcoQpHG3jYTaeipyPGsfvGZ6vmFQ,12052
15
+ superset_showtime-0.6.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
+ superset_showtime-0.6.3.dist-info/entry_points.txt,sha256=rDW7oZ57mqyBUS4N_3_R7bZNGVHB-104jwmY-hHC_ck,85
17
+ superset_showtime-0.6.3.dist-info/RECORD,,
@@ -1,17 +0,0 @@
1
- showtime/__init__.py,sha256=Rdi2WLB7sEHeBvwFnKdebcbskh9xDjmvIwxcOs9VU_0,449
2
- showtime/__main__.py,sha256=EVaDaTX69yIhCzChg99vqvFSCN4ELstEt7Mpb9FMZX8,109
3
- showtime/cli.py,sha256=8vIJT5TiqXuHDGxRBg6jV3oNv5nKrmDOs5OgltycPeI,31664
4
- showtime/core/__init__.py,sha256=54hbdFNGrzuNMBdraezfjT8Zi6g221pKlJ9mREnKwCw,34
5
- showtime/core/aws.py,sha256=uTjJAvEBQMyTccS93WZeNPhfeKQhJgOQQ0BJdnQjvCU,35007
6
- showtime/core/emojis.py,sha256=MHEDuPIdfNiop4zbNLuviz3eY05QiftYSHHCVbkfKhw,2129
7
- showtime/core/git_validation.py,sha256=3dmSGpMDplDAmKWHUyoUEPgt3__8oTuBZxbfuhocT00,6831
8
- showtime/core/github.py,sha256=gMPJ5TOT6DdZk4y0XqW-C69I7O8A4eI40TgT4IFPqhQ,9623
9
- showtime/core/github_messages.py,sha256=MfgwCukrEsWWesMsuL8saciDgP4nS-gijzu8DXr-Alg,7450
10
- showtime/core/label_colors.py,sha256=efhbFnz_3nqEnEqmgyF6_hZbxtCu_fmb68BIIUpSsnk,3895
11
- showtime/core/pull_request.py,sha256=L9d0gHJihc6GYDWWM3oK-sGNu-yysUopMGXnqwP6I_4,28748
12
- showtime/core/show.py,sha256=FpxDm52LASCJvf8UF998AtNiVzfdYIwNEsPAsOAAwL0,9701
13
- showtime/data/ecs-task-definition.json,sha256=d-NLkIhvr4C6AnwDfDIwUTx-6KFMH9wRkt6pVCbqZY4,2365
14
- superset_showtime-0.5.12.dist-info/METADATA,sha256=orUqMEiS9J1OUBm2uH7o-7aHfIEKvp1CZ4e7z5gk70I,12053
15
- superset_showtime-0.5.12.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
- superset_showtime-0.5.12.dist-info/entry_points.txt,sha256=rDW7oZ57mqyBUS4N_3_R7bZNGVHB-104jwmY-hHC_ck,85
17
- superset_showtime-0.5.12.dist-info/RECORD,,