superset-showtime 0.1.0__py3-none-any.whl → 0.2.2__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 +1 -3
- showtime/cli.py +302 -173
- showtime/core/aws.py +73 -24
- showtime/core/circus.py +42 -58
- showtime/core/emojis.py +0 -10
- showtime/core/github.py +43 -1
- showtime/core/label_colors.py +105 -0
- showtime/data/ecs-task-definition.json +4 -0
- {superset_showtime-0.1.0.dist-info → superset_showtime-0.2.2.dist-info}/METADATA +97 -96
- superset_showtime-0.2.2.dist-info/RECORD +16 -0
- showtime/core/config.py +0 -152
- superset_showtime-0.1.0.dist-info/RECORD +0 -16
- {superset_showtime-0.1.0.dist-info → superset_showtime-0.2.2.dist-info}/WHEEL +0 -0
- {superset_showtime-0.1.0.dist-info → superset_showtime-0.2.2.dist-info}/entry_points.txt +0 -0
showtime/core/aws.py
CHANGED
|
@@ -56,6 +56,8 @@ class AWSInterface:
|
|
|
56
56
|
sha: str,
|
|
57
57
|
github_user: str = "unknown",
|
|
58
58
|
feature_flags: List[Dict[str, str]] = None,
|
|
59
|
+
image_tag_override: Optional[str] = None,
|
|
60
|
+
force: bool = False,
|
|
59
61
|
) -> EnvironmentResult:
|
|
60
62
|
"""
|
|
61
63
|
Create ephemeral environment with blue-green deployment support
|
|
@@ -81,20 +83,45 @@ class AWSInterface:
|
|
|
81
83
|
requested_by=github_user,
|
|
82
84
|
)
|
|
83
85
|
|
|
84
|
-
service_name =
|
|
85
|
-
image_tag = show.aws_image_tag # pr-{pr_number}-{sha}-ci
|
|
86
|
+
service_name = show.ecs_service_name # pr-{pr_number}-{sha}-service
|
|
86
87
|
|
|
87
88
|
try:
|
|
88
|
-
#
|
|
89
|
-
if
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
# Handle force flag - delete existing service for this SHA first
|
|
90
|
+
if force:
|
|
91
|
+
print(f"🗑️ Force flag: Checking for existing service {service_name}")
|
|
92
|
+
if self._service_exists(service_name):
|
|
93
|
+
print(f"🗑️ Deleting existing service: {service_name}")
|
|
94
|
+
success = self._delete_ecs_service(service_name)
|
|
95
|
+
if success:
|
|
96
|
+
print("✅ Service deletion initiated, waiting for completion...")
|
|
97
|
+
# Wait for service to be fully deleted before proceeding
|
|
98
|
+
if self._wait_for_service_deletion(service_name):
|
|
99
|
+
print("✅ Service deletion completed, proceeding with fresh deployment")
|
|
100
|
+
else:
|
|
101
|
+
print("⚠️ Service deletion timeout, proceeding anyway")
|
|
102
|
+
else:
|
|
103
|
+
print("⚠️ Failed to delete existing service, proceeding anyway")
|
|
104
|
+
else:
|
|
105
|
+
print("ℹ️ No existing service found, proceeding with new deployment")
|
|
106
|
+
# Step 1: Determine which Docker image to use (DockerHub direct)
|
|
107
|
+
if image_tag_override:
|
|
108
|
+
# Use explicit override (can be any format)
|
|
109
|
+
docker_image = f"apache/superset:{image_tag_override}"
|
|
110
|
+
print(f"✅ Using override image: {docker_image}")
|
|
111
|
+
else:
|
|
112
|
+
# Use supersetbot SHA-based format (what supersetbot creates)
|
|
113
|
+
supersetbot_tag = f"{sha[:7]}-ci" # Matches supersetbot format: abc123f-ci
|
|
114
|
+
docker_image = f"apache/superset:{supersetbot_tag}"
|
|
115
|
+
print(f"✅ Using DockerHub image: {docker_image} (supersetbot SHA format)")
|
|
116
|
+
print(
|
|
117
|
+
"💡 To test with different image: --image-tag latest or --image-tag abc123f-ci"
|
|
93
118
|
)
|
|
94
119
|
|
|
120
|
+
# Note: No ECR image check needed - ECS will pull from DockerHub directly
|
|
121
|
+
|
|
95
122
|
# Step 2: Create/update ECS task definition with feature flags
|
|
96
123
|
task_def_arn = self._create_task_definition_with_image_and_flags(
|
|
97
|
-
|
|
124
|
+
docker_image, feature_flags or []
|
|
98
125
|
)
|
|
99
126
|
if not task_def_arn:
|
|
100
127
|
return EnvironmentResult(success=False, error="Failed to create task definition")
|
|
@@ -128,9 +155,9 @@ class AWSInterface:
|
|
|
128
155
|
if not self._wait_for_service_stability(service_name):
|
|
129
156
|
return EnvironmentResult(success=False, error="Service failed to become stable")
|
|
130
157
|
|
|
131
|
-
# Step 7: Health check the new service
|
|
158
|
+
# Step 7: Health check the new service (longer timeout for Superset + examples)
|
|
132
159
|
print(f"🏥 Health checking service {service_name}...")
|
|
133
|
-
if not self._health_check_service(service_name):
|
|
160
|
+
if not self._health_check_service(service_name, max_attempts=20): # 10 minutes total
|
|
134
161
|
return EnvironmentResult(success=False, error="Service failed health checks")
|
|
135
162
|
|
|
136
163
|
# Step 8: Get IP after health checks pass
|
|
@@ -175,7 +202,9 @@ class AWSInterface:
|
|
|
175
202
|
return True
|
|
176
203
|
|
|
177
204
|
except Exception as e:
|
|
178
|
-
raise AWSError(
|
|
205
|
+
raise AWSError(
|
|
206
|
+
message=str(e), operation="delete_environment", resource=service_name
|
|
207
|
+
) from e
|
|
179
208
|
|
|
180
209
|
def get_environment_ip(self, service_name: str) -> Optional[str]:
|
|
181
210
|
"""
|
|
@@ -284,23 +313,18 @@ class AWSInterface:
|
|
|
284
313
|
return False
|
|
285
314
|
|
|
286
315
|
def _create_task_definition_with_image_and_flags(
|
|
287
|
-
self,
|
|
316
|
+
self, docker_image: str, feature_flags: List[Dict[str, str]]
|
|
288
317
|
) -> Optional[str]:
|
|
289
|
-
"""Create ECS task definition with image and feature flags
|
|
318
|
+
"""Create ECS task definition with DockerHub image and feature flags"""
|
|
290
319
|
try:
|
|
291
320
|
# Load base task definition template
|
|
292
321
|
task_def_path = Path(__file__).parent.parent / "data" / "ecs-task-definition.json"
|
|
293
322
|
with open(task_def_path) as f:
|
|
294
323
|
task_def = json.load(f)
|
|
295
324
|
|
|
296
|
-
#
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
registry_url = registry_url.replace("https://", "")
|
|
300
|
-
full_image_url = f"{registry_url}/{self.repository}:{image_tag}"
|
|
301
|
-
|
|
302
|
-
# Update image in container definition (replicate GHA render-task-definition)
|
|
303
|
-
task_def["containerDefinitions"][0]["image"] = full_image_url
|
|
325
|
+
# Use DockerHub image directly (no ECR needed)
|
|
326
|
+
# docker_image is already in format: apache/superset:abc123f-ci
|
|
327
|
+
task_def["containerDefinitions"][0]["image"] = docker_image
|
|
304
328
|
|
|
305
329
|
# Add feature flags to environment (replicate GHA jq environment update)
|
|
306
330
|
container_env = task_def["containerDefinitions"][0]["environment"]
|
|
@@ -353,7 +377,7 @@ class AWSInterface:
|
|
|
353
377
|
"""Create ECS service (replicate exact GHA create-service step)"""
|
|
354
378
|
try:
|
|
355
379
|
# Replicate exact GHA create-service command parameters
|
|
356
|
-
|
|
380
|
+
self.ecs_client.create_service(
|
|
357
381
|
cluster=self.cluster,
|
|
358
382
|
serviceName=service_name, # pr-{pr_number}-service
|
|
359
383
|
taskDefinition=self.cluster, # Uses cluster name as task def family
|
|
@@ -469,7 +493,7 @@ class AWSInterface:
|
|
|
469
493
|
return orphaned
|
|
470
494
|
|
|
471
495
|
except Exception as e:
|
|
472
|
-
raise AWSError(message=str(e), operation="cleanup_orphaned_environments")
|
|
496
|
+
raise AWSError(message=str(e), operation="cleanup_orphaned_environments") from e
|
|
473
497
|
|
|
474
498
|
def update_feature_flags(self, service_name: str, feature_flags: Dict[str, bool]) -> bool:
|
|
475
499
|
"""Update feature flags in running environment"""
|
|
@@ -582,7 +606,7 @@ class AWSInterface:
|
|
|
582
606
|
if time_match.group(2) == "d":
|
|
583
607
|
hours *= 24
|
|
584
608
|
|
|
585
|
-
cutoff_timestamp = time.time() - (hours * 3600)
|
|
609
|
+
# cutoff_timestamp = time.time() - (hours * 3600) # Not used in current implementation
|
|
586
610
|
expired_services = []
|
|
587
611
|
|
|
588
612
|
# List all services in cluster
|
|
@@ -756,3 +780,28 @@ class AWSInterface:
|
|
|
756
780
|
except Exception as e:
|
|
757
781
|
print(f"❌ Health check error: {e}")
|
|
758
782
|
return False
|
|
783
|
+
|
|
784
|
+
def _wait_for_service_deletion(self, service_name: str, timeout_minutes: int = 5) -> bool:
|
|
785
|
+
"""Wait for ECS service to be fully deleted"""
|
|
786
|
+
import time
|
|
787
|
+
|
|
788
|
+
try:
|
|
789
|
+
max_attempts = timeout_minutes * 12 # 5s intervals
|
|
790
|
+
|
|
791
|
+
for attempt in range(max_attempts):
|
|
792
|
+
# Check if service still exists
|
|
793
|
+
if not self._service_exists(service_name):
|
|
794
|
+
print(f"✅ Service {service_name} fully deleted after {attempt * 5}s")
|
|
795
|
+
return True
|
|
796
|
+
|
|
797
|
+
if attempt % 6 == 0: # Every 30s
|
|
798
|
+
print(f"⏳ Waiting for service deletion... ({attempt * 5}s elapsed)")
|
|
799
|
+
|
|
800
|
+
time.sleep(5) # Check every 5 seconds
|
|
801
|
+
|
|
802
|
+
print(f"⚠️ Service deletion timeout after {timeout_minutes} minutes")
|
|
803
|
+
return False
|
|
804
|
+
|
|
805
|
+
except Exception as e:
|
|
806
|
+
print(f"❌ Error waiting for service deletion: {e}")
|
|
807
|
+
return False
|
showtime/core/circus.py
CHANGED
|
@@ -23,7 +23,6 @@ class Show:
|
|
|
23
23
|
created_at: Optional[str] = None # ISO timestamp
|
|
24
24
|
ttl: str = "24h" # 24h, 48h, close, etc.
|
|
25
25
|
requested_by: Optional[str] = None # GitHub username
|
|
26
|
-
config: str = "standard" # Configuration (alerts,debug)
|
|
27
26
|
|
|
28
27
|
@property
|
|
29
28
|
def aws_service_name(self) -> str:
|
|
@@ -35,6 +34,11 @@ class Show:
|
|
|
35
34
|
"""Deterministic ECR image tag: pr-{pr_number}-{sha}-ci"""
|
|
36
35
|
return f"pr-{self.pr_number}-{self.sha}-ci"
|
|
37
36
|
|
|
37
|
+
@property
|
|
38
|
+
def ecs_service_name(self) -> str:
|
|
39
|
+
"""Deterministic ECS service name with -service suffix: pr-{pr_number}-{sha}-service"""
|
|
40
|
+
return f"{self.aws_service_name}-service"
|
|
41
|
+
|
|
38
42
|
@property
|
|
39
43
|
def is_active(self) -> bool:
|
|
40
44
|
"""Check if this is the currently active show"""
|
|
@@ -72,9 +76,6 @@ class Show:
|
|
|
72
76
|
if self.requested_by:
|
|
73
77
|
labels.append(f"🎪 {self.sha} 🤡 {self.requested_by}")
|
|
74
78
|
|
|
75
|
-
if self.config != "standard":
|
|
76
|
-
labels.append(f"🎪 {self.sha} ⚙️ {self.config}")
|
|
77
|
-
|
|
78
79
|
return labels
|
|
79
80
|
|
|
80
81
|
@classmethod
|
|
@@ -112,8 +113,6 @@ class Show:
|
|
|
112
113
|
show_data["ttl"] = value
|
|
113
114
|
elif emoji == "🤡": # User (clown!)
|
|
114
115
|
show_data["requested_by"] = value
|
|
115
|
-
elif emoji == "⚙️": # Config
|
|
116
|
-
show_data["config"] = value
|
|
117
116
|
|
|
118
117
|
# Only return Show if we found relevant labels for this SHA
|
|
119
118
|
if any(label.endswith(f" {sha}") for label in labels if "🎯" in label or "🏗️" in label):
|
|
@@ -220,66 +219,51 @@ class PullRequest:
|
|
|
220
219
|
self._shows = self._parse_shows_from_labels()
|
|
221
220
|
|
|
222
221
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
return label.startswith("🎪 conf-")
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
def parse_configuration_command(label: str) -> Optional[str]:
|
|
230
|
-
"""
|
|
231
|
-
Parse configuration command from label
|
|
232
|
-
|
|
233
|
-
Args:
|
|
234
|
-
label: Label like "🎪 conf-enable-ALERTS"
|
|
235
|
-
|
|
236
|
-
Returns:
|
|
237
|
-
Command string like "enable-ALERTS" or None if not a config label
|
|
238
|
-
"""
|
|
239
|
-
if not is_configuration_label(label):
|
|
240
|
-
return None
|
|
222
|
+
def parse_ttl_days(ttl_str: str) -> Optional[float]:
|
|
223
|
+
"""Parse TTL string to days"""
|
|
224
|
+
import re
|
|
241
225
|
|
|
242
|
-
|
|
226
|
+
if ttl_str == "never":
|
|
227
|
+
return None # Never expire
|
|
243
228
|
|
|
229
|
+
if ttl_str == "close":
|
|
230
|
+
return None # Special handling needed
|
|
244
231
|
|
|
245
|
-
|
|
246
|
-
""
|
|
247
|
-
|
|
232
|
+
# Parse number + unit: 24h, 7d, 2w, etc.
|
|
233
|
+
match = re.match(r"(\d+(?:\.\d+)?)(h|d|w)", ttl_str.lower())
|
|
234
|
+
if not match:
|
|
235
|
+
return 2.0 # Default 2 days if invalid
|
|
248
236
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
command: New command like "enable-DASHBOARD_RBAC" or "disable-ALERTS"
|
|
237
|
+
value = float(match.group(1))
|
|
238
|
+
unit = match.group(2)
|
|
252
239
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
""
|
|
256
|
-
|
|
240
|
+
if unit == "h":
|
|
241
|
+
return value / 24.0 # Hours to days
|
|
242
|
+
elif unit == "d":
|
|
243
|
+
return value # Already in days
|
|
244
|
+
elif unit == "w":
|
|
245
|
+
return value * 7.0 # Weeks to days
|
|
257
246
|
|
|
258
|
-
#
|
|
259
|
-
if command.startswith("enable-"):
|
|
260
|
-
feature = command.replace("enable-", "").lower()
|
|
261
|
-
configs = [c for c in configs if not c.startswith(f"no-{feature}")]
|
|
262
|
-
configs.append(feature)
|
|
247
|
+
return 2.0 # Default
|
|
263
248
|
|
|
264
|
-
elif command.startswith("disable-"):
|
|
265
|
-
feature = command.replace("disable-", "").lower()
|
|
266
|
-
configs = [c for c in configs if c != feature]
|
|
267
|
-
configs.append(f"no-{feature}")
|
|
268
249
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
configs.append("debug")
|
|
250
|
+
def get_effective_ttl(pr) -> Optional[float]:
|
|
251
|
+
"""Get effective TTL in days for a PR (handles multiple labels, conflicts)"""
|
|
252
|
+
ttl_labels = []
|
|
273
253
|
|
|
274
|
-
|
|
275
|
-
|
|
254
|
+
# Find all TTL labels for all shows
|
|
255
|
+
for show in pr.shows:
|
|
256
|
+
if show.ttl:
|
|
257
|
+
ttl_days = parse_ttl_days(show.ttl)
|
|
258
|
+
if ttl_days is None: # "never" or "close"
|
|
259
|
+
if show.ttl == "never":
|
|
260
|
+
return None # Never expire takes precedence
|
|
261
|
+
# For "close", continue checking others
|
|
262
|
+
else:
|
|
263
|
+
ttl_labels.append(ttl_days)
|
|
276
264
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
size = command # Keep full size command
|
|
280
|
-
configs = [c for c in configs if not c.startswith("size-")]
|
|
281
|
-
configs.append(size)
|
|
265
|
+
if not ttl_labels:
|
|
266
|
+
return 2.0 # Default 2 days
|
|
282
267
|
|
|
283
|
-
#
|
|
284
|
-
|
|
285
|
-
return ",".join(unique_configs) if unique_configs else "standard"
|
|
268
|
+
# Use longest duration if multiple labels
|
|
269
|
+
return max(ttl_labels)
|
showtime/core/emojis.py
CHANGED
|
@@ -18,7 +18,6 @@ EMOJI_MEANINGS = {
|
|
|
18
18
|
"🌐": "ip", # Globe for IP address
|
|
19
19
|
"⌛": "ttl", # Hourglass for time-to-live
|
|
20
20
|
"🤡": "requested_by", # Clown for who requested (circus theme!)
|
|
21
|
-
"⚙️": "config", # Gear for configuration
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
# Reverse mapping for creating labels
|
|
@@ -30,13 +29,9 @@ STATUS_DISPLAY = {
|
|
|
30
29
|
"running": "🟢",
|
|
31
30
|
"updating": "🔄",
|
|
32
31
|
"failed": "❌",
|
|
33
|
-
"configuring": "⚙️",
|
|
34
32
|
"stopping": "🛑",
|
|
35
33
|
}
|
|
36
34
|
|
|
37
|
-
# Configuration command prefixes
|
|
38
|
-
CONFIG_PREFIX = f"{CIRCUS_PREFIX} conf-"
|
|
39
|
-
|
|
40
35
|
|
|
41
36
|
def create_circus_label(emoji_key: str, value: str) -> str:
|
|
42
37
|
"""Create a circus tent label with proper spacing"""
|
|
@@ -79,8 +74,3 @@ def parse_circus_label(label: str) -> tuple[str, str]:
|
|
|
79
74
|
def is_circus_label(label: str) -> bool:
|
|
80
75
|
"""Check if label is a circus tent label"""
|
|
81
76
|
return label.startswith(f"{CIRCUS_PREFIX} ")
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def is_configuration_command(label: str) -> bool:
|
|
85
|
-
"""Check if label is a configuration command"""
|
|
86
|
-
return label.startswith(CONFIG_PREFIX)
|
showtime/core/github.py
CHANGED
|
@@ -72,13 +72,29 @@ class GitHubInterface:
|
|
|
72
72
|
return [label["name"] for label in labels_data]
|
|
73
73
|
|
|
74
74
|
def add_label(self, pr_number: int, label: str) -> None:
|
|
75
|
-
"""Add a label to a PR"""
|
|
75
|
+
"""Add a label to a PR (automatically creates label definition if needed)"""
|
|
76
|
+
|
|
77
|
+
# Ensure label definition exists with proper color/description
|
|
78
|
+
self._ensure_label_definition_exists(label)
|
|
79
|
+
|
|
76
80
|
url = f"{self.base_url}/repos/{self.org}/{self.repo}/issues/{pr_number}/labels"
|
|
77
81
|
|
|
78
82
|
with httpx.Client() as client:
|
|
79
83
|
response = client.post(url, headers=self.headers, json={"labels": [label]})
|
|
80
84
|
response.raise_for_status()
|
|
81
85
|
|
|
86
|
+
def _ensure_label_definition_exists(self, label: str) -> None:
|
|
87
|
+
"""Ensure label definition exists in repository with proper color/description"""
|
|
88
|
+
from .label_colors import get_label_color, get_label_description
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
color = get_label_color(label)
|
|
92
|
+
description = get_label_description(label)
|
|
93
|
+
self.create_or_update_label(label, color, description)
|
|
94
|
+
except Exception:
|
|
95
|
+
# If label creation fails, continue anyway - label might already exist
|
|
96
|
+
pass
|
|
97
|
+
|
|
82
98
|
def remove_label(self, pr_number: int, label: str) -> None:
|
|
83
99
|
"""Remove a label from a PR"""
|
|
84
100
|
# URL encode the label name for special characters like emojis
|
|
@@ -212,3 +228,29 @@ class GitHubInterface:
|
|
|
212
228
|
return deleted_labels
|
|
213
229
|
|
|
214
230
|
return sha_labels
|
|
231
|
+
|
|
232
|
+
def create_or_update_label(self, name: str, color: str, description: str) -> bool:
|
|
233
|
+
"""Create or update a label with color and description"""
|
|
234
|
+
import urllib.parse
|
|
235
|
+
|
|
236
|
+
# Check if label exists
|
|
237
|
+
encoded_name = urllib.parse.quote(name, safe="")
|
|
238
|
+
url = f"{self.base_url}/repos/{self.org}/{self.repo}/labels/{encoded_name}"
|
|
239
|
+
|
|
240
|
+
label_data = {"name": name, "color": color, "description": description}
|
|
241
|
+
|
|
242
|
+
with httpx.Client() as client:
|
|
243
|
+
# Try to update first (if exists)
|
|
244
|
+
response = client.patch(url, headers=self.headers, json=label_data)
|
|
245
|
+
|
|
246
|
+
if response.status_code == 200:
|
|
247
|
+
return False # Updated existing
|
|
248
|
+
elif response.status_code == 404:
|
|
249
|
+
# Label doesn't exist, create it
|
|
250
|
+
create_url = f"{self.base_url}/repos/{self.org}/{self.repo}/labels"
|
|
251
|
+
response = client.post(create_url, headers=self.headers, json=label_data)
|
|
252
|
+
response.raise_for_status()
|
|
253
|
+
return True # Created new
|
|
254
|
+
else:
|
|
255
|
+
response.raise_for_status()
|
|
256
|
+
return False
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
🎪 Circus tent label color scheme and definitions
|
|
3
|
+
|
|
4
|
+
Centralized color map for all GitHub labels with descriptions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# Color Palette - Bright Yellow Circus Theme
|
|
8
|
+
COLORS = {
|
|
9
|
+
# Theme Colors
|
|
10
|
+
"circus_yellow": "FFD700", # Bright yellow - primary circus theme
|
|
11
|
+
"metadata_yellow": "FFF9C4", # Light yellow - metadata labels
|
|
12
|
+
# Status Colors (Semantic)
|
|
13
|
+
"status_running": "28a745", # Green - healthy/running
|
|
14
|
+
"status_building": "FFD700", # Bright yellow - in progress
|
|
15
|
+
"status_failed": "dc3545", # Red - error/failed
|
|
16
|
+
"status_updating": "fd7e14", # Orange - updating/transitioning
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# Label Definitions with Colors and Descriptions
|
|
20
|
+
LABEL_DEFINITIONS = {
|
|
21
|
+
# Action/Trigger Labels (Bright Yellow - User-facing, namespaced)
|
|
22
|
+
"🎪 ⚡ showtime-trigger-start": {
|
|
23
|
+
"color": COLORS["circus_yellow"],
|
|
24
|
+
"description": "Create new ephemeral environment for this PR",
|
|
25
|
+
},
|
|
26
|
+
"🎪 🛑 showtime-trigger-stop": {
|
|
27
|
+
"color": COLORS["circus_yellow"],
|
|
28
|
+
"description": "Destroy ephemeral environment and clean up AWS resources",
|
|
29
|
+
},
|
|
30
|
+
"🎪 🧊 showtime-freeze": {
|
|
31
|
+
"color": "FFE4B5", # Light orange
|
|
32
|
+
"description": "Freeze PR - prevent auto-sync on new commits",
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Status-specific label patterns (generated dynamically)
|
|
37
|
+
STATUS_LABEL_COLORS = {
|
|
38
|
+
"running": COLORS["status_running"], # 🎪 abc123f 🚦 running
|
|
39
|
+
"building": COLORS["status_building"], # 🎪 abc123f 🚦 building
|
|
40
|
+
"failed": COLORS["status_failed"], # 🎪 abc123f 🚦 failed
|
|
41
|
+
"updating": COLORS["status_updating"], # 🎪 abc123f 🚦 updating
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Metadata label color (for all other circus tent labels)
|
|
45
|
+
METADATA_LABEL_COLOR = COLORS["metadata_yellow"] # 🎪 abc123f 📅 ..., 🎪 abc123f 🌐 ..., etc.
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_label_color(label_text: str) -> str:
|
|
49
|
+
"""Get appropriate color for any circus tent label"""
|
|
50
|
+
|
|
51
|
+
# Check for exact matches in definitions
|
|
52
|
+
if label_text in LABEL_DEFINITIONS:
|
|
53
|
+
return LABEL_DEFINITIONS[label_text]["color"]
|
|
54
|
+
|
|
55
|
+
# Check for status labels with dynamic SHA
|
|
56
|
+
if " 🚦 " in label_text:
|
|
57
|
+
status = label_text.split(" 🚦 ")[-1]
|
|
58
|
+
return STATUS_LABEL_COLORS.get(status, COLORS["circus_yellow"])
|
|
59
|
+
|
|
60
|
+
# All other metadata labels (timestamps, IPs, TTL, users, pointers)
|
|
61
|
+
if label_text.startswith("🎪 "):
|
|
62
|
+
return METADATA_LABEL_COLOR
|
|
63
|
+
|
|
64
|
+
# Fallback
|
|
65
|
+
return COLORS["circus_yellow"]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_label_description(label_text: str) -> str:
|
|
69
|
+
"""Get appropriate description for any circus tent label"""
|
|
70
|
+
|
|
71
|
+
# Check for exact matches
|
|
72
|
+
if label_text in LABEL_DEFINITIONS:
|
|
73
|
+
return LABEL_DEFINITIONS[label_text]["description"]
|
|
74
|
+
|
|
75
|
+
# Dynamic descriptions for SHA-based labels
|
|
76
|
+
if " 🚦 " in label_text:
|
|
77
|
+
sha, status = label_text.replace("🎪 ", "").split(" 🚦 ")
|
|
78
|
+
return f"Environment {sha} status: {status}"
|
|
79
|
+
|
|
80
|
+
if " 📅 " in label_text:
|
|
81
|
+
sha, timestamp = label_text.replace("🎪 ", "").split(" 📅 ")
|
|
82
|
+
return f"Environment {sha} created at {timestamp}"
|
|
83
|
+
|
|
84
|
+
if " 🌐 " in label_text:
|
|
85
|
+
sha, url = label_text.replace("🎪 ", "").split(" 🌐 ")
|
|
86
|
+
return f"Environment {sha} URL: http://{url} (click to visit)"
|
|
87
|
+
|
|
88
|
+
if " ⌛ " in label_text:
|
|
89
|
+
sha, ttl = label_text.replace("🎪 ", "").split(" ⌛ ")
|
|
90
|
+
return f"Environment {sha} expires after {ttl}"
|
|
91
|
+
|
|
92
|
+
if " 🤡 " in label_text:
|
|
93
|
+
sha, user = label_text.replace("🎪 ", "").split(" 🤡 ")
|
|
94
|
+
return f"Environment {sha} requested by {user}"
|
|
95
|
+
|
|
96
|
+
if "🎪 🎯 " in label_text:
|
|
97
|
+
sha = label_text.replace("🎪 🎯 ", "")
|
|
98
|
+
return f"Active environment pointer - {sha} is receiving traffic"
|
|
99
|
+
|
|
100
|
+
if "🎪 🏗️ " in label_text:
|
|
101
|
+
sha = label_text.replace("🎪 🏗️ ", "")
|
|
102
|
+
return f"Building environment - {sha} deployment in progress"
|
|
103
|
+
|
|
104
|
+
# Fallback
|
|
105
|
+
return "Circus tent showtime label"
|