# 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//.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: ` 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!='` 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: ` 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