repomatic.labels module¶
Repository label management.
The label domain in one place: matching an issue or pull request against the
[tool.repomatic.labels] rules to decide which labels it earns, and applying
label definitions to a repository through labelmaker. Backs the
apply-labels and sync-labels commands.
A rule is one label mapped to a list of patterns: regexes or keywords over the
threadâs text (DEFAULT_CONTENT_RULES), globs over a pull requestâs
changed paths (DEFAULT_FILE_RULES). Any pattern matching applies the
label. A project entry for a label replaces the default entry wholesale, and
an empty list disables it; see resolve_content_rules().
Note
This schema replaced the actions/labeler v5 and github/issue-labeler
dialects when the matching moved in-tree. The retired shapes earned their
complexity serving the actions (per-matcher quantifiers, branch regexes,
any/all group nesting, per-pattern AND-joins), and none of it was used by
any repository this toolkit manages: every real rule was âlabel X when any
changed file matches any of these globsâ or âwhen any of these words appearsâ,
which is exactly what the schema now says and nothing more.
- repomatic.labels.DEFAULT_CONTENT_RULES: dict[str, tuple[str, ...]] = {'đ changelog': ('change-log', 'changelog'), 'đ bug': ('bug', 'error', 'exception', 'fix', 'traceback'), 'đ documentation': ('docstring', 'license', 'mailmap', 'markdown', 'readme', 'sphinx', 'typo'), 'đ dependencies': ('.lock', 'pyproject.toml'), 'đ€ ci': ('.github', 'actions', 'ci-cd', 'cicd', 'coverage', 'gitignore', 'workflow')}¶
Default content rules: keywords matched against a threadâs title and body.
Every entry is a plain keyword, compiled case-insensitively with word boundaries on its word-character edges (see
compile_content_pattern()), soBugmatches andprefixdoes not tripfix. The keys must name labels thatrepomatic/data/labels.tomldefines, or the labelling call fails on a label GitHub does not have;tests/test_labels.pyenforces that.Tune for precision, not recall: a missing label costs one manual click, a wrong one is noise on every issue that trips it. Never key a rule off a token the project prints in its own output, or a user pasting a trace sets every label at once.
Note
đ sponsordeliberately has no rule here, nor inDEFAULT_FILE_RULES. It means âa sponsor is involvedâ, which is a fact about the author that only the GraphQL sponsorship query can establish, andsponsor-labelapplies it from exactly that. Matching the words âfundingâ or âsponsorâ (or a pull request touching.github/funding.yml) labels the topic instead, so anyone opening âAdd a funding.ymlâ read as a sponsor. Precision-first means no rule beats an ambiguous one when an authoritative source already exists.
- repomatic.labels.DEFAULT_FILE_RULES: dict[str, tuple[str, ...]] = {'đ changelog': ('.github/workflows/changelog.yaml', '.github/workflows/release.yaml', 'changelog.md'), 'đ documentation': ('.github/code-of-conduct.md', '.github/workflows/docs.yaml', '.mailmap', 'docs/**/*', 'license', 'readme.md'), 'đ dependencies': ('*.lock', '**/pyproject.toml'), 'đ€ ci': ('.github/**/*', '.gitignore', 'pyproject.toml')}¶
Default file rules: globs matched against the paths a pull request changes.
The dialect is
minimatchâs (seeGLOB_FLAGS):**crosses directories,{a,b}expands, a leading!subtracts from the labelâs other globs, and a leading dot is matched like any other character. Keep the globs precise: one broad enough to catch unrelated changes mislabels every pull request touching them.
- repomatic.labels.CONTENT_PATTERN_RE = re.compile('^/(?P<body>.*)/(?P<flags>[a-z]*)$', re.DOTALL)¶
The
/body/flagsspelling a content pattern may take, mirroring JavaScript.Matching this shape is what makes a pattern a regex: the body is passed to
reas written, and only the flags named between the slashes apply, so a bare/foo/is case-sensitive. A pattern not in this shape is a literal keyword instead, escaped and word-anchored and always matched case-insensitively (seecompile_content_pattern()). So the slashed form is the one to reach for when a rule genuinely needs regex syntax, and the one that has to spelliout to get back the case-insensitivity the bare form gives for free.
- repomatic.labels.CONTENT_PATTERN_FLAGS: dict[str, int] = {'i': re.IGNORECASE, 'm': re.MULTILINE, 's': re.DOTALL}¶
JavaScript regex flags with a Python equivalent, and their translation.
The rest of JavaScriptâs set is accepted and ignored rather than rejected:
gandygovern stateful iteration that a single membership test never reaches,uandvdescribe a Unicode mode Pythonâsreis always in, anddonly adds capture-group offsets nobody here reads. Refusing them would fail a rule over a flag that changes nothing about whether it matches.
- repomatic.labels.GLOB_FLAGS = 33608¶
wcmatchflags reproducing theminimatchdialectactions/labelerused.GLOBSTARgives**its cross-directory meaning,BRACEexpands{a,b}, andNEGATEhonours a leading!. The two worth spelling out:DOTGLOB, becauseactions/labelerpassed{dot: true}and half the globs a repository cares about start with a dot (.github/**/*). Without it a workflow change matches nothing.NEGATEALL, becauseminimatchreads a lone!**/*.mdas âeverything that is not markdownâ, whilewcmatchdefaults to matching nothing at all when no positive pattern accompanies the exclusion.
- repomatic.labels.INLINE_LABEL_FIELDS: tuple[str, ...] = ('name', 'color', 'description', 'create', 'update', 'enforce-case', 'rename-from', 'on-rename-clash')¶
Per-label fields of labelmakerâs specification, in its documented order.
serialize_inline_labelspasses them through verbatim (colors get their leading#stripped), so declarative renames and the other per-label knobs ride the regular sync.rename-fromis the one field with a constraint worth knowing before use: it is strictly one-to-one, renaming only when the target is absent and exactly one listed source exists. It therefore cannot merge several labels into one, and is useless once a sync has already created the target. See âRetiring a label is a migration, not a deletionâ inclaude.md.
- repomatic.labels.resolve_content_rules(config=None)[source]¶
The content rules in force: bundled defaults overlaid with the projectâs.
- repomatic.labels.resolve_file_rules(config=None)[source]¶
The file rules in force: bundled defaults overlaid with the projectâs.
- repomatic.labels.compile_content_pattern(pattern: str) Pattern[str] | None[source]¶
Compile one content pattern, as a keyword or a
/body/flagsregex.Memoized: the rule tables hand the same patterns to every matching call, so each spelling compiles once per process (and a malformed one is warned about once instead of on every thread it is matched against).
A bare pattern is a literal keyword: escaped, matched case-insensitively, and word-anchored on each edge that is itself a word character, so
fixdoes not fire insideprefixwhile.lockstill matches the tail ofuv.lock(a\bbefore the dot would demand a word character ahead of it). Case-insensitivity is the point of defaulting this way: users capitalize freely, and a convention every rule must remember to spell is a convention half of them forget.The
/body/flagsform passes the body through as a regex, mirroring JavaScript because that is what the retiredgithub/issue-labeleraction read and what existing rules are written in. No flags means case-sensitive.Returns
Noneon a body theremodule rejects, having logged it. A single malformed rule must not take the whole labelling run down with it: the job is a convenience that runs once per opened issue, and the other rules still have work to do.
- repomatic.labels.match_content_rules(rules, text)[source]¶
Return every label with a pattern matching text.
- repomatic.labels.match_file_rules(rules, files)[source]¶
Return every label whose globs match a changed file.
A labelâs globs are evaluated as one set, so a
!-negated entry subtracts from its siblings (["docs/**", "!docs/generated/**"]reads the way a.gitignorewould) rather than standing alone. A pull request that changes no files matches nothing.
- repomatic.labels.serialize_inline_labels(entries)[source]¶
Serialize
[tool.repomatic.labels.extra]entries to a labelmaker TOML config.Each entry becomes a
[[profiles.default.labels]]block under thedefaultprofile, carrying every per-label field of labelmakerâs specification (INLINE_LABEL_FIELDS): arename-fromlist renames a label in place on GitHub, preserving its issue and PR associations, and thecreate,update,enforce-caseandon-rename-clashknobs pass through alike. Leading#on hex colors is stripped, on both single colors and multi-color lists, so the output matches labelmakerâs convention.Entries missing a
nameare skipped with a warning, and unknown fields are dropped with a warning: labelmaker rejects both and would abort the whole sync.Returns an empty string when there are no valid entries, so the caller can skip writing a temp file and invoking labelmaker entirely.
- Return type:
- repomatic.labels.apply_labels(config, repository, *, is_awesome, labels_dir=None)[source]¶
Apply every configured label source to repository via
labelmaker.Applies, in order: the exported
labels.tomlunder thedefaultprofile, theawesomeprofile forawesome-*repositories, any hand-written or downloaded files underextra-labels/, and the inline[tool.repomatic.labels.extra]definitions. The exported files are expected to exist already (written byrun_init()for thelabelscomponent).- Parameters:
config (
Config) â The resolved[tool.repomatic]configuration.repository (
str) â GitHub repository inowner/nameform.is_awesome (
bool) â Whether the repository is anawesome-*list.labels_dir (
Path|None) â Directory holding the exportedlabels.tomland theextra-labels/downloads. Defaults to the current directory. Point it at a scratch directory to keep the export out of the working tree.
- Raises:
RuntimeError â When a labelmaker invocation fails.
- Return type: