Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions .github/workflows/clean-pr-body.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
Comment on lines +1 to +2
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The copyright header format differs from other workflow files in this repository. Other workflow files use the standard Apache 2.0 header with # comment style, not the SPDX-FileCopyrightText format. For consistency, this should follow the same format as files like ci-checks.yml, copyright-check.yml, and conventional-commit.yml.

Suggested change
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

Copilot uses AI. Check for mistakes.

name: Clean PR Body

on:
pull_request:
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow triggers on pull_request with type opened and requires pull-requests: write permission. For PRs from forks, the workflow will run with the fork's GITHUB_TOKEN, which typically does not have write permissions to the base repository. This means the workflow will fail when trying to update the PR body for fork-based PRs. Consider using pull_request_target instead, which runs in the context of the base repository and has write permissions. However, be cautious with pull_request_target as it poses security risks if user-controlled code is executed. In this case, since the workflow only reads the PR body and updates it via the GitHub API (no code checkout or execution), pull_request_target should be safe.

Suggested change
pull_request:
pull_request_target:

Copilot uses AI. Check for mistakes.
types: [opened]

permissions:
pull-requests: write

jobs:
clean-pr-body:
runs-on: ubuntu-latest
steps:
- name: Strip template boilerplate from PR body
uses: actions/github-script@v7
with:
script: |
const { owner, repo } = context.repo;
const pr_number = context.payload.pull_request.number;
const body = context.payload.pull_request.body || '';

// 1. Strip HTML comments that start at the beginning of a line,
// preserving inline references like `<!-- -->` in code spans.
let stripped = body.replace(/^[ \t]*<!--[\s\S]*?-->[ \t]*\n?/gm, '');
Comment on lines +26 to +27
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The HTML comment stripping regex /^[ \t]*<!--[\s\S]*?-->[ \t]*\n?/gm has an issue: it will strip comments that appear at the start of a line within code blocks. For example, in a Markdown code block showing HTML/XML examples, a line starting with <!-- would be removed even though it's legitimate content, not template boilerplate. The regex should avoid matching inside code fences (delimited by triple backticks). Consider processing the body to skip code blocks, or use a more sophisticated approach that preserves code block contents.

Suggested change
// preserving inline references like `<!-- -->` in code spans.
let stripped = body.replace(/^[ \t]*<!--[\s\S]*?-->[ \t]*\n?/gm, '');
// preserving inline references like `<!-- -->` in code spans,
// and avoiding matches inside fenced code blocks (``` ... ```).
const commentRe = /^[ \t]*<!--[\s\S]*?-->[ \t]*\n?/gm;
const segments = body.split(/(```[\s\S]*?```)/);
let stripped = segments
.map((segment, idx) => {
// Captured fenced code blocks appear at odd indices and start with ```
if (idx % 2 === 1 && segment.startsWith('```')) {
return segment;
}
return segment.replace(commentRe, '');
})
.join('');

Copilot uses AI. Check for mistakes.

