Skip to content

Commit d16692c

Browse files
[CI] Add PR Line change report (#624)
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent ab20656 commit d16692c

4 files changed

Lines changed: 1062 additions & 0 deletions

File tree

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
name: PR change report
2+
on:
3+
workflow_dispatch:
4+
pull_request:
5+
types:
6+
- opened
7+
- reopened
8+
- synchronize
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
11+
cancel-in-progress: true
12+
permissions:
13+
contents: read
14+
pull-requests: write
15+
checks: write
16+
jobs:
17+
pr-change-report:
18+
name: PR change report
19+
runs-on: ubuntu-latest
20+
steps:
21+
- uses: actions/checkout@v4
22+
with:
23+
fetch-depth: 0
24+
25+
- name: Wait using dead-reckoning for builds to finish, before running, to save AI cost, if someone pushes many commits in a row
26+
run: sleep 1800
27+
28+
- name: Set up Python
29+
uses: actions/setup-python@v5
30+
with:
31+
python-version: '3.10'
32+
33+
- name: Build agent inputs (per-file totals, diffs, base contents)
34+
id: inputs
35+
env:
36+
BASE_REF: ${{ github.base_ref }}
37+
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
38+
run: |
39+
SHORT_SHA=$(echo "${HEAD_SHA:-$(git rev-parse HEAD)}" | cut -c1-9)
40+
python .github/workflows/scripts_new/pr_change_report/build_inputs.py \
41+
--base-ref "$BASE_REF" \
42+
--output-dir /tmp/pr_change_report \
43+
--commit-hash "$SHORT_SHA"
44+
45+
if [ ! -s /tmp/pr_change_report/file_list.txt ]; then
46+
echo "skip=true" >> "$GITHUB_OUTPUT"
47+
echo "No source files changed."
48+
else
49+
echo "skip=false" >> "$GITHUB_OUTPUT"
50+
echo "short_sha=$SHORT_SHA" >> "$GITHUB_OUTPUT"
51+
echo "Files in report:"
52+
cat /tmp/pr_change_report/file_list.txt
53+
echo
54+
echo "Pre-computed file headers:"
55+
cat /tmp/pr_change_report/report_header.md
56+
fi
57+
58+
- name: Install Cursor CLI
59+
if: steps.inputs.outputs.skip != 'true'
60+
run: |
61+
curl https://cursor.com/install -fsS | bash
62+
echo "$HOME/.cursor/bin" >> $GITHUB_PATH
63+
64+
- name: Identify changed function ranges with Cursor agent
65+
if: steps.inputs.outputs.skip != 'true'
66+
env:
67+
CURSOR_API_KEY: ${{ secrets.CURSOR_KEY_HUGH }}
68+
run: |
69+
agent -p "$(cat <<'PROMPT'
70+
You are identifying which functions were touched by a PR. Line counts and +/-
71+
attribution are computed deterministically AFTER your output, so do NOT count
72+
lines yourself.
73+
74+
Inputs under /tmp/pr_change_report/ :
75+
- file_list.txt changed source paths (.py / .c / .cc / .cpp / .h / .hpp /
76+
.cu), one per line.
77+
- touched_ranges.txt per-file, the line-number ranges in HEAD and base that the
78+
diff hunks cover. This tells you exactly where to look --
79+
you should only need to inspect lines inside (or close to)
80+
these ranges, not the whole file.
81+
- diffs/<path>.diff per-file unified diff vs merge-base.
82+
- head/<path> full HEAD content of that file (also reachable from the
83+
workspace at the same path).
84+
- base/<path> full base content (missing if the file is newly added).
85+
86+
For each file in file_list.txt, identify every function or method whose body
87+
contains at least one ``+`` or ``-`` line in the diff. Emit ONE JSON object per
88+
such function on its own line, with these keys:
89+
path : the file path, exactly as it appears in file_list.txt.
90+
name : the function or method name. C++ class methods MUST be
91+
"ClassName::method"; free functions are just "func". Use the synthetic
92+
name "<module>" ONLY for top-level non-function code (imports,
93+
module-level constants, type-alias / dataclass declarations, free
94+
``static`` C/C++ definitions, etc.) -- and ONLY when at least one ``+``
95+
or ``-`` line lands OUTSIDE every function/method body. Do NOT use
96+
"<module>" as a catch-all for an entire file.
97+
head : [start_line, end_line] in HEAD, both 1-indexed and inclusive (signature
98+
line through closing brace / dedent), or null if the function was
99+
DELETED by this PR.
100+
base : [start_line, end_line] in the base version, or null if the function was
101+
NEWLY added by this PR.
102+
103+
Rules:
104+
- Only emit functions that overlap a hunk in touched_ranges.txt. Skip every
105+
other function in the file.
106+
- For functions modified by this PR, both head and base are filled (their line
107+
ranges may differ).
108+
- Anonymous lambdas inside another function are NOT separate functions; their
109+
touched lines belong to the enclosing function.
110+
- The end_line of a function is the line of the closing brace (C/C++) or the
111+
last indented body line (Python), inclusive.
112+
- Use the touched_ranges.txt hints to narrow your reading -- you should not need
113+
to read whole large files. Read only enough HEAD and base context to bracket
114+
each touched function with confidence.
115+
- Function ranges within a single file should NOT overlap each other. A
116+
"<module>" range (when present) must NOT overlap any function/method range
117+
you also emit for that file.
118+
119+
Output: JSON Lines. ONE JSON object per line. Nothing else. NO code fences, NO
120+
preamble, NO closing remarks, NO commentary, NO blank lines between objects.
121+
PROMPT
122+
)" --model composer-2 --mode ask --output-format text --trust \
123+
> /tmp/pr_change_report/function_ranges.jsonl
124+
125+
echo "----- function_ranges.jsonl -----"
126+
cat /tmp/pr_change_report/function_ranges.jsonl
127+
echo "---------------------------------"
128+
129+
- name: Render report.txt deterministically
130+
if: steps.inputs.outputs.skip != 'true'
131+
run: |
132+
python .github/workflows/scripts_new/pr_change_report/render_report.py \
133+
--output-dir /tmp/pr_change_report \
134+
--function-ranges /tmp/pr_change_report/function_ranges.jsonl \
135+
--output /tmp/pr_change_report/report.txt
136+
137+
echo "----- report.txt -----"
138+
cat /tmp/pr_change_report/report.txt
139+
echo "----------------------"
140+
141+
- name: Assemble Check-page markdown
142+
if: steps.inputs.outputs.skip != 'true'
143+
env:
144+
SHORT_SHA: ${{ steps.inputs.outputs.short_sha }}
145+
run: |
146+
{
147+
echo "## PR change report (\`${SHORT_SHA}\`)"
148+
echo
149+
echo "Per-file totals + per-function breakdown of code-line additions and removals."
150+
echo "Code lines exclude blank lines, comment-only lines, and Python multi-line strings."
151+
echo
152+
# Reuse the table from report_comment.md (skip its leading heading + intro lines).
153+
sed -n '/^| File /,/^\*\*Total\*\*/p' /tmp/pr_change_report/report_comment.md
154+
echo
155+
echo "### Per-function breakdown"
156+
echo
157+
echo '```text'
158+
cat /tmp/pr_change_report/report.txt
159+
echo '```'
160+
} > /tmp/pr_change_report/report_check.md
161+
162+
echo "----- report_check.md (head) -----"
163+
head -40 /tmp/pr_change_report/report_check.md
164+
echo "..."
165+
echo "size: $(wc -c < /tmp/pr_change_report/report_check.md) bytes"
166+
167+
- name: Upload report artifacts
168+
if: steps.inputs.outputs.skip != 'true'
169+
uses: actions/upload-artifact@v4
170+
with:
171+
name: pr-change-report
172+
path: |
173+
/tmp/pr_change_report/report.txt
174+
/tmp/pr_change_report/report_check.md
175+
/tmp/pr_change_report/report_comment.md
176+
/tmp/pr_change_report/summary.json
177+
/tmp/pr_change_report/report_header.md
178+
/tmp/pr_change_report/touched_ranges.txt
179+
/tmp/pr_change_report/function_ranges.jsonl
180+
181+
- name: Publish PR change report Check
182+
if: steps.inputs.outputs.skip != 'true' && github.event_name == 'pull_request'
183+
id: publish_check
184+
uses: actions/github-script@v8
185+
with:
186+
script: |
187+
const fs = require('fs');
188+
let body = fs.readFileSync('/tmp/pr_change_report/report_check.md', 'utf8');
189+
const MAX_TEXT = 65000;
190+
let truncated = false;
191+
if (body.length > MAX_TEXT) {
192+
body = body.substring(0, MAX_TEXT) +
193+
'\n\n---\n*Report truncated. Download the full report from the workflow artifacts.*';
194+
truncated = true;
195+
}
196+
const summary = truncated
197+
? 'Per-file totals + per-function breakdown (truncated — see artifacts for full report).'
198+
: 'Per-file totals + per-function breakdown of code-line additions and removals.';
199+
const check = await github.rest.checks.create({
200+
owner: context.repo.owner,
201+
repo: context.repo.repo,
202+
head_sha: context.payload.pull_request.head.sha,
203+
name: 'PR change report',
204+
status: 'completed',
205+
conclusion: 'success',
206+
output: {
207+
title: 'PR change report',
208+
summary: summary,
209+
text: body,
210+
},
211+
});
212+
core.setOutput('check-url', check.data.html_url);
213+
214+
- name: Post PR comment
215+
if: steps.inputs.outputs.skip != 'true' && github.event_name == 'pull_request'
216+
uses: actions/github-script@v8
217+
env:
218+
REPORT_URL: ${{ steps.publish_check.outputs.check-url }}
219+
with:
220+
script: |
221+
const fs = require('fs');
222+
let body = fs.readFileSync('/tmp/pr_change_report/report_comment.md', 'utf8');
223+
const url = process.env.REPORT_URL;
224+
if (url) {
225+
body += `\n\n[Full per-function report](${url})`;
226+
}
227+
await github.rest.issues.createComment({
228+
owner: context.repo.owner,
229+
repo: context.repo.repo,
230+
issue_number: context.payload.pull_request.number,
231+
body: body,
232+
});

0 commit comments

Comments
 (0)