Skip to content

Enforce the pull request template with a required status check - #182

Merged
MarkusPaulsen merged 2 commits into
mainfrom
ci/enforce-pull-request-template
Aug 8, 2026
Merged

Enforce the pull request template with a required status check#182
MarkusPaulsen merged 2 commits into
mainfrom
ci/enforce-pull-request-template

Conversation

@MarkusPaulsen

@MarkusPaulsen MarkusPaulsen commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a pr-template check that validates the pull request body against
.github/PULL_REQUEST_TEMPLATE.md, and documents the rule in AGENTS.md. The checker is
a single-file Java program. Intended to become a required status check once merged.

Linked issues

None.

1. Problem

The pull request template is a prefill, not a rule. GitHub inserts
.github/PULL_REQUEST_TEMPLATE.md into the description box in the web UI and then never
looks at it again: no ruleset, branch protection or CODEOWNERS setting references it, and
nothing validates what is actually submitted.

Creating a pull request from the command line skips even the prefill. gh pr create --body and --body-file set the description verbatim, so a contributor or automated
agent working through the CLI never sees the template at all. The observed failure is a
pull request opened with headings from an entirely different project's conventions, which
a reviewer then cannot use: no linked issues, no testing manual, no statement of whether
the change is a false negative or a false positive, no breaking-change declaration.

The root cause sits in the CI configuration rather than in Ares itself: the repository
documented a contribution shape it had no mechanism to require. It is neither a false
negative nor a false positive in the security sense, because no student code is involved;
the cost is borne entirely by review quality.

2. Improvement from the user's perspective

No Improvement.

3. Improvement from the maintainer's perspective

A reviewer can rely on the body being complete before spending time on the diff. The
sections that most often go missing are exactly the ones a reviewer of a security tool
needs: the testing manual with its negative case, the modes exercised, and the
breaking-change declaration for an artefact consumed by exercise repositories.

The required headings are read out of the template at check time rather than duplicated
in the checker, so future edits to the template cannot silently leave the check behind.

AGENTS.md gains the rule and, more usefully, the reason: agents that read AGENTS.md
now learn that CLI pull request creation bypasses the template, which is the specific
mechanism behind the malformed bodies.

The checker is Java, run through the single-file source-code launcher, so the repository
stays monolingual. An earlier revision of this branch used Python; it was the only Python
file here, nothing linted or formatted it, and it needed its own .gitattributes rule to
survive the repository's eol=crlf default with a working shebang. .java needs no such
exception, because the file is passed to java rather than executed directly.

4. Testing manual

Prerequisites

  1. A checkout of this branch, a JDK 11 or later (the workflow uses Temurin 21) and
    actionlint. No Maven build or exercise is required.

Steps

  1. Not reproducible from an exercise. The change adds a GitHub Actions workflow and a
    helper script, neither of which is reachable from an Ares exercise.
  2. Confirm the workflow parses, with the actionlint version pinned by checksum in
    .github/workflows/actionlint.yml: actionlint -color
  3. Run the checker against every open pull request body, to confirm it does not reject
    bodies that already follow the template:
    gh pr list --state open --json number,body --limit 50 > bodies.json
    python3 - <<'PY'
    import json, os, subprocess
    for p in json.load(open('bodies.json')):
        env = dict(os.environ, PR_BODY=p['body'] or '')
        r = subprocess.run(['java', '.github/scripts/CheckPullRequestTemplate.java'],
                           env=env, capture_output=True, text=True)
        print(p['number'], 'PASS' if r.returncode == 0 else 'FAIL')
    PY
    
  4. Run the checker against a body that does not follow the template, for example an empty
    one: PR_BODY='' java .github/scripts/CheckPullRequestTemplate.java
  5. Inspect the pr-template job on this pull request. The workflow runs here because a
    pull_request run uses the merge ref, which already contains the new file.

Expected result

  • Step 2 exits 0 and prints no diagnostics.
  • Step 3 prints PASS for all ten open pull requests. This was the acceptance criterion:
    the job becomes a required check, so a single false positive would block a merge.
  • Step 4 exits 1 and prints
    ::error::The pull request body is empty. Start from .github/PULL_REQUEST_TEMPLATE.md and fill in every section.
  • Step 5 shows pr-template green, with a job summary reading
    All 10 required sections are present and filled in.

Negative case (what must still be rejected)

Three bodies must fail, and were confirmed to fail:

  1. An empty body.
  2. A body using another project's headings, for example ### Motivation / ### Description. Rejected with one Missing section heading error per required section.
  3. .github/PULL_REQUEST_TEMPLATE.md pasted in unchanged. Rejected because every section
    is empty once the HTML comments are stripped. This case matters: pasting the template
    without filling it in must not satisfy the check.
  4. A body that is complete except for the coverage table, left as the template's blank
    row. This is the case the earlier Python revision wrongly accepted, see below.

The check must remain permissive about substance. It deliberately does not require
checklist boxes to be ticked, since a check that demands ticks only trains contributors to
tick them without reading.

