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()), so Bug matches and prefix does not trip fix. The keys must name labels that repomatic/data/labels.toml defines, or the labelling call fails on a label GitHub does not have; tests/test_labels.py enforces 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

💖 sponsor deliberately has no rule here, nor in DEFAULT_FILE_RULES. It means “a sponsor is involved”, which is a fact about the author that only the GraphQL sponsorship query can establish, and sponsor-label applies 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 (see GLOB_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/flags spelling a content pattern may take, mirroring JavaScript.

Matching this shape is what makes a pattern a regex: the body is passed to re as 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 (see compile_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 spell i out 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: g and y govern stateful iteration that a single membership test never reaches, u and v describe a Unicode mode Python’s re is always in, and d only 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¶

wcmatch flags reproducing the minimatch dialect actions/labeler used.

GLOBSTAR gives ** its cross-directory meaning, BRACE expands {a,b}, and NEGATE honours a leading !. The two worth spelling out:

  • DOTGLOB, because actions/labeler passed {dot: true} and half the globs a repository cares about start with a dot (.github/**/*). Without it a workflow change matches nothing.

  • NEGATEALL, because minimatch reads a lone !**/*.md as “everything that is not markdown”, while wcmatch defaults 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_labels passes them through verbatim (colors get their leading # stripped), so declarative renames and the other per-label knobs ride the regular sync.

rename-from is 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” in claude.md.

repomatic.labels.resolve_content_rules(config=None)[source]¶

The content rules in force: bundled defaults overlaid with the project’s.

Parameters:

config (Config | None) – The resolved [tool.repomatic] configuration, or None for the bundled defaults alone.

Return type:

dict[str, tuple[str, ...]]

Returns:

Patterns keyed by label.

repomatic.labels.resolve_file_rules(config=None)[source]¶

The file rules in force: bundled defaults overlaid with the project’s.

Parameters:

config (Config | None) – The resolved [tool.repomatic] configuration, or None for the bundled defaults alone.

Return type:

dict[str, tuple[str, ...]]

Returns:

Glob patterns keyed by label.

repomatic.labels.compile_content_pattern(pattern: str) Pattern[str] | None[source]¶

Compile one content pattern, as a keyword or a /body/flags regex.

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 fix does not fire inside prefix while .lock still matches the tail of uv.lock (a \b before 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/flags form passes the body through as a regex, mirroring JavaScript because that is what the retired github/issue-labeler action read and what existing rules are written in. No flags means case-sensitive.

Returns None on a body the re module 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.

Return type:

Pattern[str] | None

repomatic.labels.match_content_rules(rules, text)[source]¶

Return every label with a pattern matching text.

Parameters:
Return type:

set[str]

Returns:

The matching labels.

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 .gitignore would) rather than standing alone. A pull request that changes no files matches nothing.

Parameters:
Return type:

set[str]

Returns:

The matching labels.

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 the default profile, carrying every per-label field of labelmaker’s specification (INLINE_LABEL_FIELDS): a rename-from list renames a label in place on GitHub, preserving its issue and PR associations, and the create, update, enforce-case and on-rename-clash knobs 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 name are 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:

str

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.toml under the default profile, the awesome profile for awesome-* repositories, any hand-written or downloaded files under extra-labels/, and the inline [tool.repomatic.labels.extra] definitions. The exported files are expected to exist already (written by run_init() for the labels component).

Parameters:
  • config (Config) – The resolved [tool.repomatic] configuration.

  • repository (str) – GitHub repository in owner/name form.

  • is_awesome (bool) – Whether the repository is an awesome-* list.

  • labels_dir (Path | None) – Directory holding the exported labels.toml and the extra-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:

None