From c205726108df54bb6fbf15b19e725a4a3add2b18 Mon Sep 17 00:00:00 2001 From: jcotant-inferact Date: Wed, 19 Aug 2026 14:45:44 -0700 Subject: [PATCH] [CI] Fix and extend PR/issue auto-labeling (#51459) Signed-off-by: Joe Cotant Co-authored-by: Claude Opus 5 (1M context) Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/600-new-model.yml | 40 ----- .github/mergify.yml | 198 ++++++++++++++++++++--- .github/workflows/issue_autolabel.yml | 153 ++++++++++++++++++ .pre-commit-config.yaml | 7 + docs/contributing/labels.md | 95 +++++++++++ tools/pre_commit/check_label_rules.py | 97 +++++++++++ 6 files changed, 528 insertions(+), 62 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/600-new-model.yml create mode 100644 docs/contributing/labels.md create mode 100644 tools/pre_commit/check_label_rules.py diff --git a/.github/ISSUE_TEMPLATE/600-new-model.yml b/.github/ISSUE_TEMPLATE/600-new-model.yml deleted file mode 100644 index 5f0125ef98..0000000000 --- a/.github/ISSUE_TEMPLATE/600-new-model.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: 🤗 Support request for a new model from huggingface -description: Submit a proposal/request for a new model from huggingface -title: "[New Model]: " -labels: ["new model"] - -body: -- type: markdown - attributes: - value: > - #### Before submitting an issue, please make sure the issue hasn't been already addressed by searching through [the existing and past issues](https://github.com/vllm-project/vllm/issues?q=is%3Aissue+sort%3Acreated-desc+). - - #### We also highly recommend you read https://docs.vllm.ai/en/latest/contributing/model/index.html first to understand how to add a new model. -- type: textarea - attributes: - label: The model to consider. - description: > - A huggingface url, pointing to the model, e.g. https://huggingface.co/openai-community/gpt2 . - validations: - required: true -- type: textarea - attributes: - label: The closest model vllm already supports. - description: > - Here is the list of models already supported by vllm: https://github.com/vllm-project/vllm/tree/main/vllm/model_executor/models . Which model is the most similar to the model you want to add support for? -- type: textarea - attributes: - label: What's your difficulty of supporting the model you want? - description: > - For example, any new operators or new architecture? -- type: markdown - attributes: - value: > - Thanks for contributing 🎉! -- type: checkboxes - id: askllm - attributes: - label: Before submitting a new issue... - options: - - label: Make sure you already searched for relevant issues, and asked the chatbot living at the bottom right corner of the [documentation page](https://docs.vllm.ai/en/latest/), which can answer lots of frequently asked questions. - required: true diff --git a/.github/mergify.yml b/.github/mergify.yml index 4e6de638ab..04a623a499 100644 --- a/.github/mergify.yml +++ b/.github/mergify.yml @@ -93,6 +93,7 @@ pull_request_rules: - files~=^examples/.*deepseek.*\.py - files~=^tests/.*deepseek.*\.py - files~=^vllm/model_executor/models/.*deepseek.*\.py + - files~=^vllm/models/deepseek.*/ - files~=^vllm/tool_parsers/.*deepseek.*\.py - files~=^vllm/reasoning/.*deepseek.*\.py - files~=^vllm/transformers_utils/.*deepseek.*\.py @@ -133,9 +134,7 @@ pull_request_rules: - files~=^examples/.*llama.*\.py - files~=^tests/.*llama.*\.py - files~=^vllm/model_executor/models/.*llama.*\.py - - files~=^vllm/reasoning/.*llama.*\.py - files~=^vllm/tool_parsers/.*llama.*\.py - - files~=^vllm/transformers_utils/.*llama.*\.py - title~=(?i)llama actions: label: @@ -177,9 +176,17 @@ pull_request_rules: description: Automatically apply new-model label conditions: - label != stale - - and: - - files~=^vllm/model_executor/models/ - - files=vllm/model_executor/models/registry.py + - or: + # A model file is added alongside a registry entry. added-files rather + # than files: touching an existing model file is a fix or a refactor, + # and a PR that removes a model is not a new one. + - and: + - added-files~=^vllm/model_executor/models/.*\.py$ + - files=vllm/model_executor/models/registry.py + # A new top-level package under vllm/models/. common/ is shared kernel + # code rather than a model, and nested packages belong to a model that + # already exists. + - added-files~=^vllm/models/(?!common/)[^/]+/__init__\.py$ actions: label: add: @@ -233,16 +240,12 @@ pull_request_rules: conditions: - label != stale - or: - - files~=^examples/.*gpt[-_]?oss.*\.py - files~=^tests/.*gpt[-_]?oss.*\.py - - files~=^tests/entrypoints/openai/test_response_api_with_harmony.py - - files~=^tests/entrypoints/test_context.py - files~=^vllm/model_executor/models/.*gpt[-_]?oss.*\.py - files~=^vllm/model_executor/layers/.*gpt[-_]?oss.*\.py - files~=^vllm/entrypoints/openai/parser/harmony_utils.py - - files~=^vllm/entrypoints/tool_server.py - - files~=^vllm/entrypoints/tool.py - - files~=^vllm/entrypoints/context.py + # Harmony rendering now lives in the Rust frontend. + - files~=^rust/src/chat/src/.*/harmony/ - title~=(?i)gpt[-_]?oss - title~=(?i)harmony actions: @@ -256,7 +259,6 @@ pull_request_rules: - label != stale - or: - files~=(?i)kimi - - files~=(?i)moonshot - title~=(?i)(?:kimi|moonshot) actions: label: @@ -275,12 +277,73 @@ pull_request_rules: add: - k3 +- name: label-glm + description: Automatically apply glm label + conditions: + - label != stale + - or: + # Anchored to the start of the filename so midashenglm.py (an unrelated + # model) does not match. + - files~=^vllm/model_executor/models/(?:chat)?glm[^/]*\.py + - files~=^tests/.*[/_](?:chat)?glm[^/]*\.py + - files~=^vllm/tool_parsers/.*glm.*\.py + - files~=^vllm/transformers_utils/.*/(?:chat)?glm[^/]*\.py + - title~=(?i)\b(?:chat)?glm + actions: + label: + add: + - glm + +- name: label-minimax + description: Automatically apply minimax label + conditions: + - label != stale + - or: + - files~=^vllm/models/minimax.*/ + - files~=^vllm/model_executor/models/.*minimax.*\.py + - files~=^vllm/tool_parsers/.*minimax.*\.py + - files~=^tests/.*minimax.*\.py + - title~=(?i)minimax + actions: + label: + add: + - minimax + +- name: label-inkling + description: Automatically apply inkling label + conditions: + - label != stale + - or: + - files~=^vllm/models/inkling/ + - files~=^vllm/tool_parsers/inkling.*\.py + - files~=^vllm/renderers/inkling.*\.py + - files~=^tests/.*inkling.*\.py + - title~=(?i)inkling + actions: + label: + add: + - inkling + +- name: label-dsv4 + description: Automatically apply DSv4 label + conditions: + - label != stale + - or: + - files~=^vllm/models/deepseek_v4/ + - title~=(?i)(?:\bDSv4\b|deepseek[-\s]?v4) + actions: + label: + add: + - DSv4 + - name: label-nvidia description: Automatically apply nvidia label conditions: - label != stale - or: - - files~=cuda + # Exclude requirements/: a dependency bump touching requirements/cuda.txt + # is not NVIDIA backend work. + - files~=^(?!requirements/).*cuda - files~=cutlass - files~=flashinfer - files~=trtllm @@ -300,7 +363,7 @@ pull_request_rules: - files~=^csrc/rocm/ - files~=^docker/Dockerfile.rocm - files~=^requirements/rocm.*\.txt - - files~=^vllm/model_executor/layers/fused_moe/rocm.*\.py + - files~=^vllm/model_executor/layers/fused_moe/.*rocm.*\.py - files~=^vllm/v1/attention/backends/rocm.*\.py - files~=^vllm/v1/attention/backends/mla/rocm.*\.py - files~=^vllm/v1/attention/ops/rocm.*\.py @@ -319,8 +382,8 @@ pull_request_rules: - label != stale - or: - files~=^docker/Dockerfile.xpu - - files~=^\\.buildkite/intel_jobs/ - - files=\.buildkite/ci_config_intel.yaml + - files~=^\.buildkite/intel_jobs/ + - files=.buildkite/ci_config_intel.yaml - files=vllm/model_executor/layers/fused_moe/experts/xpu_moe.py - files=vllm/model_executor/kernels/linear/mixed_precision/xpu.py - files=vllm/model_executor/kernels/linear/mxfp8/xpu.py @@ -331,7 +394,6 @@ pull_request_rules: - files=vllm/v1/worker/xpu_worker.py - files=vllm/v1/worker/xpu_model_runner.py - files=vllm/_xpu_ops.py - - files=vllm/kernels/xpu_ops.py - files~=^vllm/lora/ops/xpu_ops - files=vllm/lora/punica_wrapper/punica_xpu.py - files=vllm/platforms/xpu.py @@ -367,7 +429,7 @@ pull_request_rules: - files=benchmarks/benchmark_serving_structured_output.py - files=benchmarks/run_structured_output_benchmark.sh - files=docs/features/structured_outputs.md - - files=^examples/features/structured_outputs/ + - files~=^examples/features/structured_outputs/ - files~=^tests/v1/structured_output/ - files=tests/entrypoints/llm/test_struct_output_generate.py - files~=^vllm/v1/structured_output/ @@ -382,8 +444,11 @@ pull_request_rules: - label != stale - or: - files~=^vllm/v1/spec_decode/ + # Model Runner V2 keeps its speculators in a separate subtree. + - files~=^vllm/v1/worker/gpu/spec_decode/ - files~=^tests/v1/spec_decode/ - - files=^examples/features/speculative_decoding/ + - files~=^tests/v1/e2e/spec_decode/ + - files~=^examples/features/speculative_decoding/ - files~=^vllm/model_executor/models/.*eagle.*\.py - files=vllm/model_executor/models/mlp_speculator.py - files~=^vllm/transformers_utils/configs/(eagle|medusa|mlp_speculator)\.py @@ -392,6 +457,33 @@ pull_request_rules: add: - speculative-decoding +# dflash covers the DFlash and DSpark speculative decoding techniques as one +# workstream, matching how they are tracked in the sprint board. It is applied +# in addition to speculative-decoding, never instead of it. +- name: label-dflash + description: Automatically apply dflash label + conditions: + - label != stale + - or: + - files~=^vllm/v1/spec_decode/dflash\.py + - files~=^vllm/v1/worker/gpu/spec_decode/dflash/ + - files~=^vllm/model_executor/models/.*dflash.*\.py + - files~=^tests/.*dflash.*\.py + - title~=(?i)dflash + - files~=^vllm/v1/worker/gpu/spec_decode/dspark/ + - files~=^vllm/model_executor/models/.*dspark.*\.py + - files~=^vllm/models/deepseek_v4/nvidia/dspark\.py + - files~=^vllm/models/deepseek_v4/amd/dspark\.py + - files~=^vllm/models/deepseek_v4/xpu/dspark\.py + - files~=^vllm/models/kimi_k3/nvidia/dspark_mla\.py + - files~=^vllm/transformers_utils/configs/k3_dspark\.py + - files~=^tests/.*dspark.*\.py + - title~=(?i)dspark + actions: + label: + add: + - dflash + - name: label-mrv2 description: Automatically apply mrv2 label conditions: @@ -416,7 +508,6 @@ pull_request_rules: - files~=_tpu - files~=tpu_ - files~=/tpu/ - - files~=pallas actions: label: add: @@ -432,7 +523,6 @@ pull_request_rules: - -files~=_tpu - -files~=tpu_ - -files~=/tpu/ - - -files~=pallas actions: label: remove: @@ -448,7 +538,6 @@ pull_request_rules: - files~=^tests/parser/ - files~=^tests/reasoning/ - files~=^tests/entrypoints/openai/.*tool.* - - files~=^tests/entrypoints/anthropic/.*tool.* - files~=^vllm/tool_parsers/ - files~=^vllm/parser/ - files~=^vllm/reasoning/ @@ -552,3 +641,68 @@ pull_request_rules: label: add: - kv-connector + +- name: label-scheduler + description: Automatically apply scheduler label + conditions: + - label != stale + - or: + - files~=^vllm/v1/core/sched/ + - files~=^tests/v1/core/test_scheduler.*\.py + actions: + label: + add: + - scheduler + +- name: label-kv-cache-manager + description: Automatically apply kv-cache-manager label + conditions: + - label != stale + - or: + # Scoped to the block/cache allocator in v1/core. KV transfer between + # instances is kv-connector, not this. + - files~=^vllm/v1/core/(?:kv_cache|block_pool|single_type_kv_cache) + - files~=^tests/v1/core/test_(?:kv_cache|single_type_kv_cache|prefix_caching).*\.py + actions: + label: + add: + - kv-cache-manager + +- name: label-torch-compile + description: Automatically apply torch.compile label + conditions: + - label != stale + - or: + - files~=^vllm/compilation/ + - files~=^tests/compile/ + - title~=(?i)torch\.compile + actions: + label: + add: + - torch.compile + +- name: label-ray + description: Automatically apply ray label + conditions: + - label != stale + - or: + - files~=^vllm/v1/executor/ray_ + - files~=^tests/.*ray_.*\.py + - title~=(?i)\bray\b + actions: + label: + add: + - ray + +- name: label-vllm-ir + description: Automatically apply vllm-ir label + conditions: + - label != stale + - or: + - files~=^vllm/ir/ + - files~=^tests/ir/ + - title~=(?i)vllm[-\s]?ir + actions: + label: + add: + - vllm-ir diff --git a/.github/workflows/issue_autolabel.yml b/.github/workflows/issue_autolabel.yml index 4a38aa2fbd..fd328e3ba8 100644 --- a/.github/workflows/issue_autolabel.yml +++ b/.github/workflows/issue_autolabel.yml @@ -190,6 +190,159 @@ jobs: }, ], }, + // Model labels. Title-only by design: issue bodies carry pasted + // collect_env output, tracebacks and configs that name unrelated + // models, hardware and libraries. + deepseek: { + keywords: [{ term: "DeepSeek", searchIn: "title" }], + }, + DSv4: { + keywords: [{ term: "DSv4", searchIn: "title" }], + substrings: [{ term: "deepseek-v4", searchIn: "title" }], + }, + llama: { + keywords: [{ term: "Llama", searchIn: "title" }], + }, + mistral: { + keywords: [ + { term: "Mistral", searchIn: "title" }, + { term: "Ministral", searchIn: "title" }, + { term: "Voxtral", searchIn: "title" }, + { term: "Mixtral", searchIn: "title" }, + { term: "Pixtral", searchIn: "title" }, + ], + }, + qwen: { + keywords: [{ term: "Qwen", searchIn: "title" }], + }, + "gpt-oss": { + keywords: [{ term: "harmony", searchIn: "title" }], + substrings: [{ term: "gpt-oss", searchIn: "title" }], + }, + glm: { + keywords: [ + { term: "GLM", searchIn: "title" }, + { term: "ChatGLM", searchIn: "title" }, + ], + }, + minimax: { + substrings: [{ term: "minimax", searchIn: "title" }], + }, + inkling: { + keywords: [{ term: "Inkling", searchIn: "title" }], + }, + // Hardware backends, mirroring the cpu/rocm/intel-gpu rules above. + nvidia: { + keywords: [ + { term: "NVIDIA", searchIn: "title" }, + { term: "CUTLASS", searchIn: "title" }, + ], + }, + tpu: { + keywords: [ + { term: "TPU", searchIn: "title" }, + { term: "XLA", searchIn: "title" }, + ], + }, + // Subsystems. + // + // Single distinctive words go in `keywords` so the word-boundary + // match keeps them from firing inside longer words. Multi-word + // phrases go in `substrings`: they are specific enough on their + // own, and a boundary match would miss ordinary variations -- + // "structured outputs", "tool parsers", "prefix caching". + "speculative-decoding": { + keywords: [ + { term: "eagle", searchIn: "title" }, + { term: "medusa", searchIn: "title" }, + ], + substrings: [ + { term: "speculative decoding", searchIn: "title" }, + { term: "speculative decode", searchIn: "title" }, + { term: "draft model", searchIn: "title" }, + ], + }, + "structured-output": { + substrings: [ + { term: "structured output", searchIn: "title" }, + { term: "guided decoding", searchIn: "title" }, + { term: "JSON schema", searchIn: "title" }, + ], + }, + "tool-calling": { + substrings: [ + { term: "tool calling", searchIn: "title" }, + { term: "tool parser", searchIn: "title" }, + { term: "function calling", searchIn: "title" }, + ], + }, + "kv-connector": { + keywords: [ + { term: "NIXL", searchIn: "title" }, + { term: "LMCache", searchIn: "title" }, + ], + substrings: [ + { term: "KV transfer", searchIn: "title" }, + { term: "disaggregat", searchIn: "title" }, + ], + }, + scheduler: { + keywords: [{ term: "scheduler", searchIn: "title" }], + }, + "kv-cache-manager": { + substrings: [ + { term: "kv cache manager", searchIn: "title" }, + { term: "block manager", searchIn: "title" }, + { term: "prefix cache", searchIn: "title" }, + { term: "prefix caching", searchIn: "title" }, + ], + }, + "torch.compile": { + substrings: [{ term: "torch.compile", searchIn: "title" }], + }, + "multi-modality": { + keywords: [ + { term: "multimodal", searchIn: "title" }, + { term: "multi-modal", searchIn: "title" }, + ], + substrings: [ + { term: "image input", searchIn: "title" }, + { term: "audio input", searchIn: "title" }, + ], + }, + "ci-failure": { + keywords: [{ term: "flaky", searchIn: "title" }], + substrings: [ + { term: "CI failure", searchIn: "title" }, + { term: "test failure", searchIn: "title" }, + ], + }, + rl: { + keywords: [{ term: "RLHF", searchIn: "title" }], + substrings: [ + { term: "reinforcement learning", searchIn: "title" }, + { term: "weight update", searchIn: "title" }, + ], + }, + ray: { + substrings: [ + { term: "Ray cluster", searchIn: "title" }, + { term: "Ray executor", searchIn: "title" }, + { term: "placement group", searchIn: "title" }, + ], + }, + "vllm-ir": { + substrings: [ + { term: "vllm-ir", searchIn: "title" }, + { term: "vllm ir", searchIn: "title" }, + ], + }, + rust: { + keywords: [ + { term: "Rust", searchIn: "title" }, + { term: "PyO3", searchIn: "title" }, + ], + }, // Add more label configurations here as needed // example: { // keywords: [...], diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c1c5f5f90b..b9344b6ffc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -165,6 +165,13 @@ repos: language: python entry: python tools/pre_commit/generate_nightly_torch_test.py files: ^requirements/test/cuda\.(in|txt)$ + - id: check-label-rules + name: Check auto-label rules still match real files + language: python + entry: python tools/pre_commit/check_label_rules.py + files: ^\.github/mergify\.yml$ + pass_filenames: false + additional_dependencies: [PyYAML, regex] - id: mypy-3.10 # TODO: Use https://github.com/pre-commit/mirrors-mypy when mypy setup is less awkward name: Run mypy for Python 3.10 entry: python tools/pre_commit/mypy.py "3.10" diff --git a/docs/contributing/labels.md b/docs/contributing/labels.md new file mode 100644 index 0000000000..891f7e4ed5 --- /dev/null +++ b/docs/contributing/labels.md @@ -0,0 +1,95 @@ +# Labels + +vLLM applies most labels automatically. Three systems do the work: + +| | Config | Applies to | +| --- | --- | --- | +| Mergify | `.github/mergify.yml` | pull requests, by changed file path or title | +| GitHub Action | `.github/workflows/issue_autolabel.yml` | issues, by title keyword | +| Issue templates | `.github/ISSUE_TEMPLATE/*.yml` | issues, by which template was used | + +A label that none of them applies has to be applied by hand, and in practice +that means it mostly doesn't get applied at all. + +A template's `labels:` field must name a label that already exists — GitHub +silently skips one that doesn't, with no error anywhere. + +`mergify.yml` also drives reviewer auto-assignment; see +[Collaboration](../governance/collaboration.md) for adding yourself as a +maintainer of an area. + +## Adding a label + +A new label needs three things. If it can't have all three, don't create it. + +1. **An audience.** Someone has to want to filter on it. "It would be nice to + know" isn't enough. +2. **An owner.** A person or SIG who triages that queue. Without one the label + accumulates issues nobody reads. +3. **A rule, in the same PR.** If you can't write a precise rule, the label + depends on humans remembering it exists. + +## Measure before you argue + +Volume claims should come from the repo, not from intuition. vLLM squash-merges, +so one commit on `main` is one PR: + +```bash +git log --since="6 months ago" --oneline -- | wc -l +``` + +For reference, at the time of writing: `speculative-decoding` 81, `llama` 85, +`mistral` 72, `tpu` 14. A subsystem in that range clears the bar comfortably. +Something in the low teens needs a stronger argument than volume. + +The same applies to claiming a rule is too broad. Check what it actually labels +before rewriting it: + +```bash +gh pr list --repo vllm-project/vllm --state merged --limit 200 \ + --json number,title,labels +``` + +Several rules in this repo have been called over-broad on inspection and turned +out to be accurate when measured. + +## Writing conditions + +**`files=` is exact equality. `files~=` is a regex.** Writing a pattern after +`=` compares the pattern text to each filename, so it can never match: + +```yaml +- files=^examples/features/speculative_decoding/ # never fires +- files~=^examples/features/speculative_decoding/ # correct +``` + +**Anchor file patterns.** An unanchored fragment matches far more than intended +— `files~=cuda` also matches `requirements/cuda.txt`. + +**Watch for names that contain other names.** `midashenglm.py` contains "glm" +but is unrelated to GLM, so the `glm` rule anchors to the start of the filename. + +**Keep an eye on where model code lives.** Newer models live in +`vllm/models//` rather than `vllm/model_executor/models/`. A rule that +only knows the old location silently stops matching. + +**Prefer titles for issue keywords.** Issue bodies contain pasted `collect_env` +output, tracebacks and configs that mention unrelated hardware and libraries. +Body matching tags the reporter's environment rather than the topic. + +**Single words go in `keywords`, phrases in `substrings`.** `keywords` matches on +word boundaries, which stops a term firing inside a longer word but also misses +ordinary variations — `structured output` will not match "structured outputs". + +## Keeping rules honest + +`tools/pre_commit/check_label_rules.py` fails if any file condition in +`mergify.yml` matches nothing in the tree. It runs via pre-commit when that file +changes. This catches conditions that were correct when written and rotted when +the code moved. + +## Not everything needs a label + +Triage and workflow labels — `bug`, `stale`, `ready`, `RFC`, `needs-rebase` — +and the bot provenance tags are applied by other mechanisms or by hand, and are +intentionally outside this system. diff --git a/tools/pre_commit/check_label_rules.py b/tools/pre_commit/check_label_rules.py new file mode 100644 index 0000000000..c5074d21d0 --- /dev/null +++ b/tools/pre_commit/check_label_rules.py @@ -0,0 +1,97 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""Check that auto-labeling rules still match real files. + +Mergify silently ignores a condition that can never match, so a file-path +condition keeps passing review long after the file it names has moved or been +deleted. This walks every `files=` / `files~=` condition in `.github/mergify.yml` +and fails if one matches nothing in the tree. + +Usage: + python tools/pre_commit/check_label_rules.py +""" + +import subprocess +import sys + +import regex as re +import yaml + +MERGIFY = ".github/mergify.yml" + + +def tracked_files() -> list[str]: + out = subprocess.run( + ["git", "ls-files"], capture_output=True, text=True, check=True + ).stdout + return [line for line in out.split("\n") if line] + + +def file_conditions(node, found: list[str]) -> list[str]: + """Collect every string condition nested under a rule's `conditions`.""" + if isinstance(node, dict): + for value in node.values(): + file_conditions(value, found) + elif isinstance(node, list): + for value in node: + file_conditions(value, found) + elif isinstance(node, str): + found.append(node) + return found + + +def check(files: list[str]) -> list[tuple[str, str, str]]: + with open(MERGIFY) as f: + rules = yaml.safe_load(f)["pull_request_rules"] + + # removed-files is excluded: it names paths the tree no longer has. + attrs = ("files", "added-files", "modified-files") + + dead = [] + for rule in rules: + for cond in file_conditions(rule.get("conditions", []), []): + cond = cond.strip() + # Negated conditions (label-tpu-remove) are expected to match nothing. + if cond.startswith("-"): + continue + attr = next( + ( + a + for a in attrs + if cond.startswith(f"{a}~=") or cond.startswith(f"{a}=") + ), + None, + ) + if attr is None: + continue + if cond.startswith(f"{attr}~="): + pattern = cond[len(attr) + 2 :] + try: + regex = re.compile(pattern) + except re.error as exc: + dead.append((rule["name"], cond, f"invalid regex: {exc}")) + continue + matched = any(regex.search(f) for f in files) + else: + matched = cond[len(attr) + 1 :] in files + if not matched: + dead.append((rule["name"], cond, "matches no tracked file")) + return dead + + +def main() -> int: + dead = check(tracked_files()) + if not dead: + return 0 + print(f"{len(dead)} label condition(s) match nothing:\n", file=sys.stderr) + for name, cond, why in dead: + print(f" [{name}] {cond}\n {why}", file=sys.stderr) + print( + "\nRepoint the condition at the file's new location, or remove it.", + file=sys.stderr, + ) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main())