superset-showtime 0.5.10__tar.gz → 0.5.11__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.11}/CLAUDE.md +65 -0
  2. {superset_showtime-0.5.10 → superset_showtime-0.5.11}/PKG-INFO +1 -1
  3. {superset_showtime-0.5.10 → superset_showtime-0.5.11}/showtime/__init__.py +1 -1
  4. {superset_showtime-0.5.10 → superset_showtime-0.5.11}/showtime/core/git_validation.py +72 -37
  5. {superset_showtime-0.5.10 → superset_showtime-0.5.11}/.claude/settings.local.json +0 -0
  6. {superset_showtime-0.5.10 → superset_showtime-0.5.11}/.gitignore +0 -0
  7. {superset_showtime-0.5.10 → superset_showtime-0.5.11}/.pre-commit-config.yaml +0 -0
  8. {superset_showtime-0.5.10 → superset_showtime-0.5.11}/Makefile +0 -0
  9. {superset_showtime-0.5.10 → superset_showtime-0.5.11}/README.md +0 -0
  10. {superset_showtime-0.5.10 → superset_showtime-0.5.11}/dev-setup.sh +0 -0
  11. {superset_showtime-0.5.10 → superset_showtime-0.5.11}/pypi-push.sh +0 -0
  12. {superset_showtime-0.5.10 → superset_showtime-0.5.11}/pyproject.toml +0 -0
  13. {superset_showtime-0.5.10 → superset_showtime-0.5.11}/requirements-dev.txt +0 -0
  14. {superset_showtime-0.5.10 → superset_showtime-0.5.11}/requirements.txt +0 -0
  15. {superset_showtime-0.5.10 → superset_showtime-0.5.11}/showtime/__main__.py +0 -0
  16. {superset_showtime-0.5.10 → superset_showtime-0.5.11}/showtime/cli.py +0 -0
  17. {superset_showtime-0.5.10 → superset_showtime-0.5.11}/showtime/core/__init__.py +0 -0
  18. {superset_showtime-0.5.10 → superset_showtime-0.5.11}/showtime/core/aws.py +0 -0
  19. {superset_showtime-0.5.10 → superset_showtime-0.5.11}/showtime/core/emojis.py +0 -0
  20. {superset_showtime-0.5.10 → superset_showtime-0.5.11}/showtime/core/github.py +0 -0
  21. {superset_showtime-0.5.10 → superset_showtime-0.5.11}/showtime/core/github_messages.py +0 -0
  22. {superset_showtime-0.5.10 → superset_showtime-0.5.11}/showtime/core/label_colors.py +0 -0
  23. {superset_showtime-0.5.10 → superset_showtime-0.5.11}/showtime/core/pull_request.py +0 -0
  24. {superset_showtime-0.5.10 → superset_showtime-0.5.11}/showtime/core/show.py +0 -0
  25. {superset_showtime-0.5.10 → superset_showtime-0.5.11}/showtime/data/ecs-task-definition.json +0 -0
  26. {superset_showtime-0.5.10 → superset_showtime-0.5.11}/tests/__init__.py +0 -0
  27. {superset_showtime-0.5.10 → superset_showtime-0.5.11}/tests/unit/__init__.py +0 -0
  28. {superset_showtime-0.5.10 → superset_showtime-0.5.11}/tests/unit/test_label_transitions.py +0 -0
  29. {superset_showtime-0.5.10 → superset_showtime-0.5.11}/tests/unit/test_pull_request.py +0 -0
  30. {superset_showtime-0.5.10 → superset_showtime-0.5.11}/tests/unit/test_sha_specific_logic.py +0 -0
  31. {superset_showtime-0.5.10 → superset_showtime-0.5.11}/tests/unit/test_show.py +0 -0
  32. {superset_showtime-0.5.10 → superset_showtime-0.5.11}/uv.lock +0 -0
  33. {superset_showtime-0.5.10 → superset_showtime-0.5.11}/workflows-reference/showtime-cleanup.yml +0 -0
  34. {superset_showtime-0.5.10 → superset_showtime-0.5.11}/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.11
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.11"
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,90 @@ 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
+ return True, None
138
+ else:
139
+ return (
140
+ False,
141
+ f"Current branch does not include required SHA {required_sha[:7]}. Please rebase onto main.",
142
+ )
143
+
110
144
  except Exception as e:
111
- return False, f"Git validation error: {e}"
145
+ # Re-raise to be caught by the caller for proper fallback handling
146
+ raise Exception(f"GitHub API validation error: {e}") from e
112
147
 
113
148
 
114
149
  def _validate_sha_in_log(repo: "Repo", sha_to_check: str) -> Tuple[bool, Optional[str]]: