superset-showtime 0.5.10__tar.gz → 0.5.12__tar.gz

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.

Files changed (34) hide show
  1. {superset_showtime-0.5.10 → superset_showtime-0.5.12}/CLAUDE.md +65 -0
  2. {superset_showtime-0.5.10 → superset_showtime-0.5.12}/PKG-INFO +1 -1
  3. {superset_showtime-0.5.10 → superset_showtime-0.5.12}/showtime/__init__.py +1 -1
  4. {superset_showtime-0.5.10 → superset_showtime-0.5.12}/showtime/core/git_validation.py +75 -37
  5. {superset_showtime-0.5.10 → superset_showtime-0.5.12}/showtime/core/github.py +0 -8
  6. {superset_showtime-0.5.10 → superset_showtime-0.5.12}/showtime/core/pull_request.py +98 -60
  7. {superset_showtime-0.5.10 → superset_showtime-0.5.12}/showtime/data/ecs-task-definition.json +1 -1
  8. {superset_showtime-0.5.10 → superset_showtime-0.5.12}/.claude/settings.local.json +0 -0
  9. {superset_showtime-0.5.10 → superset_showtime-0.5.12}/.gitignore +0 -0
  10. {superset_showtime-0.5.10 → superset_showtime-0.5.12}/.pre-commit-config.yaml +0 -0
  11. {superset_showtime-0.5.10 → superset_showtime-0.5.12}/Makefile +0 -0
  12. {superset_showtime-0.5.10 → superset_showtime-0.5.12}/README.md +0 -0
  13. {superset_showtime-0.5.10 → superset_showtime-0.5.12}/dev-setup.sh +0 -0
  14. {superset_showtime-0.5.10 → superset_showtime-0.5.12}/pypi-push.sh +0 -0
  15. {superset_showtime-0.5.10 → superset_showtime-0.5.12}/pyproject.toml +0 -0
  16. {superset_showtime-0.5.10 → superset_showtime-0.5.12}/requirements-dev.txt +0 -0
  17. {superset_showtime-0.5.10 → superset_showtime-0.5.12}/requirements.txt +0 -0
  18. {superset_showtime-0.5.10 → superset_showtime-0.5.12}/showtime/__main__.py +0 -0
  19. {superset_showtime-0.5.10 → superset_showtime-0.5.12}/showtime/cli.py +0 -0
  20. {superset_showtime-0.5.10 → superset_showtime-0.5.12}/showtime/core/__init__.py +0 -0
  21. {superset_showtime-0.5.10 → superset_showtime-0.5.12}/showtime/core/aws.py +0 -0
  22. {superset_showtime-0.5.10 → superset_showtime-0.5.12}/showtime/core/emojis.py +0 -0
  23. {superset_showtime-0.5.10 → superset_showtime-0.5.12}/showtime/core/github_messages.py +0 -0
  24. {superset_showtime-0.5.10 → superset_showtime-0.5.12}/showtime/core/label_colors.py +0 -0
  25. {superset_showtime-0.5.10 → superset_showtime-0.5.12}/showtime/core/show.py +0 -0
  26. {superset_showtime-0.5.10 → superset_showtime-0.5.12}/tests/__init__.py +0 -0
  27. {superset_showtime-0.5.10 → superset_showtime-0.5.12}/tests/unit/__init__.py +0 -0
  28. {superset_showtime-0.5.10 → superset_showtime-0.5.12}/tests/unit/test_label_transitions.py +0 -0
  29. {superset_showtime-0.5.10 → superset_showtime-0.5.12}/tests/unit/test_pull_request.py +0 -0
  30. {superset_showtime-0.5.10 → superset_showtime-0.5.12}/tests/unit/test_sha_specific_logic.py +0 -0
  31. {superset_showtime-0.5.10 → superset_showtime-0.5.12}/tests/unit/test_show.py +0 -0
  32. {superset_showtime-0.5.10 → superset_showtime-0.5.12}/uv.lock +0 -0
  33. {superset_showtime-0.5.10 → superset_showtime-0.5.12}/workflows-reference/showtime-cleanup.yml +0 -0
  34. {superset_showtime-0.5.10 → superset_showtime-0.5.12}/workflows-reference/showtime-trigger.yml +0 -0
