-
Notifications
You must be signed in to change notification settings - Fork 1.4k
224 lines (196 loc) · 10.9 KB
/
weekly-api-diff.yml
File metadata and controls
224 lines (196 loc) · 10.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
name: Weekly API Diff
on:
schedule:
- cron: '0 17 * * 1' # Monday 9am PST / 10am PDT (GH Actions cron is UTC)
workflow_dispatch: # manual trigger for testing
jobs:
weekly-api-diff:
runs-on: ubuntu-latest
env:
SNAPSHOTS_REPO: LFDanLu/react-spectrum-api-snapshots
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # required for build:api-published to find the last Publish commit
- uses: actions/setup-node@v4
with:
node-version: '24'
cache: 'yarn'
- run: yarn --immutable
# Build current main API (~2 min, always fresh)
- name: Build current API snapshot
run: yarn build:api-branch
# Build release baseline using the last minor/major release commit (skips patch-only releases)
- name: Build release baseline
run: yarn build:api-published
- name: Generate diff
run: yarn compare:apis --isCI > /tmp/diff-current.txt || true
# Check out snapshots repo so we can read the previous diff and commit the new one
- uses: actions/checkout@v4
with:
repository: ${{ env.SNAPSHOTS_REPO }}
path: snapshots
token: ${{ secrets.SNAPSHOTS_REPO_TOKEN }}
# Compute week-to-week delta and commit new diff
- name: Save diff and compute delta
run: |
TODAY=$(date +%Y-%m-%d)
echo "TODAY=$TODAY" >> $GITHUB_ENV
CURRENT_PUBLISH=$(git log --grep='^Publish$' --oneline -1 | awk '{print $1}')
PREV_PUBLISH=$(cat snapshots/last-publish-hash.txt 2>/dev/null || echo "")
echo "=== Release detection ==="
echo "CURRENT_PUBLISH=$CURRENT_PUBLISH"
echo "PREV_PUBLISH=$PREV_PUBLISH"
if [ -n "$PREV_PUBLISH" ] && [ "$CURRENT_PUBLISH" != "$PREV_PUBLISH" ]; then
# Detect minor/major vs patch: check if s2 or react-aria-components got a x.y.0 tag on this Publish commit
IS_MINOR=$(git tag --points-at "$CURRENT_PUBLISH" | grep -E '^(@react-spectrum/s2|react-aria-components)@[0-9]+\.[0-9]+\.0$' | head -1)
echo "IS_MINOR_OR_MAJOR=${IS_MINOR:-(none)}"
if [ -n "$IS_MINOR" ]; then
echo "Minor/major release detected, resetting baseline"
echo "NEW_RELEASE=true" >> $GITHUB_ENV
else
echo "Patch release detected, updating hash, continuing with delta"
echo "$CURRENT_PUBLISH" > snapshots/last-publish-hash.txt
fi
else
echo "No new release"
fi
NEW_RELEASE="${NEW_RELEASE:-false}"
if [ "$NEW_RELEASE" != "true" ]; then
# Compare against the last diff in the snapshots repo
PREV=$(ls snapshots/diffs/*.txt 2>/dev/null | sort -r | head -1)
echo ""
echo "=== Delta computation ==="
echo "Comparing against: ${PREV:-(none, first run)}"
if [ -n "$PREV" ]; then
diff "$PREV" /tmp/diff-current.txt > /tmp/weekly-delta.txt || true
else
echo "(first run — no previous diff to compare against)" > /tmp/weekly-delta.txt
fi
fi
echo ""
echo "=== File sizes ==="
echo "diff-current.txt: $(wc -c < /tmp/diff-current.txt) bytes"
ls /tmp/weekly-delta.txt 2>/dev/null && echo "weekly-delta.txt: $(wc -c < /tmp/weekly-delta.txt) bytes" || echo "weekly-delta.txt: not created"
echo ""
echo "=== Delta content (first 20 lines) ==="
head -20 /tmp/weekly-delta.txt 2>/dev/null || echo "(none)"
# Commit a diff if there is a new minor/major release (fresh baseline), or diff changed from last week
# Skip if no difference from last diff (aka no change from last week/last run), or if the diff against the release code is empty
echo ""
echo "=== Commit decision ==="
if [ -s /tmp/diff-current.txt ] && ([ "$NEW_RELEASE" = "true" ] || [ -s /tmp/weekly-delta.txt ]); then
echo "Committing diff to snapshots repo"
cp /tmp/diff-current.txt snapshots/diffs/$TODAY.txt
mkdir -p snapshots/deltas
if [ -s /tmp/weekly-delta.txt ]; then
# Save processed delta: strip > / < markers and line-number markers,
# split into two labeled sections so it can be passed directly to the model
{
echo "=== NEW API CHANGES THIS WEEK (not in last week's diff) ==="
grep '^> ' /tmp/weekly-delta.txt | sed 's/^> //'
RELEASED=$(grep '^< ' /tmp/weekly-delta.txt | sed 's/^< //')
if [ -n "$RELEASED" ]; then
echo ""
echo "=== API CHANGES RELEASED SINCE LAST WEEK (were in last week's diff, now gone) ==="
echo "$RELEASED"
fi
} > snapshots/deltas/$TODAY.txt
fi
cd snapshots
git config user.email "github-actions@github.com"
git config user.name "GitHub Actions"
git add diffs/$TODAY.txt
[ -f deltas/$TODAY.txt ] && git add deltas/$TODAY.txt
echo "$CURRENT_PUBLISH" > last-publish-hash.txt
git add last-publish-hash.txt
echo "=== Staged changes ==="
git diff --cached --stat
git diff --cached --quiet || (git commit -m "weekly api diff $TODAY" && git push)
else
echo "Skipping commit — diff-current empty=$([ ! -s /tmp/diff-current.txt ] && echo yes || echo no), new_release=$NEW_RELEASE, delta_empty=$([ ! -s /tmp/weekly-delta.txt ] && echo yes || echo no)"
fi
# Summarize with GitHub Models (free via GITHUB_TOKEN) and post to Slack
- name: Summarize and post to Slack
env:
SLACK_TSDIFF_CHROMATIC_BOT_TOKEN: ${{ secrets.SLACK_TSDIFF_CHROMATIC_BOT_TOKEN }}
SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }}
TEST_SLACK_ID: ${{ secrets.TEST_SLACK_ID }}
GITHUB_TOKEN: ${{ github.token }}
run: |
python3 << 'PYEOF'
import glob, json, os, urllib.request
required = ['SLACK_TSDIFF_CHROMATIC_BOT_TOKEN', 'SLACK_CHANNEL_ID', 'GITHUB_TOKEN', 'TODAY', 'SNAPSHOTS_REPO', 'GITHUB_WORKSPACE']
missing = [k for k in required if not os.environ.get(k)]
if missing:
raise SystemExit(f"Missing required environment variables: {', '.join(missing)}")
today = os.environ['TODAY']
channel = os.environ['SLACK_CHANNEL_ID']
snapshots_repo = os.environ['SNAPSHOTS_REPO']
slack_token = os.environ['SLACK_TSDIFF_CHROMATIC_BOT_TOKEN']
github_token = os.environ['GITHUB_TOKEN']
workspace = os.environ['GITHUB_WORKSPACE']
diff_url = f"https://github.com/{snapshots_repo}/blob/main/diffs/{today}.txt"
delta_url = f"https://github.com/{snapshots_repo}/blob/main/deltas/{today}.txt"
vs_release_size = os.path.getsize('/tmp/diff-current.txt')
vs_last_week_size = os.path.getsize('/tmp/weekly-delta.txt') if os.path.exists('/tmp/weekly-delta.txt') else 0
prev_files = sorted(glob.glob(f"{workspace}/snapshots/diffs/*.txt"), reverse=True)
prev_date = os.path.basename(prev_files[0]).replace('.txt', '') if prev_files else None
prev_url = f"https://github.com/{snapshots_repo}/blob/main/diffs/{prev_date}.txt" if prev_date else None
new_release = os.environ.get('NEW_RELEASE') == 'true'
if vs_release_size == 0:
message = f"📊 Weekly API Diff — {today}\n\nNo API changes detected vs last release — all pending changes have been included in a release."
elif vs_last_week_size == 0 and not new_release:
prev_ref = f"last diff ({prev_date}): {prev_url}" if prev_date else "last diff"
message = f"📊 Weekly API Diff — {today}\n\nNo new API changes since {prev_ref}."
elif new_release:
message = f"📊 Weekly API Diff — {today}\n\nNew release since last diff — resetting baseline. Full diff vs release: {diff_url}\n\nReact ✅ if changes look expected, or 🚨 if something looks wrong."
else:
# Read the already-processed delta saved by the shell step
delta_path = f"{workspace}/snapshots/deltas/{today}.txt"
model_input = open(delta_path).read() if os.path.exists(delta_path) else ""
# Extract classification rules from prompt.md (single source of truth)
prompt_md = open(f"{workspace}/scripts/weekly-api-diff/prompt.md").read()
rules_start = prompt_md.find("Apply these grouping and classification rules")
rules_end = prompt_md.find("\n## Step 9", rules_start)
rules = prompt_md[rules_start:rules_end].strip() if rules_start != -1 else ""
payload = {
"model": "gpt-4o-mini",
"max_tokens": 800,
"messages": [{
"role": "user",
"content": (
f"Summarize this week's react-spectrum API changes using grouped bullet points. Group related changes together (e.g. multiple layout classes losing the same method = one bullet). For each group or item:\n"
f"- Name the component(s) or interface(s)\n"
f"- Say what changed (added, removed, signature changed)\n"
f"- Flag net-new components \n"
f"Don't spell out full type signatures. Aim for a knowledgeable teammate skimming Slack.\n\n"
f"IMPORTANT: Only report what is explicitly listed below. Do not infer or add anything from your training knowledge.\n\n"
f"{rules}\n\n"
f"{model_input}"
)
}]
}
req = urllib.request.Request(
'https://models.inference.ai.azure.com/chat/completions',
data=json.dumps(payload).encode(),
headers={
'Authorization': f'Bearer {github_token}',
'Content-Type': 'application/json'
}
)
summary = json.loads(urllib.request.urlopen(req).read())['choices'][0]['message']['content']
message = f"📊 Weekly API Diff — {today}\n\n{summary}\n\nWhat's new this week: {delta_url}\nFull diff vs release: {diff_url}\n\nReact ✅ if changes look expected, or 🚨 if something looks wrong."
req = urllib.request.Request(
'https://slack.com/api/chat.postMessage',
data=json.dumps({"channel": channel, "text": message}).encode(),
headers={
'Authorization': f'Bearer {slack_token}',
'Content-Type': 'application/json'
}
)
resp = json.loads(urllib.request.urlopen(req).read())
print("Slack response:", resp.get('ok'), resp.get('error', ''))
if not resp.get('ok'):
raise SystemExit(f"Slack error: {resp.get('error')}")
PYEOF