Skip to content
Merged
Show file tree
Hide file tree
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
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ This GitHub Action Helps with the following operations:
| fail-label | `needs:feedback` | The label to be added to PR if the pull request doesn't pass the validation. Pass `false` to skip adding the label. |
| pass-label | `needs:code-review` | The label to be added to PR if the pull request pass the validation. Pass `false` to skip adding the label. |
| conflict-label | `needs:refresh` | The label to be added to PR if the pull request has conflicts. Pass `false` to skip adding the label. |
| comment-template | `{author} thanks for the PR! Could you please fill out the PR template with description, changelog, and credits information so that we can properly review and merge this?` | Comment template for adding comment on PR if it doesn't pass the validation. Pass `false` to skip adding the comment. |
| comment-template | `{author} thanks for the PR! Could you please fill out the PR template so that we can properly review and merge this?` | Comment template for adding comment on PR if it doesn't pass the validation. Pass `false` to skip adding the comment. |
| conflict-comment | `{author} thanks for the PR! Could you please rebase your PR on top of the latest changes in the base branch?` | Comment template for adding comment on PR if it has conflicts. Pass `false` to skip adding the comment. |
| issue-welcome-message | false | Comment template for adding a welcome message on an issue for first-time issue creators |
| pr-welcome-message | false | Comment template for adding a welcome message on a PR for first-time PR creators |
Expand All @@ -48,9 +48,34 @@ This GitHub Action Helps with the following operations:
| validate-description | true | Whether to validate the pull request description. Pass `false` to disable description validation |
| validate-changelog | true | Whether to validate the pull request changelog entry. Pass `false` to disable changelog validation |
| validate-credits | true | Whether to validate the props given in pull request. Pass `false` to disable credits validation |
| validate-pr-template-sections | - | Multiline list of PR template section headings (without `#` markers) that must have non-empty content. Heading level is ignored, so `What?`, `## Why?`, and `### Use of AI Tools` all work the same way. See [PR Template Section Validation](#pr-template-section-validation) for details. |
| wait-ms | `15000` | Time to wait in milliseconds between retries to check PR mergeable status |
| max-retries | `5` | Maximum number of retries to check PR mergeable status |

## PR Template Section Validation

The `validate-pr-template-sections` input lets you validate that specific sections of your repository's PR template have been filled out. This is an alternative to the built-in `validate-description`, `validate-changelog`, and `validate-credits` checks, and is useful when your PR template uses different headings or sections than those defaults.

Provide a multiline list of section heading names (without the `#` markers). The heading level is ignored — `What?`, `## Why?`, and `### Use of AI Tools` all resolve to the same heading text. Content under a section is considered empty if it contains only whitespace or blockquote lines (lines starting with `>`), which are commonly used as placeholder examples in PR templates.

For example, to require that contributors fill out the `What?`, `Why?`, and `Use of AI Tools` sections of a custom PR template:

```yml
- uses: 10up/action-repo-automator@trunk
with:
validate-description: false
validate-changelog: false
validate-credits: false
validate-pr-template-sections: |
What?
Why?
Use of AI Tools
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

Each section that fails validation will produce a separate error and contribute to the `fail-label` being applied to the PR.

## Example Workflow File

To get started, you will want to copy the contents of the given example into `.github/workflows/repo-automator.yml` and push that to your repository. You are welcome to name the file something else.
Expand Down
6 changes: 5 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ inputs:
comment-template:
description: "Comment template for adding comment on PR if it doesn't pass the validation"
required: false
default: "{author} thanks for the PR! Could you please fill out the PR template with description, changelog, and credits information so that we can properly review and merge this?"
default: "{author} thanks for the PR! Could you please fill out the PR template so that we can properly review and merge this?"
issue-welcome-message:
description: "Comment template for adding a welcome message on an issue for first-time issue creators"
required: false
Expand All @@ -58,6 +58,10 @@ inputs:
description: "Whether to enable automatic synchronization of the pull request branch with the base branch"
required: false
default: false
validate-pr-template-sections:
description: "Multiline list of PR template section headings (without # markers) that must have non-empty content. Heading level is ignored, so 'What?', '## Why?', and '### Use of AI Tools' all work. Defaults to empty (disabled)."
required: false
default: ""
validate-description:
description: "Whether to validate the pull request description"
required: false
Expand Down
74 changes: 72 additions & 2 deletions dist/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 22 additions & 1 deletion src/pr-validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const {
getCredits,
getDescription,
getInputs,
getSectionContent,
} = require("./utils.js");

export default class PRValidation {
Expand Down Expand Up @@ -34,9 +35,15 @@ export default class PRValidation {
validateChangelog,
validateCredits,
validateDescription,
validatePRTemplateSections,
} = getInputs();

if (!validateChangelog && !validateCredits && !validateDescription) {
if (
!validateChangelog &&
!validateCredits &&
!validateDescription &&
!validatePRTemplateSections.length
) {
core.info("PR validation is disabled");
return;
}
Expand Down Expand Up @@ -73,6 +80,20 @@ export default class PRValidation {
}
}

if (validatePRTemplateSections.length) {
core.info("Running PR template section validation");
for (const heading of validatePRTemplateSections) {
const content = getSectionContent(pullRequest, heading);
core.debug(`Section "${heading}": ${content}`);
Comment thread
iamdharmesh marked this conversation as resolved.
if (!content.length) {
failed = true;
errors.push(
`Please fill out the **${heading}** section of the PR template`
);
}
}
}

if (failed) {
// Remove Pass Label if already there.
await this.gh.removeLabel(issueNumber, labels, passLabel);
Expand Down
50 changes: 49 additions & 1 deletion src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,13 @@ export function getInputs(pullRequest = {}) {
core.getInput("comment-template") === "false"
? false
: core.getInput("comment-template") ||
"{author} thanks for the PR! Could you please fill out the PR template with description, changelog, and credits information so that we can properly review and merge this?";
"{author} thanks for the PR! Could you please fill out the PR template so that we can properly review and merge this?";

// PR template section validation
const rawTemplateSections = core.getMultilineInput("validate-pr-template-sections") || [];
const validatePRTemplateSections = rawTemplateSections
.map((s) => s.replace(/^#+\s*/, "").trim())
.filter(Boolean);

// Welcome message inputs
const issueWelcomeMessage =
Expand Down Expand Up @@ -133,9 +139,13 @@ export function getInputs(pullRequest = {}) {
`PR Welcome Message: ${prWelcomeMessage} (${typeof prWelcomeMessage})`
);
core.debug(`Ignore Users: ${ignoreUsers} (${typeof ignoreUsers})`);
core.debug(
`Validate PR Template Sections: ${validatePRTemplateSections} (${typeof validatePRTemplateSections})`
);

return {
assignIssues,
validatePRTemplateSections,
addMilestone,
assignPullRequest,
validateChangelog,
Expand Down Expand Up @@ -222,6 +232,44 @@ export function getChangelog(payload) {
return entries.filter((entry) => entry.length > 0);
}

/**
* Get the content under a specific section heading in the PR body.
* Matches any heading level (e.g. #, ##, ###) followed by the given heading text.
*
* @param {object} payload Pull request payload
* @param {string} headingName Section heading text, without leading # markers
* @returns string
*/
export function getSectionContent(payload, headingName) {
const cleanBody = removeHtmlComments(payload?.body || "");
const lines = cleanBody.split(/\r?\n/);
let inSection = false;
const content = [];

for (const line of lines) {
const isHeading = /^#{1,6}\s+/.test(line);
if (isHeading) {
// Stop collecting once we hit the next heading after our target.
if (inSection) {
break;
}
// Start collecting if this heading matches the target (any level).
if (line.replace(/^#{1,6}\s+/, "").trim() === headingName) {
inSection = true;
}
continue;
}
Comment thread
iamdharmesh marked this conversation as resolved.
if (inSection) {
content.push(line);
}
}

return content
.filter((line) => !/^>\s/.test(line))
Comment thread
iamdharmesh marked this conversation as resolved.
.join("\n")
.trim();
}

/**
* Compare two version strings.
*
Expand Down
Loading