Skip to content

Commit 667addb

Browse files
committed
Post the pull request report as a comment on it (#307)
The summary a run writes beside its report is meant to be commented on the pull request, and --comment does that: it looks for the comment an earlier run of the same pull request left, recognises it by a marker, and replaces it rather than adding to a pile. It posts as whoever the token belongs to, GITHUB_TOKEN or GH_TOKEN in the environment or the account gh is logged in as, which is how it can be run by hand. In Jenkins it is the pull_request_comment parameter, taking the token from a github-token credential. The credential is bound where it is used rather than for the stage, so a job that does not ask for a comment runs whether one is configured or not; without a token the report is written and published as before and the summary says where it is. --- Generated by Claude Code.
1 parent 8cd8f57 commit 667addb

3 files changed

Lines changed: 79 additions & 5 deletions

File tree

‎.CI/Jenkinsfile‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ pipeline {
4242
string(name: 'pull_request_baseline', defaultValue: 'master', description: 'The branch a pull request is compared against. Its newest run has to come from pull_request_node, or the report compares the machines as much as the pull request.')
4343
string(name: 'pull_request_config', defaultValue: 'configs/conf.json', description: 'What a pull request run tests. A full run takes days, so a smaller configuration file is often the better question to ask.')
4444
choice(name: 'pull_request_node', choices: ['ryzen-5950x-1', 'ryzen-5950x-2-1', 'ryzen-9950x'], description: 'The machine a pull request runs on. The default is the one that produces the master runs it is compared against.')
45+
booleanParam(name: 'pull_request_comment', defaultValue: false, description: 'Post the summary of a pull request run as a comment on the pull request, replacing the one an earlier run posted. Needs a github-token credential; without it the report is still written and published, only not commented.')
4546
booleanParam(name: 'drop_stale_pull_request_tables', defaultValue: false, description: 'Drop the pr-<N> tables of pull requests that have been merged or closed, and of those tested more than 60 days ago. The reports published for them are not touched.')
4647
}
4748
environment {
@@ -634,9 +635,19 @@ pipeline {
634635
}
635636
}
636637
sh 'rm -rf history'
637-
sh "./pr-report.py '${pullRequest()}' --baseline='${(params.pull_request_baseline ?: 'master').trim()}'"
638-
// The summary to comment on the pull request with, in the build log
639-
// until there is a token to post it with.
638+
script {
639+
def report = "./pr-report.py '${pullRequest()}' --baseline='${(params.pull_request_baseline ?: 'master').trim()}'"
640+
if (params.pull_request_comment) {
641+
// Whoever the token belongs to is who the comment comes from.
642+
withCredentials([string(credentialsId: 'github-token', variable: 'GITHUB_TOKEN')]) {
643+
sh "${report} --comment"
644+
}
645+
} else {
646+
sh report
647+
}
648+
}
649+
// The summary is in the build log as well, so a run without a token
650+
// still leaves it somewhere to copy from.
640651
sh 'cat history/pr-*/00_comment.md'
641652
sshPublisher(publishers: [sshPublisherDesc(configName: 'LibraryTestingReports', transfers: [sshTransfer(sourceFiles: 'history/**')])])
642653
}

‎README.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,14 @@ kind of page as the nightly regression reports, next to `00_comment.md`, a
288288
summary to comment on the pull request with. Both are published with the other
289289
reports.
290290

