Skip to main content
A condition is a yes-or-no question about a pull request or issue, such as “does the title start with fix?” or “does it change files under docs/?”. Rules use conditions to decide when they apply: a labelling rule adds its label while its conditions pass, and a convention fails while they do not. A condition group is a list of conditions and the number of them that must pass. It appears wherever a rule has a when: labelling rules, convention rules, requested and automatic approvals, and stale.exempt.when.
requires defaults to the number of conditions, so without it every condition must pass. With requires: 1 any one is enough. Field names are the same as v1 (type, condition, requires, label, min, max), so v1 conditions work unchanged.

Patterns

Conditions that match text take a pattern: a regular expression written either bare (^feat) or delimited with flags (/^feat/i). An invalid pattern is rejected when the config is loaded, not on the first event.

Patterns smartcloud refuses: catastrophic backtracking

What it is. A regular expression engine tries the ways a pattern could match, one after another, and goes back to try another way when one fails. That going back is called backtracking. For most patterns there are only a few ways to try. For some, a repeated part can match the same text in more than one way: in ^(a+)+$, the text aaaa can be one run of four as, two runs of two, four runs of one, and so on. When the text almost matches but not quite, such as thirty as followed by !, the engine tries every one of those ways before giving up, and the number of ways doubles with every extra character. Thirty characters can take minutes. This is called catastrophic backtracking. Why smartcloud refuses these patterns. Your patterns run on text anyone can write: pull request titles and descriptions, branch names and comments. Someone who spots a pattern like that in your config could open a pull request whose title keeps the run busy until it times out. So smartcloud checks every pattern when it reads the config and refuses one that can backtrack like this, whichever condition or feature it belongs to. What you will see.
  • smartcloud validate, the editor schema check and the MCP server’s validate_config report it as an error, naming the pattern: invalid pattern "^fix: (\w+\s?)+$": the repeated part around character 8 can match the same text in more than one way, so some inputs make it run for minutes (catastrophic backtracking); rewrite it so each character can be matched only one way. The character number counts from 1 and points into the repeated part.
  • A run treats it like any other invalid pattern: it leaves out the condition group that holds the pattern, and with it the rule that needs that group, and names both in the warnings of the smartcloud / config check. Everything else still runs. Where a setting only tightens policy (see Unknown keys and invalid values), the run fails instead, as it does for any invalid value there.
How to rewrite a refused pattern. Make sure each character of the text can be matched by only one part of the pattern. Three rewrites cover almost every case: The rule of thumb: after a repeated part, put something the repeated part cannot match, such as a separator (-, /, a space) that is not in its character class. What passes. Ordinary patterns are fine: prefixes (^feat), alternatives (^(feat|fix)(\(.+\))?: ), repeated parts ended by a separator (^[a-z]+(-[a-z]+)*$), and version numbers (^v\d+\.\d+\.\d+$). A repeat at the very end of an unanchored pattern, such as ^fix: (\w+\s?)+ without the final $, also passes: nothing after it can fail, so the engine never goes back into it. Limits of the check. It reads the pattern and never runs it. It treats anchors (^, $, \b), lookarounds and backreferences as matching nothing, so it can refuse a pattern that one of those would have made safe; rewrite it as above. It does not refuse patterns that are only slow on very long text without being exponential, such as .*a.*b, and a pattern too large to check (thousands of parts) is refused with “too large to check”: split it into smaller patterns. filesMatch is the exception: it takes a glob such as docs/** or **/*.md.

Conditions

