[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())