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.
The validation engine splits checks into three distinct asynchronous status streams grouped natively under the GitHub App interface:
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.
-
New Package on a Release Branch (
check_new_package_on_stable): On a pull request whose base is a release branch (openwrt-XX.YY,lede-17.01,for-XX.YY), resolves every package Makefile it adds to its package root and asks the default branch, in one GraphQL lookup, whether that path has ever existed in its history. The roots that never did are reported: release branches carry security and bug-fix backports only, and a new package lands on the default branch first. A package the default branch has ever carried, including one it later moved or dropped, is never reported, and neither is a Makefile below a package root. The lookup covers the first 12 added roots and says so when there are more. The default-branch pull request may still be open beside this one, which the lookup cannot see, so this is a warning by default — set"check_new_package_on_stable": "error"to enforce it. -
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.comemail 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_accountbelow. -
Autosquash Compliance: Automatically bypasses style constraints for development-phase
fixup!andsquash!syntax blocks. -
Revert Compliance (
allow_revert): Accepts the subject linegit revertgenerates —Revert "<original subject>", its nestedRevert "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 theRevert "..."wrapper is excluded from the subject length limits. A revert also putsPKG_VERSIONandPKG_RELEASEback 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 theThis reverts commit <sha>.linegit revertgenerates or theReverts <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 (including versioned packages likelua5.4:, repository files likeCONTRIBUTING.md:,README.md:,rules.mk:,feeds.conf.default:, dotfiles like.gitignore:,.github/workflows:, or source tree paths such astools/cmake:,toolchain/musl:,package/network/services/hostapd:), checks lowercase starting strings post-prefix, rejects trailing periods, and requires a blank line between the subject and the description body. -
Subject Scope Check (
check_subject_scope): Compares the subject prefix with the files the commit changes, and warns when it names none of them —hostapd: fix WPSon a commit that only touchespackage/network/services/dropbear/. Any part of a chained or path prefix may match any path segment of any changed file, or a package name the touched Makefiles define (PKG_NAME,define Package/<name>, kernel modules askmod-<name>), solibiwinfo:oniwinfoandvideo-support:onhardware-supportpass. So do the subsystem words of the submission guidelines (kernel,build,toolchain,tools,target,scripts,config,ci,docs,i18n) where they cover a changed file,treewide, the repository's own name, and anything insubject_scope_prefixes. Commits made only of quilt patches, only of shared build files, or (inopenwrt/openwrt) only of files undertarget/are not graded: their prefix legitimately names an upstream subsystem, a device or a subtarget. Reverts are graded on the quoted subject; verbatim backports andfixup!/squash!commits are skipped, and a Makefile that could not be read leaves the prefix unchecked. It catches typos and copy-paste slips, not every mislabel. Against 18,000 merged commits it flagged 0.54% of those it graded, about half of them deliberate prefixes naming the package a change is for, so this is a warning by default and should stay one. -
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.
-
Imperative Mood Warning (
warn_imperative_mood): Issues a non-blocking warning when the subject starts with a past-tense or gerund verb after the prefix — the guidelines ask for "add support for X", not "added support for X" (default true). -
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.
Important
A green check never means "not checked". When a pull request is too large for the available API budget — file lookups skipped, upstream comparisons skipped, or the commit scan capped — a check that found no problems reports GitHub's neutral conclusion titled Partially checked instead of a pass. neutral does not block the pull request; it only stops the bot from claiming it inspected something it never fetched. A check that found real problems still fails.
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 ofPKG_MAINTAINER,PKG_LICENSE, andPKG_LICENSE_FILESvariables whenever a new package is introduced (fully configurable list). New LuCI packages (built onluci.mk) are only asked forPKG_LICENSE— their maintainer field is generated byluci.mkand they carry SPDX headers instead of license files. A new package that includesu-boot.mkortrusted-firmware-a.mkis asked for neitherPKG_LICENSEnorPKG_LICENSE_FILES, which those helpers set. -
SPDX License Check (
check_spdx_license): Validates every identifier in aPKG_LICENSEvalue against the official SPDX license list (all 701 current identifiers, 85 exceptions and 32 deprecated ids are embedded incloudflare-worker/src/spdx-licenses.js, generated from spdx/license-list-data). Non-blocking warnings cover deprecated ids (GPL-2.0→GPL-2.0-only/GPL-2.0-or-later), wrong capitalization (BSD-2-clause), informal spellings (GPLv2,GPL-3.0-later,MIT/X11), hyphenated exceptions (GPL-2.0-or-later-with-Autoconf-exception-2.0→... WITH Autoconf-exception-2.0), bare license families (BSD,Apache,Public-Domain) and unknown placeholders (VARIOUS,Custom,COPYING). A replacement is only ever suggested after it has been confirmed to be a real SPDX identifier; where SPDX retired an id without a successor the warning names none.LicenseRef-<name>— the SPDX form for a license that is not on the list — is accepted as-is (default true).[!NOTE]
spdx-licenses.jsis generated — never edit it by hand. Runnode scripts/update-spdx-data.mjsto pull a newer SPDX release; the file records the release it came from inSPDX_LICENSE_LIST_VERSION, and the test suite verifies that every replacement it names is itself a valid identifier. -
Init Script Check (
check_init_scripts): Issues non-blocking warnings when a newly added file that looks like an init script (*.initor underetc/init.d/) does not start with#!/bin/sh /etc/rc.commonor defines noSTART=priority.rc.common'senable()only creates the/etc/rc.d/S<START><name>boot symlink whenSTARTis set, so such a script is not started at boot through that path — the file is identified by its path alone, so the wording asks the author to confirm rather than asserting the service is broken (default true). -
Conffiles Tracker: Mandates the definition of the
Package/.../conffilestracking macro whenever configuration is installed — anINSTALL_CONFline or a destination under$(1)/etc/config. Other payload under/etc(capability files,profile.dsnippets, …) is not configuration and does not trigger the demand. Each entry must be one unindented absolute path per line, or a make expansion that produces one; a file must not end with a slash, while a directory may be written with or without it. -
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 fileissue in diffs (customizable level: warning/error/disabled). -
PKG_RELEASE Validation: Enforces correct release values on package changes: new packages must initialize
PKG_RELEASEto1, version updates must resetPKG_RELEASEto1, 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. Packages built on a shared helper that never usesPKG_RELEASE(u-boot.mk,trusted-firmware-a.mk,luci.mk— LuCI versions come from git history) are exempt from all release requirements, and so is any package with noPKG_RELEASEand no version anyone could move:package/kernel/linuxis the kernel itself andpackage/kernel/bpf-headersderives itsPKG_VERSIONfrom the kernel's, so asking either for a bump is advice nobody can follow. A package that still defines a literalPKG_VERSION,PKG_SOURCE_VERSIONorPKG_SOURCE_DATEhas something to bump and is asked for it as before. -
PKG_HASH Validation (
check_pkg_hash): Points out source checksums that did not move together with the version: aPKG_VERSION/PKG_SOURCE_VERSION/PKG_SOURCE_DATEchange that keeps the oldPKG_HASH, and new packages that download a plain archive without defining one. Whether the sources really changed is an inference the bot cannot prove, so these are warnings by default — set"check_pkg_hash": "error"to enforce them. Packages usingPKG_SOURCE_PROTO(git, svn, …) are left alone, because their generated tarball is not always reproducible and the docs name cases wherePKG_MIRROR_HASHis deliberately omitted. Checksum values themselves are graded the way the build system treats them: a value that is neither a SHA256 norskipmakesscripts/download.plabort and is always an error, while an MD5 (whichcheck_hashonly calls a deprecated hash) andskip(explicitly honored, but turns verification off) are warnings. Like the release audit, the bump comparison trusts the pull request's own diff — a version difference caused only by a stalebase.shais never reported. -
Source Pin Validation (
check_pkg_source_pin): Checks the shape of the two variables a repository checkout is pinned with, on the lines the pull request itself writes.PKG_SOURCE_DATEhas to be a real date inYYYY-MM-DDform, andPKG_SOURCE_VERSIONhas to name a revision — not a branch head such asmaster, which moves under every build, and not an abbreviated hash in a git checkout. A new package that pins a revision with neither a date nor aPKG_VERSIONis told its version would read0~<revision>(seeinclude/download.mk). Computed values (v$(PKG_VERSION)), tags and SVN revision numbers are left alone, and the date is never required to move with the revision: upstream can commit twice in one day. The rule comes from the package policies, not from anything the build refuses, so these are warnings by default — set"check_pkg_source_pin": "error"to enforce them.PKG_RELEASEand the checksums stay with the two audits above. -
DTS Checklist (
check_dts_checklist): Checks device trees a pull request adds (.dts/.dtsi/.dtso) for the three items of OpenWrt's DTS checklist a diff settles on its own: a missingSPDX-License-Identifierfile header, a redundantdefault-state = "off"on an LED inside acompatible = "gpio-leds"node (offis the default already — see commit 0d23fd2), and the deprecatedlabel = "<colour>:<function>"LED naming that openwrt/openwrt's own review rules replace withcolor/function. An added file carries its whole text in the diff, so no file is read unless a later commit changed it again. A device tree the pull request only modifies is not checked, and the audit names it rather than passing it. Other LED classes (register-bit-ledand friends),&ledsoverrides whosegpio-ledsnode lives in an included.dtsi, reverts and verbatim backport commits are left alone. The wiki's device support policies still document thelabel = "color:function"scheme; where the two disagree this check follows the rules file in the repository. The wiki asks rather than requires, so these are warnings by default — set"check_dts_checklist": "error"to enforce them. -
UCI Config Validation: Ensures that any configuration files destined to be installed into
/etc/config/conform to the standard OpenWrt UCI format (consisting of onlypackage,config,option,liststatements, comments, and empty lines). A Makefile install line naming the exact file is authoritative about its destination, so a file sharing a base name with the package's config (uhttpd.aclnext touhttpd.config) is never mistaken for UCI just because of the shared stem. -
PKG_NAME Reuse Prevention: Ensures
PKG_NAMEis not reused insidecall,define, andevalMakefile lines, requiring the literal package name instead to keep recipes readable and searchable (default true). -
Dead Package Variable Detection: Rejects per-package variables (
PROVIDES,TITLE,DEPENDS,MAINTAINER, ...) assigned at the top level of a package Makefile, where the build system resets them before the package definition is read — so the assignment silently does nothing. Suggests the working replacement: thedefine Package/<name>block, thePKG_*twin (e.g.PKG_MAINTAINER), or the LuCI equivalent (PKG_PROVIDES,LUCI_TITLE, ...) for Makefiles built onluci.mk. Always on — a dead assignment is a bug in any repository, so there is nothing to configure. -
Feed Buildbot-Default Guard (
check_buildbot_default): FlagsDEFAULTassignments (=,:=,::=,+=,?=, including backslash-continued ones) that condition inclusion onBUILDBOTinside feed repositories — both brand-new assignments and aBUILDBOTcondition newly added to an existing one, since this forces a package into buildbot default images without going through the mainopenwrt/openwrtrepo's own default-package process (customizable level: warning/error/disabled; skipped entirely for the main repo).
Scans the contribution tree for nested downstream patch targets:
- Git-Am Compliance: Checks that each changed
.patchfile carries theFrom <hash> Mon Sep 17 00:00:00 2001envelope line,From:andSubject:, asgit format-patchwrites them. The envelope is required because patches are also concatenated into one mbox stream forgit am, where it is the only thing separating them;Date:is optional,git amfills it in. The report names only the headers that are missing. A patch the pull request adds, or an edit that removes a header, fails the check (customizable level: warning/error/disabled); editing an older patch that already lacks them only warns. - Refreshed Patches (
check_patch_refresh): Reports a patch underpatches*/,backport-*/,pending-*/orhack-*/whose diff was not produced bymake package/<pkg>/refresh: adiff --gitorindexline fromgit format-patch, anIndex:line,---/+++timestamps, or paths without thea/andb/prefixes. Only what the pull request itself introduces counts: a marker already in the file, or carried over with it by a rename, is left alone. The two rules read different parts of the file: the headers above the diff come fromgit format-patch, the diff below them from the refresh target, which keeps those headers, so a patch made withgit format-patchand then refreshed meets both. The rule is openwrt/packages' CONTRIBUTING.md, and openwrt/openwrt has mergedgit format-patchoutput, so these are warnings by default, reported next to the header warnings, and filtered against the upstream commit of a backport the same way.
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. Both are removed again once the pull request no longer adds or drops a package.- 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, and removes a release tag that no longer matches after the pull request is retargeted. Labels matched fromlabeler.ymlare only ever added. - Issue Labeller: Replaces the GitHub Actions
issue-labeller.ymlworkflow. 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.ymlconfiguration file (same spirit aslabeler.ymlfor 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 instale_ignored_usersare ignored, so an automated review can never keep a dead PR alive forever. Pushing new commits also removes thestalelabel immediately via the webhook, without waiting for the nightly scan. The scan asks GitHub for each repository's configuration and its labelled pull requests in two GraphQL queries, timelines included, so its request count no longer grows with the number of stale pull requests. A pull request whose timeline is longer than the fetched window (the most recent 100 entries) is left untouched rather than judged on incomplete history.
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.
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_associationon the pull request or comment isOWNER,MEMBERorCOLLABORATOR, 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. Where the answer cannot change the outcome it is not asked at all: a PR description without an override command, and a comment in the thread that carries none, are decided by the association alone. A comment that arrives as its own event is the exception — the commenter is resolved before the body is read, because only a maintainer's comment re-runs the checks.
Answers are cached for the duration of the webhook and bot accounts are never resolved. An override is honored only for a maintainer the bot could confirm, so a lookup GitHub refused outright (403) is "not a maintainer" — but a lookup that failed (a 5xx, a network error) is neither: the override is not honored and the check it would have waived reports itself unchecked rather than violated, because a pull request must not fail on an answer nobody got.
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.
The engine is built as a headless JavaScript service hosted on Cloudflare Workers. The runtime has no third-party production dependencies — the Worker ships only this repository's own source — which keeps the deployed attack surface small. Tests run on Node's built-in test runner, so development needs no dependencies either.
The GitHub App requires the following permissions and event subscriptions:
- Repository permissions:
- Checks:
Read & write(to publish validation audit check runs) - Pull requests:
Read & write(to post the audit comment and manage triage labels) - Issues:
Read & write(to label and comment on issues, and required by GitHub for theissuesandissue_commentevent subscriptions below) - Contents:
Read-only(to fetch repository-specific configurations like.github/formalities.json) - Metadata:
Read-only(mandatory for every App)
- Checks:
- Event Subscriptions:
- Subscribe to Pull request events (triggers on opened, synchronized, reopened, and edited when the title, body or base branch changed).
- 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, reopened, and edited when the body changed — 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.
- Deploy the Worker to your Cloudflare account (managed automatically via the CI/CD pipeline).
- 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
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-slimcontainer. - Deploys only after the shared test and dry-run build workflow passes, and runs one deployment at a time.
- 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.
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 betrue(enforces standardPKG_MAINTAINER,PKG_LICENSE, andPKG_LICENSE_FILESfor new packages),false(disabled), or an array of custom required fields (e.g.,["PKG_MAINTAINER", "PKG_LICENSE"]).check_patch_headers: Can betrue(default, hard error),"warning"(non-blocking), orfalseto disable. Even attrue, only a patch the pull request adds, or an edit that removes a header, fails; an edit to an older patch that already lacks the headers is reported as a warning.check_patch_refresh: Can be"warning"(default, non-blocking),true/"error"(hard error), orfalse/"disabled"to disable. Graded on its own, whatevercheck_patch_headersis set to.check_trailing_newline: Can betrue(default, hard error),"warning"(non-blocking), orfalseto disable.check_binary_files: Can betrue(default, hard error),"warning"(non-blocking), orfalse/"disabled"to disable. Rejects a file git carries as binary, in any encoding, since such a file reaches none of the content checks and cannot be reviewed in a diff. Deleting one is always allowed.check_pkg_release: Can be"warning","error", orfalseto disable.check_pkg_hash:"warning"(default) reports checksum findings without blocking,"error"enforces them,falsedisables the check. While it is enabled, a checksum value the build cannot use is an error at either level.check_new_package_on_stable: Can be"warning"(default, non-blocking),"error"/true(hard error), orfalse/"disabled"to disable. Only applies to pull requests whose base branch is a release branch; a backport of a package the default branch already carries is never reported.check_subject_scope: Can be"warning"(default, non-blocking),"error"/true(hard error, not recommended — a prefix naming the package a change is for rather than the file it edits is legitimate and looks exactly like a mislabel), orfalse/"disabled"to disable.subject_scope_prefixes: Prefixes that name no directory and are always accepted (default:["lint", "css", "housekeeping", "meta", "wiki", "npm", "vscode", "editor", "devcontainer", "i18n", "docs", "treewide", "tree-wide"]). Add a repository's own habitual prefixes here rather than switching the check off.check_pkg_source_pin:"warning"(default) reportsPKG_SOURCE_DATE/PKG_SOURCE_VERSIONshape findings without blocking,"error"enforces them,falsedisables the check.check_dts_checklist:"warning"(default) reports DTS checklist findings without blocking,"error"enforces them,falsedisables the check. Only device trees the pull request adds are checked.require_linked_github_account: Can betrue(default, hard error),"warning"(non-blocking), orfalse/"disabled"to disable.check_uci_config: Set totrue(default) to validate UCI configurations. Set tofalseor"disabled"to disable.check_spdx_license: Set totrue(default) to validatePKG_LICENSEidentifiers against the official SPDX license list, orfalseto disable.check_init_scripts: Set totrue(default) to warn when a new init script lacks the#!/bin/sh /etc/rc.commoninterpreter line or aSTART=priority, orfalseto disable.check_space_after_assignment: Set totrue(default) to detect and reject spaces/indentation immediately after the:=assignment operator in Makefiles, orfalseto disable.check_missing_colon: Set totrue(default) to detect and reject the use of=instead of:=for standard variables (e.g.PKG_NAME,TITLE,URL, etc.) in Makefiles, orfalseto disable.check_makefile_indentation: Set totrue(default) to validate package metadata/description blocks are indented with 2 spaces and install/build blocks are indented with tabs in Makefiles, orfalseto disable.check_pkg_name_reuse: Set totrue(default) to detect and reject reuse of thePKG_NAMEvariable incall,define, andevallines, orfalseto disable.check_buildbot_default: Can be"warning"(default, non-blocking),"error"/true(hard error), or"disabled"/falseto disable. Never applies to the mainopenwrt/openwrtrepository, where default package selection legitimately lives.show_force_push_tip: Set totrue(default) to append a helpful tip regarding how to correct validation errors using force-pushing. Set tofalseto disable.check_openwrt_spelling: Set totrue(default) to validate the correct capitalization of "OpenWrt" in commit subjects and descriptions. Set tofalseto disable.allow_revert: Set totrue(default) to accept the subject format produced bygit revert(Revert "<original subject>", nested reverts, and the prefixed<package>: Revert "..."variant) and thePKG_VERSION/PKG_RELEASEvalues a revert restores, for commits whose body references the reverted commit (This reverts commit <sha>.orReverts <owner>/<repo>#<number>). Set tofalseto hold revert commits to the regular subject and release bump rules.enable_stale_bot: Set totrueto enable the stale PR bot cleanup for this repository. Defaults tofalse(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 totrueto enable dynamic pull request labeling based on matching files in the.github/labeler.ymlconfiguration file. Defaults tofalse(opt-in).enable_issue_labeller: Set totrueto enable automated issue form validation and labelling (replaces the GitHub Actionsissue-labeller.ymlworkflow). Defaults tofalse(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,
"warn_imperative_mood": true,
"require_release_notes": true,
"require_body": true,
"check_pkg_version": true,
"check_binary_files": true,
"check_crlf": true,
"check_trailing_newline": true,
"add_package_label": true,
"drop_package_label": true,
"branch_labeling": true,
"check_openwrt_meta": true,
"check_spdx_license": true,
"check_init_scripts": true,
"check_conffiles": true,
"check_uci_config": true,
"check_dts_checklist": "warning",
"check_space_after_assignment": true,
"check_missing_colon": true,
"check_makefile_indentation": true,
"check_pkg_name_reuse": true,
"check_buildbot_default": "warning",
"check_patch_headers": true,
"check_patch_refresh": "warning",
"check_pkg_release": "warning",
"check_pkg_hash": "warning",
"check_pkg_source_pin": "warning",
"check_new_package_on_stable": "warning",
"require_linked_github_account": true,
"check_openwrt_spelling": true,
"check_subject_scope": "warning",
"subject_scope_prefixes": ["lint", "css", "housekeeping", "meta", "wiki", "npm", "vscode", "editor", "devcontainer", "i18n", "docs", "treewide", "tree-wide"],
"enable_stale_bot": false,
"stale_ignored_users": ["openwrt-ai"],
"enable_labeler_yml": false,
"enable_issue_labeller": false
}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/mt7621→ramips,mt7621){major},{minor},{patch}— dot-separated parts (24.10.0→24,10,0){hash}— trailing hex string after last-(r28945-24a9f1c224→24a9f1c224)
Condition types:
-
field— form field name to check (case-insensitive, normalized to snake_case) -
format— regex the field value must match (invalid format →invalidlabel + comment) -
exists— existence check via GraphQL:"tag:v{value}"or"path:target/linux/{segment0}"(anything that is nottag:is read as a path) -
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_header/_invalid_comment_footer— text above and below the list of invalid fields. The per-field line between them is fixed, and names the field, its value and the hint_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,MEMBERandCOLLABORATORare 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_labelis 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: trueIf the file is absent, the bot falls back to the default rules shown above.