291+
`--comment` posts that summary on the pull request, and replaces it rather than
292+
adding to it when the same pull request is tested again. It posts as whoever the
293+
token belongs to: `GITHUB_TOKEN` or `GH_TOKEN` in the environment, or the account
294+
[`gh`](https://cli.github.com) is logged in as. In Jenkins it is the
295+
`pull_request_comment` parameter, which takes the token from a `github-token`
296+
credential; without one the report is still written and published, and the
297+
summary is in the build log.
298+
291299
Two things make a difference mean something other than "the pull request did
292300
this", and the report says so when they apply: **the machine**, since two runs
293301
produced on different hardware compare the hardware as much as the change, and

‎pr-report.py‎

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
what each phase cost - only the two runs it is given come from two branches.
1111
"""
1212

13-
import argparse, codecs, datetime, html, os, re, time
13+
import argparse, codecs, datetime, html, json, os, re, subprocess, time
14+
import urllib.error, urllib.request
1415
import shared, resultsdb
1516
from omcommon import friendlyStr, multiple_replace
1617

@@ -24,6 +25,7 @@
2425
parser.add_argument('--historypath', default="history")
2526
parser.add_argument('--githuburl', default="https://github.com/OpenModelica/OpenModelica")
2627
parser.add_argument('--markdown', default="", help='where to write the summary to comment on the pull request with (default: <historypath>/<branch>/00_comment.md)')
28+
parser.add_argument('--comment', action='store_true', help='post that summary on the pull request, replacing the one posted by an earlier run')
2729
resultsdb.addArgument(parser)
2830
args = parser.parse_args()
2931

@@ -45,6 +47,7 @@
4547
branch = "pr-%s" % pr
4648
baseline = args.baseline.split("/")[-1]
4749
prurl = "%s/pull/%s" % (args.githuburl, pr)
50+
repo = args.githuburl.split("github.com/")[-1].strip("/")
4851

4952
db = resultsdb.connect(args.db)
5053
cursor = db.cursor()
@@ -213,6 +216,54 @@ def classify(group, times):
213216
return (colour, " ".join(msgs),
214217
"performance improved" if colour == "betterPerformance" else "performance regression")
215218

219+
# A run of the same pull request replaces the comment of the one before it
220+
# rather than adding to a pile; this is how it recognises its own.
221+
COMMENTMARKER = "<!-- openmodelica-library-testing: pull request report -->"
222+
223+
def githubToken():
224+
"""A token to post with: the environment, or whoever gh is logged in as."""
225+
for var in ["GITHUB_TOKEN", "GH_TOKEN"]:
226+
if os.environ.get(var):
227+
return os.environ[var]
228+
try:
229+
return subprocess.check_output(["gh", "auth", "token"],
230+
stderr=subprocess.DEVNULL).decode("utf-8").strip()
231+
except Exception:
232+
return None
233+
234+
def github(url, token, data=None, method=None):
235+
request = urllib.request.Request(
236+
url, method=method,
237+
data=json.dumps(data).encode("utf-8") if data is not None else None,
238+
headers={"Accept": "application/vnd.github+json",
239+
"Authorization": "Bearer %s" % token,
240+
"Content-Type": "application/json"})
241+
return json.loads(urllib.request.urlopen(request).read().decode("utf-8"))
242+
243+
def postComment(number, body):
244+
"""Post the summary on the pull request, or update the one already there."""
245+
token = githubToken()
246+
if not token:
247+
return ("No token to comment with: set GITHUB_TOKEN, or log in with gh. "
248+
"The comment is in %s." % markdownname)
249+
api = "https://api.github.com/repos/%s/issues" % repo
250+
try:
251+
page = 1
252+
while True:
253+
comments = github("%s/%s/comments?per_page=100&page=%d" % (api, number, page), token)
254+
for comment in comments:
255+
if COMMENTMARKER in (comment.get("body") or ""):
256+
github(comment["url"], token, {"body": body}, method="PATCH")
257+
return "Updated %s" % comment["html_url"]
258+
if len(comments) < 100:
259+
break
260+
page += 1
261+
return "Commented on %s" % github("%s/%s/comments" % (api, number), token,
262+
{"body": body})["html_url"]
263+
except urllib.error.HTTPError as e:
264+
raise SystemExit("Could not comment on %s#%s: %s\n%s"
265+
% (repo, number, e, e.read().decode("utf-8", "replace")))
266+
216267
counts = {"improved": 0, "regression": 0, "performance improved": 0, "performance regression": 0}
217268
rows = []
218269
markdownrows = []
@@ -337,8 +388,12 @@ def classify(group, times):
337388
markdown += ["- %s" % c.replace("&rarr;", "->") for c in caveats + [note]]
338389
markdown += ["", "</details>", "", "---", "Generated by the OpenModelica library testing"]
339390
markdownname = args.markdown or os.path.join(historydir, "00_comment.md")
391+
comment = "\n".join([COMMENTMARKER] + markdown) + "\n"
340392
with codecs.open(markdownname, "w", encoding="utf-8") as fout:
341-
fout.write("\n".join(markdown) + "\n")
393+
fout.write(comment)
394+
395+
if args.comment:
396+
print(postComment(pr, comment))
342397

343398
print("%s: %s" % (branch, summary))
344399
print("Report: %s" % os.path.join(historydir, reportname))

0 commit comments

Comments
 (0)