superset-showtime 0.3.2__py3-none-any.whl → 0.4.0__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 +3 -2
- showtime/cli.py +268 -1513
- showtime/core/aws.py +30 -26
- showtime/core/github.py +10 -6
- showtime/core/github_messages.py +4 -4
- showtime/core/pull_request.py +534 -0
- showtime/core/show.py +279 -0
- {superset_showtime-0.3.2.dist-info → superset_showtime-0.4.0.dist-info}/METADATA +17 -9
- superset_showtime-0.4.0.dist-info/RECORD +16 -0
- showtime/commands/__init__.py +0 -1
- showtime/commands/start.py +0 -40
- showtime/core/circus.py +0 -289
- superset_showtime-0.3.2.dist-info/RECORD +0 -17
- {superset_showtime-0.3.2.dist-info → superset_showtime-0.4.0.dist-info}/WHEEL +0 -0
- {superset_showtime-0.3.2.dist-info → superset_showtime-0.4.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
"""
|
|
2
|
+
🎪 PullRequest class - PR-level orchestration and state management
|
|
3
|
+
|
|
4
|
+
Handles atomic transactions, trigger processing, and environment orchestration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Any, List, Optional
|
|
10
|
+
|
|
11
|
+
from .aws import AWSInterface
|
|
12
|
+
from .github import GitHubInterface
|
|
13
|
+
from .show import Show, short_sha
|
|
14
|
+
|
|
15
|
+
# Lazy singletons to avoid import-time failures
|
|
16
|
+
_github = None
|
|
17
|
+
_aws = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_github() -> GitHubInterface:
|
|
21
|
+
global _github
|
|
22
|
+
if _github is None:
|
|
23
|
+
_github = GitHubInterface()
|
|
24
|
+
return _github
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_aws() -> AWSInterface:
|
|
28
|
+
global _aws
|
|
29
|
+
if _aws is None:
|
|
30
|
+
_aws = AWSInterface()
|
|
31
|
+
return _aws
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Use get_github() and get_aws() directly in methods
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class SyncResult:
|
|
39
|
+
"""Result of a PullRequest.sync() operation"""
|
|
40
|
+
|
|
41
|
+
success: bool
|
|
42
|
+
action_taken: str # create_environment, rolling_update, cleanup, no_action
|
|
43
|
+
show: Optional[Show] = None
|
|
44
|
+
error: Optional[str] = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class AnalysisResult:
|
|
49
|
+
"""Result of a PullRequest.analyze() operation"""
|
|
50
|
+
|
|
51
|
+
action_needed: str
|
|
52
|
+
build_needed: bool
|
|
53
|
+
sync_needed: bool
|
|
54
|
+
target_sha: str
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class PullRequest:
|
|
58
|
+
"""GitHub PR with its shows parsed from circus labels"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, pr_number: int, labels: List[str]):
|
|
61
|
+
self.pr_number = pr_number
|
|
62
|
+
self.labels = labels
|
|
63
|
+
self._shows = self._parse_shows_from_labels()
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def shows(self) -> List[Show]:
|
|
67
|
+
"""All shows found in labels"""
|
|
68
|
+
return self._shows
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def current_show(self) -> Optional[Show]:
|
|
72
|
+
"""The currently active show (from 🎯 label)"""
|
|
73
|
+
# Find the SHA that's marked as active (🎯)
|
|
74
|
+
active_sha = None
|
|
75
|
+
for label in self.labels:
|
|
76
|
+
if label.startswith("🎪 🎯 "):
|
|
77
|
+
active_sha = label.split(" ")[2]
|
|
78
|
+
break
|
|
79
|
+
|
|
80
|
+
if not active_sha:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
# Find the show with that SHA
|
|
84
|
+
for show in self.shows:
|
|
85
|
+
if show.sha == active_sha:
|
|
86
|
+
return show
|
|
87
|
+
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def building_show(self) -> Optional[Show]:
|
|
92
|
+
"""The currently building show (from 🏗️ label)"""
|
|
93
|
+
building_sha = None
|
|
94
|
+
for label in self.labels:
|
|
95
|
+
if label.startswith("🎪 🏗️ "):
|
|
96
|
+
building_sha = label.split(" ")[2]
|
|
97
|
+
break
|
|
98
|
+
|
|
99
|
+
if not building_sha:
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
for show in self.shows:
|
|
103
|
+
if show.sha == building_sha:
|
|
104
|
+
return show
|
|
105
|
+
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def circus_labels(self) -> List[str]:
|
|
110
|
+
"""All circus tent emoji labels"""
|
|
111
|
+
return [label for label in self.labels if label.startswith("🎪")]
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def has_shows(self) -> bool:
|
|
115
|
+
"""Check if PR has any active shows"""
|
|
116
|
+
return len(self.shows) > 0
|
|
117
|
+
|
|
118
|
+
def get_show_by_sha(self, sha: str) -> Optional[Show]:
|
|
119
|
+
"""Get show by SHA"""
|
|
120
|
+
for show in self.shows:
|
|
121
|
+
if show.sha == sha:
|
|
122
|
+
return show
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
def _parse_shows_from_labels(self) -> List[Show]:
|
|
126
|
+
"""Parse all shows from circus tent labels"""
|
|
127
|
+
# Find all unique SHAs from circus labels
|
|
128
|
+
shas = set()
|
|
129
|
+
for label in self.labels:
|
|
130
|
+
if not label.startswith("🎪"):
|
|
131
|
+
continue
|
|
132
|
+
parts = label.split(" ")
|
|
133
|
+
if len(parts) >= 3 and len(parts[1]) == 7: # SHA is 7 chars
|
|
134
|
+
shas.add(parts[1])
|
|
135
|
+
|
|
136
|
+
# Create Show objects for each SHA
|
|
137
|
+
shows = []
|
|
138
|
+
for sha in shas:
|
|
139
|
+
show = Show.from_circus_labels(self.pr_number, self.labels, sha)
|
|
140
|
+
if show:
|
|
141
|
+
shows.append(show)
|
|
142
|
+
|
|
143
|
+
return shows
|
|
144
|
+
|
|
145
|
+
@classmethod
|
|
146
|
+
def from_id(cls, pr_number: int) -> "PullRequest":
|
|
147
|
+
"""Load PR with current labels from GitHub"""
|
|
148
|
+
labels = get_github().get_labels(pr_number)
|
|
149
|
+
return cls(pr_number, labels)
|
|
150
|
+
|
|
151
|
+
def refresh_labels(self) -> None:
|
|
152
|
+
"""Refresh labels from GitHub and reparse shows"""
|
|
153
|
+
self.labels = get_github().get_labels(self.pr_number)
|
|
154
|
+
self._shows = self._parse_shows_from_labels()
|
|
155
|
+
|
|
156
|
+
def analyze(self, target_sha: str, pr_state: str = "open") -> AnalysisResult:
|
|
157
|
+
"""Analyze what actions are needed (read-only, for --check-only)
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
target_sha: Target commit SHA to analyze
|
|
161
|
+
pr_state: PR state (open/closed)
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
AnalysisResult with action plan and flags
|
|
165
|
+
"""
|
|
166
|
+
# Handle closed PRs
|
|
167
|
+
if pr_state == "closed":
|
|
168
|
+
return AnalysisResult(
|
|
169
|
+
action_needed="cleanup", build_needed=False, sync_needed=True, target_sha=target_sha
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Determine action needed
|
|
173
|
+
action_needed = self._determine_action(target_sha)
|
|
174
|
+
|
|
175
|
+
# Determine if Docker build is needed
|
|
176
|
+
build_needed = action_needed in ["create_environment", "rolling_update", "auto_sync"]
|
|
177
|
+
|
|
178
|
+
# Determine if sync execution is needed
|
|
179
|
+
sync_needed = action_needed != "no_action"
|
|
180
|
+
|
|
181
|
+
return AnalysisResult(
|
|
182
|
+
action_needed=action_needed,
|
|
183
|
+
build_needed=build_needed,
|
|
184
|
+
sync_needed=sync_needed,
|
|
185
|
+
target_sha=target_sha,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def sync(
|
|
189
|
+
self,
|
|
190
|
+
target_sha: str,
|
|
191
|
+
dry_run_github: bool = False,
|
|
192
|
+
dry_run_aws: bool = False,
|
|
193
|
+
dry_run_docker: bool = False,
|
|
194
|
+
) -> SyncResult:
|
|
195
|
+
"""Sync PR to desired state with atomic transaction management
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
target_sha: Target commit SHA to sync to
|
|
199
|
+
github: GitHub interface for label operations
|
|
200
|
+
aws: AWS interface for environment operations
|
|
201
|
+
dry_run_github: Skip GitHub operations if True
|
|
202
|
+
dry_run_aws: Skip AWS operations if True
|
|
203
|
+
dry_run_docker: Skip Docker operations if True
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
SyncResult with success status and details
|
|
207
|
+
|
|
208
|
+
Raises:
|
|
209
|
+
Exception: On unrecoverable errors (caller should handle)
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
# 1. Determine what action is needed
|
|
213
|
+
action_needed = self._determine_action(target_sha)
|
|
214
|
+
|
|
215
|
+
# 2. Atomic claim for environment changes (PR-level lock)
|
|
216
|
+
if action_needed in ["create_environment", "rolling_update", "auto_sync"]:
|
|
217
|
+
print(f"🔒 Claiming environment for {action_needed}...")
|
|
218
|
+
if not self._atomic_claim(target_sha, action_needed, dry_run_github):
|
|
219
|
+
print("❌ Claim failed - another job is active")
|
|
220
|
+
return SyncResult(
|
|
221
|
+
success=False,
|
|
222
|
+
action_taken="claim_failed",
|
|
223
|
+
error="Another job is already active",
|
|
224
|
+
)
|
|
225
|
+
print("✅ Environment claimed successfully")
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
# 3. Execute action with error handling
|
|
229
|
+
if action_needed == "create_environment":
|
|
230
|
+
show = self._create_new_show(target_sha)
|
|
231
|
+
print(f"🏗️ Creating environment {show.sha}...")
|
|
232
|
+
self._post_building_comment(show, dry_run_github)
|
|
233
|
+
|
|
234
|
+
# Phase 1: Docker build
|
|
235
|
+
print("🐳 Building Docker image...")
|
|
236
|
+
show.build_docker(dry_run_docker)
|
|
237
|
+
show.status = "built"
|
|
238
|
+
print("✅ Docker build completed")
|
|
239
|
+
self._update_show_labels(show, dry_run_github)
|
|
240
|
+
|
|
241
|
+
# Phase 2: AWS deployment
|
|
242
|
+
print("☁️ Deploying to AWS ECS...")
|
|
243
|
+
show.deploy_aws(dry_run_aws)
|
|
244
|
+
show.status = "running"
|
|
245
|
+
print(f"✅ Deployment completed - environment running at {show.ip}:8080")
|
|
246
|
+
self._update_show_labels(show, dry_run_github)
|
|
247
|
+
|
|
248
|
+
self._post_success_comment(show, dry_run_github)
|
|
249
|
+
return SyncResult(success=True, action_taken="create_environment", show=show)
|
|
250
|
+
|
|
251
|
+
elif action_needed in ["rolling_update", "auto_sync"]:
|
|
252
|
+
old_show = self.current_show
|
|
253
|
+
if not old_show:
|
|
254
|
+
return SyncResult(
|
|
255
|
+
success=False,
|
|
256
|
+
action_taken="no_current_show",
|
|
257
|
+
error="No current show for rolling update",
|
|
258
|
+
)
|
|
259
|
+
new_show = self._create_new_show(target_sha)
|
|
260
|
+
print(f"🔄 Rolling update: {old_show.sha} → {new_show.sha}")
|
|
261
|
+
self._post_rolling_start_comment(old_show, new_show, dry_run_github)
|
|
262
|
+
|
|
263
|
+
# Phase 1: Docker build
|
|
264
|
+
print("🐳 Building updated Docker image...")
|
|
265
|
+
new_show.build_docker(dry_run_docker)
|
|
266
|
+
new_show.status = "built"
|
|
267
|
+
print("✅ Docker build completed")
|
|
268
|
+
self._update_show_labels(new_show, dry_run_github)
|
|
269
|
+
|
|
270
|
+
# Phase 2: Blue-green deployment
|
|
271
|
+
print("☁️ Deploying updated environment...")
|
|
272
|
+
new_show.deploy_aws(dry_run_aws)
|
|
273
|
+
new_show.status = "running"
|
|
274
|
+
print(f"✅ Rolling update completed - new environment at {new_show.ip}:8080")
|
|
275
|
+
self._update_show_labels(new_show, dry_run_github)
|
|
276
|
+
|
|
277
|
+
self._post_rolling_success_comment(old_show, new_show, dry_run_github)
|
|
278
|
+
return SyncResult(success=True, action_taken=action_needed, show=new_show)
|
|
279
|
+
|
|
280
|
+
elif action_needed == "destroy_environment":
|
|
281
|
+
if self.current_show:
|
|
282
|
+
print(f"🗑️ Destroying environment {self.current_show.sha}...")
|
|
283
|
+
self.current_show.stop(dry_run_github=dry_run_github, dry_run_aws=dry_run_aws)
|
|
284
|
+
print("☁️ AWS resources deleted")
|
|
285
|
+
self._post_cleanup_comment(self.current_show, dry_run_github)
|
|
286
|
+
# Remove all circus labels after successful stop
|
|
287
|
+
if not dry_run_github:
|
|
288
|
+
get_github().remove_circus_labels(self.pr_number)
|
|
289
|
+
print("🏷️ GitHub labels cleaned up")
|
|
290
|
+
print("✅ Environment destroyed")
|
|
291
|
+
return SyncResult(success=True, action_taken="destroy_environment")
|
|
292
|
+
|
|
293
|
+
else:
|
|
294
|
+
return SyncResult(success=True, action_taken="no_action")
|
|
295
|
+
|
|
296
|
+
except Exception as e:
|
|
297
|
+
# Transaction failed - set failed state and update labels
|
|
298
|
+
if "show" in locals():
|
|
299
|
+
show.status = "failed"
|
|
300
|
+
self._update_show_labels(show, dry_run_github)
|
|
301
|
+
# TODO: Post failure comment
|
|
302
|
+
return SyncResult(success=False, action_taken="failed", error=str(e))
|
|
303
|
+
|
|
304
|
+
def start_environment(self, sha: Optional[str] = None, **kwargs: Any) -> SyncResult:
|
|
305
|
+
"""Start a new environment (CLI start command logic)"""
|
|
306
|
+
target_sha = sha or get_github().get_latest_commit_sha(self.pr_number)
|
|
307
|
+
return self.sync(target_sha, **kwargs)
|
|
308
|
+
|
|
309
|
+
def stop_environment(self, **kwargs: Any) -> SyncResult:
|
|
310
|
+
"""Stop current environment (CLI stop command logic)"""
|
|
311
|
+
if not self.current_show:
|
|
312
|
+
return SyncResult(
|
|
313
|
+
success=True, action_taken="no_environment", error="No environment to stop"
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
try:
|
|
317
|
+
self.current_show.stop(**kwargs)
|
|
318
|
+
# Remove all circus labels after successful stop
|
|
319
|
+
if not kwargs.get("dry_run_github", False):
|
|
320
|
+
get_github().remove_circus_labels(self.pr_number)
|
|
321
|
+
return SyncResult(success=True, action_taken="stopped")
|
|
322
|
+
except Exception as e:
|
|
323
|
+
return SyncResult(success=False, action_taken="stop_failed", error=str(e))
|
|
324
|
+
|
|
325
|
+
def get_status(self) -> dict:
|
|
326
|
+
"""Get current status (CLI status command logic)"""
|
|
327
|
+
if not self.current_show:
|
|
328
|
+
return {"status": "no_environment", "show": None}
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
"status": "active",
|
|
332
|
+
"show": {
|
|
333
|
+
"sha": self.current_show.sha,
|
|
334
|
+
"status": self.current_show.status,
|
|
335
|
+
"ip": self.current_show.ip,
|
|
336
|
+
"ttl": self.current_show.ttl,
|
|
337
|
+
"requested_by": self.current_show.requested_by,
|
|
338
|
+
"created_at": self.current_show.created_at,
|
|
339
|
+
"aws_service_name": self.current_show.aws_service_name,
|
|
340
|
+
},
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
@classmethod
|
|
344
|
+
def list_all_environments(cls) -> List[dict]:
|
|
345
|
+
"""List all environments across all PRs (CLI list command logic)"""
|
|
346
|
+
# Find all PRs with circus tent labels
|
|
347
|
+
pr_numbers = get_github().find_prs_with_shows()
|
|
348
|
+
|
|
349
|
+
all_environments = []
|
|
350
|
+
for pr_number in pr_numbers:
|
|
351
|
+
pr = cls.from_id(pr_number)
|
|
352
|
+
if pr.current_show:
|
|
353
|
+
status = pr.get_status()
|
|
354
|
+
status["pr_number"] = pr_number
|
|
355
|
+
all_environments.append(status)
|
|
356
|
+
|
|
357
|
+
return all_environments
|
|
358
|
+
|
|
359
|
+
def _determine_action(self, target_sha: str) -> str:
|
|
360
|
+
"""Determine what sync action is needed"""
|
|
361
|
+
# Check for explicit trigger labels
|
|
362
|
+
trigger_labels = [label for label in self.labels if "showtime-trigger-" in label]
|
|
363
|
+
|
|
364
|
+
if trigger_labels:
|
|
365
|
+
for trigger in trigger_labels:
|
|
366
|
+
if "showtime-trigger-start" in trigger:
|
|
367
|
+
if self.current_show and self.current_show.needs_update(target_sha):
|
|
368
|
+
return "rolling_update"
|
|
369
|
+
elif self.current_show:
|
|
370
|
+
return "no_action" # Same commit
|
|
371
|
+
else:
|
|
372
|
+
return "create_environment"
|
|
373
|
+
elif "showtime-trigger-stop" in trigger:
|
|
374
|
+
return "destroy_environment"
|
|
375
|
+
|
|
376
|
+
# No explicit triggers - check for auto-sync or creation
|
|
377
|
+
if self.current_show and self.current_show.status != "failed" and self.current_show.needs_update(target_sha):
|
|
378
|
+
return "auto_sync"
|
|
379
|
+
elif not self.current_show or self.current_show.status == "failed":
|
|
380
|
+
# No environment exists OR failed environment - allow creation without trigger (for CLI start)
|
|
381
|
+
return "create_environment"
|
|
382
|
+
|
|
383
|
+
return "no_action"
|
|
384
|
+
|
|
385
|
+
def _atomic_claim(self, target_sha: str, action: str, dry_run: bool = False) -> bool:
|
|
386
|
+
"""Atomically claim this PR for the current job"""
|
|
387
|
+
# 1. Validate current state allows this action
|
|
388
|
+
if action in ["create_environment", "rolling_update", "auto_sync"]:
|
|
389
|
+
if self.current_show and self.current_show.status in [
|
|
390
|
+
"building",
|
|
391
|
+
"built",
|
|
392
|
+
"deploying",
|
|
393
|
+
"running",
|
|
394
|
+
]:
|
|
395
|
+
return False # Another job active
|
|
396
|
+
|
|
397
|
+
if dry_run:
|
|
398
|
+
print(f"🎪 [DRY-RUN] Would atomically claim PR for {action}")
|
|
399
|
+
return True
|
|
400
|
+
|
|
401
|
+
# 2. Remove trigger labels (atomic operation)
|
|
402
|
+
trigger_labels = [label for label in self.labels if "showtime-trigger-" in label]
|
|
403
|
+
for trigger_label in trigger_labels:
|
|
404
|
+
get_github().remove_label(self.pr_number, trigger_label)
|
|
405
|
+
|
|
406
|
+
# 3. Set building state immediately (claim the PR)
|
|
407
|
+
if action in ["create_environment", "rolling_update", "auto_sync"]:
|
|
408
|
+
building_show = self._create_new_show(target_sha)
|
|
409
|
+
building_show.status = "building"
|
|
410
|
+
# Update labels to reflect building state
|
|
411
|
+
get_github().remove_circus_labels(self.pr_number)
|
|
412
|
+
for label in building_show.to_circus_labels():
|
|
413
|
+
get_github().add_label(self.pr_number, label)
|
|
414
|
+
|
|
415
|
+
return True
|
|
416
|
+
|
|
417
|
+
def _create_new_show(self, target_sha: str) -> Show:
|
|
418
|
+
"""Create a new Show object for the target SHA"""
|
|
419
|
+
return Show(
|
|
420
|
+
pr_number=self.pr_number,
|
|
421
|
+
sha=short_sha(target_sha),
|
|
422
|
+
status="building",
|
|
423
|
+
created_at=datetime.utcnow().strftime("%Y-%m-%dT%H-%M"),
|
|
424
|
+
ttl="24h",
|
|
425
|
+
requested_by="github_actor", # TODO: Get from context
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
def _post_building_comment(self, show: Show, dry_run: bool = False) -> None:
|
|
429
|
+
"""Post building comment for new environment"""
|
|
430
|
+
from .github_messages import building_comment
|
|
431
|
+
|
|
432
|
+
if not dry_run:
|
|
433
|
+
comment = building_comment(show)
|
|
434
|
+
get_github().post_comment(self.pr_number, comment)
|
|
435
|
+
|
|
436
|
+
def _post_success_comment(self, show: Show, dry_run: bool = False) -> None:
|
|
437
|
+
"""Post success comment for completed environment"""
|
|
438
|
+
from .github_messages import success_comment
|
|
439
|
+
|
|
440
|
+
if not dry_run:
|
|
441
|
+
comment = success_comment(show)
|
|
442
|
+
get_github().post_comment(self.pr_number, comment)
|
|
443
|
+
|
|
444
|
+
def _post_rolling_start_comment(
|
|
445
|
+
self, old_show: Show, new_show: Show, dry_run: bool = False
|
|
446
|
+
) -> None:
|
|
447
|
+
"""Post rolling update start comment"""
|
|
448
|
+
from .github_messages import rolling_start_comment
|
|
449
|
+
|
|
450
|
+
if not dry_run:
|
|
451
|
+
full_sha = new_show.sha + "0" * (40 - len(new_show.sha))
|
|
452
|
+
comment = rolling_start_comment(old_show, full_sha)
|
|
453
|
+
get_github().post_comment(self.pr_number, comment)
|
|
454
|
+
|
|
455
|
+
def _post_rolling_success_comment(
|
|
456
|
+
self, old_show: Show, new_show: Show, dry_run: bool = False
|
|
457
|
+
) -> None:
|
|
458
|
+
"""Post rolling update success comment"""
|
|
459
|
+
from .github_messages import rolling_success_comment
|
|
460
|
+
|
|
461
|
+
if not dry_run:
|
|
462
|
+
comment = rolling_success_comment(old_show, new_show)
|
|
463
|
+
get_github().post_comment(self.pr_number, comment)
|
|
464
|
+
|
|
465
|
+
def _post_cleanup_comment(self, show: Show, dry_run: bool = False) -> None:
|
|
466
|
+
"""Post cleanup completion comment"""
|
|
467
|
+
from .github_messages import cleanup_comment
|
|
468
|
+
|
|
469
|
+
if not dry_run:
|
|
470
|
+
comment = cleanup_comment(show)
|
|
471
|
+
get_github().post_comment(self.pr_number, comment)
|
|
472
|
+
|
|
473
|
+
def stop_if_expired(self, max_age_hours: int, dry_run: bool = False) -> bool:
|
|
474
|
+
"""Stop environment if it's expired based on age
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
max_age_hours: Maximum age in hours before expiration
|
|
478
|
+
dry_run: If True, just check don't actually stop
|
|
479
|
+
|
|
480
|
+
Returns:
|
|
481
|
+
True if environment was expired (and stopped), False otherwise
|
|
482
|
+
"""
|
|
483
|
+
if not self.current_show:
|
|
484
|
+
return False
|
|
485
|
+
|
|
486
|
+
# Use Show's expiration logic
|
|
487
|
+
if self.current_show.is_expired(max_age_hours):
|
|
488
|
+
if dry_run:
|
|
489
|
+
print(f"🎪 [DRY-RUN] Would stop expired environment: PR #{self.pr_number}")
|
|
490
|
+
return True
|
|
491
|
+
|
|
492
|
+
print(f"🧹 Stopping expired environment: PR #{self.pr_number}")
|
|
493
|
+
result = self.stop_environment(dry_run_github=False, dry_run_aws=False)
|
|
494
|
+
return result.success
|
|
495
|
+
|
|
496
|
+
return False # Not expired
|
|
497
|
+
|
|
498
|
+
@classmethod
|
|
499
|
+
def find_all_with_environments(cls) -> List[int]:
|
|
500
|
+
"""Find all PR numbers that have active environments"""
|
|
501
|
+
return get_github().find_prs_with_shows()
|
|
502
|
+
|
|
503
|
+
def _update_show_labels(self, show: Show, dry_run: bool = False) -> None:
|
|
504
|
+
"""Update GitHub labels to reflect show state with proper status replacement"""
|
|
505
|
+
if dry_run:
|
|
506
|
+
return
|
|
507
|
+
|
|
508
|
+
# First, remove any existing status labels for this SHA to ensure clean transitions
|
|
509
|
+
sha_status_labels = [
|
|
510
|
+
label for label in self.labels
|
|
511
|
+
if label.startswith(f"🎪 {show.sha} 🚦 ")
|
|
512
|
+
]
|
|
513
|
+
for old_status_label in sha_status_labels:
|
|
514
|
+
get_github().remove_label(self.pr_number, old_status_label)
|
|
515
|
+
|
|
516
|
+
# Now do normal differential updates
|
|
517
|
+
current_labels = {label for label in self.labels if label.startswith("🎪")}
|
|
518
|
+
desired_labels = set(show.to_circus_labels())
|
|
519
|
+
|
|
520
|
+
# Remove the status labels we already cleaned up from the differential
|
|
521
|
+
current_labels = current_labels - set(sha_status_labels)
|
|
522
|
+
|
|
523
|
+
# Only add labels that don't exist
|
|
524
|
+
labels_to_add = desired_labels - current_labels
|
|
525
|
+
for label in labels_to_add:
|
|
526
|
+
get_github().add_label(self.pr_number, label)
|
|
527
|
+
|
|
528
|
+
# Only remove labels that shouldn't exist (excluding status labels already handled)
|
|
529
|
+
labels_to_remove = current_labels - desired_labels
|
|
530
|
+
for label in labels_to_remove:
|
|
531
|
+
get_github().remove_label(self.pr_number, label)
|
|
532
|
+
|
|
533
|
+
# Refresh our label cache
|
|
534
|
+
self.refresh_labels()
|