Activity is GitHub’s updated_at time for the issue or pull request. Reviews count each reviewer’s latest decisive review: comments and pending reviews do not change an earlier approval or change request. isApproved waits for every requested reviewer by default, as in v1. To pass once enough reviewers have approved, even while others are still requested, set allowPending. A change request still blocks.
baseBranchMatches matches the branch a pull request targets, so a rule can treat pull requests into a release branch differently from those into main. Anchor the pattern to match the whole name: ^main$ does not match maintenance.
authorAssociation reads GitHub’s author association, so a rule can treat newcomers, outside collaborators or bots differently from maintainers. It passes when any listed value applies:
It applies to issues and pull requests, and works in a stale sweep’s exempt.when. When GitHub reports an association smartcloud does not know, only bot can match. hasAssignee and assigneeMatches read who is assigned, so a rule can label unowned issues for triage or route work by owner. assigneeMatches passes when any assignee’s login matches, and never when nobody is assigned. Both apply to issues and pull requests, and work in a stale sweep’s exempt.when: to keep claimed issues open, exempt anything assigned.
reviewerMatches passes when any reviewer of a pull request matches: the users and teams asked to review, and everyone who has reviewed. A requested team matches by its slug, such as security, without the organisation. GitHub stops listing a requested reviewer once they review, and they then count as someone who has reviewed, so the condition holds before and after their review.
hasMilestone and milestoneMatches read the milestone the subject is in, matched by its title, so a rule can flag planned work that has no milestone yet, or label everything in a release. Both apply to issues and pull requests, and work in a stale sweep’s exempt.when: to keep work planned for a release open, exempt its milestone.
linksIssue passes when a pull request is tied to an issue, so a convention can ask every pull request to link its issue. A link is any of:
  • A closing reference in the description: GitHub’s closing keywords (close, closes, closed, fix, fixes, fixed, resolve, resolves, resolved, optionally followed by a colon) and then #12, owner/repo#12, an issue URL or a Linear key, such as Closes SMC-55. A bare mention such as See #12, or a keyword inside code, does not count. The reference counts whatever branch the pull request targets: it records the author’s link, although GitHub closes the issue only on a merge into the default branch.
  • A Linear key in the head branch, in any case, since Linear names branches in lower case: claude/smc-55-links-issue.
  • A Linear key in the title, such as feat(conditions): linksIssue (SMC-55).
A Linear key is a team key, a dash and a number. List your teams in keys to count only their keys, matched in any case. Without keys, any key counts, in upper case only in the title, so text shaped like a key, such as utf-8 in a branch, also passes. The description is read from the event, so the condition costs no API call; issues linked only from the pull request’s sidebar are not seen. In a stale sweep’s exempt.when, the listing has no branch, so only the description and title are read.

Pull request only conditions

branchMatches, baseBranchMatches, reviewerMatches, isDraft, filesMatch, changesSize, lockfileChanged, fileCount, binaryFilesAdded, pendingReview, requestedChanges, isApproved, commitMessagesMatch, commitsSignedOff, commitsVerified, hasTrailer, hasConflict, checksPass, checkStatus, linksIssue, codeownersTouched and dependencyUpdateType only apply to pull requests. On an issue they fail, with the explanation “only applies to pull requests”.

Commit conditions

commitMessagesMatch, commitsSignedOff and hasTrailer ignore merge commits. scope is all (the default: every commit must match) or any (at least one must). Trailers are read the way git reads them: only from the final paragraph of the message, with keys compared ignoring case. A message with a single paragraph has no trailers, so a subject such as feat: add x is never read as a trailer.

Signed commits

commitsVerified passes when every commit on the pull request, merge commits included, carries a signature that GitHub verified: GPG, SSH or S/MIME, from a key registered to the signer. It reads the same verification GitHub shows as the “Verified” badge, so a signature from an unknown key, an expired key or an email the signer has not verified on GitHub does not count. GitHub lists at most 250 commits of a pull request, so on a longer one the condition fails either way: the commits past the first 250 cannot be checked. This is separate from commitsSignedOff. A Signed-off-by trailer is the DCO: text in the message that anyone can write. A verified signature proves who made the commit. Check both when you need both.
It reads the pull request’s commits, the same API call as commitMessagesMatch, commitsSignedOff and hasTrailer, so using them together still costs one call.

Merge conflicts

hasConflict reads GitHub’s mergeability for the pull request. GitHub works it out in the background, so a freshly updated pull request can briefly be unknown: smartcloud reads it again a few times, two seconds apart, and if it is still unknown, counts the pull request as not conflicting.
Like every labelling rule, it is evaluated when the pull request gets one of the events your workflow runs on, such as a push to its branch, an edit, reopening it or a review. A push to the base branch can create a conflict without any pull request event, and push and schedule runs do not evaluate labelling rules yet, so the label follows on the pull request’s next event.

