superset-showtime 0.2.6__tar.gz → 0.2.8__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 (31) hide show
  1. {superset_showtime-0.2.6 → superset_showtime-0.2.8}/.claude/settings.local.json +3 -1
  2. {superset_showtime-0.2.6 → superset_showtime-0.2.8}/PKG-INFO +1 -1
  3. {superset_showtime-0.2.6 → superset_showtime-0.2.8}/showtime/__init__.py +1 -1
  4. {superset_showtime-0.2.6 → superset_showtime-0.2.8}/showtime/cli.py +121 -58
  5. {superset_showtime-0.2.6 → superset_showtime-0.2.8}/showtime/core/circus.py +10 -0
  6. superset_showtime-0.2.8/showtime/core/github_messages.py +227 -0
  7. {superset_showtime-0.2.6 → superset_showtime-0.2.8}/.gitignore +0 -0
  8. {superset_showtime-0.2.6 → superset_showtime-0.2.8}/.pre-commit-config.yaml +0 -0
  9. {superset_showtime-0.2.6 → superset_showtime-0.2.8}/CLAUDE.md +0 -0
  10. {superset_showtime-0.2.6 → superset_showtime-0.2.8}/Makefile +0 -0
  11. {superset_showtime-0.2.6 → superset_showtime-0.2.8}/README.md +0 -0
  12. {superset_showtime-0.2.6 → superset_showtime-0.2.8}/dev-setup.sh +0 -0
  13. {superset_showtime-0.2.6 → superset_showtime-0.2.8}/pypi-push.sh +0 -0
  14. {superset_showtime-0.2.6 → superset_showtime-0.2.8}/pyproject.toml +0 -0
  15. {superset_showtime-0.2.6 → superset_showtime-0.2.8}/requirements-dev.txt +0 -0
  16. {superset_showtime-0.2.6 → superset_showtime-0.2.8}/requirements.txt +0 -0
  17. {superset_showtime-0.2.6 → superset_showtime-0.2.8}/showtime/__main__.py +0 -0
  18. {superset_showtime-0.2.6 → superset_showtime-0.2.8}/showtime/commands/__init__.py +0 -0
  19. {superset_showtime-0.2.6 → superset_showtime-0.2.8}/showtime/commands/start.py +0 -0
  20. {superset_showtime-0.2.6 → superset_showtime-0.2.8}/showtime/core/__init__.py +0 -0
  21. {superset_showtime-0.2.6 → superset_showtime-0.2.8}/showtime/core/aws.py +0 -0
  22. {superset_showtime-0.2.6 → superset_showtime-0.2.8}/showtime/core/emojis.py +0 -0
  23. {superset_showtime-0.2.6 → superset_showtime-0.2.8}/showtime/core/github.py +0 -0
  24. {superset_showtime-0.2.6 → superset_showtime-0.2.8}/showtime/core/label_colors.py +0 -0
  25. {superset_showtime-0.2.6 → superset_showtime-0.2.8}/showtime/data/ecs-task-definition.json +0 -0
  26. {superset_showtime-0.2.6 → superset_showtime-0.2.8}/tests/__init__.py +0 -0
  27. {superset_showtime-0.2.6 → superset_showtime-0.2.8}/tests/unit/__init__.py +0 -0
  28. {superset_showtime-0.2.6 → superset_showtime-0.2.8}/tests/unit/test_circus.py +0 -0
  29. {superset_showtime-0.2.6 → superset_showtime-0.2.8}/uv.lock +0 -0
  30. {superset_showtime-0.2.6 → superset_showtime-0.2.8}/workflows-reference/showtime-cleanup.yml +0 -0
  31. {superset_showtime-0.2.6 → superset_showtime-0.2.8}/workflows-reference/showtime-trigger.yml +0 -0
@@ -28,7 +28,9 @@
28
28
  "Bash(env:*)",
29
29
  "Bash(grep:*)",
30
30
  "Bash(git grep:*)",