@@ -164,3 +164,68 @@ showtime list --status running
164
164
  showtime start 1234 --sha abc123f
165
165
  showtime stop 1234 --force
166
166
  ```
167
+
168
+ ## Race Condition Handling
169
+
170
+ ### Problem Description
171
+
172
+ Double triggers can create race conditions in two scenarios:
173
+
174
+ 1. **Same SHA conflicts**: User pushes commit abc123f twice, creating 2 workflows for identical SHA
175
+ 2. **Stale locks**: Jobs crash or get killed, leaving environments stuck in "building/deploying" state indefinitely
176
+
177
+ ### Current Atomic Claim Mechanism
178
+
179
+ The `PullRequest._atomic_claim()` method handles basic conflicts by:
180
+ 1. Checking if target SHA is already in progress states (`building`, `built`, `deploying`)
181
+ 2. Removing trigger labels atomically
182
+ 3. Setting building state immediately
183
+
184
+ **Limitations**:
185
+ - No distinction between valid locks and stale locks (>1 hour old)
186
+ - `refresh_labels()` is expensive (~500ms) but called on every claim attempt
187
+ - Crashed jobs can leave permanent locks that block future deployments
188
+
189
+ ### Proposed Smart Lock Detection Strategy
190
+
191
+ **Two-phase approach optimizing for the common case**:
192
+
193
+ #### Phase 1: Fast Path (95% of calls, ~5ms)
194
+ ```python
195
+ def can_start_job(self, target_sha: str, action: str, use_cached: bool = True) -> tuple[bool, str]:
196
+ """Fast check using cached self.labels"""
197
+ # Check cached labels for basic conflicts
198
+ # Returns (can_start, reason)
199
+ ```
200
+
201
+ #### Phase 2: Recovery Path (5% of calls, ~500ms)
202
+ ```python
203
+ def double_check_and_cleanup_stale_locks(self, target_sha: str, stale_hours: int = 1, dry_run: bool = False) -> bool:
204
+ """Expensive: refresh labels, detect stale locks (>1h), clean them up"""
205
+ # Only called when fast path detects potential conflict
206
+ # Refreshes labels, checks timestamps, cleans stale AWS resources + GitHub labels
207
+ ```
208
+
209
+ #### Enhanced Atomic Claim Logic
210
+ ```python
211
+ def _atomic_claim(self, target_sha: str, action: str, dry_run: bool = False) -> bool:
212
+ # 1. Fast check with cached labels
213
+ can_start, reason = self.can_start_job(target_sha, action, use_cached=True)
214
+
215
+ if not can_start:
216
+ # 2. Expensive double-check and cleanup
217
+ can_start = self.double_check_and_cleanup_stale_locks(target_sha, stale_hours=1, dry_run=dry_run)
218
+
219
+ # 3. Continue with existing trigger removal + building setup
220
+ ```
221
+
222
+ ### Key Benefits
223
+
224
+ - **Performance**: 95% fast path using cached labels (~5ms vs ~500ms)
225
+ - **Reliability**: Automatic recovery from stale locks (crashed/killed jobs)
226
+ - **Clarity**: Clear distinction between valid conflicts and recoverable states
227
+ - **Safety**: Only cleans locks older than configurable threshold (default: 1 hour)
228
+
229
+ ### Implementation Notes
230
+
231
+ This enhancement can be implemented when race conditions become problematic in practice. The current trigger removal mechanism already handles most same-SHA conflicts effectively due to the speed of GitHub label operations.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: superset-showtime
3
- Version: 0.5.10
3
+ Version: 0.5.12
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/
@@ -4,7 +4,7 @@
4
4
  Circus tent emoji state tracking for Apache Superset ephemeral environments.
5
5
  """
6
6
 
7
- __version__ = "0.5.10"
7
+ __version__ = "0.5.12"
8
8
  __author__ = "Maxime Beauchemin"
9
9
  __email__ = "maximebeauchemin@gmail.com"
10
10
 
@@ -50,7 +50,7 @@ def is_git_repository(path: str = ".") -> bool:
50
50
  def validate_required_sha(required_sha: Optional[str] = None) -> Tuple[bool, Optional[str]]:
51
51
  """
52
52
  Validate that the required SHA exists in the current Git repository.
53
- Tries to fetch the SHA from origin if validation fails in a shallow clone.
53
+ Uses GitHub API for reliable validation in shallow clone environments.
54
54
 
55
55
  Args:
56
56
  required_sha: SHA to validate (default: REQUIRED_SHA constant)
@@ -60,55 +60,93 @@ def validate_required_sha(required_sha: Optional[str] = None) -> Tuple[bool, Opt
60
60
  - (True, None) if validation passes
61
61
  - (False, error_message) if validation fails
62
62
  """
63
- if Repo is None:
64
- return False, "GitPython not available for SHA validation"
65
-
66
63
  sha_to_check = required_sha or REQUIRED_SHA
67
64
  if not sha_to_check:
68
65
  return True, None # No requirement set
69
66
 
67
+ # Try GitHub API validation first (works in shallow clones)
70
68
  try:
71
- repo = Repo(".")
69
+ return _validate_sha_via_github_api(sha_to_check)
70
+ except Exception as e:
71
+ print(f"⚠️ GitHub API validation failed: {e}")
72
+
73
+ # Fallback to Git validation for non-GitHub origins
74
+ if Repo is None:
75
+ print("⚠️ GitPython not available, skipping SHA validation")
76
+ return True, None
72
77
 
73
- # First attempt: Search for SHA in git log (has to work in shallow clones where merge_base fails)
78
+ try:
79
+ repo = Repo(".")
74
80
  is_valid, error = _validate_sha_in_log(repo, sha_to_check)
75
81
  if is_valid:
76
82
  return True, None
83
+ else:
84
+ print(f"⚠️ Git validation failed: {error}")
85
+ return True, None # Allow operation to continue
77
86
 
78
- # If validation failed, check if we're in a shallow clone and try fetching
79
- try:
80
- is_shallow = repo.git.rev_parse("--is-shallow-repository") == "true"
81
- if is_shallow:
82
- try:
83
- print(f"🌊 Shallow clone detected, attempting to fetch {sha_to_check[:7]}...")
84
- repo.git.fetch("origin", sha_to_check)
85
-
86
- # Retry validation after fetch
87
- is_valid_after_fetch, error_after_fetch = _validate_sha_in_log(
88
- repo, sha_to_check
89
- )
90
- if is_valid_after_fetch:
91
- print(f"✅ Successfully fetched and validated {sha_to_check[:7]}")
92
- return True, None
93
- else:
94
- return False, error_after_fetch
95
-
96
- except Exception as fetch_error:
97
- return False, (
98
- f"Required commit {sha_to_check} not found in shallow clone. "
99
- f"Failed to fetch from origin: {fetch_error}"
100
- )
101
- else:
102
- return False, error
87
+ except InvalidGitRepositoryError:
88
+ print("⚠️ Not a Git repository, skipping SHA validation")
89
+ return True, None
90
+ except Exception as e:
91
+ print(f"⚠️ Git validation error: {e}")
92
+ return True, None
103
93
 
104
- except Exception:
105
- # If shallow check fails, return original error
106
- return False, error
107
94
 
108
- except InvalidGitRepositoryError:
109
- return False, "Current directory is not a Git repository"
95
+ def _validate_sha_via_github_api(required_sha: str) -> Tuple[bool, Optional[str]]:
96
+ """Validate SHA using GitHub API - works reliably in shallow clones"""
97
+ try:
98
+ import httpx
99
+ from git import Repo
100
+
101
+ from .github import GitHubInterface
102
+
103
+ # Get current SHA from Git
104
+ repo = Repo(".")
105
+ current_sha = repo.head.commit.hexsha
106
+
107
+ # Use existing GitHubInterface (handles all the setup/token detection)
108
+ github = GitHubInterface()
109
+
110
+ # 1. Check if required SHA exists
111
+ commit_url = f"{github.base_url}/repos/{github.org}/{github.repo}/commits/{required_sha}"
112
+
113
+ with httpx.Client() as client:
114
+ response = client.get(commit_url, headers=github.headers)
115
+ if response.status_code == 404:
116
+ return False, f"Required SHA {required_sha[:7]} not found in repository"
117
+ response.raise_for_status()
118
+
119
+ # 2. Compare SHAs to verify ancestry
120
+ compare_url = f"{github.base_url}/repos/{github.org}/{github.repo}/compare/{required_sha}...{current_sha}"
121
+
122
+ with httpx.Client() as client:
123
+ response = client.get(compare_url, headers=github.headers)
124
+ if response.status_code == 404:
125
+ return (
126
+ False,
127
+ f"Current branch does not include required SHA {required_sha[:7]}. Please rebase onto main.",
128
+ )
129
+ response.raise_for_status()
130
+
131
+ data = response.json()
132
+ status = data.get("status")
133
+
134
+ # If status is 'ahead' or 'identical', required SHA is ancestor (good)
135
+ # If status is 'behind', current is behind required (bad)
136
+ if status in ["ahead", "identical"]:
137
+ print(
138
+ f"✅ Validated that required SHA {required_sha[:7]} is included in current branch"
139
+ )
140
+ return True, None
141
+ else:
142
+ return (
143
+ False,
144
+ f"Current branch does not include required SHA {required_sha[:7]}. Please rebase onto main.",
145
+ )
146
+
110
147
  except Exception as e:
111
- return False, f"Git validation error: {e}"
148
+ # Re-raise to be caught by the caller for proper fallback handling
149
+ raise Exception(f"GitHub API validation error: {e}") from e
112
150
 
113
151
 
114
152
  def _validate_sha_in_log(repo: "Repo", sha_to_check: str) -> Tuple[bool, Optional[str]]:
@@ -111,14 +111,6 @@ class GitHubInterface:
111
111
  if response.status_code not in (200, 204, 404):
112
112
  response.raise_for_status()
113
113
 
114
- def set_labels(self, pr_number: int, labels: List[str]) -> None:
115
- """Replace all labels on a PR"""
116
- url = f"{self.base_url}/repos/{self.org}/{self.repo}/issues/{pr_number}/labels"
117
-
118
- with httpx.Client() as client:
119
- response = client.put(url, headers=self.headers, json={"labels": labels})
120
- response.raise_for_status()
121
-
122
114
  def get_latest_commit_sha(self, pr_number: int) -> str:
123
115
  """Get the latest commit SHA for a PR"""
124
116
  pr_data = self.get_pr_data(pr_number)
@@ -60,7 +60,7 @@ class PullRequest:
60
60
 
61
61
  def __init__(self, pr_number: int, labels: List[str]):
62
62
  self.pr_number = pr_number
63
- self.labels = labels
63
+ self.labels = set(labels) # Convert to set for O(1) operations
64
64
  self._shows = self._parse_shows_from_labels()
65
65
 
66
66
  @property
@@ -151,9 +151,40 @@ class PullRequest:
151
151
 
152
152
  def refresh_labels(self) -> None:
153
153
  """Refresh labels from GitHub and reparse shows"""
154
- self.labels = get_github().get_labels(self.pr_number)
154
+ self.labels = set(get_github().get_labels(self.pr_number))
155
155
  self._shows = self._parse_shows_from_labels()
156
156
 
157
+ def add_label(self, label: str) -> None:
158
+ """Add label with logging and optimistic state update"""
159
+ print(f"🏷️ Added: {label}")
160
+ get_github().add_label(self.pr_number, label)
161
+ self.labels.add(label)
162
+
163
+ def remove_label(self, label: str) -> None:
164
+ """Remove label with logging and optimistic state update"""
165
+ print(f"🗑️ Removed: {label}")
166
+ get_github().remove_label(self.pr_number, label)
167
+ self.labels.discard(label) # Safe - won't raise if not present
168
+
169
+ def remove_sha_labels(self, sha: str) -> None:
170
+ """Remove all labels for a specific SHA"""
171
+ sha_short = sha[:7]
172
+ labels_to_remove = [
173
+ label for label in self.labels if label.startswith("🎪") and sha_short in label
174
+ ]
175
+ if labels_to_remove:
176
+ print(f"🗑️ Removing SHA {sha_short} labels: {labels_to_remove}")
177
+ for label in labels_to_remove:
178
+ self.remove_label(label)
179
+
180
+ def remove_showtime_labels(self) -> None:
181
+ """Remove ALL circus tent labels"""
182
+ circus_labels = [label for label in self.labels if label.startswith("🎪 ")]
183
+ if circus_labels:
184
+ print(f"🎪 Removing all showtime labels: {circus_labels}")
185
+ for label in circus_labels:
186
+ self.remove_label(label)
187
+
157
188
  def analyze(self, target_sha: str, pr_state: str = "open") -> AnalysisResult:
158
189
  """Analyze what actions are needed (read-only, for --check-only)
159
190
 
@@ -245,10 +276,12 @@ class PullRequest:
245
276
  show.status = "running"
246
277
  print(f"✅ Deployment completed - environment running at {show.ip}:8080")
247
278
  self._update_show_labels(show, dry_run_github)
248
-
279
+
249
280
  # Blue-green cleanup: stop all other environments for this PR
250
- cleaned_count = self.stop_previous_environments(show.sha, dry_run_github, dry_run_aws)
251
-
281
+ cleaned_count = self.stop_previous_environments(
282
+ show.sha, dry_run_github, dry_run_aws
283
+ )
284
+
252
285
  # Show AWS console URLs for monitoring
253
286
  self._show_service_urls(show)
254
287
 
@@ -267,7 +300,7 @@ class PullRequest:
267
300
  print(f"🔄 Rolling update: {old_show.sha} → {new_show.sha}")
268
301
  self._post_rolling_start_comment(old_show, new_show, dry_run_github)
269
302
 
270
- # Phase 1: Docker build
303
+ # Phase 1: Docker build
271
304
  print("🐳 Building updated Docker image...")
272
305
  new_show.build_docker(dry_run_docker)
273
306
  new_show.status = "built"
@@ -280,10 +313,12 @@ class PullRequest:
280
313
  new_show.status = "running"
281
314
  print(f"✅ Rolling update completed - new environment at {new_show.ip}:8080")
282
315
  self._update_show_labels(new_show, dry_run_github)
283
-
316
+
284
317
  # Blue-green cleanup: stop all other environments for this PR
285
- cleaned_count = self.stop_previous_environments(new_show.sha, dry_run_github, dry_run_aws)
286
-
318
+ cleaned_count = self.stop_previous_environments(
319
+ new_show.sha, dry_run_github, dry_run_aws
320
+ )
321
+
287
322
  # Show AWS console URLs for monitoring
288
323
  self._show_service_urls(new_show)
289
324
 
@@ -298,7 +333,7 @@ class PullRequest:
298
333
  self._post_cleanup_comment(self.current_show, dry_run_github)
299
334
  # Remove all circus labels after successful stop
300
335
  if not dry_run_github:
301
- get_github().remove_circus_labels(self.pr_number)
336
+ self.remove_showtime_labels()
302
337
  print("🏷️ GitHub labels cleaned up")
303
338
  print("✅ Environment destroyed")
304
339
  return SyncResult(success=True, action_taken="destroy_environment")
@@ -330,7 +365,7 @@ class PullRequest:
330
365
  self.current_show.stop(**kwargs)
331
366
  # Remove all circus labels after successful stop
332
367
  if not kwargs.get("dry_run_github", False):
333
- get_github().remove_circus_labels(self.pr_number)
368
+ self.remove_showtime_labels()
334
369
  return SyncResult(success=True, action_taken="stopped")
335
370
  except Exception as e:
336
371
  return SyncResult(success=False, action_taken="stop_failed", error=str(e))
@@ -366,15 +401,15 @@ class PullRequest:
366
401
  for show in pr.shows:
367
402
  # Determine show type based on pointer presence
368
403
  show_type = "orphaned" # Default
369
-
404
+
370
405
  # Check for active pointer
371
406
  if any(label == f"🎪 🎯 {show.sha}" for label in pr.labels):
372
407
  show_type = "active"
373
- # Check for building pointer
408
+ # Check for building pointer
374
409
  elif any(label == f"🎪 🏗️ {show.sha}" for label in pr.labels):
375
410
  show_type = "building"
376
411
  # No pointer = orphaned
377
-
412
+
378
413
  environment_data = {
379
414
  "pr_number": pr_number,
380
415
  "status": "active", # Keep for compatibility
@@ -397,12 +432,12 @@ class PullRequest:
397
432
  """Determine what sync action is needed based on target SHA state"""
398
433
  # CRITICAL: Get fresh labels before any decisions
399
434
  self.refresh_labels()
400
-
435
+
401
436
  target_sha_short = target_sha[:7] # Ensure we're working with short SHA
402
-
437
+
403
438
  # Get the specific show for the target SHA
404
439
  target_show = self.get_show_by_sha(target_sha_short)
405
-
440
+
406
441
  # Check for explicit trigger labels
407
442
  trigger_labels = [label for label in self.labels if "showtime-trigger-" in label]
408
443
 
@@ -440,16 +475,16 @@ class PullRequest:
440
475
  """Atomically claim this PR for the current job based on target SHA state"""
441
476
  # CRITICAL: Get fresh labels before any decisions
442
477
  self.refresh_labels()
443
-
478
+
444
479
  target_sha_short = target_sha[:7]
445
480
  target_show = self.get_show_by_sha(target_sha_short)
446
-
447
- # 1. Validate current state allows this action for target SHA
481
+
482
+ # 1. Validate current state allows this action for target SHA
448
483
  if action in ["create_environment", "rolling_update", "auto_sync"]:
449
484
  if target_show and target_show.status in [
450
485
  "building",
451
- "built",
452
- "deploying",
486
+ "built",
487
+ "deploying",
453
488
  ]:
454
489
  return False # Target SHA already in progress - ONLY conflict case returns
455
490
 
@@ -462,7 +497,7 @@ class PullRequest:
462
497
  if trigger_labels:
463
498
  print(f"🏷️ Removing trigger labels: {trigger_labels}")
464
499
  for trigger_label in trigger_labels:
465
- get_github().remove_label(self.pr_number, trigger_label)
500
+ self.remove_label(trigger_label)
466
501
  else:
467
502
  print("🏷️ No trigger labels to remove")
468
503
 
@@ -470,16 +505,16 @@ class PullRequest:
470
505
  if action in ["create_environment", "rolling_update", "auto_sync"]:
471
506
  building_show = self._create_new_show(target_sha)
472
507
  building_show.status = "building"
473
-
474
- # Update labels to reflect building state
475
- print(f"🏷️ Removing existing circus labels...")
476
- get_github().remove_circus_labels(self.pr_number)
477
-
508
+
509
+ # Update labels to reflect building state - only remove for this SHA
510
+ print(f"🏷️ Removing labels for SHA {target_sha[:7]}...")
511
+ self.remove_sha_labels(target_sha)
512
+
478
513
  new_labels = building_show.to_circus_labels()
479
514
  print(f"🏷️ Creating new labels: {new_labels}")
480
515
  for label in new_labels:
481
516
  try:
482
- get_github().add_label(self.pr_number, label)
517
+ self.add_label(label)
483
518
  print(f" ✅ Added: {label}")
484
519
  except Exception as e:
485
520
  print(f" ❌ Failed to add {label}: {e}")
@@ -583,23 +618,21 @@ class PullRequest:
583
618
 
584
619
  # First, remove any existing status labels for this SHA to ensure clean transitions
585
620
  sha_status_labels = [
586
- label for label in self.labels
587
- if label.startswith(f"🎪 {show.sha} 🚦 ")
621
+ label for label in self.labels if label.startswith(f"🎪 {show.sha} 🚦 ")
588
622
  ]
589
623
  for old_status_label in sha_status_labels:
590
- get_github().remove_label(self.pr_number, old_status_label)
624
+ self.remove_label(old_status_label)
591
625
 
592
626
  # For running environments, ensure only ONE active pointer exists
593
627
  if show.status == "running":
594
628
  # Remove ALL existing active pointers (there should only be one)
595
629
  existing_active_pointers = [
596
- label for label in self.labels
597
- if label.startswith("🎪 🎯 ")
630
+ label for label in self.labels if label.startswith("🎪 🎯 ")
598
631
  ]
599
632
  for old_pointer in existing_active_pointers:
600
633
  print(f"🎯 Removing old active pointer: {old_pointer}")
601
- get_github().remove_label(self.pr_number, old_pointer)
602
-
634
+ self.remove_label(old_pointer)
635
+
603
636
  # CRITICAL: Refresh after removals before differential calculation
604
637
  if existing_active_pointers:
605
638
  print("🔄 Refreshing labels after pointer cleanup...")
@@ -607,11 +640,13 @@ class PullRequest:
607
640
 
608
641
  # Now do normal differential updates - only for this SHA
609
642
  current_sha_labels = {
610
- label for label in self.labels
611
- if label.startswith("🎪") and (
612
- label.startswith(f"🎪 {show.sha} ") or # SHA-first format: 🎪 abc123f 📅 ...
613
- label.startswith(f"🎪 🎯 {show.sha}") or # Pointer format: 🎪 🎯 abc123f
614
- label.startswith(f"🎪 🏗️ {show.sha}") # Building pointer: 🎪 🏗️ abc123f
643
+ label
644
+ for label in self.labels
645
+ if label.startswith("🎪")
646
+ and (
647
+ label.startswith(f"🎪 {show.sha} ") # SHA-first format: 🎪 abc123f 📅 ...
648
+ or label.startswith(f"🎪 🎯 {show.sha}") # Pointer format: 🎪 🎯 abc123f
649
+ or label.startswith(f"🎪 🏗️ {show.sha}") # Building pointer: 🎪 🏗️ abc123f
615
650
  )
616
651
  }
617
652
  desired_labels = set(show.to_circus_labels())
@@ -622,12 +657,12 @@ class PullRequest:
622
657
  # Only add labels that don't exist
623
658
  labels_to_add = desired_labels - current_sha_labels
624
659
  for label in labels_to_add:
625
- get_github().add_label(self.pr_number, label)
660
+ self.add_label(label)
626
661
 
627
662
  # Only remove labels that shouldn't exist (excluding status labels already handled)
628
663
  labels_to_remove = current_sha_labels - desired_labels
629
664
  for label in labels_to_remove:
630
- get_github().remove_label(self.pr_number, label)
665
+ self.remove_label(label)
631
666
 
632
667
  # Final refresh to update cache with all changes
633
668
  self.refresh_labels()
@@ -635,54 +670,57 @@ class PullRequest:
635
670
  def _show_service_urls(self, show: Show) -> None:
636
671
  """Show AWS console URLs for monitoring deployment"""
637
672
  from .github_messages import get_aws_console_urls
638
-
673
+
639
674
  urls = get_aws_console_urls(show.ecs_service_name)
640
- print(f"\n🎪 Monitor deployment progress:")
675
+ print("\n🎪 Monitor deployment progress:")
641
676
  print(f"📝 Logs: {urls['logs']}")
642
677
  print(f"📊 Service: {urls['service']}")
643
678
  print("")
644
679
 
645
- def stop_previous_environments(self, keep_sha: str, dry_run_github: bool = False, dry_run_aws: bool = False) -> int:
680
+ def stop_previous_environments(
681
+ self, keep_sha: str, dry_run_github: bool = False, dry_run_aws: bool = False
682
+ ) -> int:
646
683
  """Stop all environments except the specified SHA (blue-green cleanup)
647
-
684
+
648
685
  Args:
649
686
  keep_sha: SHA of environment to keep running
650
- dry_run_github: Skip GitHub label operations
687
+ dry_run_github: Skip GitHub label operations
651
688
  dry_run_aws: Skip AWS operations
652
-
689
+
653
690
  Returns:
654
691
  Number of environments stopped
655
692
  """
656
693
  # Note: Labels should be fresh from recent _update_show_labels() call
657
694
  stopped_count = 0
658
-
695
+
659
696
  for show in self.shows:
660
697
  if show.sha != keep_sha:
661
698
  print(f"🧹 Cleaning up old environment: {show.sha} ({show.status})")
662
699
  try:
663
700
  show.stop(dry_run_github=dry_run_github, dry_run_aws=dry_run_aws)
664
-
701
+
665
702
  # Remove ONLY existing labels for this old environment (not theoretical ones)
666
703
  if not dry_run_github:
667
704
  existing_labels = [
668
- label for label in self.labels
669
- if label.startswith(f"🎪 {show.sha} ") or
670
- label == f"🎪 🎯 {show.sha}" or
671
- label == f"🎪 🏗️ {show.sha}"
705
+ label
706
+ for label in self.labels
707
+ if label.startswith(f"🎪 {show.sha} ")
708
+ or label == f"🎪 🎯 {show.sha}"
709
+ or label == f"🎪 🏗️ {show.sha}"
672
710
  ]
673
711
  print(f"🏷️ Removing existing labels for {show.sha}: {existing_labels}")
674
712
  for label in existing_labels:
675
713
  try:
676
- get_github().remove_label(self.pr_number, label)
714
+ self.remove_label(label)
677
715
  except Exception as e:
678
716
  print(f"⚠️ Failed to remove label {label}: {e}")
679
-
717
+
680
718
  stopped_count += 1
681
719
  print(f"✅ Stopped environment {show.sha}")
682
-
720
+
683
721
  except Exception as e:
684
722
  print(f"❌ Failed to stop environment {show.sha}: {e}")
685
-
723
+
686
724
  if stopped_count > 0:
687
725
  print(f"🧹 Blue-green cleanup: stopped {stopped_count} old environments")
688
726
  # Refresh labels after cleanup
@@ -690,5 +728,5 @@ class PullRequest:
690
728
  self.refresh_labels()
691
729
  else:
692
730
  print("ℹ️ No old environments to clean up")
693
-
731
+
694
732
  return stopped_count
@@ -34,7 +34,7 @@
34
34
  },
35
35
  {
36
36
  "name": "SUPERSET__SQLALCHEMY_EXAMPLES_URI",
37
- "value": "duckdb:////app/data/examples.duckdb"
37
+ "value": "duckdb:////app/data/examples.duckdb?access_mode=read_only"
38
38
  },
39
39
  {
40
40
  "name": "SUPERSET_LOG_LEVEL",