From b1a6e4e496da5d88eefd54d2fcfd61a622a54bcd Mon Sep 17 00:00:00 2001 From: Felipe Bergamin Date: Wed, 21 Oct 2026 17:26:52 -0300 Subject: [PATCH 1/2] feat(notifications): copy tree recipients into regression/ Seed backend/data/notifications/regression/ with one unscoped entry per non-hardware subscription tree, copied from default_recipients. After this commit the directory is edited by hand. default.yaml holds the regression To address used with --add-mailing-lists. Nothing reads these files yet. Part of #2114 Assisted-by: Cursor Signed-off-by: Felipe Bergamin Co-authored-by: Cursor --- backend/data/notifications/regression/amlogic.yaml | 3 +++ backend/data/notifications/regression/android.yaml | 5 +++++ backend/data/notifications/regression/ardb.yaml | 3 +++ backend/data/notifications/regression/arm64.yaml | 4 ++++ backend/data/notifications/regression/arnd.yaml | 3 +++ backend/data/notifications/regression/broonie-misc.yaml | 3 +++ backend/data/notifications/regression/broonie-regmap.yaml | 3 +++ .../data/notifications/regression/broonie-regulator.yaml | 3 +++ backend/data/notifications/regression/broonie-sound.yaml | 3 +++ backend/data/notifications/regression/broonie-spi.yaml | 3 +++ backend/data/notifications/regression/chrome-platform.yaml | 4 ++++ backend/data/notifications/regression/chromiumos.yaml | 6 ++++++ backend/data/notifications/regression/cip.yaml | 3 +++ backend/data/notifications/regression/clk.yaml | 4 ++++ backend/data/notifications/regression/default.yaml | 3 +++ backend/data/notifications/regression/efi.yaml | 3 +++ backend/data/notifications/regression/hyperv.yaml | 3 +++ backend/data/notifications/regression/khilman.yaml | 3 +++ backend/data/notifications/regression/krzysztof.yaml | 3 +++ backend/data/notifications/regression/kselftest.yaml | 3 +++ backend/data/notifications/regression/lee-backlight.yaml | 3 +++ backend/data/notifications/regression/lee-mfd.yaml | 3 +++ backend/data/notifications/regression/linux-pci.yaml | 3 +++ backend/data/notifications/regression/mainline.yaml | 3 +++ backend/data/notifications/regression/media-committers.yaml | 3 +++ backend/data/notifications/regression/media.yaml | 3 +++ backend/data/notifications/regression/mediatek.yaml | 3 +++ backend/data/notifications/regression/next.yaml | 3 +++ backend/data/notifications/regression/omap.yaml | 3 +++ backend/data/notifications/regression/padovan.yaml | 3 +++ backend/data/notifications/regression/pm.yaml | 3 +++ backend/data/notifications/regression/qcom.yaml | 4 ++++ backend/data/notifications/regression/renesas.yaml | 3 +++ backend/data/notifications/regression/riscv.yaml | 3 +++ backend/data/notifications/regression/robh.yaml | 4 ++++ backend/data/notifications/regression/rppt.yaml | 4 ++++ backend/data/notifications/regression/sashal-next.yaml | 3 +++ backend/data/notifications/regression/soc.yaml | 3 +++ backend/data/notifications/regression/stable-rc.yaml | 3 +++ backend/data/notifications/regression/stable-rt.yaml | 3 +++ backend/data/notifications/regression/stable.yaml | 3 +++ backend/data/notifications/regression/tegra.yaml | 3 +++ backend/data/notifications/regression/thermal.yaml | 3 +++ backend/data/notifications/regression/tip.yaml | 3 +++ backend/data/notifications/regression/ulfh.yaml | 3 +++ backend/data/notifications/regression/vireshk.yaml | 3 +++ 46 files changed, 149 insertions(+) create mode 100644 backend/data/notifications/regression/amlogic.yaml create mode 100644 backend/data/notifications/regression/android.yaml create mode 100644 backend/data/notifications/regression/ardb.yaml create mode 100644 backend/data/notifications/regression/arm64.yaml create mode 100644 backend/data/notifications/regression/arnd.yaml create mode 100644 backend/data/notifications/regression/broonie-misc.yaml create mode 100644 backend/data/notifications/regression/broonie-regmap.yaml create mode 100644 backend/data/notifications/regression/broonie-regulator.yaml create mode 100644 backend/data/notifications/regression/broonie-sound.yaml create mode 100644 backend/data/notifications/regression/broonie-spi.yaml create mode 100644 backend/data/notifications/regression/chrome-platform.yaml create mode 100644 backend/data/notifications/regression/chromiumos.yaml create mode 100644 backend/data/notifications/regression/cip.yaml create mode 100644 backend/data/notifications/regression/clk.yaml create mode 100644 backend/data/notifications/regression/default.yaml create mode 100644 backend/data/notifications/regression/efi.yaml create mode 100644 backend/data/notifications/regression/hyperv.yaml create mode 100644 backend/data/notifications/regression/khilman.yaml create mode 100644 backend/data/notifications/regression/krzysztof.yaml create mode 100644 backend/data/notifications/regression/kselftest.yaml create mode 100644 backend/data/notifications/regression/lee-backlight.yaml create mode 100644 backend/data/notifications/regression/lee-mfd.yaml create mode 100644 backend/data/notifications/regression/linux-pci.yaml create mode 100644 backend/data/notifications/regression/mainline.yaml create mode 100644 backend/data/notifications/regression/media-committers.yaml create mode 100644 backend/data/notifications/regression/media.yaml create mode 100644 backend/data/notifications/regression/mediatek.yaml create mode 100644 backend/data/notifications/regression/next.yaml create mode 100644 backend/data/notifications/regression/omap.yaml create mode 100644 backend/data/notifications/regression/padovan.yaml create mode 100644 backend/data/notifications/regression/pm.yaml create mode 100644 backend/data/notifications/regression/qcom.yaml create mode 100644 backend/data/notifications/regression/renesas.yaml create mode 100644 backend/data/notifications/regression/riscv.yaml create mode 100644 backend/data/notifications/regression/robh.yaml create mode 100644 backend/data/notifications/regression/rppt.yaml create mode 100644 backend/data/notifications/regression/sashal-next.yaml create mode 100644 backend/data/notifications/regression/soc.yaml create mode 100644 backend/data/notifications/regression/stable-rc.yaml create mode 100644 backend/data/notifications/regression/stable-rt.yaml create mode 100644 backend/data/notifications/regression/stable.yaml create mode 100644 backend/data/notifications/regression/tegra.yaml create mode 100644 backend/data/notifications/regression/thermal.yaml create mode 100644 backend/data/notifications/regression/tip.yaml create mode 100644 backend/data/notifications/regression/ulfh.yaml create mode 100644 backend/data/notifications/regression/vireshk.yaml diff --git a/backend/data/notifications/regression/amlogic.yaml b/backend/data/notifications/regression/amlogic.yaml new file mode 100644 index 000000000..98a431726 --- /dev/null +++ b/backend/data/notifications/regression/amlogic.yaml @@ -0,0 +1,3 @@ +amlogic: + - recipients: + - linux-amlogic@lists.infradead.org diff --git a/backend/data/notifications/regression/android.yaml b/backend/data/notifications/regression/android.yaml new file mode 100644 index 000000000..2a98324c3 --- /dev/null +++ b/backend/data/notifications/regression/android.yaml @@ -0,0 +1,5 @@ +android: + - recipients: + - tkjos@google.com + - joneslee@google.com + - ynaffit@google.com diff --git a/backend/data/notifications/regression/ardb.yaml b/backend/data/notifications/regression/ardb.yaml new file mode 100644 index 000000000..f5dba6788 --- /dev/null +++ b/backend/data/notifications/regression/ardb.yaml @@ -0,0 +1,3 @@ +ardb: + - recipients: + - ardb@kernel.org diff --git a/backend/data/notifications/regression/arm64.yaml b/backend/data/notifications/regression/arm64.yaml new file mode 100644 index 000000000..2ea60e48b --- /dev/null +++ b/backend/data/notifications/regression/arm64.yaml @@ -0,0 +1,4 @@ +arm64: + - recipients: + - will@kernel.org + - linux-arm-kernel@lists.infradead.org diff --git a/backend/data/notifications/regression/arnd.yaml b/backend/data/notifications/regression/arnd.yaml new file mode 100644 index 000000000..0600e5c88 --- /dev/null +++ b/backend/data/notifications/regression/arnd.yaml @@ -0,0 +1,3 @@ +arnd: + - recipients: + - arnd@arndb.de diff --git a/backend/data/notifications/regression/broonie-misc.yaml b/backend/data/notifications/regression/broonie-misc.yaml new file mode 100644 index 000000000..bd1a1b329 --- /dev/null +++ b/backend/data/notifications/regression/broonie-misc.yaml @@ -0,0 +1,3 @@ +broonie-misc: + - recipients: + - broonie@kernel.org diff --git a/backend/data/notifications/regression/broonie-regmap.yaml b/backend/data/notifications/regression/broonie-regmap.yaml new file mode 100644 index 000000000..a38a55c10 --- /dev/null +++ b/backend/data/notifications/regression/broonie-regmap.yaml @@ -0,0 +1,3 @@ +broonie-regmap: + - recipients: + - broonie@kernel.org diff --git a/backend/data/notifications/regression/broonie-regulator.yaml b/backend/data/notifications/regression/broonie-regulator.yaml new file mode 100644 index 000000000..3a750e60e --- /dev/null +++ b/backend/data/notifications/regression/broonie-regulator.yaml @@ -0,0 +1,3 @@ +broonie-regulator: + - recipients: + - broonie@kernel.org diff --git a/backend/data/notifications/regression/broonie-sound.yaml b/backend/data/notifications/regression/broonie-sound.yaml new file mode 100644 index 000000000..ab8746408 --- /dev/null +++ b/backend/data/notifications/regression/broonie-sound.yaml @@ -0,0 +1,3 @@ +broonie-sound: + - recipients: + - broonie@kernel.org diff --git a/backend/data/notifications/regression/broonie-spi.yaml b/backend/data/notifications/regression/broonie-spi.yaml new file mode 100644 index 000000000..53afb88d7 --- /dev/null +++ b/backend/data/notifications/regression/broonie-spi.yaml @@ -0,0 +1,3 @@ +broonie-spi: + - recipients: + - broonie@kernel.org diff --git a/backend/data/notifications/regression/chrome-platform.yaml b/backend/data/notifications/regression/chrome-platform.yaml new file mode 100644 index 000000000..1de785578 --- /dev/null +++ b/backend/data/notifications/regression/chrome-platform.yaml @@ -0,0 +1,4 @@ +chrome-platform: + - recipients: + - chrome-platform@lists.linux.dev + - tzungbi@kernel.org diff --git a/backend/data/notifications/regression/chromiumos.yaml b/backend/data/notifications/regression/chromiumos.yaml new file mode 100644 index 000000000..8c7b99fa7 --- /dev/null +++ b/backend/data/notifications/regression/chromiumos.yaml @@ -0,0 +1,6 @@ +chromiumos: + - recipients: + - laura.nao@collabora.com + - vignesh.raman@collabora.com + - arnaud.ferraris@collabora.com + - mhiramat@google.com diff --git a/backend/data/notifications/regression/cip.yaml b/backend/data/notifications/regression/cip.yaml new file mode 100644 index 000000000..e3d143493 --- /dev/null +++ b/backend/data/notifications/regression/cip.yaml @@ -0,0 +1,3 @@ +cip: + - recipients: + - cip-dev@lists.cip-project.org diff --git a/backend/data/notifications/regression/clk.yaml b/backend/data/notifications/regression/clk.yaml new file mode 100644 index 000000000..d780b6203 --- /dev/null +++ b/backend/data/notifications/regression/clk.yaml @@ -0,0 +1,4 @@ +clk: + - recipients: + - linux-clk@vger.kernel.org + - sboyd@kernel.org diff --git a/backend/data/notifications/regression/default.yaml b/backend/data/notifications/regression/default.yaml new file mode 100644 index 000000000..5aa5589de --- /dev/null +++ b/backend/data/notifications/regression/default.yaml @@ -0,0 +1,3 @@ +# Replaces KERNELCI_RESULTS on the regression path only. The [STATUS] cron +# keeps --add-mailing-lists and the constants in common.py. +to: kernelci-results@groups.io diff --git a/backend/data/notifications/regression/efi.yaml b/backend/data/notifications/regression/efi.yaml new file mode 100644 index 000000000..be4258440 --- /dev/null +++ b/backend/data/notifications/regression/efi.yaml @@ -0,0 +1,3 @@ +efi: + - recipients: + - ardb@kernel.org diff --git a/backend/data/notifications/regression/hyperv.yaml b/backend/data/notifications/regression/hyperv.yaml new file mode 100644 index 000000000..b26b890e1 --- /dev/null +++ b/backend/data/notifications/regression/hyperv.yaml @@ -0,0 +1,3 @@ +hyperv: + - recipients: + - kernelcialerts@microsoft.com diff --git a/backend/data/notifications/regression/khilman.yaml b/backend/data/notifications/regression/khilman.yaml new file mode 100644 index 000000000..c16e388ad --- /dev/null +++ b/backend/data/notifications/regression/khilman.yaml @@ -0,0 +1,3 @@ +khilman: + - recipients: + - khilman@kernel.org diff --git a/backend/data/notifications/regression/krzysztof.yaml b/backend/data/notifications/regression/krzysztof.yaml new file mode 100644 index 000000000..72b915522 --- /dev/null +++ b/backend/data/notifications/regression/krzysztof.yaml @@ -0,0 +1,3 @@ +krzysztof: + - recipients: + - krzk@kernel.org diff --git a/backend/data/notifications/regression/kselftest.yaml b/backend/data/notifications/regression/kselftest.yaml new file mode 100644 index 000000000..77ef30063 --- /dev/null +++ b/backend/data/notifications/regression/kselftest.yaml @@ -0,0 +1,3 @@ +kselftest: + - recipients: + - shuah@kernel.org diff --git a/backend/data/notifications/regression/lee-backlight.yaml b/backend/data/notifications/regression/lee-backlight.yaml new file mode 100644 index 000000000..ad3ca083e --- /dev/null +++ b/backend/data/notifications/regression/lee-backlight.yaml @@ -0,0 +1,3 @@ +lee-backlight: + - recipients: + - lee@kernel.org diff --git a/backend/data/notifications/regression/lee-mfd.yaml b/backend/data/notifications/regression/lee-mfd.yaml new file mode 100644 index 000000000..bc16c7740 --- /dev/null +++ b/backend/data/notifications/regression/lee-mfd.yaml @@ -0,0 +1,3 @@ +lee-mfd: + - recipients: + - lee@kernel.org diff --git a/backend/data/notifications/regression/linux-pci.yaml b/backend/data/notifications/regression/linux-pci.yaml new file mode 100644 index 000000000..224fc1736 --- /dev/null +++ b/backend/data/notifications/regression/linux-pci.yaml @@ -0,0 +1,3 @@ +linux-pci: + - recipients: + - linux-pci@vger.kernel.org diff --git a/backend/data/notifications/regression/mainline.yaml b/backend/data/notifications/regression/mainline.yaml new file mode 100644 index 000000000..9c8c847b8 --- /dev/null +++ b/backend/data/notifications/regression/mainline.yaml @@ -0,0 +1,3 @@ +mainline: + - recipients: + - broonie@kernel.org diff --git a/backend/data/notifications/regression/media-committers.yaml b/backend/data/notifications/regression/media-committers.yaml new file mode 100644 index 000000000..9f0c444ee --- /dev/null +++ b/backend/data/notifications/regression/media-committers.yaml @@ -0,0 +1,3 @@ +media-committers: + - recipients: + - linux-media@vger.kernel.org diff --git a/backend/data/notifications/regression/media.yaml b/backend/data/notifications/regression/media.yaml new file mode 100644 index 000000000..8083c67fb --- /dev/null +++ b/backend/data/notifications/regression/media.yaml @@ -0,0 +1,3 @@ +media: + - recipients: + - media@linuxtv.org diff --git a/backend/data/notifications/regression/mediatek.yaml b/backend/data/notifications/regression/mediatek.yaml new file mode 100644 index 000000000..2ed60f764 --- /dev/null +++ b/backend/data/notifications/regression/mediatek.yaml @@ -0,0 +1,3 @@ +mediatek: + - recipients: + - angelogioacchino.delregno@collabora.com diff --git a/backend/data/notifications/regression/next.yaml b/backend/data/notifications/regression/next.yaml new file mode 100644 index 000000000..22da5aaaa --- /dev/null +++ b/backend/data/notifications/regression/next.yaml @@ -0,0 +1,3 @@ +next: + - recipients: + - linux-next@vger.kernel.org diff --git a/backend/data/notifications/regression/omap.yaml b/backend/data/notifications/regression/omap.yaml new file mode 100644 index 000000000..b87149991 --- /dev/null +++ b/backend/data/notifications/regression/omap.yaml @@ -0,0 +1,3 @@ +omap: + - recipients: + - khilman@baylibre.com diff --git a/backend/data/notifications/regression/padovan.yaml b/backend/data/notifications/regression/padovan.yaml new file mode 100644 index 000000000..8040da8cf --- /dev/null +++ b/backend/data/notifications/regression/padovan.yaml @@ -0,0 +1,3 @@ +padovan: + - recipients: + - padovan@kernel.org diff --git a/backend/data/notifications/regression/pm.yaml b/backend/data/notifications/regression/pm.yaml new file mode 100644 index 000000000..daee7420d --- /dev/null +++ b/backend/data/notifications/regression/pm.yaml @@ -0,0 +1,3 @@ +pm: + - recipients: + - rafael@kernel.org diff --git a/backend/data/notifications/regression/qcom.yaml b/backend/data/notifications/regression/qcom.yaml new file mode 100644 index 000000000..ac1ac6bb2 --- /dev/null +++ b/backend/data/notifications/regression/qcom.yaml @@ -0,0 +1,4 @@ +qcom: + - recipients: + - linux-arm-msm@vger.kernel.org + - Bjorn Andersson diff --git a/backend/data/notifications/regression/renesas.yaml b/backend/data/notifications/regression/renesas.yaml new file mode 100644 index 000000000..1b4cfe737 --- /dev/null +++ b/backend/data/notifications/regression/renesas.yaml @@ -0,0 +1,3 @@ +renesas: + - recipients: + - linux-renesas-soc@vger.kernel.org diff --git a/backend/data/notifications/regression/riscv.yaml b/backend/data/notifications/regression/riscv.yaml new file mode 100644 index 000000000..abd5cca2f --- /dev/null +++ b/backend/data/notifications/regression/riscv.yaml @@ -0,0 +1,3 @@ +riscv: + - recipients: + - linux-riscv@lists.infradead.org diff --git a/backend/data/notifications/regression/robh.yaml b/backend/data/notifications/regression/robh.yaml new file mode 100644 index 000000000..78d1a62c3 --- /dev/null +++ b/backend/data/notifications/regression/robh.yaml @@ -0,0 +1,4 @@ +robh: + - recipients: + - robh@kernel.org + - devicetree@vger.kernel.org diff --git a/backend/data/notifications/regression/rppt.yaml b/backend/data/notifications/regression/rppt.yaml new file mode 100644 index 000000000..f1bf22147 --- /dev/null +++ b/backend/data/notifications/regression/rppt.yaml @@ -0,0 +1,4 @@ +rppt: + - recipients: + - rppt@kernel.org + - linux-mm@kvack.org diff --git a/backend/data/notifications/regression/sashal-next.yaml b/backend/data/notifications/regression/sashal-next.yaml new file mode 100644 index 000000000..de6404b9c --- /dev/null +++ b/backend/data/notifications/regression/sashal-next.yaml @@ -0,0 +1,3 @@ +sashal-next: + - recipients: + - sashal@kernel.org diff --git a/backend/data/notifications/regression/soc.yaml b/backend/data/notifications/regression/soc.yaml new file mode 100644 index 000000000..e56a6e89d --- /dev/null +++ b/backend/data/notifications/regression/soc.yaml @@ -0,0 +1,3 @@ +soc: + - recipients: + - soc@lists.linux.dev diff --git a/backend/data/notifications/regression/stable-rc.yaml b/backend/data/notifications/regression/stable-rc.yaml new file mode 100644 index 000000000..682817cc0 --- /dev/null +++ b/backend/data/notifications/regression/stable-rc.yaml @@ -0,0 +1,3 @@ +stable-rc: + - recipients: + - stable@vger.kernel.org diff --git a/backend/data/notifications/regression/stable-rt.yaml b/backend/data/notifications/regression/stable-rt.yaml new file mode 100644 index 000000000..e2c938e42 --- /dev/null +++ b/backend/data/notifications/regression/stable-rt.yaml @@ -0,0 +1,3 @@ +stable-rt: + - recipients: + - linux-rt-devel@lists.linux.dev diff --git a/backend/data/notifications/regression/stable.yaml b/backend/data/notifications/regression/stable.yaml new file mode 100644 index 000000000..141e97a73 --- /dev/null +++ b/backend/data/notifications/regression/stable.yaml @@ -0,0 +1,3 @@ +stable: + - recipients: + - stable@vger.kernel.org diff --git a/backend/data/notifications/regression/tegra.yaml b/backend/data/notifications/regression/tegra.yaml new file mode 100644 index 000000000..1c996c70c --- /dev/null +++ b/backend/data/notifications/regression/tegra.yaml @@ -0,0 +1,3 @@ +tegra: + - recipients: + - linux-tegra@vger.kernel.org diff --git a/backend/data/notifications/regression/thermal.yaml b/backend/data/notifications/regression/thermal.yaml new file mode 100644 index 000000000..1afac3b55 --- /dev/null +++ b/backend/data/notifications/regression/thermal.yaml @@ -0,0 +1,3 @@ +thermal: + - recipients: + - linux-pm@vger.kernel.org diff --git a/backend/data/notifications/regression/tip.yaml b/backend/data/notifications/regression/tip.yaml new file mode 100644 index 000000000..ee0bd413e --- /dev/null +++ b/backend/data/notifications/regression/tip.yaml @@ -0,0 +1,3 @@ +tip: + - recipients: + - tip@kernel.org diff --git a/backend/data/notifications/regression/ulfh.yaml b/backend/data/notifications/regression/ulfh.yaml new file mode 100644 index 000000000..2d234142e --- /dev/null +++ b/backend/data/notifications/regression/ulfh.yaml @@ -0,0 +1,3 @@ +ulfh: + - recipients: + - linux-mmc@vger.kernel.org diff --git a/backend/data/notifications/regression/vireshk.yaml b/backend/data/notifications/regression/vireshk.yaml new file mode 100644 index 000000000..fc6646621 --- /dev/null +++ b/backend/data/notifications/regression/vireshk.yaml @@ -0,0 +1,3 @@ +vireshk: + - recipients: + - vireshk@kernel.org From 760f005c01f1ebf345949023c564654b6f4ec865 Mon Sep 17 00:00:00 2001 From: Felipe Bergamin Date: Wed, 21 Oct 2026 17:30:59 -0300 Subject: [PATCH 2/2] feat(notifications): Cc regression reports from regression/ [REGRESSION] Cc came from the default_recipients of the subscription whose url matched the issue, regardless of branch or architecture. That list stays the [STATUS] Cc. Regression Cc now comes from backend/data/notifications/regression/, where an entry can be scoped by issues, branch, architecture, config_name and compiler. The loader validates every file against the subscription tree keys and raises with the file, entry index and key. A unit test loads the committed directory, so a bad file fails CI. Branch is matched against the issue's first incident and the build keys against the incidents already queried, so no new query is added. test_report keeps using only the unscoped entries. With --add-mailing-lists, regression reports take their To from regression/default.yaml. Build and boot report bodies name the file the Cc came from. send_email_report no longer looks up recipients itself, so the signup_folder and git_url plumbing on the regression path is removed. Closes #2114 Closes #1080 Assisted-by: Cursor Signed-off-by: Felipe Bergamin Co-authored-by: Cursor --- .../management/commands/helpers/common.py | 140 +++++++------ .../management/commands/notifications.py | 25 +-- .../commands/templates/issue_boot.txt.j2 | 2 + .../commands/templates/issue_build.txt.j2 | 2 + .../fixtures/issue_boot_report_example.txt | 2 + .../fixtures/issue_build_report_example.txt | 2 + .../commands/regression_recipients_test.py | 191 ++++++++++++++++++ .../typeModels/notificationRecipients.py | 162 +++++++++++++++ docs/notifications.md | 43 +++- 9 files changed, 488 insertions(+), 81 deletions(-) create mode 100644 backend/kernelCI_app/tests/unitTests/commands/regression_recipients_test.py create mode 100644 backend/kernelCI_app/typeModels/notificationRecipients.py diff --git a/backend/kernelCI_app/management/commands/helpers/common.py b/backend/kernelCI_app/management/commands/helpers/common.py index 4cea995fc..930460f1a 100644 --- a/backend/kernelCI_app/management/commands/helpers/common.py +++ b/backend/kernelCI_app/management/commands/helpers/common.py @@ -1,13 +1,17 @@ import os +from collections.abc import Iterable +from email.utils import parseaddr from typing import Optional from django.conf import settings from jinja2 import Environment, FileSystemLoader, Template from kernelCI_app.helpers.email import smtp_send_email -from kernelCI_app.helpers.logger import log_message -from kernelCI_app.management.commands.helpers.summary import SIGNUP_FOLDER -from kernelCI_app.utils import read_yaml_file +from kernelCI_app.typeModels.notificationRecipients import ( + RecipientEntry, + load_recipient_default, + load_recipient_files, +) KERNELCI_RESULTS = "kernelci-results@groups.io" KERNELCI_REPLYTO = "kernelci@lists.linux.dev" @@ -15,6 +19,11 @@ REGZBOT_TREES = {"mainline", "next", "stable"} +REGRESSION_FOLDER = "notifications/regression/" +BUILD_INCIDENT_KEYS = {"architecture", "config_name", "compiler"} +REGRESSION_KEYS = {"issues", "branch", *BUILD_INCIDENT_KEYS} +REGRESSION_ISSUES = {"build"} + def is_regzbot_tree(tree_name) -> bool: """Trees whose regression reports are tracked by regzbot. @@ -38,50 +47,69 @@ def setup_jinja_template(template_name: str) -> Template: return jinja_env.get_template(template_name) -def _get_default_tree_recipients( +def _regression_directory() -> str: + return os.path.join(settings.BACKEND_DATA_DIR, REGRESSION_FOLDER) + + +def load_regression_files( + directory: Optional[str] = None, +) -> dict[str, list[RecipientEntry]]: + return load_recipient_files( + directory or _regression_directory(), + allowed_keys=REGRESSION_KEYS, + allowed_issues=REGRESSION_ISSUES, + ) + + +def _union_recipients(entries: Iterable[RecipientEntry]) -> list[str]: + """Recipients of every entry in order, dropping repeated addresses + even when they are written with different display names.""" + seen: dict[str, str] = {} + for entry in entries: + for recipient in entry.recipients: + seen.setdefault(parseaddr(recipient)[1].lower() or recipient, recipient) + return list(seen.values()) + + +def _entry_matches(entry: RecipientEntry, issue: dict, build_incidents) -> bool: + issue_kind = "build" if issue.get("build_id") else "test" + if entry.issues and issue_kind not in entry.issues: + return False + if entry.branch and issue.get("git_repository_branch") not in entry.branch: + return False + return all( + any(incident.get(key) in values for incident in build_incidents) + for key, values in entry.scope.items() + if key in BUILD_INCIDENT_KEYS + ) + + +def resolve_regression_recipients( + issue: dict, + build_incidents: list[dict], *, - base_dir: Optional[str] = None, - signup_folder: Optional[str] = None, - search_url: Optional[str] = None, + entries_by_url: Optional[dict[str, list[RecipientEntry]]] = None, ) -> list[str]: - """ - Searches in every subscription file to find the matching git_url from a tree, - and returns the default recipients of that tree - - Params: - base_dir: the base path for the signup folder; - signup_folder: the relative path of the folder where all the subscription files are; - search_url: the url of the tree that is being searched; + """Cc for a [REGRESSION] report: every regression/ entry on the issue's + tree url whose scope matches the issue and its build incidents.""" + if entries_by_url is None: + entries_by_url = load_regression_files() + entries = entries_by_url.get(issue.get("git_repository_url"), []) + return _union_recipients( + entry for entry in entries if _entry_matches(entry, issue, build_incidents) + ) - Returns: - A list of str with recipient emails. - The list will be empty if there was an error or the tree was not found. - """ - if not search_url: - return [] - - if not base_dir: - base_dir = settings.BACKEND_DATA_DIR - if not signup_folder: - signup_folder = SIGNUP_FOLDER - - full_path = os.path.join(base_dir, signup_folder) - for filename in os.listdir(full_path): - if filename.endswith(".yaml") or filename.endswith(".yml"): - file_path = os.path.join(signup_folder, filename) - file_data = read_yaml_file(base_dir=base_dir, file=file_path) - - # Although there is a for loop here, there should only be a single tree per file - for tree_values in file_data.values(): - tree_url = tree_values.get("url") - default_recipients = tree_values.get("default_recipients", []) - if search_url == tree_url: - return default_recipients - else: - log_message( - f"Skipping file {filename} on loading summary files. Not a yaml file." - ) +def unscoped_regression_recipients( + git_url: Optional[str], + *, + entries_by_url: Optional[dict[str, list[RecipientEntry]]] = None, +) -> list[str]: + """Cc for a test report: only the regression/ entries without scope keys.""" + if entries_by_url is None: + entries_by_url = load_regression_files() + entries = entries_by_url.get(git_url, []) + return _union_recipients(entry for entry in entries if not entry.scope) def _ask_confirmation(): @@ -100,8 +128,6 @@ def send_email_report( service, report, email_args, - git_url: Optional[str] = None, - signup_folder: Optional[str] = None, recipients: Optional[list[str]] = None, message_id: Optional[str] = None, ): @@ -111,13 +137,7 @@ def send_email_report( service: the email service used to send the email itself report: the dict with information about the report, usually set up with jinja email_args: the SimpleNamespace with options for the email sending - git_url: the git_repository_url of a tree that is used to retrieve the recipients for the email.\n - Requires signup_folder to be set in order to take effect. - signup_folder: the folder with the tree submissions files that is used - to retrieve the recipients for the email.\n - Is only used if git_url is set and default recipients are not ignored. - recipients: a direct list of email recipients to use, - this parameter overrides the use of git_url + signup_folder. + recipients: the Cc list, skipped when email_args.ignore_recipients is set message_id: the Message-ID to send the email with, generated by the smtp helper when not given. Pass one when the report body references its own Message-ID (e.g. a lore.kernel.org link). @@ -137,19 +157,17 @@ def send_email_report( cc = "" reply_to = None if email_args.add_mailing_lists: - to = KERNELCI_RESULTS + to = ( + load_recipient_default(_regression_directory()).to + if email_args.regression_report + else KERNELCI_RESULTS + ) reply_to = KERNELCI_REPLYTO else: to = email_args.to - if not email_args.ignore_recipients: - if not recipients: - recipients = _get_default_tree_recipients( - signup_folder=signup_folder, - search_url=git_url, - ) - formatted_recipients = ", ".join(recipients) if recipients else "" - cc = ", ".join([formatted_recipients, cc]) if cc else formatted_recipients + if not email_args.ignore_recipients and recipients: + cc = ", ".join(recipients) if email_args.cc: cc = ", ".join([email_args.cc, cc]) if cc else email_args.cc diff --git a/backend/kernelCI_app/management/commands/notifications.py b/backend/kernelCI_app/management/commands/notifications.py index 19ed1eef6..b9f127564 100644 --- a/backend/kernelCI_app/management/commands/notifications.py +++ b/backend/kernelCI_app/management/commands/notifications.py @@ -16,8 +16,10 @@ from kernelCI_app.helpers.trees import sanitize_tree from kernelCI_app.management.commands.helpers.common import ( is_regzbot_tree, + resolve_regression_recipients, send_email_report, setup_jinja_template, + unscoped_regression_recipients, ) from kernelCI_app.management.commands.helpers.healthcheck import ( MONITORING_ID_PARAM_HELP_TEXT, @@ -100,7 +102,7 @@ def exclude_already_found_and_store(issues: list[dict]) -> list[dict]: return new_issues -def look_for_new_issues(*, service, signup_folder, email_args): +def look_for_new_issues(*, service, email_args): if get_running_instance() == "staging": print("This command only runs on production or dev environments.") return @@ -134,7 +136,6 @@ def look_for_new_issues(*, service, signup_folder, email_args): service=service, report=report, email_args=email_args, - signup_folder=signup_folder, ) email_args.add_mailing_lists = True @@ -145,7 +146,6 @@ def look_for_new_issues(*, service, signup_folder, email_args): service=service, issue_id=issue["id"], email_args=email_args, - signup_folder=signup_folder, ) @@ -208,7 +208,6 @@ def generate_issue_report( issue_id, email_args, ask_ignore=False, - signup_folder, ): result = kcidb_issue_details(issue_id) if not result: @@ -249,6 +248,7 @@ def generate_issue_report( if last_build: incident["last_pass_commit"] = last_build[0]["git_commit_hash"] report = generate_build_issue_report(issue, incidents, message_id) + recipients = resolve_regression_recipients(issue, incidents) elif issue["test_id"]: incidents = kcidb_test_incidents(issue_id) issue_type = "boot" if is_boot(path=incidents[0]["path"]) else "test" @@ -258,6 +258,7 @@ def generate_issue_report( incident["last_pass_commit"] = last_test[0]["git_commit_hash"] incident["last_pass_id"] = last_test[0]["id"] report = generate_boot_issue_report(issue, incidents, message_id) + recipients = resolve_regression_recipients(issue, []) else: print(f"unable to generate issue report for {issue_id}", file=sys.stderr) sys.exit(-1) @@ -268,8 +269,7 @@ def generate_issue_report( service=service, report=report, email_args=email_args, - signup_folder=signup_folder, - git_url=issue["git_repository_url"], + recipients=recipients, message_id=message_id, ) @@ -549,7 +549,7 @@ def run_checkout_summary( ) -def create_and_send_issue_reports(*, service, email_args, signup_folder): +def create_and_send_issue_reports(*, service, email_args): unsent_issues = get_unsent_issues() for issue in unsent_issues: if issue.issue_type != "build": @@ -560,11 +560,10 @@ def create_and_send_issue_reports(*, service, email_args, signup_folder): issue_id=issue.issue_id, email_args=email_args, ask_ignore=True, - signup_folder=signup_folder, ) -def generate_test_report(*, service, test_id, email_args, signup_folder): +def generate_test_report(*, service, test_id, email_args): """Generate a test regression report for a specific test ID.""" test_data = get_test_details_data(test_id=test_id) @@ -660,8 +659,7 @@ def generate_test_report(*, service, test_id, email_args, signup_folder): service=service, report=report, email_args=email_args, - git_url=test["git_repository_url"], - signup_folder=signup_folder, + recipients=unscoped_regression_recipients(test["git_repository_url"]), message_id=message_id, ) @@ -785,7 +783,6 @@ def generate_hardware_summary_report( service=service, report=report, email_args=email_args, - signup_folder=signup_folder, recipients=recipients, ) @@ -1070,7 +1067,6 @@ def _run_action(self, options): look_for_new_issues( service=service, email_args=email_args, - signup_folder=signup_folder, ) case "issue_report": @@ -1079,7 +1075,6 @@ def _run_action(self, options): create_and_send_issue_reports( service=service, email_args=email_args, - signup_folder=signup_folder, ) else: issue_id = options.get("id") @@ -1094,7 +1089,6 @@ def _run_action(self, options): service=service, issue_id=issue_id, email_args=email_args, - signup_folder=signup_folder, ) self.stdout.write( self.style.SUCCESS( @@ -1131,7 +1125,6 @@ def _run_action(self, options): service=service, test_id=test_id, email_args=email_args, - signup_folder=signup_folder, ) self.stdout.write( self.style.SUCCESS(f"Test report generated for test {test_id}") diff --git a/backend/kernelCI_app/management/commands/templates/issue_boot.txt.j2 b/backend/kernelCI_app/management/commands/templates/issue_boot.txt.j2 index 3036276ed..022763413 100644 --- a/backend/kernelCI_app/management/commands/templates/issue_boot.txt.j2 +++ b/backend/kernelCI_app/management/commands/templates/issue_boot.txt.j2 @@ -1,6 +1,8 @@ {% extends "base.txt" %} {% block header %}{% endblock %} {% block content -%} +Cc from: regression/{{ issue["tree_name"] }}.yaml + New boot regression found on {{ issue["tree_name"] }}/{{ issue["git_repository_branch"] }}: --- diff --git a/backend/kernelCI_app/management/commands/templates/issue_build.txt.j2 b/backend/kernelCI_app/management/commands/templates/issue_build.txt.j2 index e1f9b4025..0deebd9cc 100644 --- a/backend/kernelCI_app/management/commands/templates/issue_build.txt.j2 +++ b/backend/kernelCI_app/management/commands/templates/issue_build.txt.j2 @@ -1,6 +1,8 @@ {% extends "base.txt" %} {% block header %}{% endblock %} {% block content -%} +Cc from: regression/{{ issue["tree_name"] }}.yaml + New build issue found on {{ issue["tree_name"] }}/{{ issue["git_repository_branch"] }}: --- diff --git a/backend/kernelCI_app/tests/unitTests/commands/fixtures/issue_boot_report_example.txt b/backend/kernelCI_app/tests/unitTests/commands/fixtures/issue_boot_report_example.txt index 9b3ee6016..2055fecc3 100644 --- a/backend/kernelCI_app/tests/unitTests/commands/fixtures/issue_boot_report_example.txt +++ b/backend/kernelCI_app/tests/unitTests/commands/fixtures/issue_boot_report_example.txt @@ -1,3 +1,5 @@ +Cc from: regression/mainline.yaml + New boot regression found on mainline/master: --- diff --git a/backend/kernelCI_app/tests/unitTests/commands/fixtures/issue_build_report_example.txt b/backend/kernelCI_app/tests/unitTests/commands/fixtures/issue_build_report_example.txt index 2c3a67dae..44eb04bea 100644 --- a/backend/kernelCI_app/tests/unitTests/commands/fixtures/issue_build_report_example.txt +++ b/backend/kernelCI_app/tests/unitTests/commands/fixtures/issue_build_report_example.txt @@ -1,3 +1,5 @@ +Cc from: regression/mainline.yaml + New build issue found on mainline/master: --- diff --git a/backend/kernelCI_app/tests/unitTests/commands/regression_recipients_test.py b/backend/kernelCI_app/tests/unitTests/commands/regression_recipients_test.py new file mode 100644 index 000000000..b119481b0 --- /dev/null +++ b/backend/kernelCI_app/tests/unitTests/commands/regression_recipients_test.py @@ -0,0 +1,191 @@ +import os + +import pytest +from django.conf import settings + +from kernelCI_app.management.commands.helpers.common import ( + REGRESSION_FOLDER, + load_regression_files, + resolve_regression_recipients, + unscoped_regression_recipients, +) +from kernelCI_app.typeModels.notificationRecipients import ( + RecipientFileError, + load_recipient_default, +) + +STABLE_URL = "https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git" + + +def write_tree(tmp_path, *, regression: str, subscriptions: dict[str, str] = None): + """Creates subscriptions/ and regression/ under tmp_path, returns regression/""" + if subscriptions is None: + subscriptions = {"stable.yaml": f"stable:\n url: {STABLE_URL}\n"} + (tmp_path / "subscriptions").mkdir() + for name, content in subscriptions.items(): + (tmp_path / "subscriptions" / name).write_text(content) + (tmp_path / "regression").mkdir() + (tmp_path / "regression" / "stable.yaml").write_text(regression) + return str(tmp_path / "regression") + + +def test_loader_valid(tmp_path): + directory = write_tree( + tmp_path, + regression=( + "stable:\n" + " - recipients: list@example.org\n" + " - branch: linux-6.12.y\n" + " architecture: [arm64]\n" + " issues: [build]\n" + " recipients: [Maintainer ]\n" + ), + ) + + entries = load_regression_files(directory) + + assert list(entries) == [STABLE_URL] + unscoped, scoped = entries[STABLE_URL] + assert unscoped.recipients == ["list@example.org"] + assert unscoped.scope == {} + assert scoped.recipients == ["Maintainer "] + assert scoped.scope == { + "issues": ["build"], + "branch": ["linux-6.12.y"], + "architecture": ["arm64"], + } + + +@pytest.mark.parametrize( + ("entry", "key"), + [ + ("{recipients: [a@example.org], unknown: x}", "unknown"), + ("{recipients: [a@example.org], platform: rk3399}", "platform"), + ("{recipients: [a@example.org], origin: maestro}", "origin"), + ("{recipients: [a@example.org], issues: [boot]}", "issues"), + ("{recipients: []}", "recipients"), + ], +) +def test_loader_rejects_entry(tmp_path, entry, key): + directory = write_tree( + tmp_path, regression=f"stable:\n - recipients: [ok@example.org]\n - {entry}\n" + ) + + with pytest.raises(RecipientFileError) as error: + load_regression_files(directory) + + message = str(error.value) + assert "stable.yaml" in message + assert "entry 1" in message + assert f"key '{key}'" in message + + +def test_loader_rejects_unknown_tree(tmp_path): + directory = write_tree( + tmp_path, regression="mainline:\n - recipients: [a@example.org]\n" + ) + + with pytest.raises(RecipientFileError, match="unknown tree 'mainline'"): + load_regression_files(directory) + + +def test_loader_rejects_duplicate_subscription_url(tmp_path): + directory = write_tree( + tmp_path, + regression="stable:\n - recipients: [a@example.org]\n", + subscriptions={ + "stable.yaml": f"stable:\n url: {STABLE_URL}\n", + "stable-copy.yaml": f"stable-copy:\n url: {STABLE_URL}\n", + }, + ) + + with pytest.raises(RecipientFileError, match="also used by"): + load_regression_files(directory) + + +def test_loader_rejects_hardware_file(tmp_path): + directory = write_tree( + tmp_path, regression="stable:\n - recipients: [a@example.org]\n" + ) + (tmp_path / "regression" / "board_hardware.yaml").write_text("board: {}\n") + + with pytest.raises(RecipientFileError, match="hardware files are not allowed"): + load_regression_files(directory) + + +def test_committed_regression_files_are_valid(): + directory = os.path.join(settings.BACKEND_DATA_DIR, REGRESSION_FOLDER) + + assert load_regression_files(directory) + assert load_recipient_default(directory).to + + +@pytest.fixture +def entries_by_url(tmp_path): + directory = write_tree( + tmp_path, + regression=( + "stable:\n" + " - recipients: [list@example.org]\n" + " - branch: linux-6.12.y\n" + " recipients: [branch@example.org]\n" + " - architecture: arm64\n" + " recipients: [arm64@example.org, List ]\n" + " - branch: linux-6.6.y\n" + " compiler: clang-17\n" + " recipients: [never@example.org]\n" + ), + ) + return load_regression_files(directory) + + +def build_issue(branch: str) -> dict: + return { + "build_id": "maestro:1", + "git_repository_url": STABLE_URL, + "git_repository_branch": branch, + } + + +def test_resolve_branch_match(entries_by_url): + recipients = resolve_regression_recipients( + build_issue("linux-6.12.y"), + [{"architecture": "x86_64"}], + entries_by_url=entries_by_url, + ) + + assert recipients == ["list@example.org", "branch@example.org"] + + +def test_resolve_architecture_match_and_dedup(entries_by_url): + recipients = resolve_regression_recipients( + build_issue("linux-6.1.y"), + [{"architecture": "x86_64"}, {"architecture": "arm64"}], + entries_by_url=entries_by_url, + ) + + assert recipients == ["list@example.org", "arm64@example.org"] + + +def test_resolve_scoped_entries_need_every_key(entries_by_url): + recipients = resolve_regression_recipients( + build_issue("linux-6.6.y"), + [{"compiler": "gcc-12"}], + entries_by_url=entries_by_url, + ) + + assert recipients == ["list@example.org"] + + +def test_resolve_no_match_for_other_tree(entries_by_url): + issue = build_issue("linux-6.12.y") | {"git_repository_url": "https://other"} + + assert resolve_regression_recipients(issue, [], entries_by_url=entries_by_url) == [] + + +def test_test_report_uses_only_unscoped_entries(entries_by_url): + recipients = unscoped_regression_recipients( + STABLE_URL, entries_by_url=entries_by_url + ) + + assert recipients == ["list@example.org"] diff --git a/backend/kernelCI_app/typeModels/notificationRecipients.py b/backend/kernelCI_app/typeModels/notificationRecipients.py new file mode 100644 index 000000000..9e778cc97 --- /dev/null +++ b/backend/kernelCI_app/typeModels/notificationRecipients.py @@ -0,0 +1,162 @@ +import os +from typing import Annotated, Optional + +import yaml +from pydantic import BaseModel, BeforeValidator, ConfigDict, Field, ValidationError + +DEFAULT_FILE = "default.yaml" +HARDWARE_SUFFIX = "_hardware.yaml" + + +def _as_list(value): + return [value] if isinstance(value, str) else value + + +type StrList = Annotated[list[str], BeforeValidator(_as_list), Field(min_length=1)] + + +class RecipientEntry(BaseModel): + model_config = ConfigDict(extra="forbid") + + recipients: StrList + issues: Optional[StrList] = None + branch: Optional[StrList] = None + architecture: Optional[StrList] = None + config_name: Optional[StrList] = None + compiler: Optional[StrList] = None + + @property + def scope(self) -> dict[str, list[str]]: + return self.model_dump(exclude={"recipients"}, exclude_none=True) + + +class RecipientDefault(BaseModel): + model_config = ConfigDict(extra="forbid") + + to: str + + +class RecipientFileError(ValueError): + def __init__(self, file: str, message: str, *, entry=None, key=None): + where = [file] + if entry is not None: + where.append(f"entry {entry}") + if key is not None: + where.append(f"key '{key}'") + super().__init__(f"{': '.join(where)}: {message}") + + +def _read_yaml(path: str): + try: + with open(path) as f: + return yaml.safe_load(f) + except (OSError, yaml.YAMLError) as exc: + raise RecipientFileError(path, str(exc)) from exc + + +def _yaml_files(directory: str) -> list[str]: + return sorted(f for f in os.listdir(directory) if f.endswith((".yaml", ".yml"))) + + +def _tree_urls(subscriptions_directory: str) -> dict[str, str]: + """Maps each tree key in the subscription files to its url""" + tree_urls: dict[str, str] = {} + files_by_url: dict[str, str] = {} + for filename in _yaml_files(subscriptions_directory): + if filename.endswith(HARDWARE_SUFFIX): + continue + path = os.path.join(subscriptions_directory, filename) + for tree_key, values in (_read_yaml(path) or {}).items(): + url = values.get("url") + if url in files_by_url: + raise RecipientFileError( + path, + f"url {url} is also used by {files_by_url[url]}", + key="url", + ) + files_by_url[url] = filename + tree_urls[tree_key] = url + return tree_urls + + +def _parse_entry(path, index, raw, *, allowed_keys, allowed_issues) -> RecipientEntry: + if not isinstance(raw, dict): + raise RecipientFileError(path, "entry must be a mapping", entry=index) + for key in raw: + if key != "recipients" and key not in allowed_keys: + raise RecipientFileError(path, "key not allowed", entry=index, key=key) + try: + entry = RecipientEntry.model_validate(raw) + except ValidationError as exc: + error = exc.errors()[0] + key = error["loc"][0] if error["loc"] else None + raise RecipientFileError(path, error["msg"], entry=index, key=key) from exc + for issue in entry.issues or []: + if issue not in allowed_issues: + raise RecipientFileError( + path, f"issue '{issue}' not allowed", entry=index, key="issues" + ) + return entry + + +def load_recipient_files( + directory: str, + *, + allowed_keys: set[str], + allowed_issues: set[str], + subscriptions_directory: Optional[str] = None, +) -> dict[str, list[RecipientEntry]]: + """Loads and validates every recipient file in `directory`. + + Each top-level key must be a tree key from the subscription files, which + default to the `subscriptions` folder next to `directory`. + + Returns the entries keyed by the tree's git url. + Raises RecipientFileError naming the file, entry index, and key. + """ + if subscriptions_directory is None: + subscriptions_directory = os.path.join( + os.path.dirname(os.path.normpath(directory)), "subscriptions" + ) + tree_urls = _tree_urls(subscriptions_directory) + + entries_by_url: dict[str, list[RecipientEntry]] = {} + for filename in _yaml_files(directory): + if filename == DEFAULT_FILE: + continue + path = os.path.join(directory, filename) + if filename.endswith(HARDWARE_SUFFIX): + raise RecipientFileError(path, "hardware files are not allowed") + data = _read_yaml(path) + if not isinstance(data, dict): + raise RecipientFileError(path, "file must map tree keys to entries") + for tree_key, raw_entries in data.items(): + if tree_key not in tree_urls: + raise RecipientFileError( + path, f"unknown tree '{tree_key}'", key=tree_key + ) + if not isinstance(raw_entries, list): + raise RecipientFileError( + path, "tree must hold a list of entries", key=tree_key + ) + entries_by_url.setdefault(tree_urls[tree_key], []).extend( + _parse_entry( + path, + index, + raw, + allowed_keys=allowed_keys, + allowed_issues=allowed_issues, + ) + for index, raw in enumerate(raw_entries) + ) + return entries_by_url + + +def load_recipient_default(directory: str) -> RecipientDefault: + path = os.path.join(directory, DEFAULT_FILE) + try: + return RecipientDefault.model_validate(_read_yaml(path)) + except ValidationError as exc: + error = exc.errors()[0] + key = error["loc"][0] if error["loc"] else None + raise RecipientFileError(path, error["msg"], key=key) from exc diff --git a/docs/notifications.md b/docs/notifications.md index 9dd741196..c2c0b26ab 100644 --- a/docs/notifications.md +++ b/docs/notifications.md @@ -39,7 +39,7 @@ These options are available for all actions and are always optional: | Option | Description | Type | Default | |--------|-------------|------|---------| | `--add-mailing-lists` | Include community mailing lists in recipients | Flag | False | -| `--ignore-recipients` | Bypass the recipients in the subscription file | Flag | False | +| `--ignore-recipients` | Bypass the recipients in the subscription or `regression/` files | Flag | False | | `--send` | Send email after generating report | Flag | False | | `--to` | Specify direct recipient email | String | None | | `--cc` | Specify CC recipient emails as "email1, email2" list | String | None | @@ -102,10 +102,45 @@ poetry run python manage.py notifications \ ## Adding yourself to recipients -Edit the corresponding tree file in the [subscriptions folder](../backend/data/notifications/subscriptions) and send a PR to the dashboard. +Edit the file for your tree and send a PR to the dashboard. Use the git tree +name reported by the Web Dashboard. -Use the git tree name reported by the -Web Dashboard. +- `[STATUS]` checkout summaries: `default_recipients` in the + [subscriptions folder](../backend/data/notifications/subscriptions). +- `[REGRESSION]` issue reports: the + [regression folder](../backend/data/notifications/regression). The report + body names the file it took the Cc from. + +### Regression recipients + +Each `regression/.yaml` has a top-level key that must be a tree key +from `subscriptions/`. The tree's `url` decides which issues the file applies +to. Each entry Cc's its `recipients` when every scope key it sets matches: + +```yaml +stable: + - recipients: [list@example.org] # no scope keys: every build issue on this tree's URL + - branch: [linux-6.12.y, linux-6.6.y] + architecture: [arm64] + recipients: [maintainer@example.com] +``` + +| Key | Values | Matched against | +|---|---|---| +| `recipients` | addresses, at least one, sent as written | | +| `issues` | `build` only | the issue kind | +| `branch` | string or list | the branch printed in the report | +| `architecture`, `config_name`, `compiler` | string or list | any build where the issue occurred | + +The Cc is the union of the matching entries, with repeated addresses dropped. +Any other key, including `platform` and `origin`, fails `pytest -m unit` with +the file, entry index, and key. + +`test_report` only uses entries without scope keys. + +With `--add-mailing-lists`, regression reports are sent to the `to` address in +[`regression/default.yaml`](../backend/data/notifications/regression/default.yaml). +`[STATUS]` summaries keep the address in `common.py`. ## Signup tree for checkout summary