31
- "Bash(git push:*)"
31
+ "Bash(git push:*)",
32
+ "Bash(make lint:*)",
33
+ "Bash(mypy:*)"
32
34
  ],
33
35
  "deny": [],
34
36
  "ask": []
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: superset-showtime
3
- Version: 0.2.6
3
+ Version: 0.2.8
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.2.6"
7
+ __version__ = "0.2.8"
8
8
  __author__ = "Maxime Beauchemin"
9
9
  __email__ = "maximebeauchemin@gmail.com"
10
10
 
@@ -10,9 +10,17 @@ import typer
10
10
  from rich.console import Console
11
11
  from rich.table import Table
12
12
 
13
- from .core.circus import PullRequest, Show
13
+ from .core.circus import PullRequest, Show, short_sha
14
14
  from .core.emojis import STATUS_DISPLAY
15
15
  from .core.github import GitHubError, GitHubInterface
16
+ from .core.github_messages import (
17
+ get_aws_console_urls,
18
+ rolling_failure_comment,
19
+ rolling_start_comment,
20
+ rolling_success_comment,
21
+ start_comment,
22
+ success_comment,
23
+ )
16
24
 
17
25
  # Constants
18
26
  DEFAULT_GITHUB_ACTOR = "unknown"
@@ -20,13 +28,7 @@ DEFAULT_GITHUB_ACTOR = "unknown"
20
28
 
21
29
  def _get_service_urls(show):
22
30
  """Get AWS Console URLs for a service"""
23
- service_name = show.ecs_service_name
24
-
25
- return {
26
- "logs": f"https://us-west-2.console.aws.amazon.com/ecs/v2/clusters/superset-ci/services/{service_name}/logs?region=us-west-2",
27
- "service": f"https://us-west-2.console.aws.amazon.com/ecs/v2/clusters/superset-ci/services/{service_name}?region=us-west-2",
28
- "health": f"https://us-west-2.console.aws.amazon.com/ecs/v2/clusters/superset-ci/services/{service_name}/health?region=us-west-2",
29
- }
31
+ return get_aws_console_urls(show.ecs_service_name)
30
32
 
31
33
 
32
34
  def _show_service_urls(show, context: str = "deployment"):
@@ -152,6 +154,28 @@ app = typer.Typer(
152
154
  console = Console()
153
155
 
154
156
 
157
+ def _get_github_workflow_url() -> str:
158
+ """Get current GitHub Actions workflow URL"""
159
+ import os
160
+
161
+ return (
162
+ os.getenv("GITHUB_SERVER_URL", "https://github.com")
163
+ + f"/{os.getenv('GITHUB_REPOSITORY', 'repo')}/actions/runs/{os.getenv('GITHUB_RUN_ID', 'run')}"
164
+ )
165
+
166
+
167
+ def _get_github_actor() -> str:
168
+ """Get current GitHub actor with fallback"""
169
+ import os
170
+
171
+ return os.getenv("GITHUB_ACTOR", DEFAULT_GITHUB_ACTOR)
172
+
173
+
174
+ def _get_showtime_footer() -> str:
175
+ """Get consistent Showtime footer for PR comments"""
176
+ return "{_get_showtime_footer()}"
177
+
178
+
155
179
  @app.command()
156
180
  def version():
157
181
  """Show version information"""
@@ -1041,7 +1065,6 @@ def _handle_start_trigger(
1041
1065
  force: bool = False,
1042
1066
  ):
1043
1067
  """Handle start trigger"""
1044
- import os
1045
1068
  import time
1046
1069
  from datetime import datetime
1047
1070
 
@@ -1050,29 +1073,12 @@ def _handle_start_trigger(
1050
1073
  try:
1051
1074
  # Get latest SHA and GitHub actor
1052
1075
  latest_sha = github.get_latest_commit_sha(pr_number)
1053
- github_actor = os.getenv("GITHUB_ACTOR", DEFAULT_GITHUB_ACTOR)
1054
-
1055
- # Post confirmation comment
1056
- workflow_url = (
1057
- os.getenv("GITHUB_SERVER_URL", "https://github.com")
1058
- + f"/{os.getenv('GITHUB_REPOSITORY', 'repo')}/actions/runs/{os.getenv('GITHUB_RUN_ID', 'run')}"
1059
- )
1060
-
1061
- confirmation_comment = f"""🎪 @{github_actor} Creating ephemeral environment for commit `{latest_sha[:7]}`
1062
-
1063
- **Action:** [View workflow]({workflow_url})
1064
- **Environment:** `{latest_sha[:7]}`
1065
- **Powered by:** [Superset Showtime](https://github.com/mistercrunch/superset-showtime)
1066
-
1067
- *Building and deploying... Watch the labels for progress updates.*"""
1068
-
1069
- if not dry_run_github:
1070
- github.post_comment(pr_number, confirmation_comment)
1076
+ github_actor = _get_github_actor()
1071
1077
 
1072
1078
  # Create new show
1073
1079
  show = Show(
1074
1080
  pr_number=pr_number,
1075
- sha=latest_sha[:7],
1081
+ sha=short_sha(latest_sha),
1076
1082
  status="building",
1077
1083
  created_at=datetime.utcnow().strftime("%Y-%m-%dT%H-%M"),
1078
1084
  ttl="24h",
@@ -1081,6 +1087,12 @@ def _handle_start_trigger(
1081
1087
 
1082
1088
  console.print(f"🎪 Creating environment {show.aws_service_name}")
1083
1089
 
1090
+ # Post confirmation comment
1091
+ confirmation_comment = start_comment(show)
1092
+
1093
+ if not dry_run_github:
1094
+ github.post_comment(pr_number, confirmation_comment)
1095
+
1084
1096
  # Set building state labels
1085
1097
  building_labels = show.to_circus_labels()
1086
1098
  console.print("🎪 Setting building state labels:")
@@ -1136,19 +1148,19 @@ def _handle_start_trigger(
1136
1148
  )
1137
1149
 
1138
1150
  # Post success comment (only in dry-run-aws mode since we have mock IP)
1139
- success_comment = f"""🎪 @{github_actor} Environment ready at **http://{mock_ip}:8080**
1140
-
1141
- **Environment:** `{show.sha}`
1142
- **Credentials:** admin / admin
1143
- **TTL:** {show.ttl} (auto-cleanup)
1144
-
1145
- **Configuration:** Modify feature flags in your PR code for new SHA
1146
- **Updates:** Environment updates automatically on new commits
1147
-
1148
- *Powered by [Superset Showtime](https://github.com/mistercrunch/superset-showtime)*"""
1151
+ # Create mock show with IP for success comment
1152
+ mock_show = Show(
1153
+ pr_number=show.pr_number,
1154
+ sha=show.sha,
1155
+ status="running",
1156
+ ip=mock_ip,
1157
+ ttl=show.ttl,
1158
+ requested_by=show.requested_by,
1159
+ )
1160
+ success_comment_text = success_comment(mock_show)
1149
1161
 
1150
1162
  if not dry_run_github:
1151
- github.post_comment(pr_number, success_comment)
1163
+ github.post_comment(pr_number, success_comment_text)
1152
1164
 
1153
1165
  else:
1154
1166
  # Real AWS operations
@@ -1246,19 +1258,12 @@ def _handle_start_trigger(
1246
1258
  console.print("🎪 ✅ Labels updated to running state!")
1247
1259
 
1248
1260
  # Post success comment with real IP
1249
- success_comment = f"""🎪 @{github_actor} Environment ready at **http://{result.ip}:8080**
1250
-
1251
- **Environment:** `{show.sha}`
1252
- **Credentials:** admin / admin
1253
- **TTL:** {show.ttl} (auto-cleanup)
1254
- **Feature flags:** {len(feature_flags)} enabled
1261
+ # Update show with real IP for comment
1262
+ show.ip = result.ip
1263
+ show.status = "running"
1264
+ success_comment_text = success_comment(show, feature_count=len(feature_flags))
1255
1265
 
1256
- **Configuration:** Modify feature flags in your PR code for new SHA
1257
- **Updates:** Environment updates automatically on new commits
1258
-
1259
- *Powered by [Superset Showtime](https://github.com/mistercrunch/superset-showtime)*"""
1260
-
1261
- github.post_comment(pr_number, success_comment)
1266
+ github.post_comment(pr_number, success_comment_text)
1262
1267
 
1263
1268
  else:
1264
1269
  console.print(f"🎪 [bold red]❌ AWS deployment failed:[/bold red] {result.error}")
@@ -1281,7 +1286,7 @@ def _handle_start_trigger(
1281
1286
 
1282
1287
  Please check the logs and try again.
1283
1288
 
1284
- *Powered by [Superset Showtime](https://github.com/mistercrunch/superset-showtime)*"""
1289
+ {_get_showtime_footer()}"""
1285
1290
 
1286
1291
  github.post_comment(pr_number, failure_comment)
1287
1292
 
@@ -1320,7 +1325,6 @@ def _handle_stop_trigger(
1320
1325
  pr_number: int, github: GitHubInterface, dry_run_aws: bool = False, dry_run_github: bool = False
1321
1326
  ):
1322
1327
  """Handle stop trigger"""
1323
- import os
1324
1328
 
1325
1329
  console.print(f"🎪 Stopping environment for PR #{pr_number}")
1326
1330
 
@@ -1396,7 +1400,7 @@ def _handle_stop_trigger(
1396
1400
  github.remove_circus_labels(pr_number)
1397
1401
 
1398
1402
  # Post cleanup comment
1399
- github_actor = os.getenv("GITHUB_ACTOR", DEFAULT_GITHUB_ACTOR)
1403
+ github_actor = _get_github_actor()
1400
1404
  cleanup_comment = f"""🎪 @{github_actor} Environment `{show.sha}` cleaned up
1401
1405
 
1402
1406
  **AWS Resources:** ECS service and ECR image deleted
@@ -1404,7 +1408,7 @@ def _handle_stop_trigger(
1404
1408
 
1405
1409
  Add `🎪 trigger-start` to create a new environment.
1406
1410
 
1407
- *Powered by [Superset Showtime](https://github.com/mistercrunch/superset-showtime)*"""
1411
+ {_get_showtime_footer()}"""
1408
1412
 
1409
1413
  if not dry_run_github:
1410
1414
  github.post_comment(pr_number, cleanup_comment)
@@ -1477,14 +1481,73 @@ def _handle_sync_trigger(
1477
1481
 
1478
1482
  Your latest changes are now live.
1479
1483
 
1480
- *Powered by [Superset Showtime](https://github.com/mistercrunch/superset-showtime)*"""
1484
+ {_get_showtime_footer()}"""
1481
1485
 
1482
1486
  if not dry_run_github:
1483
1487
  github.post_comment(pr_number, update_comment)
1484
1488
 
1485
1489
  else:
1486
- # TODO: Real rolling update
1487
- console.print("🎪 [bold yellow]Real rolling update not yet implemented[/bold yellow]")
1490
+ # Real rolling update - use same blue-green deployment logic
1491
+
1492
+ from .core.aws import AWSInterface, EnvironmentResult
1493
+
1494
+ console.print("🎪 [bold blue]Starting real rolling update...[/bold blue]")
1495
+
1496
+ # Post rolling update start comment
1497
+ start_comment_text = rolling_start_comment(pr.current_show, latest_sha)
1498
+
1499
+ if not dry_run_github:
1500
+ github.post_comment(pr_number, start_comment_text)
1501
+
1502
+ aws = AWSInterface()
1503
+
1504
+ # Get feature flags from PR description
1505
+ feature_flags = _extract_feature_flags_from_pr(pr_number, github)
1506
+ github_actor = _get_github_actor()
1507
+
1508
+ # Use blue-green deployment (create_environment handles existing services)
1509
+ result: EnvironmentResult = aws.create_environment(
1510
+ pr_number=pr_number,
1511
+ sha=latest_sha,
1512
+ github_user=github_actor,
1513
+ feature_flags=feature_flags,
1514
+ force=False, # Don't force - let blue-green handle it
1515
+ )
1516
+
1517
+ if result.success:
1518
+ console.print(
1519
+ f"🎪 [bold green]✅ Rolling update complete![/bold green] New IP: {result.ip}"
1520
+ )
1521
+
1522
+ # Update labels to point to new service
1523
+ pr.refresh_labels(github)
1524
+ new_show = pr.get_show_by_sha(latest_sha)
1525
+ if new_show:
1526
+ new_show.status = "running"
1527
+ new_show.ip = result.ip
1528
+
1529
+ # Update GitHub labels
1530
+ github.remove_circus_labels(pr_number)
1531
+ for label in new_show.to_circus_labels():
1532
+ github.add_label(pr_number, label)
1533
+
1534
+ console.print("🎪 ✅ Labels updated to point to new environment")
1535
+
1536
+ # Post rolling update success comment
1537
+ success_comment_text = rolling_success_comment(pr.current_show, new_show)
1538
+
1539
+ if not dry_run_github:
1540
+ github.post_comment(pr_number, success_comment_text)
1541
+ else:
1542
+ console.print(f"🎪 [bold red]❌ Rolling update failed:[/bold red] {result.error}")
1543
+
1544
+ # Post rolling update failure comment
1545
+ failure_comment_text = rolling_failure_comment(
1546
+ pr.current_show, latest_sha, result.error
1547
+ )
1548
+
1549
+ if not dry_run_github:
1550
+ github.post_comment(pr_number, failure_comment_text)
1488
1551
 
1489
1552
  except Exception as e:
1490
1553
  console.print(f"🎪 [bold red]Sync trigger failed:[/bold red] {e}")
@@ -39,6 +39,11 @@ class Show:
39
39
  """Deterministic ECS service name with -service suffix: pr-{pr_number}-{sha}-service"""
40
40
  return f"{self.aws_service_name}-service"
41
41
 
42
+ @property
43
+ def short_sha(self) -> str:
44
+ """7-character SHA for display (GitHub standard)"""
45
+ return self.sha[:7]
46
+
42
47
  @property
43
48
  def is_active(self) -> bool:
44
49
  """Check if this is the currently active show"""
@@ -267,3 +272,8 @@ def get_effective_ttl(pr) -> Optional[float]:
267
272
 
268
273
  # Use longest duration if multiple labels
269
274
  return max(ttl_labels)
275
+
276
+
277
+ def short_sha(full_sha: str) -> str:
278
+ """Truncate SHA to 7 characters for display (GitHub standard)"""
279
+ return full_sha[:7]
@@ -0,0 +1,227 @@
1
+ """
2
+ 🎪 GitHub PR comment templates and messaging utilities
3
+
4
+ Centralized PR comment functions with type hints and clean formatting.
5
+ """
6
+
7
+ import os
8
+ import textwrap
9
+ from typing import Dict, List, Optional
10
+
11
+ from .circus import Show
12
+
13
+ # AWS Console URL constants
14
+ BASE_AWS_URL = "https://us-west-2.console.aws.amazon.com/ecs/v2/clusters/superset-ci/services"
15
+ AWS_REGION = "us-west-2"
16
+
17
+
18
+ def get_github_actor() -> str:
19
+ """Get current GitHub actor with fallback"""
20
+ return os.getenv("GITHUB_ACTOR", "unknown")
21
+
22
+
23
+ def get_github_workflow_url() -> str:
24
+ """Get current GitHub Actions workflow URL"""
25
+ return (
26
+ os.getenv("GITHUB_SERVER_URL", "https://github.com")
27
+ + f"/{os.getenv('GITHUB_REPOSITORY', 'repo')}/actions/runs/{os.getenv('GITHUB_RUN_ID', 'run')}"
28
+ )
29
+
30
+
31
+ def get_showtime_footer() -> str:
32
+ """Get consistent Showtime footer for PR comments"""
33
+ return "*Powered by [Superset Showtime](https://github.com/mistercrunch/superset-showtime)*"
34
+
35
+
36
+ def _create_header_links(sha: str) -> Dict[str, str]:
37
+ """Create standard header links for comments
38
+
39
+ Args:
40
+ sha: Commit SHA for the environment
41
+
42
+ Returns:
43
+ Dict with showtime_link, gha_link, commit_link
44
+ """
45
+ from .circus import short_sha
46
+
47
+ repo_path = get_repo_path()
48
+ return {
49
+ "showtime_link": "[Showtime](https://github.com/mistercrunch/superset-showtime)",
50
+ "gha_link": f"[GHA]({get_github_workflow_url()})",
51
+ "commit_link": f"[{short_sha(sha)}]({get_commit_url(repo_path, sha)})",
52
+ }
53
+
54
+
55
+ def _format_comment(header: str, bullets: List[str]) -> str:
56
+ """Format comment with header and bullet points
57
+
58
+ Args:
59
+ header: Comment header text
60
+ bullets: List of bullet point strings (without •)
61
+ """
62
+ bullet_text = "\n".join(f"• {bullet}" for bullet in bullets)
63
+ return textwrap.dedent(
64
+ f"""
65
+ {header}
66
+
67
+ {bullet_text}
68
+ """
69
+ ).strip()
70
+
71
+
72
+ def get_commit_url(repo_path: str, sha: str) -> str:
73
+ """Get GitHub commit URL
74
+
75
+ Args:
76
+ repo_path: Repository path like 'apache/superset'
77
+ sha: Full or short commit SHA
78
+ """
79
+ return f"https://github.com/{repo_path}/commit/{sha}"
80
+
81
+
82
+ def get_repo_path() -> str:
83
+ """Get current repository path from environment"""
84
+ return os.getenv("GITHUB_REPOSITORY", "apache/superset")
85
+
86
+
87
+ def get_aws_console_urls(service_name: str) -> Dict[str, str]:
88
+ """Get AWS Console URLs for a service"""
89
+ return {
90
+ "logs": f"{BASE_AWS_URL}/{service_name}/logs?region={AWS_REGION}",
91
+ "service": f"{BASE_AWS_URL}/{service_name}?region={AWS_REGION}",
92
+ "health": f"{BASE_AWS_URL}/{service_name}/health?region={AWS_REGION}",
93
+ }
94
+
95
+
96
+ # Typed comment functions with clear parameter requirements
97
+
98
+
99
+ def start_comment(show: Show) -> str:
100
+ """Create environment start comment
101
+
102
+ Args:
103
+ show: Show object with SHA and other metadata
104
+ """
105
+ links = _create_header_links(show.sha)
106
+ return f"🎪 {links['showtime_link']} is building environment on {links['gha_link']} for {links['commit_link']}"
107
+
108
+
109
+ def success_comment(show: Show, feature_count: Optional[int] = None) -> str:
110
+ """Environment success comment
111
+
112
+ Args:
113
+ show: Show object with SHA, IP, TTL, etc.
114
+ feature_count: Number of enabled feature flags (optional)
115
+ """
116
+ links = _create_header_links(show.sha)
117
+ header = f"🎪 {links['showtime_link']} deployed environment on {links['gha_link']} for {links['commit_link']}"
118
+
119
+ bullets = [
120
+ f"**Environment:** http://{show.ip}:8080 (admin/admin)",
121
+ f"**Lifetime:** {show.ttl} auto-cleanup",
122
+ ]
123
+
124
+ if feature_count:
125
+ bullets.insert(-1, f"**Features:** {feature_count} enabled")
126
+
127
+ bullets.append("**Updates:** New commits create fresh environments automatically")
128
+
129
+ return _format_comment(header, bullets)
130
+
131
+
132
+ def failure_comment(show: Show, error: str) -> str:
133
+ """Environment failure comment
134
+
135
+ Args:
136
+ show: Show object with SHA and metadata
137
+ error: Error message describing what went wrong
138
+ """
139
+ links = _create_header_links(show.sha)
140
+ header = f"🎪 {links['showtime_link']} failed building environment on {links['gha_link']} for {links['commit_link']}"
141
+
142
+ bullets = [
143
+ f"**Error:** {error}",
144
+ "**Action:** Check logs and try again with `🎪 ⚡ showtime-trigger-start`",
145
+ ]
146
+
147
+ return _format_comment(header, bullets)
148
+
149
+
150
+ def cleanup_comment(show: Show) -> str:
151
+ """Environment cleanup comment
152
+
153
+ Args:
154
+ show: Show object with SHA and metadata
155
+ """
156
+ links = _create_header_links(show.sha)
157
+ header = f"🎪 {links['showtime_link']} cleaned up environment on {links['gha_link']} for {links['commit_link']}"
158
+
159
+ bullets = [
160
+ "**Resources:** ECS service and ECR image deleted",
161
+ "**Cost:** No further charges",
162
+ "**Action:** Add `🎪 ⚡ showtime-trigger-start` to create new environment",
163
+ ]
164
+
165
+ return _format_comment(header, bullets)
166
+
167
+
168
+ def rolling_start_comment(current_show: Show, new_sha: str) -> str:
169
+ """Rolling update start comment
170
+
171
+ Args:
172
+ current_show: Current Show object with SHA and IP
173
+ new_sha: New environment SHA (full SHA, will be truncated)
174
+ """
175
+ from .circus import short_sha
176
+
177
+ links = _create_header_links(new_sha)
178
+ header = f"🎪 {links['showtime_link']} is updating {current_show.short_sha}→{short_sha(new_sha)} on {links['gha_link']} for {links['commit_link']}"
179
+
180
+ bullets = [
181
+ f"**Current:** http://{current_show.ip}:8080 (still active)",
182
+ "**Update:** Zero-downtime blue-green deployment",
183
+ ]
184
+
185
+ return _format_comment(header, bullets)
186
+
187
+
188
+ def rolling_success_comment(old_show: Show, new_show: Show) -> str:
189
+ """Rolling update success comment
190
+
191
+ Args:
192
+ old_show: Previous Show object
193
+ new_show: New Show object with updated IP, SHA, TTL
194
+ """
195
+ links = _create_header_links(new_show.sha)
196
+ header = f"🎪 {links['showtime_link']} updated environment {old_show.short_sha}→{new_show.short_sha} on {links['gha_link']} for {links['commit_link']}"
197
+
198
+ bullets = [
199
+ f"**Environment:** http://{new_show.ip}:8080 (admin/admin)",
200
+ f"**Lifetime:** {new_show.ttl} auto-cleanup",
201
+ "**Deployment:** Zero-downtime blue-green",
202
+ "**Updates:** New commits create fresh environments automatically",
203
+ ]
204
+
205
+ return _format_comment(header, bullets)
206
+
207
+
208
+ def rolling_failure_comment(current_show: Show, new_sha: str, error: str) -> str:
209
+ """Rolling update failure comment
210
+
211
+ Args:
212
+ current_show: Current Show object (still active)
213
+ new_sha: Failed new environment SHA (full SHA, will be truncated)
214
+ error: Error message describing what went wrong
215
+ """
216
+ from .circus import short_sha
217
+
218
+ links = _create_header_links(new_sha)
219
+ header = f"🎪 {links['showtime_link']} failed updating to {short_sha(new_sha)} on {links['gha_link']} for {links['commit_link']}"
220
+
221
+ bullets = [
222
+ f"**Error:** {error}",
223
+ f"**Current:** http://{current_show.ip}:8080 (still active)",
224
+ "**Action:** Check logs and try again with `🎪 ⚡ showtime-trigger-start`",
225
+ ]
226
+
227
+ return _format_comment(header, bullets)