From 7db9c7c08c9b30f7749a73d794d1c091c25e8864 Mon Sep 17 00:00:00 2001 From: croway Date: Mon, 31 Aug 2026 13:49:54 +0200 Subject: [PATCH] CI: Upload regen patch artifact when the uncommitted-changes check fails When the Build and test workflow fails because regenerated files are not committed, save the regenerated changes as a regen-patch artifact (patch + PR number) instead of discarding them, and extend the CI commenter to post apply instructions on the PR. Anyone with push access to the PR branch (the author, or committers via Allow edits from maintainers) can then fix the PR in seconds without re-running the ~15 minute regen build locally. A later clean run marks the comment as resolved. Co-authored-by: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01WLj5xMfVRiqGwEge1JKC81 --- .github/workflows/pr-build-main.yml | 30 +++++- .github/workflows/pr-test-commenter.yml | 122 ++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-build-main.yml b/.github/workflows/pr-build-main.yml index c4eae869ee773..e066df2a41ffe 100644 --- a/.github/workflows/pr-build-main.yml +++ b/.github/workflows/pr-build-main.yml @@ -161,7 +161,35 @@ jobs: if: ${{ !inputs.skip_full_build }} shell: bash run: | - [[ -z $(git status --porcelain) ]] || { echo 'There are uncommitted changes'; git status; echo; echo; git diff; exit 1; } + if [[ -z $(git status --porcelain) ]]; then + exit 0 + fi + echo 'There are uncommitted changes' + git status + echo + echo + git diff + # Save the regenerated changes as a patch (with the PR number, since + # the ci-comment artifact is not produced on this failure path) so + # they can be applied to the PR branch without re-running the regen + # build locally. The commenter workflow posts apply instructions. + git add -A + mkdir -p regen-patch-artifact + git diff --cached --binary > regen-patch-artifact/regen.patch + echo "${{ github.event.number || inputs.pr_number }}" > regen-patch-artifact/pr-number + exit 1 + # Both non-experimental matrix entries regenerate the same files, so the + # patch content is identical across JDKs — last writer wins, same as the + # ci-comment artifact below. + - name: Upload regen patch artifact + if: > + failure() && !matrix.experimental && + hashFiles('regen-patch-artifact/regen.patch') != '' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: regen-patch + path: regen-patch-artifact/ + overwrite: true - name: mvn test uses: ./.github/actions/incremental-build with: diff --git a/.github/workflows/pr-test-commenter.yml b/.github/workflows/pr-test-commenter.yml index 60dc485d08fdb..90eb416c39cc8 100644 --- a/.github/workflows/pr-test-commenter.yml +++ b/.github/workflows/pr-test-commenter.yml @@ -119,3 +119,125 @@ jobs: } catch (error) { core.warning(`Failed to post CI test summary comment: ${error.message}`); } + - name: Download regen patch artifact + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: ${{ github.event.workflow_run.id }}, + }); + const match = artifacts.data.artifacts.find(a => a.name === 'regen-patch'); + if (!match) { + core.info('No regen-patch artifact found, skipping'); + return; + } + const download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: match.id, + archive_format: 'zip', + }); + const fs = require('fs'); + fs.writeFileSync('${{ github.workspace }}/regen-patch.zip', Buffer.from(download.data)); + - name: Extract regen patch artifact + run: | + if [ -f regen-patch.zip ]; then + unzip -o regen-patch.zip -d regen-patch-artifact + fi + - name: Post or update regen patch comment + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + + const regenPatchFile = 'regen-patch-artifact/regen.patch'; + const regenPrFile = 'regen-patch-artifact/pr-number'; + const ciPrFile = 'ci-comment-artifact/pr-number'; + + const regenNeeded = fs.existsSync(regenPatchFile); + + // The regen artifact carries its own PR number (the ci-comment + // artifact is not produced when the build fails at the regen + // check); on clean runs fall back to the ci-comment artifact so an + // earlier warning comment can be marked resolved. + let prNumber = 0; + if (fs.existsSync(regenPrFile)) { + prNumber = parseInt(fs.readFileSync(regenPrFile, 'utf8').trim(), 10); + } else if (fs.existsSync(ciPrFile)) { + prNumber = parseInt(fs.readFileSync(ciPrFile, 'utf8').trim(), 10); + } + if (!prNumber) { + core.info('No PR number available, skipping regen patch comment'); + return; + } + + const marker = ''; + const runId = ${{ github.event.workflow_run.id }}; + const runUrl = `${{ github.server_url }}/${{ github.repository }}/actions/runs/${runId}`; + const repoSlug = `${context.repo.owner}/${context.repo.repo}`; + + let body; + if (regenNeeded) { + body = [ + marker, + '### :warning: Generated files are out of date', + '', + `The [CI build](${runUrl}) regenerated files that are not committed on this PR.`, + 'The missing changes are available as the `regen-patch` artifact of that run.', + '', + 'Anyone with push access to this branch (the PR author, or a committer when', + '*Allow edits from maintainers* is enabled) can apply and push them:', + '', + '```bash', + `gh pr checkout ${prNumber} --repo ${repoSlug}`, + `gh run download ${runId} --repo ${repoSlug} -n regen-patch`, + 'git apply --index regen.patch && rm regen.patch pr-number', + 'git commit -m "Regen" && git push', + '```', + '', + 'Alternatively, run `./etc/scripts/regen.sh` locally and commit the resulting changes.', + 'If the patch does not apply cleanly, update the branch from the base branch first —', + 'the patch was generated against the merge with the base branch.', + ].join('\n'); + } else { + body = [ + marker, + '### :white_check_mark: Generated files are up to date', + '', + `An earlier CI run reported uncommitted generated changes; the [latest run](${runUrl}) no longer does.`, + ].join('\n'); + } + + try { + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + }); + const existing = comments.find(c => c.body && c.body.includes(marker)); + + if (existing) { + if (!regenNeeded && existing.body.includes(':white_check_mark:')) { + return; // already marked resolved + } + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body: body, + }); + } else if (regenNeeded) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: body, + }); + } + } catch (error) { + core.warning(`Failed to post regen patch comment: ${error.message}`); + }