summaryrefslogtreecommitdiff
path: root/.mergify.yml
blob: 23b1682da1f9b5953f9789399b9b1894537fceda (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
# yaml-language-server: $schema=https://docs.mergify.com/configuration/file-format/
# Central Mergify configuration for the `vyos` GitHub organization.
#
# This file lives at vyos/mergify:.mergify.yml and is referenced from
# every vyos/<repo>/.mergify.yml via `extends: mergify`.
#
# Inheritance semantics (verbatim from docs.mergify.com/configuration/sharing):
#   1. "The configuration from the specified repository will be loaded and
#      applied before the one in the current repository." — i.e., this file
#      (the parent) loads first; the per-repo file (the child) is merged in
#      afterwards and same-name rules in the child replace the parent's.
#   2. "Values in the `defaults` and `commands_restrictions` key will be
#      merged and remote default values will apply to local configuration,
#      unless a same default value already exist in the local configuration."
#   3. "Values in the `shared` key will not be merged and shared between
#      local and remote configurations." — define YAML anchors locally only.

defaults:
  actions:
    backport:
      # Create the backport PR even when the cherry-pick conflicts. Mergify
      # commits the literal conflict markers (<<<<<<< / ======= / >>>>>>>)
      # into the destination branch, opens the PR, applies the
      # `backport-conflict` label, and assigns the source PR author. The PR
      # is the workspace where the conflict gets resolved: the assignee pushes
      # a resolution commit onto the same branch, CI flips green, Mergify
      # merges.
      #
      # `label_conflicts` fires ONLY on backport (cherry-pick) conflicts.
      # The separate `Label conflicting pull requests` rule below (predicate:
      # `conflict`) fires on base-branch merge conflicts and would NOT catch
      # this case — cherry-pick conflict markers live in committed content,
      # not as unmerged paths, so the `conflict` attribute stays false. The
      # two labels are intentionally distinct (`backport-conflict` vs
      # `conflicts`) so triage can tell them apart.
      #
      # Why the explicit `{{ author }}` assignment: a labeled-but-unassigned
      # PR is easy to overlook. Pinging the source PR author closes the
      # accountability loop — they wrote the change, they know what the
      # backport should look like.
      #
      # Merge is blocked while the label is present — see the
      # `merge_protections` block at the bottom of this file. Mergify
      # documents that merge_protections are "safe to edit live;
      # re-evaluations happen automatically", so a PR that gains the
      # label after queueing will fail the merge check at the next
      # evaluation. Active ejection of a queued PR vs. holding it
      # indefinitely is not explicitly documented; manual fallback is
      # `@Mergifyio dequeue` (allowed to Maintainers per
      # commands_restrictions below).
      #
      # This does NOT replace branch protection. A determined operator can
      # still bypass via the GitHub "Merge" button if branch protection
      # doesn't require Mergify checks. The defense-in-depth piece (required
      # CI status that fails on `<<<<<<<` in tracked files, or branch-
      # protection ruleset on the label) is tracked under T8850 (Cat 1
      # branch governance rulesets).
      #
      # Incident history (2026-05-12):
      # vyos/vyos-documentation#1994 was backported via `@Mergifyio backport
      # sagitta circinus`. Cherry-pick conflicted in docs/conf.py; Mergify's
      # default (ignore_conflicts: true) committed the markers, the resulting
      # PRs (#1998, #1999) were merged anyway, and RTD builds on both branches
      # failed with `SyntaxError: invalid syntax (conf.py)`. Fix-forwards
      # #2000 (circinus) and #2001 (sagitta) removed the markers and
      # re-applied the patch correctly. A prior flip to `ignore_conflicts:
      # false` stopped silent merges but eliminated the in-PR workspace,
      # leaving operators with no place to resolve the conflict — this
      # config restores the workspace while adding the label + assignee so
      # the conflicted PR is impossible to miss in triage.
      ignore_conflicts: true
      label_conflicts: backport-conflict
      assignees:
        - "{{ author }}"

pull_request_rules:
  - name: Label conflicting pull requests
    description: Add a label to a pull request with conflict to spot it easily
    conditions:
      - conflict
      - '-closed'
    actions:
      label:
        toggle:
          - conflicts

  # ------------------------------------------------------------------
  # Title + body sanity checks (T8966 — strict T-ID moved per-repo).
  #
  # The strict `[scope: ]T<digits>: <text>` format check was REMOVED from
  # this central config and relocated to each PRODUCT repo's own
  # .github/mergify.yml (rule `Flag product T-ID format violation…`,
  # toggling `invalid-task-id`). Non-product repos that extend this config
  # no longer get T-ID enforcement; they get the generic, convention-
  # agnostic title + body checks below instead. See https://vyos.dev/T8966.
  # ------------------------------------------------------------------

  - name: Flag malformed PR title
    description: >
      Generic, convention-agnostic title sanity check (every repo that extends
      this config). Product-specific T-ID format is enforced per-repo via the
      invalid-task-id rule. Toggles invalid-title when the title is empty/too
      short or carries a WIP / do-not-merge marker.
    conditions:
      - '-closed'
      - '-merged'
      - '-author~=\[bot\]$'
      - 'author!=copilot-swe-agent'
      - 'author!=vyosbot'
      - or:
          - '#title < 10'
          - 'title~=(?i)\bwip\b'
          - 'title~=(?i)do.not.merge'
    actions:
      label:
        toggle:
          - invalid-title

  - name: Flag empty or too-short PR body
    description: >
      v1 minimal body check — PR description must contain some content.
      Mergify's `body` strips comments, so this catches blank/near-blank
      descriptions only; template-structure + checkbox matching is v2.
      The threshold (80) sits below the smallest comment-stripped template
      baseline (vyos-documentation, ~239) measured in Phase 0.
    conditions:
      - '-closed'
      - '-merged'
      - '-author~=\[bot\]$'
      - 'author!=copilot-swe-agent'
      - 'author!=vyosbot'
      - '#body < 80'
    actions:
      label:
        toggle:
          - invalid-body

  # ------------------------------------------------------------------
  # Opt-in auto-update — refresh PR with base when it falls behind.
  #
  # Uses the `update` action (merges base into PR via merge commit), NOT
  # `rebase`. Three reasons for the choice in this environment:
  #
  #   1. `update` works on fork PRs indefinitely; `rebase` loses fork
  #      support after 2026-07-01 per Mergify docs. We get fork PRs on
  #      vyos/vyos-1x, vyos/vyos-documentation, and a few others.
  #
  #   2. `update` does NOT rewrite commit SHAs. The gen-1 cross-org
  #      mirror pipeline (vyos/* → VyOS-Networks/*) copies refs by SHA;
  #      a force-pushed rebase would create mirror noise at best and
  #      break consumer PRs at worst. `update` preserves SHAs.
  #
  #   3. `update` is cheaper for CI — one new merge commit triggers one
  #      re-run; rebase changes every SHA and re-runs the entire PR.
  #
  # Opt-in via the `auto-update` label so this is per-PR (or per-repo
  # via default-label automation) rather than a fleet-wide behavior
  # change. Repos that want it apply the label; repos that don't,
  # ignore the rule.
  #
  # Skipped on `backport-conflict` — we don't want to auto-update a PR
  # whose conflict workspace is mid-resolution. Skipped on draft PRs
  # and PRs already in merge-conflict with base (the existing
  # `conflicts` label rule above flags those for human attention).
  # ------------------------------------------------------------------
  - name: Auto-update PR when behind base (opt-in via label)
    description: >
      Merge base branch into PR via `update` action when the PR carries
      the `auto-update` label and has at least one commit behind base.
      Preserves SHAs (fork-safe, mirror-safe, CI-cache-friendly).
    conditions:
      - label=auto-update
      - '#commits-behind>0'
      - '-draft'
      - '-closed'
      - '-merged'
      - '-conflict'
      - '-label=backport-conflict'
    actions:
      update: {}

  # ------------------------------------------------------------------
  # Auto-close PRs targeting EOL release branches.
  #
  # Replaces vyos/vyos-1x:.github/workflows/pr-auto-close.yml, which only
  # covered `sagitta`. Closes every PR (including Mergify-created
  # backports) opened against `sagitta`, `crux`, or `circinus` in any
  # vyos/* repo that extends this config, except the two excluded below.
  #
  # Excluded repos:
  #   - vyos-documentation — still accepts per-branch docs maintenance
  #     and backports to `sagitta` and `circinus`.
  #   - vyos.vyos — Ansible Galaxy collection on `main`; listed
  #     defensively so a future branch named `sagitta`/`crux`/`circinus`
  #     would not be auto-closed.
  #
  # Mergify's `repository-name` attribute is the bare repo name (no
  # org prefix). Since this central config is consumed only by vyos/*
  # repos via `extends: mergify`, the bare name is unambiguous.
  #
  # Backport interaction: the `defaults.actions.backport` block above
  # creates backport PRs targeting whatever branch a Maintainer asks
  # for. If a Maintainer issues `@Mergifyio backport sagitta` on a
  # vyos/* repo other than the two excluded ones, the backport PR is
  # created and then immediately closed by this rule. That is the
  # intended behavior — backports to EOL branches are not accepted.
  # ------------------------------------------------------------------
  - name: Auto-close PRs targeting EOL release branches
    description: >
      Close PRs (and Mergify-created backports) targeting the EOL release
      branches sagitta, crux, and circinus. Excludes vyos-documentation
      and vyos.vyos. Replaces vyos/vyos-1x:.github/workflows/pr-auto-close.yml.
    conditions:
      - '-closed'
      - '-merged'
      - or:
          - base=sagitta
          - base=crux
          - base=circinus
      - repository-name != vyos-documentation
      - repository-name != vyos.vyos
    actions:
      close:
        message: >
          Pull requests targeting EOL release branches (`sagitta`, `crux`,
          `circinus`) are not accepted and have been closed automatically.

  # ------------------------------------------------------------------
  # Base-branch labeling — replaces vyos/.github:add-pr-labels.yml.
  #
  # `toggle:` adds the label when the base matches and removes it when
  # the base changes. `-closed` is INTENTIONALLY omitted — including it
  # would cause toggle to remove the label on PR close/merge, breaking
  # historical query like `is:pr merged label:current`. Labels are kept
  # post-merge for triage history; toggle only reacts to base-branch
  # changes, not lifecycle state.
  #
  # The `labeler.yml` config in vyos/.github (consumed by the GHA being
  # retired) covers four branches: equuleus, current, circinus, sagitta.
  # ------------------------------------------------------------------
  - name: Label PR with equuleus
    description: Mirror base-branch as label. Replaces vyos/.github:add-pr-labels.yml.
    conditions: [base=equuleus]
    actions: { label: { toggle: [equuleus] } }
  - name: Label PR with current
    description: Mirror base-branch as label. Replaces vyos/.github:add-pr-labels.yml.
    conditions: [base=current]
    actions: { label: { toggle: [current] } }
  # rollout 1c (T8943): additive label rules for the renamed trunk names.
  # `rolling` replaces `current` as the release-train trunk; `production`
  # is the new default for non-release-train repos. The `current` rule
  # above is retained through the bake period (removed in Task 7 cleanup).
  - name: Label PR with rolling
    description: Mirror base-branch as label. Replaces vyos/.github:add-pr-labels.yml.
    conditions: [base=rolling]
    actions: { label: { toggle: [rolling] } }
  - name: Label PR with production
    description: Mirror base-branch as label. Replaces vyos/.github:add-pr-labels.yml.
    conditions: [base=production]
    actions: { label: { toggle: [production] } }
  - name: Label PR with circinus
    description: Mirror base-branch as label. Replaces vyos/.github:add-pr-labels.yml.
    conditions: [base=circinus]
    actions: { label: { toggle: [circinus] } }
  - name: Label PR with sagitta
    description: Mirror base-branch as label. Replaces vyos/.github:add-pr-labels.yml.
    conditions: [base=sagitta]
    actions: { label: { toggle: [sagitta] } }

  # ------------------------------------------------------------------
  # Auto-assign PR author — replaces vyos/.github:assign-author.yml.
  #
  # The negative-regex condition `-author~=\[bot\]$` skips identities
  # ending in [bot] (GitHub App-bot convention per data/github.md).
  # The literal-author exclusions cover non-suffix identities in the
  # current fleet (copilot-swe-agent, vyosbot legacy PAT).
  # Maintenance: when a new non-[bot]-suffix bot identity joins the
  # fleet, append `'author!=<login>'` here.
  # ------------------------------------------------------------------
  - name: Auto-assign PR author
    description: Assign the PR author. Replaces vyos/.github:assign-author.yml.
    conditions:
      - -closed
      - -merged
      - '-author~=\[bot\]$'
      - 'author!=copilot-swe-agent'
      - 'author!=vyosbot'
    actions:
      assign:
        add_users:
          - "{{ author }}"

commands_restrictions:
  # Both Maintainers teams are listed for defense-in-depth.
  #
  # This file is consumed by vyos/* repos via the org-local
  # `extends: mergify` (which resolves to vyos/mergify within the vyos
  # org). On a vyos repo PR, `@vyos/maintainers` is the local team and
  # matches normally; `@VyOS-Networks/maintainers` is cross-org and
  # evaluates to a non-match (Mergify surfaces it as
  # "Team @VyOS-Networks/maintainers is not part of the organization
  # vyos") — but does not fail the `or:` rule, so any of the remaining
  # branches can still match.
  #
  # The defense-in-depth case is the cross-org mirror pipeline: if this
  # file (or any per-repo file copying these conditions) is ever
  # replicated byte-for-byte to a VyOS-Networks repo without being
  # migrated to `extends:`, the `@VyOS-Networks/maintainers` entry
  # ensures VyOS-Networks Maintainers can still issue Mergify commands
  # on the mirrored PR.
  #
  # See VyOS-Networks/vyatta-cfg#33 for the incident that motivated
  # listing both teams: the T8531 predecessor inline config (single
  # team, `@vyos/maintainers`) was mirrored to VyOS-Networks/vyatta-cfg
  # and rejected `@Mergifyio backport` from a VyOS-Networks Maintainer.
  backport: &allowed
    conditions:
      - or:
          - sender=@vyos/maintainers
          - sender=@VyOS-Networks/maintainers
          - sender=vyosbot
          # vyos-bot[bot] is the App identity that replaces the legacy vyosbot
          # user PAT under Mirror Pipeline Rollout 1 (PAT → App migration).
          # The App's GitHub login is literal `vyos-bot[bot]`. Single-quoted
          # to keep YAML parsers from interpreting `[bot]` as flow-sequence
          # syntax. Empirically caught when canary mirror PR vyos-bot[bot]
          # @Mergifyio backport was rejected (VyOS-Networks/mirror-canary#6,
          # 2026-05-26). `sender=vyosbot` retained for the 1b staggered
          # migration window; removal deferred to 1b Task 12 post-PAT-revoke.
          - 'sender=vyos-bot[bot]'
  copy: *allowed
  dequeue: *allowed
  queue: *allowed
  rebase: *allowed
  refresh: *allowed
  requeue: *allowed
  squash: *allowed
  update: *allowed

# ------------------------------------------------------------------
# Same-repo (and cross-repo, same-org) PR dependencies — `Depends-On:`
# keyword in PR body.
#
# Mergify ships a built-in merge protection that reads `Depends-On:`
# lines from the PR description and blocks merge until every referenced
# PR has merged. The protection is always on — no YAML required to
# enable it. This block exists so contributors reading the central
# config can find the feature.
#
# Syntax (any of the following, repeated as needed in the PR body):
#
#   Depends-On: #42                                  # same-repo
#   Depends-On: owner/repo#42                        # qualified, same org
#   Depends-On: https://github.com/owner/repo/pull/42 # full URL
#
# Constraints (from Mergify built-in-protections docs):
#   - The referenced PR must live in a repository where Mergify is
#     enabled and that belongs to the same GitHub organization. Both
#     vyos/* and VyOS-Networks/* have the Mergify app installed with
#     `repository_selection: all`, so any repo in either org qualifies.
#   - Self-references and cycles are silently ignored.
#   - When dependencies target different branches, `@Mergifyio refresh`
#     may be required to re-evaluate protections (per Mergify docs).
#
# Merge-queue interaction (theoretical — no consumer repo in either
# org defines `queue_rules` as of 2026-05-15):
#   - When a repo adopts the merge queue, the built-in protection
#     composes with that repo's `queue_rules.queue_conditions`: a PR
#     with an unmet dependency stays out of the queue.
#   - Stacked PRs (each PR's base = previous PR's head AND each PR
#     carries `Depends-On: #N` for the previous) are auto-detected by
#     the queue and merged in order against the stack root. The
#     `Depends-On:` marker is load-bearing — without it the PRs queue
#     independently.
#
# No condition attribute exposes whether a declared dependency is
# satisfied; that state lives inside the built-in protection. The
# `depends-on` attribute on a PR is the list of declared references,
# not their merged state — do not write `pull_request_rules` that try
# to label PRs by dependency satisfaction.
# ------------------------------------------------------------------

merge_protections:
  # Block merge while the backport-conflict label is set on a PR. The
  # `backport` action above adds this label whenever a backport cherry-pick
  # produces conflict markers; this rule ensures the conflicted PR cannot
  # proceed to merge until a human removes the label (which they should
  # only do after pushing a resolution commit that overwrites the markers).
  #
  # Why merge_protections and not a pull_request_rules `dequeue` action:
  # Mergify exposes `dequeue` as a user-facing slash command and as a key
  # under `commands_restrictions`, but NOT as a YAML action in
  # pull_request_rules. `merge_protections` is the declarative equivalent —
  # a PR with the label fails the success_conditions and is held back from
  # merge; manual `@Mergifyio dequeue` remains available as the escape
  # hatch (allowed to Maintainers per commands_restrictions above).
  #
  # Inheritance: per Mergify docs, merge_protections from `extends:`
  # merges across parent and child, with same-named rules in the child
  # overriding the parent. Consumer repos can disable this rule by
  # defining a same-named rule locally with weaker conditions.
  #
  # Caveat: merge_protections only governs Mergify-driven merges. A
  # determined operator can still click GitHub's merge button if branch
  # protection doesn't require Mergify checks. Defense-in-depth (required
  # CI status, branch-protection ruleset on the label) is tracked under
  # T8850 Cat 1 branch governance.
  - name: backport-conflict label must be absent to merge
    description: >
      Block merge while the backport-conflict label is present. The label
      is auto-applied by the backport action on cherry-pick conflicts and
      must be manually removed after the conflict is resolved in the PR.
    if:
      - label = backport-conflict
    success_conditions:
      - label != backport-conflict

  # Block merge while the `conflicts` label is set. The label is applied
  # by the `Label conflicting pull requests` rule above (Mergify's
  # built-in `conflict` attribute = GitHub's `mergeable_state == "dirty"`)
  # and by vyos/.github:scripts/check-pr-conflicts.py (committed conflict
  # markers on non-Mergify PRs). Either case must be resolved — by
  # rebasing/merging the base, or by removing committed marker text —
  # before merge can proceed. The script ignores PRs that already carry
  # `backport-conflict` so the two labels stay disjoint in practice (see
  # T8934).
  - name: conflicts label must be absent to merge
    description: >
      Block merge while the conflicts label is present. The label is
      auto-applied by the central `Label conflicting pull requests`
      rule (git-level merge conflicts) and by
      vyos/.github:scripts/check-pr-conflicts.py (committed conflict
      markers on non-Mergify PRs). Must be cleared by resolving the
      conflict.
    if:
      - label = conflicts
    success_conditions:
      - label != conflicts

  # Block merge while the `invalid-title` label is set. The label is
  # auto-toggled by the central `Flag malformed PR title` rule above when
  # the PR title is empty/too short or carries a WIP / do-not-merge marker.
  # (Strict `[scope: ]T<digits>: <text>` enforcement moved to the per-repo
  # `invalid-task-id` rule under T8966.)
  - name: invalid-title label must be absent to merge
    description: >
      Block merge while the invalid-title label is present. The label is
      auto-toggled by the `Flag malformed PR title` rule when the PR title
      is empty/too short or carries a WIP / do-not-merge marker. Must be
      cleared by fixing the title.
    if:
      - label = invalid-title
    success_conditions:
      - label != invalid-title

  # Block merge while the `invalid-body` label is set. The label is
  # auto-toggled by the central `Flag empty or too-short PR body` rule
  # above when the PR description is blank/near-blank (T8966).
  - name: invalid-body label must be absent to merge
    description: >
      Block merge while the invalid-body label is present. Auto-toggled by
      the `Flag empty or too-short PR body` rule when the PR description is
      blank or near-blank. Must be cleared by adding a description.
    if:
      - label = invalid-body
    success_conditions:
      - label != invalid-body

  # Dormant for non-product repos (the `if:` never matches where the label is
  # never set). Activated per-repo by the `Flag product T-ID format violation
  # in PR title or commit messages` rule that product repos add to their own
  # .github/mergify.yml (rollout T8966 / IS-529). Added BEFORE the per-repo
  # rules so `invalid-task-id` blocks the moment a product repo sets it.
  - name: invalid-task-id label must be absent to merge
    description: >
      Block merge while the invalid-task-id label is present. Set by the
      per-repo product T-ID rule (product repos only); dormant where the
      label is never applied.
    if:
      - label = invalid-task-id
    success_conditions:
      - label != invalid-task-id

  # Block merge while a maintainer-applied WIP label is present. Unlike the
  # auto-applied gates above, WIP is set by hand to hold a PR from merging
  # (it has teeth once the Mergify Merge Protections check is made required —
  # T8850 Cat 1). Bot authors are exempt via the if: scope so cross-org
  # mirror-pipeline PRs (vyos-bot[bot]) to rolling are never held — they
  # merge right away via the pipeline's admin-merge regardless.
  - name: WIP label must be absent to merge
    description: >
      Block merge while the WIP label is present. Maintainers apply WIP by
      hand to hold a PR. Bot authors (mirror pipeline vyos-bot, mergify) are
      exempt so mirror PRs to rolling are never held.
    if:
      - label = WIP
      - '-author~=\[bot\]$'
      - 'author!=copilot-swe-agent'
      - 'author!=vyosbot'
    success_conditions:
      - label != WIP
merge_protections_settings:
  reporting_method: check-runs