repomatic.github.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

The schema says “label X when any changed file matches any of these globs” or “when any of these words appears”, and nothing more. Per-matcher quantifiers, branch regexes and any/all group nesting are left out on purpose: no rule of a repository this toolkit manages needs them.

repomatic.github.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': ('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

🤖 ci deliberately does not match .github, which is the case the rule above names. Every issue form repomatic bundles for an awesome list links its own .github/code-of-conduct.md and .github/contributing.md in the self-check boxes, and GitHub renders those option labels into the filed body verbatim, so the keyword pre-labelled every submission filed through one. A pull request that actually touches those paths is already covered by the .github/**/* glob in DEFAULT_FILE_RULES, and prose about CI still matches workflow, actions or ci-cd.

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.github.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.github.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.github.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.github.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.github.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 the github-housekeeping skill.

repomatic.github.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.github.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.github.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, in the JavaScript spelling 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.github.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.github.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.github.labels.labels_for_thread(config, number, *, kind, repository=None)[source]

The labels the project’s rules give one issue or pull request.

In the labeller job the event payload carries the thread. A thread the payload does not carry has its text read from the API instead, so the content rules see the real title and body rather than an empty string. File rules need a diff, which only a pull request has.

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

  • number (int) – The issue or pull request number.

  • kind (ThreadKind) – Whether number names an issue or a pull request.

  • repository (str | None) – Repository in owner/name form, or None to let gh resolve it.

Return type:

list[str]

Returns:

The matched labels, sorted.

repomatic.github.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.github.labels.EXTRA_LABELS_DIR = 'extra-labels'

Directory of a repository holding its hand-written label definitions.

repomatic.github.labels.extra_label_filename(url)[source]

The filename a labels.extra-files download is saved under.

Shared by the download of repomatic init labels and by label_sources(), which keys a fetched definition the same way, so it shadows a committed extra-labels/ file of that name.

Parameters:

url (str) – One labels.extra-files entry.

Return type:

str

Returns:

The last segment of the URL path.

class repomatic.github.labels.LabelSource(path, text, profiles=None)[source]

Bases: object

One labelmaker definition file, as sync-labels applies it.

path: str

Where the file is staged for labelmaker, relative to the staging root.

Its extension tells labelmaker the format.

text: str

The definitions.

profiles: tuple[str, ...] | None = None

The profiles applied, one --profile each. None applies the file without one, and reads every profile it declares.

repomatic.github.labels.label_sources(config, *, is_awesome, root=None)[source]

Every label definition the configuration applies to a repository.

In apply order: the bundled labels.toml under the default profile (and the awesome one for an awesome-* repository), the hand-written files of extra-labels/ and the labels.extra-files downloads, then the inline [tool.repomatic.labels.extra] definitions. Both apply_labels() and declared_label_names() read this one list, so a label the sync creates is never reported as undeclared.

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

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

  • root (Path | None) – Repository root holding extra-labels/, the current directory when None.

Yields:

Each definition file.

Raises:

repomatic.http.FetchError – When a labels.extra-files URL cannot be fetched. The list would otherwise come back short, and every label only that file declares would be lost.

exception repomatic.github.labels.LabelConfigError[source]

Bases: ValueError

A label definition file that declared_label_names() cannot read.

repomatic.github.labels.declared_label_names(config, *, is_awesome, labels_dir=None)[source]

Every label name the configured sources declare for a repository.

Reads the label_sources() that apply_labels() applies, which is what lets check_undeclared_labels() call anything outside this set an orphan.

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

  • is_awesome (bool) – Whether the repository is an awesome-* list, which adds the awesome profile on top of default.

  • labels_dir (Path | None) – Repository root holding extra-labels/, the current directory when None.

Return type:

set[str]

Returns:

The declared label names.

Raises:
  • repomatic.http.FetchError – When a labels.extra-files URL cannot be fetched.

  • LabelConfigError – When an extra-labels/ or labels.extra-files definition cannot be read. The set would otherwise come back short.

repomatic.github.labels.apply_labels(config, repository, *, is_awesome, root=None)[source]

Apply every configured label source to repository via labelmaker.

Stages each of the label_sources() in a scratch directory, so a sync leaves the working tree untouched, then applies them in order.

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.

  • root (Path | None) – Repository root holding extra-labels/, the current directory when None.

Raises:

RuntimeError – When a labelmaker invocation fails, or when a labels.extra-files URL cannot be fetched.

Return type:

None