From 865d486d82c5464c0542e6a4e8c23d542cdd9ccd Mon Sep 17 00:00:00 2001 From: Timur Mashara <62305878+tmashara@users.noreply.github.com> Date: Thu, 29 Oct 2026 20:36:39 +0100 Subject: [PATCH 1/2] Action: opt-in sticky PR comment (comment: true) Posts the verdict and compact output as one PR comment, found by a per-artifact-name marker and edited on later runs. Works on pull_request and deployment_status; warns instead of failing on a read-only token. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/action.yml | 5 ++++ action.yml | 51 +++++++++++++++++++++++++++++++++++- docs/github-action.md | 18 +++++++++++++ 3 files changed, 73 insertions(+), 1 deletion(-) diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml index 13cba59..7880fd8 100644 --- a/.github/workflows/action.yml +++ b/.github/workflows/action.yml @@ -8,6 +8,8 @@ on: jobs: self-test: runs-on: ubuntu-latest + env: + RENDERDIFF_COMMENT_DRY_RUN: "1" steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 @@ -42,6 +44,7 @@ jobs: threshold: "0.999" out: differ-out artifact-name: differ + comment: "true" - id: accept uses: ./ @@ -76,3 +79,5 @@ jobs: test "$DOWN" = 2 node -e 'const r = require("./differ-out/results.json").results; for (const p of ["/color", "/tint"]) if (!(r.find((x) => x.url === p)?.score < 0.999)) throw p' grep -q '"/color"' accept-out/renderdiff.accepted.json + grep -q '' "$RUNNER_TEMP/renderdiff-comment-differ.md" + grep -q '/color' "$RUNNER_TEMP/renderdiff-comment-differ.md" diff --git a/action.yml b/action.yml index 828b18c..76b444b 100644 --- a/action.yml +++ b/action.yml @@ -42,6 +42,9 @@ inputs: artifact-name: description: Name of the uploaded artifact with the report, results.json and crops. default: renderdiff + comment: + description: "true posts the result as one PR comment and updates it on later runs. Needs pull-requests: write." + default: "false" outputs: exit-code: @@ -151,13 +154,17 @@ runs: echo "### renderdiff: $verdict" echo echo "\`$LEFT\` (your change) vs \`$RIGHT\` (reference). Report, results.json and crops are in the \`$ARTIFACT\` artifact." + } > "$RUNNER_TEMP/renderdiff-$ARTIFACT.md" + { + cat "$RUNNER_TEMP/renderdiff-$ARTIFACT.md" echo echo '```text' head -c 900000 "$log" echo '```' } >> "$GITHUB_STEP_SUMMARY" - - if: always() && steps.run.outcome == 'success' + - id: upload + if: always() && steps.run.outcome == 'success' uses: actions/upload-artifact@v4 with: name: ${{ inputs.artifact-name }} @@ -165,6 +172,48 @@ runs: if-no-files-found: ignore retention-days: 14 + - if: always() && inputs.comment == 'true' && steps.run.outcome == 'success' + shell: bash + env: + GH_TOKEN: ${{ github.token }} + PR: ${{ github.event.pull_request.number }} + ARTIFACT: ${{ inputs.artifact-name }} + ARTIFACT_URL: ${{ steps.upload.outputs.artifact-url }} + MARKER: + run: | + log="$RUNNER_TEMP/renderdiff-$ARTIFACT.txt" + body="$RUNNER_TEMP/renderdiff-comment-$ARTIFACT.md" + run="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" + { + echo "$MARKER" + cat "$RUNNER_TEMP/renderdiff-$ARTIFACT.md" + echo + echo "[This run]($run)${ARTIFACT_URL:+ · [artifact]($ARTIFACT_URL)}" + echo + echo '```text' + # Cut lines are dropped whole so a split UTF-8 char can't glue onto the closing fence. + if [ "$(wc -c < "$log")" -gt 60000 ]; then head -c 60000 "$log" | sed '$d'; echo '…'; else cat "$log"; fi + echo '```' + [ "$(wc -c < "$log")" -le 60000 ] || echo "Output truncated; the full output is in the job summary of [this run]($run)." + } > "$body" + + pr=$PR + if [ -z "$pr" ] && [ "$GITHUB_EVENT_NAME" = deployment_status ]; then + pr=$(gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/pulls" --jq 'map(select(.state == "open"))[0].number // empty') || pr= + fi + if [ -z "$pr" ]; then + echo "::notice::renderdiff: no open pull request for this run, comment skipped" + exit 0 + fi + + if ids=$(gh api --paginate "repos/$GITHUB_REPOSITORY/issues/$pr/comments" --jq '.[] | select(.user.login == "github-actions[bot]" and (.body | startswith(env.MARKER))) | .id'); then + id=${ids%%$'\n'*} + if [ -n "$id" ]; then req=(-X PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$id"); else req=(-X POST "repos/$GITHUB_REPOSITORY/issues/$pr/comments"); fi + if [ -n "${RENDERDIFF_COMMENT_DRY_RUN:-}" ]; then echo "dry run: gh api ${req[*]}"; exit 0; fi + gh api "${req[@]}" -F body=@"$body" > /dev/null && exit 0 + fi + echo "::warning::renderdiff: could not comment on PR #$pr (a fork or Dependabot PR gets a read-only token); the result is in the job summary" + - if: steps.run.outputs.exit-code != '0' shell: bash env: diff --git a/docs/github-action.md b/docs/github-action.md index 0cd58a0..061a3d6 100644 --- a/docs/github-action.md +++ b/docs/github-action.md @@ -32,6 +32,7 @@ exits non-zero. | `wait-timeout` | Whole seconds to wait for `left` to answer (any status under 500). Default 300. Past it, `exit-code` is 2. | | `out` | Output directory, over the config's `outDir`. Default `renderdiff-out`. | | `artifact-name` | Default `renderdiff`. | +| `comment` | `"true"` posts the result as one PR comment and edits it on every later run. One comment per `artifact-name`, so two runs keep their own. | To run the action twice in one job, give each run its own `out` and `artifact-name`; in a matrix, put the matrix value in `artifact-name`. The action sets up Node 24, which later @@ -90,6 +91,23 @@ jobs: The action waits for `left` to answer, so the server can still be starting. +### Comment on the pull request + +With `comment: "true"` the job needs write access to pull requests: + +```yaml +permissions: + contents: read + pull-requests: write +``` + +It works on `pull_request`, and on `deployment_status` by finding the open PR for the +deployed commit. A fork's PR and Dependabot's get a read-only token, so the step warns and +the result stays in the job summary. Don't switch to `pull_request_target` to get write +access: it runs the PR's code with the base repo's token and secrets. The step needs `gh` +on the runner (preinstalled on GitHub-hosted ones). The comment is cut at 60 000 bytes, +with a link to the full job summary. + ### Dependency upgrades from Renovate or Dependabot A dependency bump should change nothing. That's the default without `threshold`: any From da0aa0ffe3dd5071215648a394b9bb9845159035 Mon Sep 17 00:00:00 2001 From: Timur Mashara <62305878+tmashara@users.noreply.github.com> Date: Thu, 29 Oct 2026 20:39:43 +0100 Subject: [PATCH 2/2] Close the comment's code fence when the log lacks a final newline Co-Authored-By: Claude Opus 5.5 --- action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yml b/action.yml index 76b444b..9c3e602 100644 --- a/action.yml +++ b/action.yml @@ -192,7 +192,7 @@ runs: echo echo '```text' # Cut lines are dropped whole so a split UTF-8 char can't glue onto the closing fence. - if [ "$(wc -c < "$log")" -gt 60000 ]; then head -c 60000 "$log" | sed '$d'; echo '…'; else cat "$log"; fi + if [ "$(wc -c < "$log")" -gt 60000 ]; then head -c 60000 "$log" | sed '$d'; echo '…'; else cat "$log"; [ -z "$(tail -c1 "$log")" ] || echo; fi echo '```' [ "$(wc -c < "$log")" -le 60000 ] || echo "Output truncated; the full output is in the job summary of [this run]($run)." } > "$body"