Modes exercised

No mode-specific behaviour changed.

  • ArchUnit + AspectJ
  • ArchUnit + instrumentation
  • WALA + AspectJ
  • WALA + instrumentation

5. Test case coverage regarding this PR

No production Java code changed.

Breaking changes and migration

None for consumers of the released artefact: the public API, the policy file format, the
generated security test code and the minimum JDK, Maven and Gradle versions are all
untouched.

For contributors, once the pr-template check is made a required status check, a pull
request whose body does not follow the template will not be mergeable. All ten currently
open pull requests already pass, so nothing in flight is affected.

One defect worth calling out for reviewers, since it is the reason the second commit is
not a pure translation. The Python revision's empty-table-row rule was dead code. Its
guard against the Markdown separator row, ^\s*\|[\s:-]*\|, also matches a row whose
cells are blank, so the rule could never fire, and a body pasting the unfilled coverage
table passed. The guard was redundant from the start, because a separator row's cells are
not whitespace, so the Java version drops it rather than repairing it. The Java version
also normalises line endings on input rather than relying on whitespace trimming to
absorb the carriage returns that both the CRLF checkout and the GitHub API deliver.

.gitattributes is unchanged from main in the final state of this branch.

Checklist

  • CI is green, or every remaining failure is explained above.
  • No secrets, tokens or absolute local paths are contained in the diff.

Review progress

  • Code review
  • Manual test

The template was a prefill only. GitHub inserts it in the web UI and never
validates the result, and `gh pr create --body`/`--body-file` bypasses it
outright, so pull requests could be opened in an arbitrary shape.

Add a `pr-template` job that checks the body against
.github/PULL_REQUEST_TEMPLATE.md. The required headings are read out of the
template itself rather than duplicated in the checker, so editing the template
cannot leave the check behind.

The check validates shape, not substance: every section present, none empty, no
unfilled stub left behind. It does not require checklist boxes to be ticked,
which would only train contributors to tick them. The escape hatches the
template documents ("No Improvement", "No production Java code changed",
"Not reproducible from an exercise") are honoured.

The body is untrusted input on fork pull requests, so it is passed through the
environment and read with os.environ rather than interpolated into the shell,
which would be a script injection sink. The job carries no `paths` filter: a
required check that never runs would leave a pull request blocked instead of
passing.

Also document the rule in AGENTS.md, including the reason command-line pull
request creation silently skips the template.

Verified against all ten open pull request bodies (no false positives) and
against an empty body, a foreign template, and the template left unfilled
(all correctly rejected).
@MarkusPaulsen
MarkusPaulsen requested a review from a team August 8, 2026 21:32
@MarkusPaulsen
MarkusPaulsen requested review from a team and krusche as code owners August 8, 2026 21:32
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@MarkusPaulsen, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 22 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9ef04d9a-a43f-4104-ad3c-e91d5135afb8

📥 Commits

Reviewing files that changed from the base of the PR and between 758087a and b6270c2.

📒 Files selected for processing (3)
  • .github/scripts/CheckPullRequestTemplate.java
  • .github/workflows/pullrequest-template.yml
  • AGENTS.md

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added docs Automated area label: docs other Automated area label: other labels Aug 8, 2026
The checker was the repository's only Python file. Nothing linted, formatted
or tested it, and it needed its own `.gitattributes` rule, because the repo
default of `* text=auto eol=crlf` would otherwise have checked it out with CRLF
line endings on the Linux runners and broken its shebang.

Rewrite it as a single-file Java program run through the source-code launcher
(`java .github/scripts/CheckPullRequestTemplate.java`, Java 11 and later). No
build step, no artefact, and no language added to a Java repository. The
`.gitattributes` rule is reverted, since `.java` needs no exception: the file is
passed to `java` rather than executed directly, so CRLF is harmless, exactly as
it already is for the 671 sources under `src/`.

The conversion exposed a defect in the Python version. Its empty-table-row rule
was dead code: the guard meant to skip the Markdown separator row,
`^\s*\|[\s:-]*\|`, also matches a row whose cells are blank, so the rule could
never fire. A body pasting the unfilled coverage table passed. The guard was
redundant to begin with, since a separator row's cells are not whitespace, so it
is dropped rather than repaired.

Line endings are now normalised on input instead of being absorbed by
whitespace trimming. The repository checks Markdown out as CRLF and GitHub
delivers bodies with CRLF, so this removes a class of latent pattern bugs.

Verified against all ten open pull request bodies (no false positives) and four
rejection cases: an empty body, foreign headings, the template left unfilled,
and the unfilled coverage table that the Python version wrongly accepted.
@MarkusPaulsen
MarkusPaulsen merged commit e435fad into main Aug 8, 2026
20 of 22 checks passed
@MarkusPaulsen
MarkusPaulsen deleted the ci/enforce-pull-request-template branch August 8, 2026 22:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Automated area label: docs other Automated area label: other

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant