Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

109 Commits
 
 
 
 
 
 
 
 

Repository files navigation

OpenWrt PR Gatekeeper

A modern GitHub App webhook engine running on Cloudflare Workers that automates Pull Request validations, styling constraints, OpenWrt build recipe (Makefile) checks, downstream patches testing, and timeline triage.

Features Matrix

The validation engine splits checks into three distinct asynchronous status streams grouped natively under the GitHub App interface:

1. Formalities Check

Focuses on Git history hygiene, developer metadata constraints, and layout standards:

  • Branch Target Enforcement: Ensures pull requests originate from dedicated feature branches, blocking accidental direct PRs from main, master, or active stable branches.

Tip

Maintainers can bypass this check by posting a pull request comment containing [allow branch] (or [allow-branch]). Alternatively, if the PR author is a maintainer, they can also include [allow branch] in the PR description. See Who counts as a maintainer.

  • Stable Branch Backports Enforcement: Ensures that pull requests targeting active stable/backport branches (e.g., openwrt-25.12, openwrt-24.10) contain the context line (cherry picked from commit ...) in every commit message.

Tip

Maintainers can bypass this check by posting a pull request comment containing [allow cherry-pick] (or [allow-cherry-pick]). Alternatively, if the PR author is a maintainer, they can also include [allow cherry-pick] in the PR description. See Who counts as a maintainer.

  • Merge Commit Elimination: Rejects merge commits inside the PR tracking chain to preserve a clean linear history.
  • Identity Integrity: Validates author and committer name formats and strictly blocks generic GitHub noreply.github.com email addresses.
  • Linked GitHub Account: Verifies that the commit author email address is registered and verified on a GitHub account, linking the commit to a valid GitHub username. Can be downgraded to a non-blocking warning or disabled entirely, see require_linked_github_account below.
  • Autosquash Compliance: Automatically bypasses style constraints for development-phase fixup! and squash! syntax blocks.
  • Revert Compliance (allow_revert): Accepts the subject line git revert generates — Revert "<original subject>", its nested Revert "Revert "<original subject>"" form, and the prefixed <package>: Revert "..." variant. The quoted text is copied verbatim from the reverted commit, so the prefix and lowercase rules are not applied to it, and the Revert "..." wrapper is excluded from the subject length limits. A revert also puts PKG_VERSION and PKG_RELEASE back to the values that preceded the reverted commit, so those restored values are accepted instead of a bump (see PKG_RELEASE Validation below). These relaxed rules require the body to identify what is being reverted — either the This reverts commit <sha>. line git revert generates or the Reverts <owner>/<repo>#<number> reference GitHub adds — so that a subject merely shaped like a revert does not lift them.
  • Subject String Hygiene: Enforces <package name or prefix>: prefix headers, checks lowercase starting strings post-prefix, rejects trailing periods, and requires a blank line between the subject and the description body.
  • Length Constraints: Implements dual-layered (soft and hard) line width boundaries for both subject lines and description body text blocks.
  • Signed-off-by Check: Ensures a consistent, properly structured Signed-off-by: declaration is present and matches the original author metadata.
  • Signature Verification: Validates cryptographic GPG/SSH commit signatures if present.
  • Description Quality Warnings: Inspects message bodies and issues non-blocking warnings for lazy/identical description text mirroring the subject or for completely missing reference links (changelogs/release notes).
  • Mandatory Description Body: Rejects commits whose description body is empty or contains only trailers (e.g. Signed-off-by:). Every commit must include a meaningful explanation of what the change does and why.
  • OpenWrt Spelling Verification (check_openwrt_spelling): Inspects both the subject line and description body to ensure the correct casing of "OpenWrt" is used, issuing warnings for incorrect capitalizations like "OpenWRT" or "Openwrt" (ignoring URLs, code blocks, and trailers).

Note

To keep API/subrequest usage predictable on extreme PRs, commit-message auditing is intentionally capped to the first 300 commits. If a PR exceeds this size, the check output includes an explicit warning about the reduced commit audit scope.

2. Makefile Check

Inspects file modification trees targeting OpenWrt build recipes:

  • PKG_VERSION Sync: Validates that if a version bump is introduced inside a Makefile, the matching version string exists within the commit subject line context.
  • Mandatory Metadata (check_openwrt_meta): Enforces the inclusion of PKG_MAINTAINER, PKG_LICENSE, and PKG_LICENSE_FILES variables whenever a new package is introduced (fully configurable list).
  • Conffiles Tracker: Mandates the definition of the Package/.../conffiles tracking macro whenever configuration file installations (INSTALL_CONF) are triggered.
  • Line Ending Sanitization: Inspects modifications for Windows-style Carriage Returns (CRLF) to guarantee exclusive UNIX (LF) formatting compliance.
  • Trailing Newline Check: Verifies that newly created or modified files end with a trailing newline character, catching the common \ No newline at end of file issue in diffs (customizable level: warning/error/disabled).
  • PKG_RELEASE Validation: Enforces correct release values on package changes: new packages must initialize PKG_RELEASE to 1, version updates must reset PKG_RELEASE to 1, and modifications to package files must be accompanied by a version/release change (customizable level: warning/error/disabled). Packages modified exclusively by revert commits are exempt from the reset/initialize rules, because a revert restores the version and release of an already released state. They must still carry a release bump if the revert changes package content without touching the version or release, otherwise users would never receive it.
  • UCI Config Validation: Ensures that any configuration files destined to be installed into /etc/config/ conform to the standard OpenWrt UCI format (consisting of only package, config, option, list statements, comments, and empty lines).
  • PKG_NAME Reuse Prevention: Ensures PKG_NAME is not reused inside call, define, and eval Makefile lines, requiring the literal package name instead to keep recipes readable and searchable (default true).

3. Patches Check

Scans the contribution tree for nested downstream patch targets:

  • Git-Am Compliance: Automatically isolates modified .patch assets and checks for accurate From: and Subject: header identifiers to ensure smooth downstream git am deployment runs (customizable level: warning/error/disabled).

Automated Triage & Stale PR Management

  • not following guidelines: A high-visibility tag automatically attached to the PR if any critical validation check drops a failure blueprint. Clears itself upon a successful push.
  • add package / drop package: Dynamically analyzes unified diff targets to label tracking trees introducing or purging software packages.
  • Stable Branch Tracking: Auto-generates matching grey release tags (e.g., release/24.10, release/25.12) whenever a PR targets an active release backport branch.
  • Issue Labeller: Replaces the GitHub Actions issue-labeller.yml workflow. When a bug-report issue is opened with the trigger label, the bot validates form fields and applies labels based on a declarative .github/issue-labeller.yml configuration file (same spirit as labeler.yml for PRs — label name → list of conditions). Supports template variables ({major}, {segment0}, etc.), format validation (regex), existence checks (tag/path via GraphQL), substring matching, and presence checks. Falls back to sensible defaults if no config file exists. Disabled by default — enable per-repository with "enable_issue_labeller": true.
  • Stale PR Cleanup: A daily scheduled cron task (05:30 UTC) scans all repositories where the App is installed. If explicitly enabled in a repository's configuration (`"enable_stale_bot": true`), it marks PRs containing the `not following guidelines` label as `stale` (with a warning comment) after 14 days of inactivity, and closes them after another 14 days of silence. Only contributor activity resets the countdown: pushed commits, force-pushes, reopens, and comments or reviews from people. Comments from GitHub Apps, *[bot] accounts and the machine accounts listed in stale_ignored_users are ignored, so an automated review can never keep a dead PR alive forever. Pushing new commits also removes the stale label immediately via the webhook, without waiting for the nightly scan.

Tip

Stale PR cleanup is completely disabled by default. If a repository wants to enable this automated cleanup flow, it must commit a .github/formalities.json file in its default branch containing "enable_stale_bot": true.

  • Clean Timelines: Drops descriptive, cleanly formatted markdown dashboards into the PR conversation section on failure, automatically editing or removing itself once instructions are followed to keep the timeline clean.
  • Header Footnote: PR comments include a dynamic footnote linking directly to this repository's issues page for reporting validation bugs.

Who counts as a maintainer

Override commands ([allow branch], [allow cherry-pick]) and comment-triggered re-checks are only honored for maintainers. A user qualifies if either of the following holds:

  • their author_association on the pull request or comment is OWNER, MEMBER or COLLABORATOR, or
  • they hold write, maintain or admin access on the repository.

The second rule exists because GitHub reports author_association: CONTRIBUTOR (or NONE) for maintainers who keep their organization membership private, which would otherwise lock them out of their own overrides. Resolving it costs one extra API lookup per login — and a lookup can retry on transient failures, so it may spend more than one outgoing subrequest — so it is only asked when the association alone is not enough and the answer can actually change the outcome — a comment carrying an override command, or a PR description that requests one. Answers are cached for the duration of the webhook, bot accounts are never resolved, and a failed lookup is treated as "not a maintainer".

Note

Read-only and triage collaborators cannot use the overrides. Organization members are trusted through their association even without write access, matching the behavior the bot has always had.


Setup & Deployment

The engine is built as a headless JavaScript service hosted on Cloudflare Workers. It operates with zero local npm/Node dependencies inside the repository, making it highly secure and maintenance-free.

1. GitHub App Configuration

The GitHub App requires the following permissions and event subscriptions:

  • Repository permissions:
    • Checks: Read & write (to publish validation audit check runs)
    • Commit statuses: Read & write (to update commit statuses)
    • Pull requests: Read & write (to add review comments and manage triage labels)
    • Contents: Read-only (to fetch repository-specific configurations like .github/formalities.json)
    • Metadata: Read-only (mandatory for every App)
  • Event Subscriptions:
    • Subscribe to Pull request events (triggers on opened, synchronized, and reopened).
    • Subscribe to Issue comment events (triggers on created — required for the override commands and comment-triggered re-checks).
    • Subscribe to Issues events (triggers on opened — required for the issue labeller feature).

Note

The maintainer fallback described in Who counts as a maintainer calls GET /repos/{owner}/{repo}/collaborators/{username}/permission. Whether an installation is allowed to call it is decided by GitHub, so verify it against a real installation rather than assuming the permissions listed above are enough. If the call is refused (403 Resource not accessible by integration), the lookup fails closed: maintainers with a private organization membership are treated as contributors, and the refusal is visible in the Worker logs.

2. Cloudflare Worker Configuration

  1. Deploy the Worker to your Cloudflare account (managed automatically via the CI/CD pipeline).
  2. In the Cloudflare Dashboard under Workers & Pages -> Settings -> Variables, configure the following variables as Secrets (encrypted):
    • APP_ID: Your GitHub App ID (e.g., 123456).
    • WEBHOOK_SECRET: The secret token used to verify GitHub webhook HMAC-SHA256 signatures.
    • PRIVATE_KEY: The complete text of your GitHub App private key PEM file.

Important

The private key must be in PKCS#8 format (starting with -----BEGIN PRIVATE KEY-----). If your downloaded key starts with -----BEGIN RSA PRIVATE KEY----- (PKCS#1), convert it using: openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in rsa_key.pem -out pkcs8_key.pem

3. CI/CD Pipeline (GitHub Actions)

The repository includes a GitHub Actions workflow in .github/workflows/deploy.yml that builds and deploys the Worker automatically on every push to main:

  • Runs on a lightweight ubuntu-slim container.
  • Automatically injects the build commit hash (DEPLOY_HASH) and deploy timestamp in Prague timezone (DEPLOY_DATE) into the wrangler variables before deployment.
  • To enable deployment, add your CLOUDFLARE_API_TOKEN (with edit permissions for Workers) as a secret in your GitHub repository's Settings -> Secrets and variables -> Actions.

4. Repository Level Customization

If individual source repositories wish to tweak defaults or scale back rule restrictions, creators can commit a custom .github/formalities.json file inside their repository branch root.

Some configuration keys offer advanced options:

  • check_openwrt_meta: Can be true (enforces standard PKG_MAINTAINER, PKG_LICENSE, and PKG_LICENSE_FILES for new packages), false (disabled), or an array of custom required fields (e.g., ["PKG_MAINTAINER", "PKG_LICENSE"]).
  • check_patch_headers: Can be true (default, hard error), "warning" (non-blocking), or false to disable.
  • check_trailing_newline: Can be true (default, hard error), "warning" (non-blocking), or false to disable.
  • check_pkg_release: Can be "warning", "error", or false to disable.
  • require_linked_github_account: Can be true (default, hard error), "warning" (non-blocking), or false/"disabled" to disable.
  • check_uci_config: Set to true (default) to validate UCI configurations. Set to false or "disabled" to disable.
  • check_space_after_assignment: Set to true (default) to detect and reject spaces/indentation immediately after the := assignment operator in Makefiles, or false to disable.
  • check_missing_colon: Set to true (default) to detect and reject the use of = instead of := for standard variables (e.g. PKG_NAME, TITLE, URL, etc.) in Makefiles, or false to disable.
  • check_makefile_indentation: Set to true (default) to validate package metadata/description blocks are indented with 2 spaces and install/build blocks are indented with tabs in Makefiles, or false to disable.
  • check_pkg_name_reuse: Set to true (default) to detect and reject reuse of the PKG_NAME variable in call, define, and eval lines, or false to disable.
  • show_force_push_tip: Set to true (default) to append a helpful tip regarding how to correct validation errors using force-pushing. Set to false to disable.
  • check_openwrt_spelling: Set to true (default) to validate the correct capitalization of "OpenWrt" in commit subjects and descriptions. Set to false to disable.
  • allow_revert: Set to true (default) to accept the subject format produced by git revert (Revert "<original subject>", nested reverts, and the prefixed <package>: Revert "..." variant) and the PKG_VERSION/PKG_RELEASE values a revert restores, for commits whose body references the reverted commit (This reverts commit <sha>. or Reverts <owner>/<repo>#<number>). Set to false to hold revert commits to the regular subject and release bump rules.
  • enable_stale_bot: Set to true to enable the stale PR bot cleanup for this repository. Defaults to false (opt-in).
  • stale_ignored_users: List of account logins whose comments and reviews never reset the stale countdown (default: ["openwrt-ai"]). GitHub Apps and *[bot] accounts are always ignored automatically; this list exists for automation that runs on a plain user account.
  • enable_labeler_yml: Set to true to enable dynamic pull request labeling based on matching files in the .github/labeler.yml configuration file. Defaults to false (opt-in).
  • enable_issue_labeller: Set to true to enable automated issue form validation and labelling (replaces the GitHub Actions issue-labeller.yml workflow). Defaults to false (opt-in).

Here is a comprehensive example containing all available toggle options:

{
  "check_branch": true,
  "check_merge_commits": true,
  "check_noreply_email": true,
  "check_signoff": true,
  "check_signature": true,
  "allow_autosquash": true,
  "allow_revert": true,
  "enable_comments": true,
  "show_force_push_tip": true,
  "max_subject_len_soft": 60,
  "max_subject_len_hard": 80,
  "max_body_line_len": 100,
  "warn_duplicate_body": true,
  "warn_generic_subjects": true,
  "require_release_notes": true,
  "require_body": true,
  "check_pkg_version": true,
  "check_crlf": true,
  "check_trailing_newline": true,
  "add_package_label": true,
  "drop_package_label": true,
  "branch_labeling": true,
  "check_openwrt_meta": true,
  "check_conffiles": true,
  "check_uci_config": true,
  "check_space_after_assignment": true,
  "check_missing_colon": true,
  "check_makefile_indentation": true,
  "check_pkg_name_reuse": true,
  "check_patch_headers": true,
  "check_pkg_release": "warning",
  "require_linked_github_account": true,
  "check_openwrt_spelling": true,
  "enable_stale_bot": false,
  "stale_ignored_users": ["openwrt-ai"],
  "enable_labeler_yml": false,
  "enable_issue_labeller": false
}

5. Issue Labeller Configuration (.github/issue-labeller.yml)

When enable_issue_labeller is true, the bot fetches .github/issue-labeller.yml from the repository's default branch. The format mirrors labeler.yml — each top-level key is a label name (supporting {template} variables), followed by a list of conditions that must all match for the label to apply.

Template variables (extracted from the matched field value):

  • {value} — full trimmed value
  • {segment0}, {segment1}, … — slash-separated parts (ramips/mt7621ramips, mt7621)
  • {major}, {minor}, {patch} — dot-separated parts (24.10.024, 10, 0)
  • {hash} — trailing hex string after last - (r28945-24a9f1c22424a9f1c224)

Condition types:

  • field — form field name to check (case-insensitive, normalized to snake_case)

  • format — regex the field value must match (invalid format → invalid label + comment)

  • exists — existence check via GraphQL: "tag:v{value}", "path:target/linux/{segment0}", or "commit:{hash}"

  • contains — case-insensitive substring match

  • not_empty — field must be non-empty

  • hint — what the reporter should do about a failed check, shown in the invalid-form comment in place of the built-in hint. Write it as an instruction, not a description:

    hint: Run `. /etc/openwrt_release && echo $DISTRIB_RELEASE` on the device and paste that value alone.

Meta keys (underscore-prefixed, not labels):

  • _trigger_label — label required on the issue to start processing (default: to-triage)
  • _invalid_label — label added when validation fails (default: invalid)
  • _remove_labels — labels always removed after processing (default: ["to-triage"])
  • _invalid_comment — comment template for invalid fields (default: Invalid {field} reported. `{value}`)
  • _invalid_comment_header / _invalid_comment_footer — text above and below the list of invalid fields
  • _valid_comment — posted when an edit fixes a previously invalid form
  • _require_form — also process issues that never received _trigger_label, and flag ones whose body has no ### sections at all as having bypassed the issue template. OWNER, MEMBER and COLLABORATOR are exempt, so maintainers' free-form tracking issues are left alone (default: false)
  • _no_form_comment — posted to those issues

Note

Every default message the bot posts lives in one place — ISSUE_LABELLER_MESSAGES in cloudflare-worker/src/config.js — with the config key that replaces it named beside each entry. The shipped text is English and its hints name OpenWrt's own commands, which suits openwrt/openwrt and nothing else in particular; a feed or a fork rewords or translates all of it from issue-labeller.yml, without touching code.

Re-checking on edit. The labeller runs on opened, edited and reopened. The trigger label is consumed on the first run, so a re-run re-enters on the _invalid_label instead: an issue stays supervised until its form validates. Consequences:

  • The bot keeps one comment per issue, tagged with an invisible <!-- issue-labeller --> marker and edited in place. Three edits produce one comment, not four.
  • When the reporter fixes the form, _invalid_label is removed and the comment is replaced by _valid_comment, so nobody is left staring at a stale complaint.
  • Labels that validated are kept when another field fails — a report with a good target and a mistyped release still gets its target/* label.

Per-label metadata (inside a label block, underscore-prefixed):

  • _color — hex color used when the label is auto-created (default: ededed)
  • _description — description used when the label is auto-created

Example:

# .github/issue-labeller.yml
_trigger_label: "to-triage"
_invalid_label: "invalid"
_remove_labels: ["to-triage"]

"release/{major}.{minor}":
  _color: "6b7280"
  _description: "OpenWrt release version reported"
  - field: "release"
    format: '^\d+\.\d+\.\d+(-rc\d+)*$|^\d+\.\d+-SNAPSHOT$'
    exists: "tag:v{value}"

"target/{segment0}":
  _color: "0e7490"
  _description: "Hardware target reported"
  - field: "target"
    format: '^[a-zA-Z0-9]+/[a-zA-Z0-9]+$'
    exists: "path:target/linux/{segment0}/{segment1}"
    hint: "Run `. /etc/openwrt_release && echo $DISTRIB_TARGET` on the device and paste that line — it is `target/subtarget`, e.g. `ramips/mt7621`, not the image file name"

"Official Image":
  - field: "image_kind"
    contains: "official"

"Self Built Image":
  - field: "image_kind"
    contains: "self"

"Supported Device":
  - field: "device"
    not_empty: true

If the file is absent, the bot falls back to the default rules shown above.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages