From ab67e5b1f0f02884c2f6c0f9abc4f88a7e0e2fc1 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Fri, 23 Oct 2026 14:39:57 +0000 Subject: [PATCH 1/4] Keep one semantic diff comment per pull request Each push, and above all each force push, added a comment of its own, so a pull request collected copies of its records. The workflow now edits one comment in place to hold every commit, and --since is gone. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AzPS5F8RDSGZ6dhfK6NdZp Signed-off-by: Christoph Knittel --- .claude/skills/records/SKILL.md | 4 +-- .github/workflows/semantic_diff.yml | 29 +++++--------------- scripts/commitRecords.js | 42 +++++------------------------ scripts/commitRecords.test.js | 42 +++++++++-------------------- scripts/recordMarkdown.js | 11 +++----- 5 files changed, 32 insertions(+), 96 deletions(-) diff --git a/.claude/skills/records/SKILL.md b/.claude/skills/records/SKILL.md index d500dbc35b..c66c1b0a6a 100644 --- a/.claude/skills/records/SKILL.md +++ b/.claude/skills/records/SKILL.md @@ -193,8 +193,8 @@ node scripts/commitRecords.js --out page.html [--pr ] [--title ] \ - `--pr` and `--title` head the page with the pull request. `.github/workflows/semantic_diff.yml` renders each pull request's range and comments the records as Markdown (`--markdown --url `, - scripts/recordMarkdown.js) with the page's link: after a push that keeps - the previous head, only the commits it added (`--since `). + scripts/recordMarkdown.js) with the page's link, one comment that each + push updates to hold every commit. - In prose, `#1234` and full pull request and issue URLs link as `#1234`, and the 9-character short sha of a commit on the page links to its card. diff --git a/.github/workflows/semantic_diff.yml b/.github/workflows/semantic_diff.yml index 33cb31b9d6..c5824095ad 100644 --- a/.github/workflows/semantic_diff.yml +++ b/.github/workflows/semantic_diff.yml @@ -2,9 +2,9 @@ name: Semantic Diff # Renders the commit records of each pull request (scripts/commitRecords.js) # as a review page on the CDN, and comments them on the pull request as -# Markdown (scripts/recordMarkdown.js) with the page's link. The page holds -# every commit; the comment holds the commits a push added, or every commit -# when the pull request is opened or reopened or its branch is force-pushed. +# Markdown (scripts/recordMarkdown.js) with the page's link. A pull request +# has one such comment, which each push updates in place to hold every +# commit, so the pull request shows its current records in one place. # # A pull request from a branch of this repository runs on pull_request, with # the workflow and renderer of its merge into the base branch: GitHub takes @@ -42,9 +42,6 @@ jobs: PR_NUMBER: ${{ github.event.pull_request.number }} PR_TITLE: ${{ github.event.pull_request.title }} BASE_SHA: ${{ github.event.pull_request.base.sha }} - # The head before a push; empty when the pull request is opened or - # reopened. - BEFORE_SHA: ${{ github.event.action == 'synchronize' && github.event.before || '' }} steps: - name: Checkout renderer uses: actions/checkout@v7 @@ -55,7 +52,6 @@ jobs: persist-credentials: false - name: Render semantic diff - id: render run: | git fetch --no-tags --filter=blob:none origin "refs/pull/${PR_NUMBER}/head:refs/heads/pr-${PR_NUMBER}" base=$(git merge-base "$BASE_SHA" "pr-${PR_NUMBER}") @@ -63,17 +59,8 @@ jobs: # The query string names the rendered commit, so each push gets a # URL that no cache has seen. url="https://cdn.rescript-lang.org/semantic-diff/pr-${PR_NUMBER}/index.html?v=${head}" - # A push that keeps the previous head adds commits on top of it: - # the comment holds those. After a force push the previous head is - # no ancestor (nor, from a fork, fetched), and the comment holds - # every commit. - since=() - if [ -n "$BEFORE_SHA" ] && git merge-base --is-ancestor "$BEFORE_SHA" "pr-${PR_NUMBER}" 2>/dev/null; then - since=(--since "$BEFORE_SHA") - fi mkdir -p semantic-diff - node scripts/commitRecords.js --out semantic-diff/index.html --markdown semantic-diff/comment.md "${since[@]}" --url "$url" --pr "$PR_NUMBER" --title "$PR_TITLE" "${base}..pr-${PR_NUMBER}" - echo "head=${head}" >> "$GITHUB_OUTPUT" + node scripts/commitRecords.js --out semantic-diff/index.html --markdown semantic-diff/comment.md --url "$url" --pr "$PR_NUMBER" --title "$PR_TITLE" "${base}..pr-${PR_NUMBER}" - name: Setup Rclone uses: cometkim/rclone-actions/setup-rclone@8d9c6b95341f7698609deee65c3d62af42893288 # main, 2025-06-18 @@ -91,14 +78,12 @@ jobs: - name: Upload semantic diff run: rclone copyto --s3-no-check-bucket semantic-diff/index.html "rescript:cdn-assets/semantic-diff/pr-${PR_NUMBER}/index.html" - # A push that adds only merge commits writes no comment. - name: Comment semantic diff - if: hashFiles('semantic-diff/comment.md') != '' uses: thollander/actions-comment-pull-request@v3 with: pr-number: ${{ github.event.pull_request.number }} - # Each push gets its own comment, so the comments of earlier pushes - # stay; a re-run of the same head updates its comment. - comment-tag: semantic-diff-${{ steps.render.outputs.head }} + # The pull request's one comment, edited in place on each push; + # edits notify no one. + comment-tag: semantic-diff mode: upsert file-path: semantic-diff/comment.md diff --git a/scripts/commitRecords.js b/scripts/commitRecords.js index c719c8e717..f139858744 100644 --- a/scripts/commitRecords.js +++ b/scripts/commitRecords.js @@ -11,9 +11,7 @@ * node scripts/commitRecords.js --out review.html ..... * --pr and --title head the page with the pull request. * --markdown also draws the records as the pull request's comment - * (scripts/recordMarkdown.js), linking the page at --url . With - * --since , the comment draws only the commits lacks, and is - * not written when there are none. + * (scripts/recordMarkdown.js), linking the page at --url . */ import { execFileSync } from "node:child_process"; @@ -67,9 +65,7 @@ const options = {}; const targets = []; for (let i = 0; i < args.length; i++) { if ( - ["--out", "--pr", "--title", "--markdown", "--url", "--since"].includes( - args[i], - ) && + ["--out", "--pr", "--title", "--markdown", "--url"].includes(args[i]) && i + 1 < args.length ) options[args[i].slice(2)] = args[++i]; @@ -78,7 +74,7 @@ for (let i = 0; i < args.length; i++) { const outFile = options.out; if (!outFile) { console.error( - "usage: commitRecords.js --out [--pr ] [--title ] [--markdown [--url ] [--since ]] ...", + "usage: commitRecords.js --out [--pr ] [--title ] [--markdown [--url ]] ...", ); process.exit(1); } @@ -89,35 +85,11 @@ const heading = { const groups = targets.map(group); const body = pageBody(groups, heading); if (options.markdown) { - const since = options.since; - // The commits of the targets that `since` does not reach. - const fresh = new Set( - since - ? targets.flatMap(t => - git(["rev-list", "--abbrev-commit", "--abbrev=9", t, `^${since}`]) - .split("\n") - .filter(Boolean), - ) - : [], + writeFileSync( + options.markdown, + commentMarkdown(groups, { url: options.url }), ); - const shown = since - ? groups.map(g => ({ - ...g, - records: g.records.filter(r => fresh.has(r.sha)), - })) - : groups; - if (shown.some(g => g.records.length)) { - writeFileSync( - options.markdown, - commentMarkdown(shown, { - url: options.url, - total: since - ? groups.reduce((n, g) => n + g.records.length, 0) - : undefined, - }), - ); - console.log(options.markdown); - } + console.log(options.markdown); } const head = `${esc(pageTitle(heading))} diff --git a/scripts/commitRecords.test.js b/scripts/commitRecords.test.js index 1f0cff5f57..15a2d008ad 100644 --- a/scripts/commitRecords.test.js +++ b/scripts/commitRecords.test.js @@ -832,7 +832,7 @@ ${SIGNED} has(html, ''); }); -test("--since leaves the comment the commits a push added, the page every commit", async () => { +test("--markdown draws every commit into the comment, with the page's link", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "commit-records-")); const git = (/** @type {string[]} */ ...a) => execFileSync("git", a, { cwd: dir, encoding: "utf8" }).trim(); @@ -841,36 +841,20 @@ test("--since leaves the comment the commits a push added, the page every commit git("config", "user.email", "a@example.com"); await commit(git, dir, { "a.txt": "a\n" }, "base"); await commit(git, dir, {}, `First change\n\n${SIGNED}`); - const before = git("rev-parse", "HEAD"); await commit(git, dir, {}, `Second change\n\n${SIGNED}`); - const out = path.join(dir, "page.html"); const md = path.join(dir, "comment.md"); - const run = (/** @type {string} */ since) => { - const r = spawnSync( - "node", - [ - script, - "--out", - out, - "--markdown", - md, - "--since", - since, - "HEAD~2..HEAD", - ], - { cwd: dir, encoding: "utf8" }, - ); - assert.equal(r.status, 0, r.stderr); - }; - run(before); + const r = spawnSync( + "node", + [ + script, + ...["--out", path.join(dir, "page.html"), "--markdown", md], + ...["--url", "https://cdn.example/pr-1/index.html", "HEAD~2..HEAD"], + ], + { cwd: dir, encoding: "utf8" }, + ); + assert.equal(r.status, 0, r.stderr); const comment = await fs.readFile(md, "utf8"); - has(comment, "1 new commit (2 total)"); + has(comment, "2 commits · [Full page](https://cdn.example/pr-1/index.html)"); + has(comment, "First change"); has(comment, "Second change"); - lacks(comment, "First change"); - const html = await fs.readFile(out, "utf8"); - has(html, "First change"); - has(html, "Second change"); - await fs.rm(md); - run("HEAD"); - await assert.rejects(fs.access(md)); }); diff --git a/scripts/recordMarkdown.js b/scripts/recordMarkdown.js index 25cd4f8fab..d3d538563f 100644 --- a/scripts/recordMarkdown.js +++ b/scripts/recordMarkdown.js @@ -422,18 +422,13 @@ export function commitMarkdown(r, full) { * The comment: a heading linking the page, then each commit. Past the * budget, the commits with the most characters are drawn without their * forms, and if that is not enough, the last commits are left to the page. - * With `total`, the commits are the new ones a push added to a pull request - * of `total` commits. * @param {Group[]} groups - * @param {{ url?: string, budget?: number, total?: number }} [options] + * @param {{ url?: string, budget?: number }} [options] */ -export function commentMarkdown( - groups, - { url = "", budget = BUDGET, total } = {}, -) { +export function commentMarkdown(groups, { url = "", budget = BUDGET } = {}) { const records = groups.flatMap(g => g.records); const n = records.length; - const head = `### Semantic Diff\n\n${n}${total === undefined ? "" : " new"} commit${n === 1 ? "" : "s"}${total === undefined || total === n ? "" : ` (${total} total)`}${url ? ` · [Full page](${url})` : ""}`; + const head = `### Semantic Diff\n\n${n} commit${n === 1 ? "" : "s"}${url ? ` · [Full page](${url})` : ""}`; const drawn = records.map(r => commitMarkdown(r, true)); const size = () => drawn.reduce((t, d) => t + d.length + 2, head.length); const order = drawn From 291da39172f0f8bd1ff8452fecc4463be869fbd7 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Fri, 23 Oct 2026 14:53:59 +0000 Subject: [PATCH 2/4] Draw an AI assistant co-author by its short name "A Person with Claude Opus 5.5" took much of the identity line, above all on a phone. A co-author whose trailer gives an assistant's email is drawn by its short name, keyed on the email since the name changes with the model, and co-authors are joined with "+". Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AzPS5F8RDSGZ6dhfK6NdZp Signed-off-by: Christoph Knittel --- .claude/skills/records/SKILL.md | 9 +++++---- scripts/commitRecords.test.js | 7 ++++--- scripts/recordMarkdown.js | 3 ++- scripts/recordMarkdown.test.js | 9 ++++++++- scripts/recordRender.js | 23 ++++++++++++++++++++--- 5 files changed, 39 insertions(+), 12 deletions(-) diff --git a/.claude/skills/records/SKILL.md b/.claude/skills/records/SKILL.md index c66c1b0a6a..883bda080f 100644 --- a/.claude/skills/records/SKILL.md +++ b/.claude/skills/records/SKILL.md @@ -201,7 +201,8 @@ node scripts/commitRecords.js --out page.html [--pr ] [--title ] \ ## What the page shows The page is the heading, then a card per commit. A card shows the commit's -short sha, author and co-authors, date, the pull request a `(#1234)` at -the end of the subject names, and the message drawn: the subject, the -prose, each object block with its example and forms, the `Breaking:` -sections as banners, and the `Review:` sections as a list. +short sha, author and co-authors (an AI assistant by its short name, such +as Claude), date, the pull request a `(#1234)` at the end of the subject +names, and the message drawn: the subject, the prose, each object block +with its example and forms, the `Breaking:` sections as banners, and the +`Review:` sections as a list. diff --git a/scripts/commitRecords.test.js b/scripts/commitRecords.test.js index 15a2d008ad..192ad9ee14 100644 --- a/scripts/commitRecords.test.js +++ b/scripts/commitRecords.test.js @@ -132,7 +132,7 @@ test("a minimal record draws its identity and subject", async () => { has(html, "

Fix a typo

"); assert.match( html, - /
[0-9a-f]{9}<\/code> · A Person with B Person · [A-Z][a-z]{2} \d{2}, \d{4} · #7<\/a><\/div>/, + /
[0-9a-f]{9}<\/code> · A Person \+ B Person · [A-Z][a-z]{2} \d{2}, \d{4} · #7<\/a><\/div>/, ); lacks(html, '
'); lacks(html, "Signed-off-by"); @@ -457,7 +457,7 @@ test("Breaking and Review sections right above the sign-off are drawn", async () ); has(html, '
  • Is -bs-x still read? No.
  • '); - has(html, "
    · A Person with B Person · "); + has(html, "
    · A Person + B Person · "); lacks(html, "Signed-off-by"); }); @@ -504,7 +504,8 @@ test("trailers of any key end the message and are not drawn", async () => { const html = await renderOne( `Fix a thing\n\nCo-Authored-By: Claude Opus 5.5 \nClaude-Session: https://claude.ai/code/session_x\n${SIGNED}\n`, ); - has(html, " · A Person with Claude Opus 5.5 · "); + has(html, " · A Person + Claude · "); + lacks(html, "Opus"); lacks(html, "Claude-Session"); lacks(html, "session_x"); lacks(html, '