// 2. Extract only the Summary section (# Summary or ## Summary),
// dropping checklists and other template sections.
let summary;
const summaryWithNext = stripped.match(/^#{1,2}\s+Summary\s*\n([\s\S]*?)(?=\n#{1,2}\s)/m);
if (summaryWithNext) {
summary = summaryWithNext[1];
} else {
// Summary is the last/only heading -- take everything after it.
const summaryToEnd = stripped.match(/^#{1,2}\s+Summary\s*\n([\s\S]*)/m);
summary = summaryToEnd ? summaryToEnd[1] : stripped;
}

// If no Summary heading at all, take everything before checklist headings.
if (!summaryWithNext && !stripped.match(/^#{1,2}\s+Summary/m)) {
const beforeChecklist = stripped.match(/^([\s\S]*?)(?=\n#{1,2}\s+(?:Pre-Review|Pre-Merge|Other Notes))/m);
summary = beforeChecklist ? beforeChecklist[1] : stripped;
}

// 3. Collect unique git trailers from the full body.
const trailerRe = /^(Signed-off-by|Co-authored-by):\s+.+$/gm;
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The trailer regex on line 48 only matches Signed-off-by and Co-authored-by trailers, but Git and GitHub support additional trailers like Reviewed-by, Tested-by, Acked-by, and others. Consider whether these should also be preserved. If they should be excluded intentionally, the current implementation is correct, but if they should be preserved, the regex should be expanded to match all common Git trailers.

Copilot uses AI. Check for mistakes.
const seen = new Set();
const trailers = [];
for (const [line] of stripped.matchAll(trailerRe)) {
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code destructures only the first element from the match array on line 51: for (const [line] of stripped.matchAll(trailerRe)). However, matchAll returns match objects where the full match is at index 0, and capture groups follow. Since the regex /^(Signed-off-by|Co-authored-by):\s+.+$/gm has one capture group, match[0] is the full match (the entire trailer line), and match[1] is the captured trailer type. Using [line] gets the full match, which is correct. However, it would be clearer to use [fullLine] or match[0] to make the intent more obvious.

Copilot uses AI. Check for mistakes.
if (!seen.has(line)) {
seen.add(line);
trailers.push(line);
}
}

// 4. Remove trailers and GitHub separator lines from summary text.
summary = summary
.split('\n')
.filter(l => !l.match(/^(Signed-off-by|Co-authored-by):\s+/) && l.trim() !== '---------')
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The separator line filter on line 61 only checks for lines that are exactly --------- after trimming. However, GitHub and Markdown may use different numbers of dashes for horizontal rules (typically 3 or more dashes: ---, ----, etc.). This overly specific check may not catch all separator variations. Consider using a regex like /^-{3,}$/ to match any line with 3 or more dashes, or clarify why only 9 dashes specifically should be removed.

Suggested change
.filter(l => !l.match(/^(Signed-off-by|Co-authored-by):\s+/) && l.trim() !== '---------')
.filter(l => !l.match(/^(Signed-off-by|Co-authored-by):\s+/) && !l.trim().match(/^-{3,}$/))

Copilot uses AI. Check for mistakes.
.join('\n')
.replace(/\n{3,}/g, '\n\n')
.trim();

let cleaned = summary;
if (trailers.length) cleaned += '\n\n' + trailers.join('\n');

if (cleaned !== body.trim()) {
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comparison cleaned !== body.trim() may cause unnecessary API calls when the body has no trailing whitespace but the workflow still reformats it. The issue is that cleaned is built from summary.trim() which removes trailing whitespace, but this is compared against body.trim(). If the original body has leading/trailing whitespace differences or internal whitespace variations (e.g., multiple newlines collapsed to double), this comparison will always be false even when semantically equivalent. Consider comparing both after normalizing whitespace, or compare against the original body without trimming.

Suggested change
if (cleaned !== body.trim()) {
if (cleaned.replace(/\s+$/, '') !== body.replace(/\s+$/, '')) {

Copilot uses AI. Check for mistakes.
await github.rest.pulls.update({
owner,
repo,
pull_number: pr_number,
body: cleaned,
});
core.info(`Cleaned PR #${pr_number} body to Summary + trailers`);
Comment on lines +70 to +76
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API call to update the PR body (line 70) has no error handling. If the API call fails (e.g., due to rate limiting, network issues, or permission problems), the workflow will fail silently or with an unclear error. Consider wrapping this in a try-catch block and providing informative error messages, or at least logging when the update succeeds vs. fails.

Suggested change
await github.rest.pulls.update({
owner,
repo,
pull_number: pr_number,
body: cleaned,
});
core.info(`Cleaned PR #${pr_number} body to Summary + trailers`);
try {
await github.rest.pulls.update({
owner,
repo,
pull_number: pr_number,
body: cleaned,
});
core.info(`Cleaned PR #${pr_number} body to Summary + trailers`);
} catch (error) {
const message = error && error.message ? error.message : String(error);
core.error(`Failed to clean PR #${pr_number} body: ${message}`);
core.setFailed(`Failed to update PR body for #${pr_number}`);
}

Copilot uses AI. Check for mistakes.
} else {
core.info(`PR #${pr_number} body already clean`);
}