[CI] Fix and extend PR/issue auto-labeling (#51459)

Signed-off-by: Joe Cotant <joe@inferact.ai> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>

jcotant-inferact committed Aug 19, 2026 at 14:45 UTC c205726108df54bb6fbf15b19e725a4a3add2b18
6 files changed +528 -62
.github/ISSUE_TEMPLATE/600-new-model.yml deleted
-40
@@ -1,40 +0,0 @@
1 -name: 🤗 Support request for a new model from huggingface
2 -description: Submit a proposal/request for a new model from huggingface
3 -title: "[New Model]: "
4 -labels: ["new model"]
5 -
6 -body:
7 -- type: markdown
8 - attributes:
9 - value: >
10 - #### 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+).
11 -
12 - #### 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.
13 -- type: textarea
14 - attributes:
15 - label: The model to consider.
16 - description: >
17 - A huggingface url, pointing to the model, e.g. https://huggingface.co/openai-community/gpt2 .
18 - validations:
19 - required: true
20 -- type: textarea
21 - attributes:
22 - label: The closest model vllm already supports.
23 - description: >
24 - 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?
25 -- type: textarea
26 - attributes:
27 - label: What's your difficulty of supporting the model you want?
28 - description: >
29 - For example, any new operators or new architecture?
30 -- type: markdown
31 - attributes:
32 - value: >
33 - Thanks for contributing 🎉!
34 -- type: checkboxes
35 - id: askllm
36 - attributes:
37 - label: Before submitting a new issue...
38 - options:
39 - - 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.
40 - required: true
.github/mergify.yml
+176 -22
@@ -93,6 +93,7 @@ pull_request_rules:
93 - files~=^examples/.*deepseek.*\.py
94 - files~=^tests/.*deepseek.*\.py
95 - files~=^vllm/model_executor/models/.*deepseek.*\.py
96 + - files~=^vllm/models/deepseek.*/
97 - files~=^vllm/tool_parsers/.*deepseek.*\.py
98 - files~=^vllm/reasoning/.*deepseek.*\.py
99 - files~=^vllm/transformers_utils/.*deepseek.*\.py
@@ -133,9 +134,7 @@ pull_request_rules:
134 - files~=^examples/.*llama.*\.py
135 - files~=^tests/.*llama.*\.py
136 - files~=^vllm/model_executor/models/.*llama.*\.py
136 - - files~=^vllm/reasoning/.*llama.*\.py
137 - files~=^vllm/tool_parsers/.*llama.*\.py
138 - - files~=^vllm/transformers_utils/.*llama.*\.py
138 - title~=(?i)llama
139 actions:
140 label:
@@ -177,9 +176,17 @@ pull_request_rules:
176 description: Automatically apply new-model label
177 conditions:
178 - label != stale
180 - - and:
181 - - files~=^vllm/model_executor/models/
182 - - files=vllm/model_executor/models/registry.py
179 + - or:
180 + # A model file is added alongside a registry entry. added-files rather
181 + # than files: touching an existing model file is a fix or a refactor,
182 + # and a PR that removes a model is not a new one.
183 + - and:
184 + - added-files~=^vllm/model_executor/models/.*\.py$
185 + - files=vllm/model_executor/models/registry.py
186 + # A new top-level package under vllm/models/. common/ is shared kernel
187 + # code rather than a model, and nested packages belong to a model that
188 + # already exists.
189 + - added-files~=^vllm/models/(?!common/)[^/]+/__init__\.py$
190 actions:
191 label:
192 add:
@@ -233,16 +240,12 @@ pull_request_rules:
240 conditions:
241 - label != stale
242 - or:
236 - - files~=^examples/.*gpt[-_]?oss.*\.py
243 - files~=^tests/.*gpt[-_]?oss.*\.py
238 - - files~=^tests/entrypoints/openai/test_response_api_with_harmony.py
239 - - files~=^tests/entrypoints/test_context.py
244 - files~=^vllm/model_executor/models/.*gpt[-_]?oss.*\.py
245 - files~=^vllm/model_executor/layers/.*gpt[-_]?oss.*\.py
246 - files~=^vllm/entrypoints/openai/parser/harmony_utils.py
243 - - files~=^vllm/entrypoints/tool_server.py
244 - - files~=^vllm/entrypoints/tool.py
245 - - files~=^vllm/entrypoints/context.py
247 + # Harmony rendering now lives in the Rust frontend.
248 + - files~=^rust/src/chat/src/.*/harmony/
249 - title~=(?i)gpt[-_]?oss
250 - title~=(?i)harmony
251 actions:
@@ -256,7 +259,6 @@ pull_request_rules:
259 - label != stale
260 - or:
261 - files~=(?i)kimi
259 - - files~=(?i)moonshot
262 - title~=(?i)(?:kimi|moonshot)
263 actions:
264 label:
@@ -275,12 +277,73 @@ pull_request_rules:
277 add:
278 - k3
279
280 +- name: label-glm
281 + description: Automatically apply glm label
282 + conditions:
283 + - label != stale
284 + - or:
285 + # Anchored to the start of the filename so midashenglm.py (an unrelated
286 + # model) does not match.
287 + - files~=^vllm/model_executor/models/(?:chat)?glm[^/]*\.py
288 + - files~=^tests/.*[/_](?:chat)?glm[^/]*\.py
289 + - files~=^vllm/tool_parsers/.*glm.*\.py
290 + - files~=^vllm/transformers_utils/.*/(?:chat)?glm[^/]*\.py
291 + - title~=(?i)\b(?:chat)?glm
292 + actions:
293 + label:
294 + add:
295 + - glm
296 +
297 +- name: label-minimax
298 + description: Automatically apply minimax label
299 + conditions:
300 + - label != stale
301 + - or:
302 + - files~=^vllm/models/minimax.*/
303 + - files~=^vllm/model_executor/models/.*minimax.*\.py
304 + - files~=^vllm/tool_parsers/.*minimax.*\.py
305 + - files~=^tests/.*minimax.*\.py
306 + - title~=(?i)minimax
307 + actions:
308 + label:
309 + add:
310 + - minimax
311 +
312 +- name: label-inkling
313 + description: Automatically apply inkling label
314 + conditions:
315 + - label != stale
316 + - or:
317 + - files~=^vllm/models/inkling/
318 + - files~=^vllm/tool_parsers/inkling.*\.py
319 + - files~=^vllm/renderers/inkling.*\.py
320 + - files~=^tests/.*inkling.*\.py
321 + - title~=(?i)inkling
322 + actions:
323 + label:
324 + add:
325 + - inkling
326 +
327 +- name: label-dsv4
328 + description: Automatically apply DSv4 label
329 + conditions:
330 + - label != stale
331 + - or:
332 + - files~=^vllm/models/deepseek_v4/
333 + - title~=(?i)(?:\bDSv4\b|deepseek[-\s]?v4)
334 + actions:
335 + label:
336 + add:
337 + - DSv4
338 +
339 - name: label-nvidia
340 description: Automatically apply nvidia label
341 conditions:
342 - label != stale
343 - or:
283 - - files~=cuda
344 + # Exclude requirements/: a dependency bump touching requirements/cuda.txt
345 + # is not NVIDIA backend work.
346 + - files~=^(?!requirements/).*cuda
347 - files~=cutlass
348 - files~=flashinfer
349 - files~=trtllm
@@ -300,7 +363,7 @@ pull_request_rules:
363 - files~=^csrc/rocm/
364 - files~=^docker/Dockerfile.rocm
365 - files~=^requirements/rocm.*\.txt
303 - - files~=^vllm/model_executor/layers/fused_moe/rocm.*\.py
366 + - files~=^vllm/model_executor/layers/fused_moe/.*rocm.*\.py
367 - files~=^vllm/v1/attention/backends/rocm.*\.py
368 - files~=^vllm/v1/attention/backends/mla/rocm.*\.py
369 - files~=^vllm/v1/attention/ops/rocm.*\.py
@@ -319,8 +382,8 @@ pull_request_rules:
382 - label != stale
383 - or:
384 - files~=^docker/Dockerfile.xpu
322 - - files~=^\\.buildkite/intel_jobs/
323 - - files=\.buildkite/ci_config_intel.yaml
385 + - files~=^\.buildkite/intel_jobs/
386 + - files=.buildkite/ci_config_intel.yaml
387 - files=vllm/model_executor/layers/fused_moe/experts/xpu_moe.py
388 - files=vllm/model_executor/kernels/linear/mixed_precision/xpu.py
389 - files=vllm/model_executor/kernels/linear/mxfp8/xpu.py
@@ -331,7 +394,6 @@ pull_request_rules:
394 - files=vllm/v1/worker/xpu_worker.py
395 - files=vllm/v1/worker/xpu_model_runner.py
396 - files=vllm/_xpu_ops.py
334 - - files=vllm/kernels/xpu_ops.py
397 - files~=^vllm/lora/ops/xpu_ops
398 - files=vllm/lora/punica_wrapper/punica_xpu.py
399 - files=vllm/platforms/xpu.py
@@ -367,7 +429,7 @@ pull_request_rules:
429 - files=benchmarks/benchmark_serving_structured_output.py
430 - files=benchmarks/run_structured_output_benchmark.sh
431 - files=docs/features/structured_outputs.md
370 - - files=^examples/features/structured_outputs/
432 + - files~=^examples/features/structured_outputs/
433 - files~=^tests/v1/structured_output/
434 - files=tests/entrypoints/llm/test_struct_output_generate.py
435 - files~=^vllm/v1/structured_output/
@@ -382,8 +444,11 @@ pull_request_rules:
444 - label != stale
445 - or:
446 - files~=^vllm/v1/spec_decode/
447 + # Model Runner V2 keeps its speculators in a separate subtree.
448 + - files~=^vllm/v1/worker/gpu/spec_decode/
449 - files~=^tests/v1/spec_decode/
386 - - files=^examples/features/speculative_decoding/
450 + - files~=^tests/v1/e2e/spec_decode/
451 + - files~=^examples/features/speculative_decoding/
452 - files~=^vllm/model_executor/models/.*eagle.*\.py
453 - files=vllm/model_executor/models/mlp_speculator.py
454 - files~=^vllm/transformers_utils/configs/(eagle|medusa|mlp_speculator)\.py
@@ -392,6 +457,33 @@ pull_request_rules:
457 add:
458 - speculative-decoding
459
460 +# dflash covers the DFlash and DSpark speculative decoding techniques as one
461 +# workstream, matching how they are tracked in the sprint board. It is applied
462 +# in addition to speculative-decoding, never instead of it.
463 +- name: label-dflash
464 + description: Automatically apply dflash label
465 + conditions:
466 + - label != stale
467 + - or:
468 + - files~=^vllm/v1/spec_decode/dflash\.py
469 + - files~=^vllm/v1/worker/gpu/spec_decode/dflash/
470 + - files~=^vllm/model_executor/models/.*dflash.*\.py
471 + - files~=^tests/.*dflash.*\.py
472 + - title~=(?i)dflash
473 + - files~=^vllm/v1/worker/gpu/spec_decode/dspark/
474 + - files~=^vllm/model_executor/models/.*dspark.*\.py
475 + - files~=^vllm/models/deepseek_v4/nvidia/dspark\.py
476 + - files~=^vllm/models/deepseek_v4/amd/dspark\.py
477 + - files~=^vllm/models/deepseek_v4/xpu/dspark\.py
478 + - files~=^vllm/models/kimi_k3/nvidia/dspark_mla\.py
479 + - files~=^vllm/transformers_utils/configs/k3_dspark\.py
480 + - files~=^tests/.*dspark.*\.py
481 + - title~=(?i)dspark
482 + actions:
483 + label:
484 + add:
485 + - dflash
486 +
487 - name: label-mrv2
488 description: Automatically apply mrv2 label
489 conditions:
@@ -416,7 +508,6 @@ pull_request_rules:
508 - files~=_tpu
509 - files~=tpu_
510 - files~=/tpu/
419 - - files~=pallas
511 actions:
512 label:
513 add:
@@ -432,7 +523,6 @@ pull_request_rules:
523 - -files~=_tpu
524 - -files~=tpu_
525 - -files~=/tpu/
435 - - -files~=pallas
526 actions:
527 label:
528 remove:
@@ -448,7 +538,6 @@ pull_request_rules:
538 - files~=^tests/parser/
539 - files~=^tests/reasoning/
540 - files~=^tests/entrypoints/openai/.*tool.*
451 - - files~=^tests/entrypoints/anthropic/.*tool.*
541 - files~=^vllm/tool_parsers/
542 - files~=^vllm/parser/
543 - files~=^vllm/reasoning/
@@ -552,3 +641,68 @@ pull_request_rules:
641 label:
642 add:
643 - kv-connector
644 +
645 +- name: label-scheduler
646 + description: Automatically apply scheduler label
647 + conditions:
648 + - label != stale
649 + - or:
650 + - files~=^vllm/v1/core/sched/
651 + - files~=^tests/v1/core/test_scheduler.*\.py
652 + actions:
653 + label:
654 + add:
655 + - scheduler
656 +
657 +- name: label-kv-cache-manager
658 + description: Automatically apply kv-cache-manager label
659 + conditions:
660 + - label != stale
661 + - or:
662 + # Scoped to the block/cache allocator in v1/core. KV transfer between
663 + # instances is kv-connector, not this.
664 + - files~=^vllm/v1/core/(?:kv_cache|block_pool|single_type_kv_cache)
665 + - files~=^tests/v1/core/test_(?:kv_cache|single_type_kv_cache|prefix_caching).*\.py
666 + actions:
667 + label:
668 + add:
669 + - kv-cache-manager
670 +
671 +- name: label-torch-compile
672 + description: Automatically apply torch.compile label
673 + conditions:
674 + - label != stale
675 + - or:
676 + - files~=^vllm/compilation/
677 + - files~=^tests/compile/
678 + - title~=(?i)torch\.compile
679 + actions:
680 + label:
681 + add:
682 + - torch.compile
683 +
684 +- name: label-ray
685 + description: Automatically apply ray label
686 + conditions:
687 + - label != stale
688 + - or:
689 + - files~=^vllm/v1/executor/ray_
690 + - files~=^tests/.*ray_.*\.py
691 + - title~=(?i)\bray\b
692 + actions:
693 + label:
694 + add:
695 + - ray
696 +
697 +- name: label-vllm-ir
698 + description: Automatically apply vllm-ir label
699 + conditions:
700 + - label != stale
701 + - or:
702 + - files~=^vllm/ir/
703 + - files~=^tests/ir/
704 + - title~=(?i)vllm[-\s]?ir
705 + actions:
706 + label:
707 + add:
708 + - vllm-ir
.github/workflows/issue_autolabel.yml
+153
@@ -190,6 +190,159 @@ jobs:
190 },
191 ],
192 },
193 + // Model labels. Title-only by design: issue bodies carry pasted
194 + // collect_env output, tracebacks and configs that name unrelated
195 + // models, hardware and libraries.
196 + deepseek: {
197 + keywords: [{ term: "DeepSeek", searchIn: "title" }],
198 + },
199 + DSv4: {
200 + keywords: [{ term: "DSv4", searchIn: "title" }],
201 + substrings: [{ term: "deepseek-v4", searchIn: "title" }],
202 + },
203 + llama: {
204 + keywords: [{ term: "Llama", searchIn: "title" }],
205 + },
206 + mistral: {
207 + keywords: [
208 + { term: "Mistral", searchIn: "title" },
209 + { term: "Ministral", searchIn: "title" },
210 + { term: "Voxtral", searchIn: "title" },
211 + { term: "Mixtral", searchIn: "title" },
212 + { term: "Pixtral", searchIn: "title" },
213 + ],
214 + },
215 + qwen: {
216 + keywords: [{ term: "Qwen", searchIn: "title" }],
217 + },
218 + "gpt-oss": {
219 + keywords: [{ term: "harmony", searchIn: "title" }],
220 + substrings: [{ term: "gpt-oss", searchIn: "title" }],
221 + },
222 + glm: {
223 + keywords: [
224 + { term: "GLM", searchIn: "title" },
225 + { term: "ChatGLM", searchIn: "title" },
226 + ],
227 + },
228 + minimax: {
229 + substrings: [{ term: "minimax", searchIn: "title" }],
230 + },
231 + inkling: {
232 + keywords: [{ term: "Inkling", searchIn: "title" }],
233 + },
234 + // Hardware backends, mirroring the cpu/rocm/intel-gpu rules above.
235 + nvidia: {
236 + keywords: [
237 + { term: "NVIDIA", searchIn: "title" },
238 + { term: "CUTLASS", searchIn: "title" },
239 + ],
240 + },
241 + tpu: {
242 + keywords: [
243 + { term: "TPU", searchIn: "title" },
244 + { term: "XLA", searchIn: "title" },
245 + ],
246 + },
247 + // Subsystems.
248 + //
249 + // Single distinctive words go in `keywords` so the word-boundary
250 + // match keeps them from firing inside longer words. Multi-word
251 + // phrases go in `substrings`: they are specific enough on their
252 + // own, and a boundary match would miss ordinary variations --
253 + // "structured outputs", "tool parsers", "prefix caching".
254 + "speculative-decoding": {
255 + keywords: [
256 + { term: "eagle", searchIn: "title" },
257 + { term: "medusa", searchIn: "title" },
258 + ],
259 + substrings: [
260 + { term: "speculative decoding", searchIn: "title" },
261 + { term: "speculative decode", searchIn: "title" },
262 + { term: "draft model", searchIn: "title" },
263 + ],
264 + },
265 + "structured-output": {
266 + substrings: [
267 + { term: "structured output", searchIn: "title" },
268 + { term: "guided decoding", searchIn: "title" },
269 + { term: "JSON schema", searchIn: "title" },
270 + ],
271 + },
272 + "tool-calling": {
273 + substrings: [
274 + { term: "tool calling", searchIn: "title" },
275 + { term: "tool parser", searchIn: "title" },
276 + { term: "function calling", searchIn: "title" },
277 + ],
278 + },
279 + "kv-connector": {
280 + keywords: [
281 + { term: "NIXL", searchIn: "title" },
282 + { term: "LMCache", searchIn: "title" },
283 + ],
284 + substrings: [
285 + { term: "KV transfer", searchIn: "title" },
286 + { term: "disaggregat", searchIn: "title" },
287 + ],
288 + },
289 + scheduler: {
290 + keywords: [{ term: "scheduler", searchIn: "title" }],
291 + },
292 + "kv-cache-manager": {
293 + substrings: [
294 + { term: "kv cache manager", searchIn: "title" },
295 + { term: "block manager", searchIn: "title" },
296 + { term: "prefix cache", searchIn: "title" },
297 + { term: "prefix caching", searchIn: "title" },
298 + ],
299 + },
300 + "torch.compile": {
301 + substrings: [{ term: "torch.compile", searchIn: "title" }],
302 + },
303 + "multi-modality": {
304 + keywords: [
305 + { term: "multimodal", searchIn: "title" },
306 + { term: "multi-modal", searchIn: "title" },
307 + ],
308 + substrings: [
309 + { term: "image input", searchIn: "title" },
310 + { term: "audio input", searchIn: "title" },
311 + ],
312 + },
313 + "ci-failure": {
314 + keywords: [{ term: "flaky", searchIn: "title" }],
315 + substrings: [
316 + { term: "CI failure", searchIn: "title" },
317 + { term: "test failure", searchIn: "title" },
318 + ],
319 + },
320 + rl: {
321 + keywords: [{ term: "RLHF", searchIn: "title" }],
322 + substrings: [
323 + { term: "reinforcement learning", searchIn: "title" },
324 + { term: "weight update", searchIn: "title" },
325 + ],
326 + },
327 + ray: {
328 + substrings: [
329 + { term: "Ray cluster", searchIn: "title" },
330 + { term: "Ray executor", searchIn: "title" },
331 + { term: "placement group", searchIn: "title" },
332 + ],
333 + },
334 + "vllm-ir": {
335 + substrings: [
336 + { term: "vllm-ir", searchIn: "title" },
337 + { term: "vllm ir", searchIn: "title" },
338 + ],
339 + },
340 + rust: {
341 + keywords: [
342 + { term: "Rust", searchIn: "title" },
343 + { term: "PyO3", searchIn: "title" },
344 + ],
345 + },
346 // Add more label configurations here as needed
347 // example: {
348 // keywords: [...],
.pre-commit-config.yaml
+7
@@ -165,6 +165,13 @@ repos:
165 language: python
166 entry: python tools/pre_commit/generate_nightly_torch_test.py
167 files: ^requirements/test/cuda\.(in|txt)$
168 + - id: check-label-rules
169 + name: Check auto-label rules still match real files
170 + language: python
171 + entry: python tools/pre_commit/check_label_rules.py
172 + files: ^\.github/mergify\.yml$
173 + pass_filenames: false
174 + additional_dependencies: [PyYAML, regex]
175 - id: mypy-3.10 # TODO: Use https://github.com/pre-commit/mirrors-mypy when mypy setup is less awkward
176 name: Run mypy for Python 3.10
177 entry: python tools/pre_commit/mypy.py "3.10"
docs/contributing/labels.md new
+95
@@ -0,0 +1,95 @@
1 +# Labels
2 +
3 +vLLM applies most labels automatically. Three systems do the work:
4 +
5 +| | Config | Applies to |
6 +| --- | --- | --- |
7 +| Mergify | `.github/mergify.yml` | pull requests, by changed file path or title |
8 +| GitHub Action | `.github/workflows/issue_autolabel.yml` | issues, by title keyword |
9 +| Issue templates | `.github/ISSUE_TEMPLATE/*.yml` | issues, by which template was used |
10 +
11 +A label that none of them applies has to be applied by hand, and in practice
12 +that means it mostly doesn't get applied at all.
13 +
14 +A template's `labels:` field must name a label that already exists — GitHub
15 +silently skips one that doesn't, with no error anywhere.
16 +
17 +`mergify.yml` also drives reviewer auto-assignment; see
18 +[Collaboration](../governance/collaboration.md) for adding yourself as a
19 +maintainer of an area.
20 +
21 +## Adding a label
22 +
23 +A new label needs three things. If it can't have all three, don't create it.
24 +
25 +1. **An audience.** Someone has to want to filter on it. "It would be nice to
26 + know" isn't enough.
27 +2. **An owner.** A person or SIG who triages that queue. Without one the label
28 + accumulates issues nobody reads.
29 +3. **A rule, in the same PR.** If you can't write a precise rule, the label
30 + depends on humans remembering it exists.
31 +
32 +## Measure before you argue
33 +
34 +Volume claims should come from the repo, not from intuition. vLLM squash-merges,
35 +so one commit on `main` is one PR:
36 +
37 +```bash
38 +git log --since="6 months ago" --oneline -- <path> | wc -l
39 +```
40 +
41 +For reference, at the time of writing: `speculative-decoding` 81, `llama` 85,
42 +`mistral` 72, `tpu` 14. A subsystem in that range clears the bar comfortably.
43 +Something in the low teens needs a stronger argument than volume.
44 +
45 +The same applies to claiming a rule is too broad. Check what it actually labels
46 +before rewriting it:
47 +
48 +```bash
49 +gh pr list --repo vllm-project/vllm --state merged --limit 200 \
50 + --json number,title,labels
51 +```
52 +
53 +Several rules in this repo have been called over-broad on inspection and turned
54 +out to be accurate when measured.
55 +
56 +## Writing conditions
57 +
58 +**`files=` is exact equality. `files~=` is a regex.** Writing a pattern after
59 +`=` compares the pattern text to each filename, so it can never match:
60 +
61 +```yaml
62 +- files=^examples/features/speculative_decoding/ # never fires
63 +- files~=^examples/features/speculative_decoding/ # correct
64 +```
65 +
66 +**Anchor file patterns.** An unanchored fragment matches far more than intended
67 +— `files~=cuda` also matches `requirements/cuda.txt`.
68 +
69 +**Watch for names that contain other names.** `midashenglm.py` contains "glm"
70 +but is unrelated to GLM, so the `glm` rule anchors to the start of the filename.
71 +
72 +**Keep an eye on where model code lives.** Newer models live in
73 +`vllm/models/<model>/` rather than `vllm/model_executor/models/`. A rule that
74 +only knows the old location silently stops matching.
75 +
76 +**Prefer titles for issue keywords.** Issue bodies contain pasted `collect_env`
77 +output, tracebacks and configs that mention unrelated hardware and libraries.
78 +Body matching tags the reporter's environment rather than the topic.
79 +
80 +**Single words go in `keywords`, phrases in `substrings`.** `keywords` matches on
81 +word boundaries, which stops a term firing inside a longer word but also misses
82 +ordinary variations — `structured output` will not match "structured outputs".
83 +
84 +## Keeping rules honest
85 +
86 +`tools/pre_commit/check_label_rules.py` fails if any file condition in
87 +`mergify.yml` matches nothing in the tree. It runs via pre-commit when that file
88 +changes. This catches conditions that were correct when written and rotted when
89 +the code moved.
90 +
91 +## Not everything needs a label
92 +
93 +Triage and workflow labels — `bug`, `stale`, `ready`, `RFC`, `needs-rebase` —
94 +and the bot provenance tags are applied by other mechanisms or by hand, and are
95 +intentionally outside this system.
tools/pre_commit/check_label_rules.py new
+97
@@ -0,0 +1,97 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""Check that auto-labeling rules still match real files.
4 +
5 +Mergify silently ignores a condition that can never match, so a file-path
6 +condition keeps passing review long after the file it names has moved or been
7 +deleted. This walks every `files=` / `files~=` condition in `.github/mergify.yml`
8 +and fails if one matches nothing in the tree.
9 +
10 +Usage:
11 + python tools/pre_commit/check_label_rules.py
12 +"""
13 +
14 +import subprocess
15 +import sys
16 +
17 +import regex as re
18 +import yaml
19 +
20 +MERGIFY = ".github/mergify.yml"
21 +
22 +
23 +def tracked_files() -> list[str]:
24 + out = subprocess.run(
25 + ["git", "ls-files"], capture_output=True, text=True, check=True
26 + ).stdout
27 + return [line for line in out.split("\n") if line]
28 +
29 +
30 +def file_conditions(node, found: list[str]) -> list[str]:
31 + """Collect every string condition nested under a rule's `conditions`."""
32 + if isinstance(node, dict):
33 + for value in node.values():
34 + file_conditions(value, found)
35 + elif isinstance(node, list):
36 + for value in node:
37 + file_conditions(value, found)
38 + elif isinstance(node, str):
39 + found.append(node)
40 + return found
41 +
42 +
43 +def check(files: list[str]) -> list[tuple[str, str, str]]:
44 + with open(MERGIFY) as f:
45 + rules = yaml.safe_load(f)["pull_request_rules"]
46 +
47 + # removed-files is excluded: it names paths the tree no longer has.
48 + attrs = ("files", "added-files", "modified-files")
49 +
50 + dead = []
51 + for rule in rules:
52 + for cond in file_conditions(rule.get("conditions", []), []):
53 + cond = cond.strip()
54 + # Negated conditions (label-tpu-remove) are expected to match nothing.
55 + if cond.startswith("-"):
56 + continue
57 + attr = next(
58 + (
59 + a
60 + for a in attrs
61 + if cond.startswith(f"{a}~=") or cond.startswith(f"{a}=")
62 + ),
63 + None,
64 + )
65 + if attr is None:
66 + continue
67 + if cond.startswith(f"{attr}~="):
68 + pattern = cond[len(attr) + 2 :]
69 + try:
70 + regex = re.compile(pattern)
71 + except re.error as exc:
72 + dead.append((rule["name"], cond, f"invalid regex: {exc}"))
73 + continue
74 + matched = any(regex.search(f) for f in files)
75 + else:
76 + matched = cond[len(attr) + 1 :] in files
77 + if not matched:
78 + dead.append((rule["name"], cond, "matches no tracked file"))
79 + return dead
80 +
81 +
82 +def main() -> int:
83 + dead = check(tracked_files())
84 + if not dead:
85 + return 0
86 + print(f"{len(dead)} label condition(s) match nothing:\n", file=sys.stderr)
87 + for name, cond, why in dead:
88 + print(f" [{name}] {cond}\n {why}", file=sys.stderr)
89 + print(
90 + "\nRepoint the condition at the file's new location, or remove it.",
91 + file=sys.stderr,
92 + )
93 + return 1
94 +
95 +
96 +if __name__ == "__main__":
97 + raise SystemExit(main())