CI checks

checksPass and checkStatus read the CI checks on the pull request’s head commit: check runs, such as GitHub Actions jobs, and commit statuses, which older CI services post, under their context name. A check is named exactly as GitHub shows it in the pull request’s checks list, for example test (ubuntu-latest); names are not patterns and compare with case.
  • A check is success when it passed, or finished as neutral or skipped. It is failure when it finished any other way: failed, cancelled, timed out or needing action. Until it finishes it is pending.
  • A named check that has not reported yet is pending, so checksPass does not pass until every named check has run.
  • When a name is reported more than once, for example by a workflow that runs on both push and pull_request, the worst state counts: failure, then pending, then success. A re-run replaces the run it repeats, so a passing re-run clears an earlier failure.
Do not name smartcloud’s own check runs: they are still running while smartcloud evaluates the rule, so they are always pending. Reading commit statuses needs the statuses: read permission in your workflow, as well as checks; without it, the rules that use these conditions fail with a permissions error. A check that finishes does not start a smartcloud run by itself, so, as with hasConflict, the result follows on the pull request’s next event.

Code owners

codeownersTouched passes when the pull request changes a file that your CODEOWNERS file assigns to the owner you name: a team such as @resnovas/security, a user such as @jane, or an email. It reads CODEOWNERS the way GitHub does:
  • From the pull request’s base branch, the first of .github/CODEOWNERS, CODEOWNERS and docs/CODEOWNERS that exists. A repository without one owns nothing, so the condition fails.
  • A file’s owners are those of the last rule that matches it, so a later rule replaces an earlier one rather than adding to it, and a rule with no owners leaves its files unowned.
  • Patterns follow gitignore syntax. As on GitHub, a line that uses ! negation, [ ] ranges or a \# escape is skipped. A backslash escapes a space in a path, as in docs/My\ Files/. A pattern with a slash before its end, such as /docs/ or docs/github, is anchored at the repository root; otherwise it matches at any depth. A pattern naming a directory also covers everything inside it, except one ending in /*, such as docs/*, which covers only the files directly in that directory.
  • Owners compare in any case.
It needs the changed files and CODEOWNERS, two API calls, both covered by contents: read. In a stale sweep’s exempt.when, the listing has no base branch, so CODEOWNERS is read from the default branch.

Lockfiles, file count and binary files

lockfileChanged passes when the pull request changes a dependency lockfile, in any directory. It recognises these file names: package-lock.json, npm-shrinkwrap.json, pnpm-lock.yaml, yarn.lock, bun.lock, bun.lockb, deno.lock, Cargo.lock, go.sum, Gemfile.lock, composer.lock, poetry.lock, Pipfile.lock, uv.lock, pdm.lock, gradle.lockfile, packages.lock.json, Podfile.lock, Package.resolved, pubspec.lock, mix.lock, flake.lock and .terraform.lock.hcl. For any other file, use filesMatch. fileCount counts the changed files, the way changesSize counts lines: it passes when the count is at least min and, when max is set, below it. GitHub lists at most 3,000 files for a pull request, so a larger one counts as 3,000. binaryFilesAdded passes when the pull request adds a binary file. GitHub shows no text diff for binary content and counts no changed lines, and that is what the condition reads, so an empty file added counts as binary too. Changing or deleting an existing binary file does not count. GitHub lists at most 3,000 files of a pull request. On a longer one, lockfileChanged and binaryFilesAdded still pass on a match in the listed files, but without one they fail whatever condition says, since the rest cannot be checked.
All three read the pull request’s changed files, one API call covered by contents: read. lockfileChanged and fileCount share it with filesMatch and codeownersTouched; binaryFilesAdded reads the listing’s status and line counts too, which is a separate call.

Comments and reactions

commentMatches passes when at least one comment on the issue or pull request matches the pattern. Comments by bot accounts, smartcloud’s own among them, are left out unless you set bots: true, so a bot quoting the pattern cannot make it pass. The description is not a comment: use descriptionMatches for it. reactionCount counts the reactions on the issue or pull request itself, not on its comments. Name one in reaction by its GitHub name: +1 (thumbs up), -1 (thumbs down), laugh, hooray, confused, heart, rocket or eyes. Without reaction, every reaction counts. It passes when the count is at least min and, when max is set, below it. Both work on issues as well as pull requests. To prioritise the issues people want most, and to keep them from going stale:
GitHub starts no workflow when someone reacts, so a label follows on the next event smartcloud runs on, while a scheduled stale sweep reads the latest counts every time it runs. A new comment is such an event on an issue only when your workflow subscribes to issue_comment. A comment on a pull request arrives as an issue_comment event without the pull request’s data, which smartcloud does not act on, so there commentMatches is read on the next pull request event. commentMatches reads the comments, one API call covered by issues: read (pull-requests: read on a pull request). reactionCount needs no call on an issue event or in a stale sweep, whose payload and listing carry the counts; on a pull request event it reads the pull request, one call under the same permission.

Dates and times

createdBefore looks at when the issue or pull request was opened. A number is an age in days, so condition: 30 passes for anything opened at least 30 days ago. A date passes for anything opened before it: 2026-01-01 means midnight UTC, and a time needs an offset, as in 2026-01-01T09:00:00+01:00. For “opened after”, wrap it in $not. timeWindow looks at the time now, when the rule runs. It passes inside a weekly window, or outside it with condition: false:
  • days: the days the window opens on, from mon, tue, wed, thu, fri, sat and sun. Every day when omitted.
  • from and to: HH:MM on the 24-hour clock. from is inclusive and to exclusive; the window opens at midnight without from and closes at midnight without to. When to is at or before from, the window runs past midnight, and the early hours belong to the day it opened: days: [fri] from 22:00 to 06:00 covers early Saturday, not early Friday.
  • timeZone: an IANA name such as Europe/London, so daylight saving time follows the zone. UTC when omitted. An unknown zone is a config error.
To hold dependency updates outside working hours, and to leave old issues out of a stale sweep:
Both cost no API call: the event or listing carries the creation time, and timeWindow reads the clock. A rule only runs when an event or a schedule starts it, so timeWindow decides on whatever runs inside the window; use a scheduled workflow to act at a set time. If the event carries no creation time, createdBefore fails and its explanation says so.

Dependency updates

dependencyUpdateType passes when a dependency bot’s pull request updates by one of the listed types. The type comes from the first part of the version that changed: major for 1.4.2 to 2.0.0, minor for 1.4.2 to 1.5.0, and patch for anything later, such as 1.4.2 to 1.4.3 or a change only in a suffix such as -rc.1, .post1 or Maven’s .Final. A missing part counts as zero, so v4 to v4.1 is minor, and a change of PEP 440 epoch, as in 1!2.0, is major.
  • The author must be dependabot[bot] or renovate[bot]. GitHub reserves the [bot] suffix for apps, so nobody can pose as them with a branch name or title. A self-hosted Renovate runs under its own login: list it in bots. Anyone else fails, with an explanation naming the author.
  • Dependabot writes from 1.4.2 to 1.4.3 in the title and description, once per dependency in a grouped update. Renovate writes a table with a 1.4.2 -> 1.4.3 change per dependency. Every change counts and the largest wins, so a group holding one major update is major.
  • Release notes, changelogs and commit lists are skipped: collapsed <details> sections, HTML comments and quoted lines. They quote other projects, whose text can mention any version.
  • An update without a numeric version change, such as a digest bump, a pin or lockfile maintenance, fails, and its explanation says no version change was found.
To label the updates that are safe to merge and flag the breaking ones:
It costs no API call: the event carries the author, title and description. Conditions that need a pull request’s files, reviews, commits, mergeability, checks or CODEOWNERS, or a subject’s comments or reactions, cost an API call each. smartcloud loads only the ones the configured rules use.

Combinators

Combinators nest condition groups.
$not also accepts the two forms v1 wrote: a one-item list holding the group, or the conditions inline with requires on the $not itself.

Explanations

Every condition produces a one-line explanation, such as title does not match ^feat or 3 changed line(s). The conventions feature uses these to say why a rule failed when the rule has no message of its own.
Last modified on September 28, 2026