Skip to content

Commit 5768e12

Browse files
jeremyederclaude
andauthored
feat(ci): auto-generate loading tips from release metadata (#1044)
## Summary - Adds `scripts/generate-loading-tips.py` that generates dynamic loading tips from git history - Integrates into the release pipeline between changelog generation and tag creation - Tips highlight **first-time contributors** and **top 3 commits by lines of code added** ## Example output (v0.1.0) 1. Welcome Aidan Reilly, who made their first contribution in v0.1.0! 2. New in v0.1.0: fix technical inaccuracies and add undocumented feature coverage (+926 lines) 3. New in v0.1.0: git credential helper for proper repo authentication (+276 lines) 4. New in v0.1.0: [Amber] Fix: Bug: Loading custom workflow from private GitHub repo downloads no files (+150 lines) ## Test plan - [ ] Run `python3 scripts/generate-loading-tips.py v0.1.0 v0.0.35 ambient-code/platform /tmp/test.ts` locally - [ ] Verify generated TypeScript is valid - [ ] Trigger a test release to confirm pipeline integration <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Loading tips are now dynamically generated for each release, automatically highlighting first-time contributors and featuring top commits from the latest release. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9c0bbeb commit 5768e12

2 files changed

Lines changed: 217 additions & 0 deletions

File tree

.github/workflows/prod-release-deploy.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,27 @@ jobs:
170170
171171
cat RELEASE_CHANGELOG.md
172172
173+
- name: Generate Loading Tips
174+
run: |
175+
LATEST_TAG="${{ steps.get_latest_tag.outputs.latest_tag }}"
176+
NEW_TAG="${{ steps.next_version.outputs.new_tag }}"
177+
REPO="${{ github.repository }}"
178+
OUTPUT="components/frontend/src/lib/loading-tips.ts"
179+
180+
python3 scripts/generate-loading-tips.py "$NEW_TAG" "$LATEST_TAG" "$REPO" "$OUTPUT"
181+
182+
# Commit the updated loading tips so the frontend build includes them
183+
git config user.name "github-actions[bot]"
184+
git config user.email "github-actions[bot]@users.noreply.github.com"
185+
git add "$OUTPUT"
186+
if git diff --cached --quiet; then
187+
echo "No loading tips changes to commit"
188+
else
189+
git commit -m "chore(frontend): update loading tips for ${NEW_TAG}
190+
191+
Auto-generated release tips highlighting contributors and changes."
192+
fi
193+
173194
- name: Create Tag
174195
id: create_tag
175196
uses: rickstaa/action-create-tag@v1

scripts/generate-loading-tips.py

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Generate dynamic loading tips for the Ambient Code platform frontend.
4+
5+
Pulls release metadata from git history to create tips that highlight:
6+
- First-time contributors
7+
- Top commits by lines of code added (noteworthy changes)
8+
9+
Outputs a TypeScript file with a RELEASE_TIPS array alongside the static DEFAULT_LOADING_TIPS.
10+
"""
11+
12+
import subprocess
13+
import sys
14+
import re
15+
16+
17+
def run_git(args: list[str]) -> str:
18+
result = subprocess.run(
19+
["git"] + args, capture_output=True, text=True
20+
)
21+
if result.returncode != 0:
22+
print(f"Warning: git {' '.join(args)} failed: {result.stderr}", file=sys.stderr)
23+
return ""
24+
return result.stdout.strip()
25+
26+
27+
def get_first_time_contributors(latest_tag: str, current_authors: set[str]) -> list[str]:
28+
if not latest_tag:
29+
return sorted(current_authors)
30+
31+
tag_date = run_git(["log", "-1", "--format=%ci", latest_tag])
32+
if not tag_date:
33+
return []
34+
35+
prior_raw = run_git(["log", "--all", f"--before={tag_date}", "--format=%an"])
36+
prior_authors = set(prior_raw.split("\n")) if prior_raw else set()
37+
38+
return sorted(current_authors - prior_authors)
39+
40+
41+
def get_top_commits_by_loc(latest_tag: str, top_n: int = 3) -> list[dict]:
42+
"""Get the top N commits by lines added between latest_tag and HEAD."""
43+
commit_range = f"{latest_tag}..HEAD" if latest_tag else "HEAD"
44+
raw = run_git([
45+
"log", commit_range,
46+
"--format=%h<DELIM>%s<DELIM>%an",
47+
"--numstat",
48+
])
49+
if not raw:
50+
return []
51+
52+
commits = []
53+
current = None
54+
55+
for line in raw.split("\n"):
56+
if "<DELIM>" in line:
57+
if current:
58+
commits.append(current)
59+
parts = line.split("<DELIM>", 2)
60+
current = {
61+
"hash": parts[0],
62+
"subject": parts[1] if len(parts) > 1 else "",
63+
"author": parts[2] if len(parts) > 2 else "",
64+
"additions": 0,
65+
}
66+
elif current and line.strip():
67+
# numstat lines: <additions>\t<deletions>\t<file>
68+
numstat = line.split("\t")
69+
if len(numstat) >= 2 and numstat[0] != "-":
70+
try:
71+
current["additions"] += int(numstat[0])
72+
except ValueError:
73+
pass
74+
75+
if current:
76+
commits.append(current)
77+
78+
commits.sort(key=lambda c: c["additions"], reverse=True)
79+
return commits[:top_n]
80+
81+
82+
def clean_subject(subject: str) -> str:
83+
"""Strip conventional commit prefix and PR number for display."""
84+
cleaned = re.sub(r"^\w+(\([^)]*\))?:\s*", "", subject)
85+
cleaned = re.sub(r"\s*\(#\d+\)$", "", cleaned)
86+
return cleaned
87+
88+
89+
def generate_tips(new_tag: str, latest_tag: str) -> list[str]:
90+
tips = []
91+
92+
commit_range = f"{latest_tag}..HEAD" if latest_tag else "HEAD"
93+
author_raw = run_git(["log", commit_range, "--format=%an"])
94+
if not author_raw:
95+
return tips
96+
current_authors = set(author_raw.split("\n"))
97+
98+
# First-time contributors (always first)
99+
first_timers = get_first_time_contributors(latest_tag, current_authors)
100+
for name in first_timers:
101+
tips.append(f"Welcome {name}, who made their first contribution in {new_tag}!")
102+
103+
# Top 3 commits by lines added
104+
top_commits = get_top_commits_by_loc(latest_tag, top_n=3)
105+
for commit in top_commits:
106+
subject = clean_subject(commit["subject"])
107+
loc = commit["additions"]
108+
if subject and loc > 0:
109+
tips.append(f"New in {new_tag}: {subject} (+{loc:,} lines)")
110+
111+
return tips
112+
113+
114+
def select_tips(tips: list[str], count: int = 10) -> list[str]:
115+
"""Select up to `count` tips, prioritizing first-timer shoutouts."""
116+
first_timer_tips = [t for t in tips if t.startswith("Welcome ")]
117+
other_tips = [t for t in tips if not t.startswith("Welcome ")]
118+
119+
selected = first_timer_tips[:count]
120+
remaining = count - len(selected)
121+
if remaining > 0:
122+
selected.extend(other_tips[:remaining])
123+
124+
return selected[:count]
125+
126+
127+
STATIC_TIPS = [
128+
"Tip: Clone sessions to quickly duplicate your setup for similar tasks",
129+
"Tip: Export chat transcripts as Markdown or PDF for documentation",
130+
"Tip: Add multiple repositories as context for cross-repo analysis",
131+
"Tip: Stopped sessions can be resumed without losing your progress",
132+
"Tip: Check MCP Servers to see which tools are available in your session",
133+
"Tip: Repository URLs are remembered for quick re-use across sessions",
134+
"Tip: Connect Google Drive to export chats directly to your Drive",
135+
"Tip: Load custom workflows from your own Git repositories",
136+
"Tip: Use the Explorer panel to browse and download files created by AI",
137+
]
138+
139+
140+
def write_loading_tips_ts(tips: list[str], output_path: str):
141+
escaped = [t.replace("\\", "\\\\").replace('"', '\\"') for t in tips]
142+
release_lines = ",\n".join(f' "{t}"' for t in escaped)
143+
144+
static_lines = ",\n".join(f' "{t}"' for t in STATIC_TIPS)
145+
146+
content = (
147+
"/**\n"
148+
" * Release-generated loading tips for the Ambient Code platform.\n"
149+
" * Auto-generated by scripts/generate-loading-tips.py during the release pipeline.\n"
150+
" * These tips highlight recent changes, contributors, and platform milestones.\n"
151+
" *\n"
152+
" * DO NOT EDIT MANUALLY — this array is regenerated on every release.\n"
153+
" */\n"
154+
"export const RELEASE_TIPS: string[] = [\n"
155+
f"{release_lines},\n"
156+
"];\n"
157+
"\n"
158+
"/**\n"
159+
" * Default loading tips shown during AI response generation.\n"
160+
" * These are used as fallback when LOADING_TIPS env var is not configured.\n"
161+
" * Tips support markdown-style links: [text](url)\n"
162+
" */\n"
163+
"export const DEFAULT_LOADING_TIPS = [\n"
164+
" ...RELEASE_TIPS,\n"
165+
f"{static_lines},\n"
166+
"];\n"
167+
)
168+
169+
with open(output_path, "w") as f:
170+
f.write(content)
171+
172+
print(f"Wrote {len(tips)} release tips to {output_path}")
173+
174+
175+
def main():
176+
if len(sys.argv) < 4:
177+
print(f"Usage: {sys.argv[0]} <new_tag> <latest_tag> <repo> [output_path]", file=sys.stderr)
178+
sys.exit(1)
179+
180+
new_tag = sys.argv[1]
181+
latest_tag = sys.argv[2]
182+
# repo arg kept for interface compatibility but not used currently
183+
output_path = sys.argv[4] if len(sys.argv) > 4 else "components/frontend/src/lib/loading-tips.ts"
184+
185+
all_tips = generate_tips(new_tag, latest_tag)
186+
selected = select_tips(all_tips, count=10)
187+
188+
print(f"Generated {len(all_tips)} candidate tips, selected {len(selected)}:")
189+
for i, tip in enumerate(selected, 1):
190+
print(f" {i}. {tip}")
191+
192+
write_loading_tips_ts(selected, output_path)
193+
194+
195+
if __name__ == "__main__":
196+
main()

0 commit comments

Comments
 (0)