diff --git a/backend/data/notifications/example-hardware-subscription.yaml b/backend/data/notifications/example-hardware-subscription.yaml index 1814d9cbc..69b844e96 100644 --- a/backend/data/notifications/example-hardware-subscription.yaml +++ b/backend/data/notifications/example-hardware-subscription.yaml @@ -6,6 +6,6 @@ qcs9100-ride: # The hardware platform name origin: maestro labs: lava-kci-qualcomm: https://lava-oss.qualcomm.com - default_recipients: + default_recipients: # Cc on the weekly hardware summary for this platform - Recipient One - Recipient Two \ No newline at end of file diff --git a/backend/data/notifications/example-subscription.yaml b/backend/data/notifications/example-subscription.yaml index b01cd0576..a717bea23 100644 --- a/backend/data/notifications/example-subscription.yaml +++ b/backend/data/notifications/example-subscription.yaml @@ -1,7 +1,12 @@ tree: # The filename and the name of the tree may follow the tree-names.yaml file, but does not matter in the code url: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git # the git_repository_url of the tree default_recipients: - - broonie@kernel.org + - email: broonie@kernel.org + issues: [build] # the issue kinds this address is notified about + - email: someone@example.com + issues: [build, boot, test] + new_issues: + issue_types: [boot] # hourly summary for these kinds on this tree reports: - default: diff --git a/backend/data/notifications/metrics/.gitkeep b/backend/data/notifications/metrics/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/backend/data/notifications/subscriptions/amlogic.yaml b/backend/data/notifications/subscriptions/amlogic.yaml index 3f12baf0c..32771229b 100644 --- a/backend/data/notifications/subscriptions/amlogic.yaml +++ b/backend/data/notifications/subscriptions/amlogic.yaml @@ -1,4 +1,5 @@ amlogic: url: https://git.kernel.org/pub/scm/linux/kernel/git/amlogic/linux.git default_recipients: - - linux-amlogic@lists.infradead.org + - email: linux-amlogic@lists.infradead.org + issues: [build] diff --git a/backend/data/notifications/subscriptions/android.yaml b/backend/data/notifications/subscriptions/android.yaml index c0df5e052..fbeb6209e 100644 --- a/backend/data/notifications/subscriptions/android.yaml +++ b/backend/data/notifications/subscriptions/android.yaml @@ -1,9 +1,12 @@ android: url: https://android.googlesource.com/kernel/common default_recipients: - - tkjos@google.com - - joneslee@google.com - - ynaffit@google.com + - email: tkjos@google.com + issues: [build] + - email: joneslee@google.com + issues: [build] + - email: ynaffit@google.com + issues: [build] reports: - mainline: diff --git a/backend/data/notifications/subscriptions/ardb.yaml b/backend/data/notifications/subscriptions/ardb.yaml index 24700133f..75156a2ef 100644 --- a/backend/data/notifications/subscriptions/ardb.yaml +++ b/backend/data/notifications/subscriptions/ardb.yaml @@ -1,4 +1,5 @@ ardb: url: https://git.kernel.org/pub/scm/linux/kernel/git/ardb/linux.git default_recipients: - - ardb@kernel.org + - email: ardb@kernel.org + issues: [build] diff --git a/backend/data/notifications/subscriptions/arm64.yaml b/backend/data/notifications/subscriptions/arm64.yaml index e0d5eea96..96670a85b 100644 --- a/backend/data/notifications/subscriptions/arm64.yaml +++ b/backend/data/notifications/subscriptions/arm64.yaml @@ -1,8 +1,10 @@ arm64: url: https://git.kernel.org/pub/scm/linux/kernel/git/arm64/linux.git default_recipients: - - will@kernel.org - - linux-arm-kernel@lists.infradead.org + - email: will@kernel.org + issues: [build] + - email: linux-arm-kernel@lists.infradead.org + issues: [build] reports: - default: branch: for-kernelci diff --git a/backend/data/notifications/subscriptions/arnd.yaml b/backend/data/notifications/subscriptions/arnd.yaml index 0aa3110c4..0184ef800 100644 --- a/backend/data/notifications/subscriptions/arnd.yaml +++ b/backend/data/notifications/subscriptions/arnd.yaml @@ -1,4 +1,5 @@ arnd: url: https://git.kernel.org/pub/scm/linux/kernel/git/arnd/playground.git default_recipients: - - arnd@arndb.de + - email: arnd@arndb.de + issues: [build] diff --git a/backend/data/notifications/subscriptions/broonie-misc.yaml b/backend/data/notifications/subscriptions/broonie-misc.yaml index cb2823baa..cacd01271 100644 --- a/backend/data/notifications/subscriptions/broonie-misc.yaml +++ b/backend/data/notifications/subscriptions/broonie-misc.yaml @@ -1,4 +1,5 @@ broonie-misc: url: https://git.kernel.org/pub/scm/linux/kernel/git/broonie/misc.git default_recipients: - - broonie@kernel.org + - email: broonie@kernel.org + issues: [build] diff --git a/backend/data/notifications/subscriptions/broonie-regmap.yaml b/backend/data/notifications/subscriptions/broonie-regmap.yaml index 924f37738..146d67c0d 100644 --- a/backend/data/notifications/subscriptions/broonie-regmap.yaml +++ b/backend/data/notifications/subscriptions/broonie-regmap.yaml @@ -1,4 +1,5 @@ broonie-regmap: url: https://git.kernel.org/pub/scm/linux/kernel/git/broonie/regmap.git default_recipients: - - broonie@kernel.org + - email: broonie@kernel.org + issues: [build] diff --git a/backend/data/notifications/subscriptions/broonie-regulator.yaml b/backend/data/notifications/subscriptions/broonie-regulator.yaml index c36d733a6..c367734f5 100644 --- a/backend/data/notifications/subscriptions/broonie-regulator.yaml +++ b/backend/data/notifications/subscriptions/broonie-regulator.yaml @@ -1,7 +1,8 @@ broonie-regulator: url: https://git.kernel.org/pub/scm/linux/kernel/git/broonie/regulator.git default_recipients: - - broonie@kernel.org + - email: broonie@kernel.org + issues: [build] reports: - for-linus: diff --git a/backend/data/notifications/subscriptions/broonie-sound.yaml b/backend/data/notifications/subscriptions/broonie-sound.yaml index c665a4b57..a0b7c0663 100644 --- a/backend/data/notifications/subscriptions/broonie-sound.yaml +++ b/backend/data/notifications/subscriptions/broonie-sound.yaml @@ -1,7 +1,8 @@ broonie-sound: url: https://git.kernel.org/pub/scm/linux/kernel/git/broonie/sound.git default_recipients: - - broonie@kernel.org + - email: broonie@kernel.org + issues: [build] reports: - for-linus: diff --git a/backend/data/notifications/subscriptions/broonie-spi.yaml b/backend/data/notifications/subscriptions/broonie-spi.yaml index fb5590555..6498364d6 100644 --- a/backend/data/notifications/subscriptions/broonie-spi.yaml +++ b/backend/data/notifications/subscriptions/broonie-spi.yaml @@ -1,7 +1,8 @@ broonie-spi: url: https://git.kernel.org/pub/scm/linux/kernel/git/broonie/spi.git default_recipients: - - broonie@kernel.org + - email: broonie@kernel.org + issues: [build] reports: - for-linus: diff --git a/backend/data/notifications/subscriptions/chrome-platform.yaml b/backend/data/notifications/subscriptions/chrome-platform.yaml index 58139a97c..43764400a 100644 --- a/backend/data/notifications/subscriptions/chrome-platform.yaml +++ b/backend/data/notifications/subscriptions/chrome-platform.yaml @@ -1,5 +1,7 @@ chrome-platform: url: https://git.kernel.org/pub/scm/linux/kernel/git/chrome-platform/linux.git default_recipients: - - chrome-platform@lists.linux.dev - - tzungbi@kernel.org + - email: chrome-platform@lists.linux.dev + issues: [build] + - email: tzungbi@kernel.org + issues: [build] diff --git a/backend/data/notifications/subscriptions/chromiumos.yaml b/backend/data/notifications/subscriptions/chromiumos.yaml index 1e8a9466c..25d6114c4 100644 --- a/backend/data/notifications/subscriptions/chromiumos.yaml +++ b/backend/data/notifications/subscriptions/chromiumos.yaml @@ -1,10 +1,14 @@ chromiumos: url: https://chromium.googlesource.com/chromiumos/third_party/kernel.git default_recipients: - - laura.nao@collabora.com - - vignesh.raman@collabora.com - - arnaud.ferraris@collabora.com - - mhiramat@google.com + - email: laura.nao@collabora.com + issues: [build] + - email: vignesh.raman@collabora.com + issues: [build] + - email: arnaud.ferraris@collabora.com + issues: [build] + - email: mhiramat@google.com + issues: [build] reports: - chromeos-5.10: diff --git a/backend/data/notifications/subscriptions/cip.yaml b/backend/data/notifications/subscriptions/cip.yaml index 78681ce88..8499504d7 100644 --- a/backend/data/notifications/subscriptions/cip.yaml +++ b/backend/data/notifications/subscriptions/cip.yaml @@ -1,7 +1,8 @@ cip: url: https://git.kernel.org/pub/scm/linux/kernel/git/cip/linux-cip.git default_recipients: - - cip-dev@lists.cip-project.org + - email: cip-dev@lists.cip-project.org + issues: [build] reports: - linux-4.4.y-cip: diff --git a/backend/data/notifications/subscriptions/clk.yaml b/backend/data/notifications/subscriptions/clk.yaml index 65e3d3ec5..79f8612a8 100644 --- a/backend/data/notifications/subscriptions/clk.yaml +++ b/backend/data/notifications/subscriptions/clk.yaml @@ -1,5 +1,7 @@ clk: url: https://git.kernel.org/pub/scm/linux/kernel/git/clk/linux.git default_recipients: - - linux-clk@vger.kernel.org - - sboyd@kernel.org + - email: linux-clk@vger.kernel.org + issues: [build] + - email: sboyd@kernel.org + issues: [build] diff --git a/backend/data/notifications/subscriptions/efi.yaml b/backend/data/notifications/subscriptions/efi.yaml index b2c53dc15..643569bff 100644 --- a/backend/data/notifications/subscriptions/efi.yaml +++ b/backend/data/notifications/subscriptions/efi.yaml @@ -1,4 +1,5 @@ efi: url: https://git.kernel.org/pub/scm/linux/kernel/git/efi/efi.git default_recipients: - - ardb@kernel.org + - email: ardb@kernel.org + issues: [build] diff --git a/backend/data/notifications/subscriptions/hyperv.yaml b/backend/data/notifications/subscriptions/hyperv.yaml index 0819b8cbf..3eb9c6a7f 100644 --- a/backend/data/notifications/subscriptions/hyperv.yaml +++ b/backend/data/notifications/subscriptions/hyperv.yaml @@ -1,7 +1,8 @@ hyperv: url: https://git.kernel.org/pub/scm/linux/kernel/git/hyperv/linux.git default_recipients: - - kernelcialerts@microsoft.com + - email: kernelcialerts@microsoft.com + issues: [build] reports: diff --git a/backend/data/notifications/subscriptions/khilman.yaml b/backend/data/notifications/subscriptions/khilman.yaml index 59bddab89..470627841 100644 --- a/backend/data/notifications/subscriptions/khilman.yaml +++ b/backend/data/notifications/subscriptions/khilman.yaml @@ -1,4 +1,5 @@ khilman: url: https://git.kernel.org/pub/scm/linux/kernel/git/khilman/linux.git default_recipients: - - khilman@kernel.org + - email: khilman@kernel.org + issues: [build] diff --git a/backend/data/notifications/subscriptions/krzysztof.yaml b/backend/data/notifications/subscriptions/krzysztof.yaml index 1d04baa6f..a2f1d343d 100644 --- a/backend/data/notifications/subscriptions/krzysztof.yaml +++ b/backend/data/notifications/subscriptions/krzysztof.yaml @@ -1,4 +1,5 @@ krzysztof: url: https://git.kernel.org/pub/scm/linux/kernel/git/krzk/linux.git default_recipients: - - krzk@kernel.org + - email: krzk@kernel.org + issues: [build] diff --git a/backend/data/notifications/subscriptions/kselftest.yaml b/backend/data/notifications/subscriptions/kselftest.yaml index c7433801e..5cf5fa278 100644 --- a/backend/data/notifications/subscriptions/kselftest.yaml +++ b/backend/data/notifications/subscriptions/kselftest.yaml @@ -1,4 +1,5 @@ kselftest: url: https://git.kernel.org/pub/scm/linux/kernel/git/shuah/linux-kselftest.git default_recipients: - - shuah@kernel.org + - email: shuah@kernel.org + issues: [build] diff --git a/backend/data/notifications/subscriptions/lee-backlight.yaml b/backend/data/notifications/subscriptions/lee-backlight.yaml index 8684c4c95..3d9d4d50b 100644 --- a/backend/data/notifications/subscriptions/lee-backlight.yaml +++ b/backend/data/notifications/subscriptions/lee-backlight.yaml @@ -1,4 +1,5 @@ lee-backlight: url: https://git.kernel.org/pub/scm/linux/kernel/git/lee/backlight.git default_recipients: - - lee@kernel.org + - email: lee@kernel.org + issues: [build] diff --git a/backend/data/notifications/subscriptions/lee-mfd.yaml b/backend/data/notifications/subscriptions/lee-mfd.yaml index 247c5b853..84fcdc03b 100644 --- a/backend/data/notifications/subscriptions/lee-mfd.yaml +++ b/backend/data/notifications/subscriptions/lee-mfd.yaml @@ -1,4 +1,5 @@ lee-mfd: url: https://git.kernel.org/pub/scm/linux/kernel/git/lee/mfd.git default_recipients: - - lee@kernel.org + - email: lee@kernel.org + issues: [build] diff --git a/backend/data/notifications/subscriptions/linux-pci.yaml b/backend/data/notifications/subscriptions/linux-pci.yaml index 4953a49af..3eac8a9a1 100644 --- a/backend/data/notifications/subscriptions/linux-pci.yaml +++ b/backend/data/notifications/subscriptions/linux-pci.yaml @@ -1,4 +1,5 @@ linux-pci: url: https://git.kernel.org/pub/scm/linux/kernel/git/pci/pci.git default_recipients: - - linux-pci@vger.kernel.org + - email: linux-pci@vger.kernel.org + issues: [build] diff --git a/backend/data/notifications/subscriptions/mainline.yaml b/backend/data/notifications/subscriptions/mainline.yaml index 1a0df548c..b1842bb60 100644 --- a/backend/data/notifications/subscriptions/mainline.yaml +++ b/backend/data/notifications/subscriptions/mainline.yaml @@ -1,7 +1,8 @@ mainline: url: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git default_recipients: - - broonie@kernel.org + - email: broonie@kernel.org + issues: [build] reports: - default: diff --git a/backend/data/notifications/subscriptions/media-committers.yaml b/backend/data/notifications/subscriptions/media-committers.yaml index 18feea2e9..69d11d69d 100644 --- a/backend/data/notifications/subscriptions/media-committers.yaml +++ b/backend/data/notifications/subscriptions/media-committers.yaml @@ -1,7 +1,8 @@ media-committers: url: https://gitlab.freedesktop.org/linux-media/media-committers.git default_recipients: - - linux-media@vger.kernel.org + - email: linux-media@vger.kernel.org + issues: [build] reports: - fixes: diff --git a/backend/data/notifications/subscriptions/media.yaml b/backend/data/notifications/subscriptions/media.yaml index 9b82729e7..beda3021c 100644 --- a/backend/data/notifications/subscriptions/media.yaml +++ b/backend/data/notifications/subscriptions/media.yaml @@ -1,4 +1,5 @@ media: url: https://git.linuxtv.org/media.git default_recipients: - - media@linuxtv.org + - email: media@linuxtv.org + issues: [build] diff --git a/backend/data/notifications/subscriptions/mediatek.yaml b/backend/data/notifications/subscriptions/mediatek.yaml index 966cce43b..d86481790 100644 --- a/backend/data/notifications/subscriptions/mediatek.yaml +++ b/backend/data/notifications/subscriptions/mediatek.yaml @@ -1,7 +1,8 @@ mediatek: url: https://git.kernel.org/pub/scm/linux/kernel/git/mediatek/linux.git default_recipients: - - angelogioacchino.delregno@collabora.com + - email: angelogioacchino.delregno@collabora.com + issues: [build] reports: - default: diff --git a/backend/data/notifications/subscriptions/next.yaml b/backend/data/notifications/subscriptions/next.yaml index a8bb4403b..b3dd7ab51 100644 --- a/backend/data/notifications/subscriptions/next.yaml +++ b/backend/data/notifications/subscriptions/next.yaml @@ -1,7 +1,8 @@ next: url: https://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git default_recipients: - - linux-next@vger.kernel.org + - email: linux-next@vger.kernel.org + issues: [build] reports: - default: diff --git a/backend/data/notifications/subscriptions/omap.yaml b/backend/data/notifications/subscriptions/omap.yaml index e2ad859dd..cb45ee2de 100644 --- a/backend/data/notifications/subscriptions/omap.yaml +++ b/backend/data/notifications/subscriptions/omap.yaml @@ -1,7 +1,8 @@ omap: url: https://git.kernel.org/pub/scm/linux/kernel/git/khilman/linux-omap.git default_recipients: - - khilman@baylibre.com + - email: khilman@baylibre.com + issues: [build] reports: - default: diff --git a/backend/data/notifications/subscriptions/padovan.yaml b/backend/data/notifications/subscriptions/padovan.yaml index 287e7a8a7..389d47b60 100644 --- a/backend/data/notifications/subscriptions/padovan.yaml +++ b/backend/data/notifications/subscriptions/padovan.yaml @@ -1,4 +1,5 @@ padovan: url: https://github.com/padovan/linux.git default_recipients: - - padovan@kernel.org + - email: padovan@kernel.org + issues: [build] diff --git a/backend/data/notifications/subscriptions/pm.yaml b/backend/data/notifications/subscriptions/pm.yaml index a459b2fb5..06fb09bc9 100644 --- a/backend/data/notifications/subscriptions/pm.yaml +++ b/backend/data/notifications/subscriptions/pm.yaml @@ -1,4 +1,5 @@ pm: url: https://git.kernel.org/pub/scm/linux/kernel/git/rafael/linux-pm.git default_recipients: - - rafael@kernel.org + - email: rafael@kernel.org + issues: [build] diff --git a/backend/data/notifications/subscriptions/qcom.yaml b/backend/data/notifications/subscriptions/qcom.yaml index 3da60daf9..705841e9d 100644 --- a/backend/data/notifications/subscriptions/qcom.yaml +++ b/backend/data/notifications/subscriptions/qcom.yaml @@ -1,5 +1,7 @@ qcom: url: https://git.kernel.org/pub/scm/linux/kernel/git/qcom/linux.git default_recipients: - - linux-arm-msm@vger.kernel.org - - Bjorn Andersson + - email: linux-arm-msm@vger.kernel.org + issues: [build] + - email: Bjorn Andersson + issues: [build] diff --git a/backend/data/notifications/subscriptions/renesas.yaml b/backend/data/notifications/subscriptions/renesas.yaml index 319ec78ef..2b22e3cc5 100644 --- a/backend/data/notifications/subscriptions/renesas.yaml +++ b/backend/data/notifications/subscriptions/renesas.yaml @@ -1,4 +1,5 @@ renesas: url: https://git.kernel.org/pub/scm/linux/kernel/git/geert/renesas-devel.git default_recipients: - - linux-renesas-soc@vger.kernel.org + - email: linux-renesas-soc@vger.kernel.org + issues: [build] diff --git a/backend/data/notifications/subscriptions/riscv.yaml b/backend/data/notifications/subscriptions/riscv.yaml index 56bc3de05..459cb95a5 100644 --- a/backend/data/notifications/subscriptions/riscv.yaml +++ b/backend/data/notifications/subscriptions/riscv.yaml @@ -1,4 +1,5 @@ riscv: url: https://git.kernel.org/pub/scm/linux/kernel/git/riscv/linux.git default_recipients: - - linux-riscv@lists.infradead.org + - email: linux-riscv@lists.infradead.org + issues: [build] diff --git a/backend/data/notifications/subscriptions/robh.yaml b/backend/data/notifications/subscriptions/robh.yaml index 9d63dafc6..daba63c17 100644 --- a/backend/data/notifications/subscriptions/robh.yaml +++ b/backend/data/notifications/subscriptions/robh.yaml @@ -1,5 +1,7 @@ robh: url: https://git.kernel.org/pub/scm/linux/kernel/git/robh/linux.git default_recipients: - - robh@kernel.org - - devicetree@vger.kernel.org + - email: robh@kernel.org + issues: [build] + - email: devicetree@vger.kernel.org + issues: [build] diff --git a/backend/data/notifications/subscriptions/rppt.yaml b/backend/data/notifications/subscriptions/rppt.yaml index 899c0d5e8..daa92d270 100644 --- a/backend/data/notifications/subscriptions/rppt.yaml +++ b/backend/data/notifications/subscriptions/rppt.yaml @@ -1,5 +1,7 @@ rppt: url: https://git.kernel.org/pub/scm/linux/kernel/git/rppt/memblock.git default_recipients: - - rppt@kernel.org - - linux-mm@kvack.org + - email: rppt@kernel.org + issues: [build] + - email: linux-mm@kvack.org + issues: [build] diff --git a/backend/data/notifications/subscriptions/sashal-next.yaml b/backend/data/notifications/subscriptions/sashal-next.yaml index bd462e653..1a9a530eb 100644 --- a/backend/data/notifications/subscriptions/sashal-next.yaml +++ b/backend/data/notifications/subscriptions/sashal-next.yaml @@ -1,7 +1,8 @@ sashal-next: url: https://git.kernel.org/pub/scm/linux/kernel/git/sashal/linus-next.git default_recipients: - - sashal@kernel.org + - email: sashal@kernel.org + issues: [build] reports: - default: diff --git a/backend/data/notifications/subscriptions/soc.yaml b/backend/data/notifications/subscriptions/soc.yaml index b6d4beaab..fadfda036 100644 --- a/backend/data/notifications/subscriptions/soc.yaml +++ b/backend/data/notifications/subscriptions/soc.yaml @@ -1,4 +1,5 @@ soc: url: https://git.kernel.org/pub/scm/linux/kernel/git/soc/soc.git default_recipients: - - soc@lists.linux.dev + - email: soc@lists.linux.dev + issues: [build] diff --git a/backend/data/notifications/subscriptions/stable-rc.yaml b/backend/data/notifications/subscriptions/stable-rc.yaml index 9e97aae74..aae6ec6a2 100644 --- a/backend/data/notifications/subscriptions/stable-rc.yaml +++ b/backend/data/notifications/subscriptions/stable-rc.yaml @@ -1,4 +1,5 @@ stable-rc: url: https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable-rc.git default_recipients: - - stable@vger.kernel.org + - email: stable@vger.kernel.org + issues: [build] diff --git a/backend/data/notifications/subscriptions/stable-rt.yaml b/backend/data/notifications/subscriptions/stable-rt.yaml index 190b133da..c60057d04 100644 --- a/backend/data/notifications/subscriptions/stable-rt.yaml +++ b/backend/data/notifications/subscriptions/stable-rt.yaml @@ -2,7 +2,8 @@ stable-rt: url: https://git.kernel.org/pub/scm/linux/kernel/git/rt/linux-stable-rt.git default_recipients: - - linux-rt-devel@lists.linux.dev + - email: linux-rt-devel@lists.linux.dev + issues: [build] reports: - v5.4-rt: diff --git a/backend/data/notifications/subscriptions/stable.yaml b/backend/data/notifications/subscriptions/stable.yaml index 14aafd9bb..c4d22f900 100644 --- a/backend/data/notifications/subscriptions/stable.yaml +++ b/backend/data/notifications/subscriptions/stable.yaml @@ -1,7 +1,8 @@ stable: url: https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git default_recipients: - - stable@vger.kernel.org + - email: stable@vger.kernel.org + issues: [build] reports: - maestro-6.12.y: diff --git a/backend/data/notifications/subscriptions/tegra.yaml b/backend/data/notifications/subscriptions/tegra.yaml index 64bd0126a..f3cf355dc 100644 --- a/backend/data/notifications/subscriptions/tegra.yaml +++ b/backend/data/notifications/subscriptions/tegra.yaml @@ -1,4 +1,5 @@ tegra: url: https://git.kernel.org/pub/scm/linux/kernel/git/tegra/linux.git default_recipients: - - linux-tegra@vger.kernel.org + - email: linux-tegra@vger.kernel.org + issues: [build] diff --git a/backend/data/notifications/subscriptions/thermal.yaml b/backend/data/notifications/subscriptions/thermal.yaml index 8dc380ff6..c7d38464e 100644 --- a/backend/data/notifications/subscriptions/thermal.yaml +++ b/backend/data/notifications/subscriptions/thermal.yaml @@ -1,4 +1,5 @@ thermal: url: https://git.kernel.org/pub/scm/linux/kernel/git/thermal/linux.git default_recipients: - - linux-pm@vger.kernel.org + - email: linux-pm@vger.kernel.org + issues: [build] diff --git a/backend/data/notifications/subscriptions/tip.yaml b/backend/data/notifications/subscriptions/tip.yaml index dcce22c88..81036c5c5 100644 --- a/backend/data/notifications/subscriptions/tip.yaml +++ b/backend/data/notifications/subscriptions/tip.yaml @@ -1,4 +1,5 @@ tip: url: https://git.kernel.org/pub/scm/linux/kernel/git/tip/tip.git default_recipients: - - tip@kernel.org + - email: tip@kernel.org + issues: [build] diff --git a/backend/data/notifications/subscriptions/ulfh.yaml b/backend/data/notifications/subscriptions/ulfh.yaml index b2ecdb513..d9f26e088 100644 --- a/backend/data/notifications/subscriptions/ulfh.yaml +++ b/backend/data/notifications/subscriptions/ulfh.yaml @@ -1,4 +1,5 @@ ulfh: url: https://git.kernel.org/pub/scm/linux/kernel/git/ulfh/mmc.git default_recipients: - - linux-mmc@vger.kernel.org + - email: linux-mmc@vger.kernel.org + issues: [build] diff --git a/backend/data/notifications/subscriptions/vireshk.yaml b/backend/data/notifications/subscriptions/vireshk.yaml index 185f153c4..f0fd4265d 100644 --- a/backend/data/notifications/subscriptions/vireshk.yaml +++ b/backend/data/notifications/subscriptions/vireshk.yaml @@ -1,4 +1,5 @@ vireshk: url: https://git.kernel.org/pub/scm/linux/kernel/git/vireshk/linux.git default_recipients: - - vireshk@kernel.org + - email: vireshk@kernel.org + issues: [build] diff --git a/backend/kernelCI_app/management/commands/helpers/common.py b/backend/kernelCI_app/management/commands/helpers/common.py index 4cea995fc..eecff59db 100644 --- a/backend/kernelCI_app/management/commands/helpers/common.py +++ b/backend/kernelCI_app/management/commands/helpers/common.py @@ -7,6 +7,7 @@ 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.typeModels.notificationSubscriptions import cc_addresses from kernelCI_app.utils import read_yaml_file KERNELCI_RESULTS = "kernelci-results@groups.io" @@ -77,7 +78,7 @@ def _get_default_tree_recipients( tree_url = tree_values.get("url") default_recipients = tree_values.get("default_recipients", []) if search_url == tree_url: - return default_recipients + return cc_addresses(default_recipients) else: log_message( f"Skipping file {filename} on loading summary files. Not a yaml file." diff --git a/backend/kernelCI_app/management/commands/helpers/summary.py b/backend/kernelCI_app/management/commands/helpers/summary.py index 4c51a1fee..541f51bfc 100644 --- a/backend/kernelCI_app/management/commands/helpers/summary.py +++ b/backend/kernelCI_app/management/commands/helpers/summary.py @@ -11,6 +11,7 @@ get_issues_summary_data, ) from kernelCI_app.typeModels.issues import CheckoutIssue, ProcessedExtraDetailedIssues +from kernelCI_app.typeModels.notificationSubscriptions import cc_addresses from kernelCI_app.utils import read_yaml_file type PossibleReportOptions = Literal["ignore_default_recipients"] @@ -91,7 +92,7 @@ def process_submissions_files( report["giturl"] = giturl report["tree_name"] = tree_name if default_recipients: - report["default_recipients"] = default_recipients + report["default_recipients"] = cc_addresses(default_recipients) tree_key = (branch, giturl, origin) tree_key_set.add(tree_key) @@ -216,7 +217,9 @@ def process_hardware_submissions_files( origin = hardware_values.get("origin", DEFAULT_ORIGIN) if hardware_origins is not None and origin not in hardware_origins: continue - default_recipients = hardware_values.get("default_recipients", []) + default_recipients = cc_addresses( + hardware_values.get("default_recipients", []) + ) labs = hardware_values.get("labs") hardware_key = (hardware_name, origin) diff --git a/backend/kernelCI_app/management/commands/notifications.py b/backend/kernelCI_app/management/commands/notifications.py index 19ed1eef6..6dbf34465 100644 --- a/backend/kernelCI_app/management/commands/notifications.py +++ b/backend/kernelCI_app/management/commands/notifications.py @@ -3,10 +3,12 @@ from collections import defaultdict from datetime import datetime, time, timedelta, timezone from email.utils import make_msgid +from pathlib import Path from types import SimpleNamespace from typing import Optional from urllib.parse import quote_plus +from django.conf import settings from django.core.management.base import BaseCommand from kernelCI_app.helpers.email import smtp_setup_connection @@ -15,6 +17,7 @@ from kernelCI_app.helpers.system import get_running_instance from kernelCI_app.helpers.trees import sanitize_tree from kernelCI_app.management.commands.helpers.common import ( + KERNELCI_RESULTS, is_regzbot_tree, send_email_report, setup_jinja_template, @@ -53,6 +56,10 @@ LabMetricsData, MetricsReportData, ) +from kernelCI_app.typeModels.notificationSubscriptions import ( + issues_for_subscriber, + new_issue_subscribers, +) from kernelCI_app.utils import group_status, is_boot from kernelCI_cache.queries.issues import ( get_all_issue_keys, @@ -100,53 +107,154 @@ def exclude_already_found_and_store(issues: list[dict]) -> list[dict]: return new_issues -def look_for_new_issues(*, service, signup_folder, email_args): - if get_running_instance() == "staging": - print("This command only runs on production or dev environments.") - return - - issues = kcidb_new_issues() - build_issues = [] - boot_issues = [] +def _tag_new_issues(issues: list[dict], *, test_only: bool = False) -> list[dict]: + tagged = [] for issue in issues: - if issue["build_id"]: + if test_only: + issue["type"] = "test" + elif issue["build_id"]: issue["type"] = "build" - build_issues.append(issue) elif issue["test_id"]: issue["type"] = "boot" - boot_issues.append(issue) + else: + continue + tagged.append(issue) + return tagged + + +def _summary_report(template, when, *, build_issues, boot_issues, test_issues): + return { + "content": template.render( + build_issues=build_issues, + boot_issues=boot_issues, + test_issues=test_issues, + ), + "title": f"new issues summary - {when.strftime('%Y-%m-%d %H:%M %Z')}", + } + + +def _community_to(email_args) -> str: + if email_args.add_mailing_lists: + return KERNELCI_RESULTS + return email_args.to or "" + + +def _show_new_issue_delivery(email_args, to: str, issues: list[dict]) -> None: + if email_args.send: + return + ids = ", ".join(issue["id"] for issue in issues) + print(f"To: {to}") + print(f"issues: {ids}") + + +def _issues_of_type(issues: list[dict], issue_type: str) -> list[dict]: + return [issue for issue in issues if issue["type"] == issue_type] + +def look_for_new_issues( + *, + service, + signup_folder, + email_args, + issue_report_origins=None, + issue_report_types=None, +): + if get_running_instance() == "staging": + print("This command only runs on production or dev environments.") + return + + if not signup_folder: + signup_folder = SIGNUP_FOLDER + if issue_report_origins is None: + issue_report_origins = ["maestro"] + if issue_report_types is None: + issue_report_types = ["build"] + + subscribers = [] + if not email_args.ignore_recipients: + signup_dir = Path(settings.BACKEND_DATA_DIR) / signup_folder + subscribers = new_issue_subscribers(signup_dir) + wants_test = any("test" in subscriber.issue_types for subscriber in subscribers) + + summary_issues = _tag_new_issues(kcidb_new_issues()) + if wants_test: + known = {issue["id"] for issue in summary_issues} + for issue in _tag_new_issues( + kcidb_new_issues(include_other_tests=True), test_only=True + ): + if issue["id"] not in known: + summary_issues.append(issue) + + build_issues = _issues_of_type(summary_issues, "build") + boot_issues = _issues_of_type(summary_issues, "boot") + test_issues = _issues_of_type(summary_issues, "test") new_build_issues = exclude_already_found_and_store(build_issues) new_boot_issues = exclude_already_found_and_store(boot_issues) - if not new_build_issues and not new_boot_issues: + new_test_issues = ( + exclude_already_found_and_store(test_issues) if test_issues else [] + ) + + community = new_build_issues + new_boot_issues + available = community + new_test_issues + deliveries = [] + for subscriber in subscribers: + matched = issues_for_subscriber(subscriber, available) + if matched: + deliveries.append((subscriber, matched)) + + if not community and not deliveries: print("No new issues") return template = setup_jinja_template("issues.txt.j2") - report = {} now = datetime.now(timezone.utc) - report["content"] = template.render( - build_issues=new_build_issues, boot_issues=new_boot_issues - ) - report["title"] = f"new issues summary - {now.strftime('%Y-%m-%d %H:%M %Z')}" + if community: + _show_new_issue_delivery(email_args, _community_to(email_args), community) + send_email_report( + service=service, + report=_summary_report( + template, + now, + build_issues=new_build_issues, + boot_issues=new_boot_issues, + test_issues=[], + ), + email_args=email_args, + signup_folder=signup_folder, + ) - send_email_report( - service=service, - report=report, - email_args=email_args, - signup_folder=signup_folder, - ) + for subscriber, matched in deliveries: + _show_new_issue_delivery(email_args, subscriber.email, matched) + copy = SimpleNamespace(**vars(email_args)) + copy.to = subscriber.email + copy.cc = None + copy.add_mailing_lists = False + copy.ignore_recipients = True + send_email_report( + service=service, + report=_summary_report( + template, + now, + build_issues=_issues_of_type(matched, "build"), + boot_issues=_issues_of_type(matched, "boot"), + test_issues=_issues_of_type(matched, "test"), + ), + email_args=copy, + ) email_args.add_mailing_lists = True email_args.update = True - for issue in new_build_issues: - if issue["origin"] == "maestro": - generate_issue_report( - service=service, - issue_id=issue["id"], - email_args=email_args, - signup_folder=signup_folder, - ) + for issue in new_build_issues + new_boot_issues + new_test_issues: + if issue["origin"] not in issue_report_origins: + continue + if issue["type"] not in issue_report_types: + continue + generate_issue_report( + service=service, + issue_id=issue["id"], + email_args=email_args, + signup_folder=signup_folder, + ) def ask_ignore_issue(): @@ -1016,6 +1124,24 @@ def add_arguments(self, parser): type=lambda s: [origin.strip() for origin in s.split(",")], help="Limit checkout summary to specific origins (summary only, comma-separated list)", ) + parser.add_argument( + "--issue-report-origins", + type=lambda s: [origin.strip() for origin in s.split(",")], + default=["maestro"], + help=( + "Origins that get an automatic regression report " + "(new_issues only, comma-separated, default: maestro)" + ), + ) + parser.add_argument( + "--issue-report-types", + type=lambda s: [issue_type.strip() for issue_type in s.split(",")], + default=["build"], + help=( + "Issue types that get an automatic regression report " + "(new_issues only, comma-separated, default: build)" + ), + ) parser.add_argument( "--skip-sent-reports", action="store_true", @@ -1071,6 +1197,8 @@ def _run_action(self, options): service=service, email_args=email_args, signup_folder=signup_folder, + issue_report_origins=options.get("issue_report_origins"), + issue_report_types=options.get("issue_report_types"), ) case "issue_report": diff --git a/backend/kernelCI_app/management/commands/templates/issues.txt.j2 b/backend/kernelCI_app/management/commands/templates/issues.txt.j2 index c406c8fa6..172bbeacc 100644 --- a/backend/kernelCI_app/management/commands/templates/issues.txt.j2 +++ b/backend/kernelCI_app/management/commands/templates/issues.txt.j2 @@ -32,4 +32,19 @@ Boot regressions: --- {% endfor %} {% endif %} +{% if test_issues %} +Test regressions: +------------- +{% for issue in test_issues %} +#kernelci issue {{ issue["id"] }} +- Comment: {{ issue["comment"] }} +- Timestamp: {{ issue["_timestamp"].strftime('%Y-%m-%d %H:%M %Z') }} +- Origin: {{ issue["origin"] }} +- Tree: {{ issue["tree_name"] }}/{{ issue["git_repository_branch"]}} +- Commit HEAD: {{ issue["git_commit_hash"] }} +- Incidents Count: {{issue["incident_count"]}} +- Dashboard: https://d.kernelci.org/issue/{{ issue["id"] }} +--- +{% endfor %} +{% endif %} {%- endblock %} \ No newline at end of file diff --git a/backend/kernelCI_app/queries/notifications.py b/backend/kernelCI_app/queries/notifications.py index 89ead4208..dcf5adda0 100644 --- a/backend/kernelCI_app/queries/notifications.py +++ b/backend/kernelCI_app/queries/notifications.py @@ -46,10 +46,19 @@ def kcidb_execute_query(query, params=None): sys.exit() -def kcidb_new_issues(): - """Fetch issues from the last few days, including related checkouts.""" +def kcidb_new_issues(*, include_other_tests: bool = False): + """Fetch issues from the last hour, including the first incident's checkout. + + Boot-path tests stay in the default result. Other tests are included only + when a new_issues subscriber asked for them, so the community summary can + keep its current rows. + """ params = {"interval": "1 hour"} + if include_other_tests: + path_predicate = "AND NOT (t.path = 'boot' OR t.path = 'boot.nfs')" + else: + path_predicate = "AND (t.path = 'boot' OR t.path = 'boot.nfs')" query = """ WITH ranked_issues AS ( @@ -112,6 +121,8 @@ def kcidb_new_issues(): c.git_repository_branch, c.git_commit_hash, c.git_commit_name, + NULL::text AS path, + NULL::text AS platform, ROW_NUMBER() OVER (PARTITION BY inc.issue_id ORDER BY inc._timestamp ASC) as incident_rn FROM incidents inc JOIN builds b ON inc.build_id = b.id @@ -131,13 +142,15 @@ def kcidb_new_issues(): c.git_repository_branch, c.git_commit_hash, c.git_commit_name, + t.path, + t.environment_misc->>'platform' AS platform, ROW_NUMBER() OVER (PARTITION BY inc.issue_id ORDER BY inc._timestamp ASC) as incident_rn FROM incidents inc JOIN tests t ON inc.test_id = t.id JOIN builds b ON t.build_id = b.id JOIN checkouts c ON b.checkout_id = c.id WHERE inc._timestamp >= NOW() - INTERVAL %(interval)s - AND (t.path = 'boot' OR t.path = 'boot.nfs') + __PATH_PREDICATE__ ) SELECT @@ -154,6 +167,8 @@ def kcidb_new_issues(): fi.git_repository_branch, fi.git_commit_hash, fi.git_commit_name, + fi.path, + fi.platform, COUNT(inc.id) AS incident_count FROM new_issues n LEFT JOIN first_incidents fi ON n.id = fi.issue_id AND fi.incident_rn = 1 @@ -171,11 +186,15 @@ def kcidb_new_issues(): fi.tree_name, fi.git_repository_branch, fi.git_commit_hash, - fi.git_commit_name + fi.git_commit_name, + fi.path, + fi.platform ORDER BY fi._timestamp DESC; """ - return kcidb_execute_query(query, params) + return kcidb_execute_query( + query.replace("__PATH_PREDICATE__", path_predicate), params + ) # TODO: include issue version as an optional parameter diff --git a/backend/kernelCI_app/tests/unitTests/commands/new_issues_notifications_test.py b/backend/kernelCI_app/tests/unitTests/commands/new_issues_notifications_test.py new file mode 100644 index 000000000..39d4d17ed --- /dev/null +++ b/backend/kernelCI_app/tests/unitTests/commands/new_issues_notifications_test.py @@ -0,0 +1,228 @@ +from datetime import datetime, timezone +from types import SimpleNamespace +from unittest.mock import patch + +from kernelCI_app.management.commands.notifications import look_for_new_issues +from kernelCI_app.typeModels.notificationSubscriptions import NewIssueSubscriber + +MODULE = "kernelCI_app.management.commands.notifications" +WHEN = datetime(2026, 10, 14, 12, 0, tzinfo=timezone.utc) + + +def _issue(**overrides): + issue = { + "id": "issue-1", + "version": 1, + "build_id": "build-1", + "test_id": None, + "origin": "maestro", + "git_repository_url": "https://example.com/sound.git", + "platform": None, + "comment": "broke the build", + "_timestamp": WHEN, + "tree_name": "sound", + "git_repository_branch": "for-next", + "git_commit_hash": "abc", + "incident_count": 1, + } + issue.update(overrides) + return issue + + +def _email_args(**overrides): + args = SimpleNamespace( + send=False, + yes=True, + to="kernelci-results@groups.io", + cc=None, + in_reply_to=None, + ignore_recipients=False, + add_mailing_lists=False, + regression_report=False, + ) + for key, value in overrides.items(): + setattr(args, key, value) + return args + + +def _run( + issues, + subscribers, + email_args, + *, + other_issues=None, + issue_report_origins=None, + issue_report_types=None, +): + def query(*, include_other_tests=False): + if include_other_tests: + return other_issues or [] + return issues + + with ( + patch(f"{MODULE}.get_running_instance", return_value=None), + patch(f"{MODULE}.kcidb_new_issues", side_effect=query) as fetched, + patch( + f"{MODULE}.exclude_already_found_and_store", side_effect=lambda rows: rows + ), + patch(f"{MODULE}.new_issue_subscribers", return_value=subscribers), + patch(f"{MODULE}.send_email_report") as send, + patch(f"{MODULE}.generate_issue_report") as regression, + ): + look_for_new_issues( + service=object(), + signup_folder="notifications/subscriptions", + email_args=email_args, + issue_report_origins=issue_report_origins, + issue_report_types=issue_report_types, + ) + return fetched, send, regression + + +def test_no_new_issues_block_sends_one_community_summary_and_maestro_build_report( + capsys, +): + build_issue = _issue(id="build-maestro") + boot_issue = _issue( + id="boot-1", + build_id=None, + test_id="test-1", + origin="microsoft", + git_repository_url="https://example.com/other.git", + ) + other_origin = _issue(id="build-other", origin="microsoft") + fetched, send, regression = _run( + [build_issue, boot_issue, other_origin], + [], + _email_args(), + ) + + fetched.assert_called_once_with() + assert send.call_count == 1 + report = send.call_args.kwargs["report"] + assert "build-maestro" in report["content"] + assert "boot-1" in report["content"] + assert "Test regressions" not in report["content"] + assert send.call_args.kwargs["email_args"].to == "kernelci-results@groups.io" + regression.assert_called_once() + assert regression.call_args.kwargs["issue_id"] == "build-maestro" + printed = capsys.readouterr().out + assert "To: kernelci-results@groups.io" in printed + assert "issues: build-maestro, build-other, boot-1" in printed + + +def test_new_issues_block_sends_a_filtered_copy_and_skips_an_empty_one(): + boot_on_tree = _issue( + id="boot-sound", + build_id=None, + test_id="test-1", + platform="board", + ) + boot_elsewhere = _issue( + id="boot-other", + build_id=None, + test_id="test-2", + git_repository_url="https://example.com/other.git", + ) + build_on_tree = _issue(id="build-sound") + subscribers = [ + NewIssueSubscriber( + email="broonie@kernel.org", + issue_types=frozenset({"boot"}), + tree_url="https://example.com/sound.git", + ), + NewIssueSubscriber( + email="nobody@example.com", + issue_types=frozenset({"test"}), + tree_url="https://example.com/missing.git", + ), + ] + _, send, _ = _run( + [boot_on_tree, boot_elsewhere, build_on_tree], + subscribers, + _email_args(send=True), + ) + + assert send.call_count == 2 + copy = send.call_args_list[1].kwargs + assert copy["email_args"].to == "broonie@kernel.org" + assert copy["email_args"].cc is None + assert copy["email_args"].add_mailing_lists is False + assert copy["email_args"].ignore_recipients is True + assert "boot-sound" in copy["report"]["content"] + assert "boot-other" not in copy["report"]["content"] + assert "build-sound" not in copy["report"]["content"] + + +def test_test_subscription_fetches_other_tests_only_for_that_copy(): + test_issue = _issue( + id="test-1", + build_id=None, + test_id="test-1", + platform="board", + ) + subscriber = NewIssueSubscriber( + email="broonie@kernel.org", + issue_types=frozenset({"test"}), + tree_url="https://example.com/sound.git", + ) + fetched, send, regression = _run( + [], + [subscriber], + _email_args(send=True), + other_issues=[test_issue], + ) + + assert fetched.call_count == 2 + assert fetched.call_args_list[1].kwargs["include_other_tests"] is True + assert send.call_count == 1 + assert send.call_args.kwargs["email_args"].to == "broonie@kernel.org" + assert "test-1" in send.call_args.kwargs["report"]["content"] + assert "Test regressions" in send.call_args.kwargs["report"]["content"] + regression.assert_not_called() + + +def test_issue_report_flags_select_origin_and_type(): + issues = [ + _issue(id="build-maestro"), + _issue(id="boot-maestro", build_id=None, test_id="t1"), + _issue(id="build-microsoft", origin="microsoft"), + ] + _, _, boot_only = _run( + issues, + [], + _email_args(send=True), + issue_report_origins=["maestro"], + issue_report_types=["boot"], + ) + assert [call.kwargs["issue_id"] for call in boot_only.call_args_list] == [ + "boot-maestro" + ] + + _, _, microsoft_build = _run( + issues, + [], + _email_args(send=True), + issue_report_origins=["microsoft"], + issue_report_types=["build"], + ) + assert [call.kwargs["issue_id"] for call in microsoft_build.call_args_list] == [ + "build-microsoft" + ] + + +def test_ignore_recipients_skips_subscription_copies(): + subscriber = NewIssueSubscriber( + email="broonie@kernel.org", + issue_types=frozenset({"boot", "test"}), + tree_url="https://example.com/sound.git", + ) + fetched, send, _ = _run( + [_issue(id="boot-sound", build_id=None, test_id="test-1")], + [subscriber], + _email_args(send=True, ignore_recipients=True), + ) + + fetched.assert_called_once_with() + assert send.call_count == 1 + assert send.call_args.kwargs["email_args"].to == "kernelci-results@groups.io" diff --git a/backend/kernelCI_app/tests/unitTests/commands/notification_subscriptions_test.py b/backend/kernelCI_app/tests/unitTests/commands/notification_subscriptions_test.py new file mode 100644 index 000000000..8fb352eca --- /dev/null +++ b/backend/kernelCI_app/tests/unitTests/commands/notification_subscriptions_test.py @@ -0,0 +1,375 @@ +from pathlib import Path + +import pytest +import yaml + +from kernelCI_app.management.commands.helpers.summary import ( + process_hardware_submissions_files, + process_submissions_files, +) +from kernelCI_app.typeModels.notificationSubscriptions import ( + NewIssueSubscriber, + NotificationConfigError, + cc_addresses, + issues_for_subscriber, + load_metrics_rules, + load_subscriptions, + new_issue_subscribers, +) + +NOTIFICATIONS = Path(__file__).resolve().parents[4] / "data" / "notifications" +SUBSCRIPTIONS = NOTIFICATIONS / "subscriptions" +METRICS = NOTIFICATIONS / "metrics" + + +def _write(directory: Path, name: str, body: dict) -> None: + directory.mkdir(parents=True, exist_ok=True) + (directory / name).write_text(yaml.safe_dump(body)) + + +def test_subscription_files_load_and_keep_one_cc_address_per_entry(): + trees, hardware = load_subscriptions(SUBSCRIPTIONS) + assert trees + assert hardware + load_metrics_rules(METRICS, trees=trees, hardware=hardware) + + for path in SUBSCRIPTIONS.glob("*.y*ml"): + data = yaml.safe_load(path.read_text()) + for body in data.values(): + raw = body.get("default_recipients") or [] + expected = [ + entry["email"] if isinstance(entry, dict) else entry for entry in raw + ] + assert cc_addresses(raw) == expected + + +def test_invalid_plain_address_names_the_address(tmp_path): + _write( + tmp_path, + "qcs6490_hardware.yaml", + { + "qcs6490-rb3gen2": { + "origin": "maestro", + "default_recipients": ["Trilok Soni "], + } + }, + ) + with pytest.raises(NotificationConfigError) as raised: + load_subscriptions(tmp_path) + error = raised.value + assert error.file == "qcs6490_hardware.yaml" + assert error.field == "qcs6490-rb3gen2.default_recipients.0.email" + assert "invalid email address: Trilok Soni " in str(error) + + +def test_new_issues_block_lists_tree_and_hardware_subscribers(tmp_path): + _write( + tmp_path, + "sound.yaml", + { + "broonie-sound": { + "url": "https://example.com/sound.git", + "default_recipients": [ + "plain@example.com", + {"email": "build-only@example.com", "issues": ["build"]}, + { + "email": "broonie@kernel.org", + "issues": ["build"], + "new_issues": {"issue_types": ["boot", "test"]}, + }, + ], + } + }, + ) + _write( + tmp_path, + "board_hardware.yaml", + { + "qcs6490-rb3gen2": { + "default_recipients": [ + { + "email": "tsoni@quicinc.com", + "new_issues": {"issue_types": ["boot"]}, + } + ], + } + }, + ) + + subscribers = new_issue_subscribers(tmp_path) + listed = [ + (item.email, item.issue_types, item.tree_url, item.hardware, item.origin) + for item in subscribers + ] + assert listed == [ + ( + "tsoni@quicinc.com", + frozenset({"boot"}), + None, + "qcs6490-rb3gen2", + "maestro", + ), + ( + "broonie@kernel.org", + frozenset({"boot", "test"}), + "https://example.com/sound.git", + None, + None, + ), + ] + assert cc_addresses( + [ + { + "email": "broonie@kernel.org", + "issues": ["build"], + "new_issues": {"issue_types": ["boot"]}, + } + ] + ) == ["broonie@kernel.org"] + + +def test_issues_for_subscriber_filters_type_and_scope(): + tree = NewIssueSubscriber( + email="a@example.com", + issue_types=frozenset({"boot"}), + tree_url="https://example.com/sound.git", + ) + hardware = NewIssueSubscriber( + email="b@example.com", + issue_types=frozenset({"boot", "build"}), + hardware="board", + origin="maestro", + ) + issues = [ + { + "id": "boot-on-tree", + "type": "boot", + "git_repository_url": "https://example.com/sound.git", + "platform": "other", + "origin": "maestro", + }, + { + "id": "build-on-tree", + "type": "build", + "git_repository_url": "https://example.com/sound.git", + "platform": None, + "origin": "maestro", + }, + { + "id": "boot-on-board", + "type": "boot", + "git_repository_url": "https://example.com/other.git", + "platform": "board", + "origin": "maestro", + }, + { + "id": "boot-on-board-other-origin", + "type": "boot", + "git_repository_url": "https://example.com/other.git", + "platform": "board", + "origin": "microsoft", + }, + ] + assert [issue["id"] for issue in issues_for_subscriber(tree, issues)] == [ + "boot-on-tree" + ] + assert [issue["id"] for issue in issues_for_subscriber(hardware, issues)] == [ + "boot-on-board" + ] + + +def test_current_subscription_files_have_no_new_issues_subscribers(): + assert new_issue_subscribers(SUBSCRIPTIONS) == [] + + +def test_invalid_new_issues_type_names_file_and_field(tmp_path): + _write( + tmp_path, + "tree.yaml", + { + "demo": { + "url": "https://example.com/linux.git", + "default_recipients": [ + { + "email": "person@example.com", + "new_issues": {"issue_types": ["flaky"]}, + } + ], + } + }, + ) + with pytest.raises(NotificationConfigError) as raised: + load_subscriptions(tmp_path) + error = raised.value + assert error.file == "tree.yaml" + assert "new_issues" in error.field + assert "issue_types" in error.field + + +def test_empty_new_issues_types_fail(tmp_path): + _write( + tmp_path, + "tree.yaml", + { + "demo": { + "url": "https://example.com/linux.git", + "default_recipients": [ + { + "email": "person@example.com", + "new_issues": {"issue_types": []}, + } + ], + } + }, + ) + with pytest.raises(NotificationConfigError) as raised: + new_issue_subscribers(tmp_path) + assert raised.value.file == "tree.yaml" + assert "issue_types" in raised.value.field + + +def test_invalid_issue_kind_names_file_and_field(tmp_path): + _write( + tmp_path, + "tree.yaml", + { + "demo": { + "url": "https://example.com/linux.git", + "default_recipients": [ + { + "email": "person@example.com", + "issues": ["flaky"], + } + ], + } + }, + ) + with pytest.raises(NotificationConfigError) as raised: + load_subscriptions(tmp_path) + error = raised.value + assert error.file == "tree.yaml" + assert "issues" in error.field + + +def test_unknown_hardware_name_fails(tmp_path): + _write( + tmp_path, + "qualcomm.yaml", + { + "qualcomm-weekly": { + "recipients": ["tsoni@quicinc.com"], + "hardware": ["missing-board"], + "period_days": 7, + } + }, + ) + with pytest.raises(NotificationConfigError) as raised: + load_metrics_rules(tmp_path, trees=set(), hardware={"qcs6490-rb3gen2"}) + assert raised.value.field == "qualcomm-weekly.hardware" + assert "missing-board" in str(raised.value) + + +def test_overlapping_metrics_recipients_fail(tmp_path): + _write( + tmp_path, + "a.yaml", + { + "weekly-a": { + "recipients": ["tsoni@quicinc.com"], + "hardware": ["qcs6490-rb3gen2"], + "period_days": 7, + } + }, + ) + _write( + tmp_path, + "b.yaml", + { + "weekly-b": { + "recipients": ["tsoni@quicinc.com"], + "hardware": ["qcs6490-rb3gen2"], + "origins": ["maestro"], + "period_days": 7, + } + }, + ) + with pytest.raises(NotificationConfigError) as raised: + load_metrics_rules(tmp_path, trees=set(), hardware={"qcs6490-rb3gen2"}) + assert "overlaps" in str(raised.value) + assert raised.value.file == "b.yaml" + + separate = tmp_path / "separate" + _write( + separate, + "boards.yaml", + { + "rb3": { + "recipients": ["tsoni@quicinc.com"], + "hardware": ["qcs6490-rb3gen2"], + "origins": ["maestro"], + "period_days": 7, + }, + "ride": { + "recipients": ["tsoni@quicinc.com"], + "hardware": ["qcs9100-ride"], + "origins": ["maestro"], + "period_days": 7, + }, + }, + ) + load_metrics_rules( + separate, + trees=set(), + hardware={"qcs6490-rb3gen2", "qcs9100-ride"}, + ) + + +def test_loader_keeps_mapping_email_on_the_cc_list(tmp_path): + subscriptions = tmp_path / "notifications" / "subscriptions" + _write( + subscriptions, + "demo.yaml", + { + "demo": { + "url": "https://example.com/linux.git", + "default_recipients": [ + "plain@example.com", + { + "email": "opted@example.com", + "issues": ["boot", "test"], + }, + ], + "reports": [{"main": None, "branch": "main"}], + } + }, + ) + _write( + subscriptions, + "board_hardware.yaml", + { + "board": { + "origin": "maestro", + "default_recipients": [ + { + "email": "Owner ", + "issues": ["boot"], + } + ], + } + }, + ) + + _, tree_props = process_submissions_files( + base_dir=str(tmp_path), + signup_folder="notifications/subscriptions", + ) + recipients = next(iter(tree_props.values()))[0]["default_recipients"] + assert recipients == ["plain@example.com", "opted@example.com"] + + _, hardware_props = process_hardware_submissions_files( + base_dir=str(tmp_path), + signup_folder="notifications/subscriptions", + ) + assert hardware_props[("board", "maestro")]["default_recipients"] == [ + "Owner " + ] diff --git a/backend/kernelCI_app/tests/unitTests/queries/notifications_test.py b/backend/kernelCI_app/tests/unitTests/queries/notifications_test.py index cedbba650..cf9528846 100644 --- a/backend/kernelCI_app/tests/unitTests/queries/notifications_test.py +++ b/backend/kernelCI_app/tests/unitTests/queries/notifications_test.py @@ -62,6 +62,20 @@ def test_kcidb_new_issues_success(self, mock_execute_query): assert result == expected_result mock_execute_query.assert_called_once() + @patch("kernelCI_app.queries.notifications.kcidb_execute_query") + def test_kcidb_new_issues_keeps_boot_filter_until_other_tests_are_requested( + self, mock_execute_query + ): + mock_execute_query.return_value = [] + kcidb_new_issues() + query = mock_execute_query.call_args[0][0] + assert "AND (t.path = 'boot' OR t.path = 'boot.nfs')" in query + assert "environment_misc->>'platform' AS platform" in query + + kcidb_new_issues(include_other_tests=True) + other = mock_execute_query.call_args[0][0] + assert "AND NOT (t.path = 'boot' OR t.path = 'boot.nfs')" in other + class TestKcidbIssueDetails: @patch("kernelCI_app.queries.notifications.kcidb_execute_query") diff --git a/backend/kernelCI_app/typeModels/notificationSubscriptions.py b/backend/kernelCI_app/typeModels/notificationSubscriptions.py new file mode 100644 index 000000000..4dccee3c0 --- /dev/null +++ b/backend/kernelCI_app/typeModels/notificationSubscriptions.py @@ -0,0 +1,294 @@ +"""Subscription YAML and metrics rules. + +The issues list records which regression mail an address already gets. +A new_issues block opts that address into the hourly summary. +""" + +from dataclasses import dataclass +from email.utils import parseaddr +from pathlib import Path +from typing import Annotated, Literal + +import yaml +from pydantic import ( + AfterValidator, + BaseModel, + ConfigDict, + Field, + TypeAdapter, + ValidationError, + model_validator, +) + +from kernelCI_app.constants.general import DEFAULT_ORIGIN + +IssueKind = Literal["build", "boot", "test"] + + +class NotificationConfigError(Exception): + def __init__(self, file: str, field: str, message: str): + self.file = file + self.field = field + super().__init__(f"{file}: {field}: {message}") + + +def validate_address(value: str) -> str: + _, address = parseaddr(value) + local, separator, domain = address.partition("@") + if not separator or not local or not domain or "@" in domain: + raise ValueError(f"invalid email address: {value}") + return value + + +Address = Annotated[str, AfterValidator(validate_address)] + + +class NewIssuesSubscription(BaseModel): + """Hourly new-issues summary opt-in. The issues list is a separate record.""" + + model_config = ConfigDict(extra="forbid") + + issue_types: list[IssueKind] = Field(min_length=1) + + +class Recipient(BaseModel): + model_config = ConfigDict(extra="forbid") + + email: Address + issues: list[IssueKind] | None = Field(default=None, min_length=1) + new_issues: NewIssuesSubscription | None = None + + @model_validator(mode="before") + @classmethod + def from_address(cls, value): + if isinstance(value, str): + return {"email": value} + return value + + +_RECIPIENTS = TypeAdapter(list[Recipient]) + + +def cc_addresses(recipients: list) -> list[str]: + """Addresses passed to current mails. Ignores a new_issues block.""" + return [item.email for item in _RECIPIENTS.validate_python(recipients)] + + +@dataclass(frozen=True) +class NewIssueSubscriber: + email: str + issue_types: frozenset[IssueKind] + tree_url: str | None = None + hardware: str | None = None + origin: str | None = None + + +def new_issue_subscribers(directory: Path) -> list[NewIssueSubscriber]: + """Recipients that opted into the hourly new-issues summary. + + A plain address or an issues list does not opt in. Tree scope is the git + URL. Hardware scope is the platform name and origin. + """ + subscribers: list[NewIssueSubscriber] = [] + for path in _yaml_files(directory): + data = _load_mapping(path) + hardware_file = is_hardware_filename(path.name) + model = TypeAdapter( + dict[str, HardwareSubscription] + if hardware_file + else dict[str, TreeSubscription] + ) + try: + parsed = model.validate_python(data) + except ValidationError as exc: + raise _validation_error(path, exc) from exc + for name, body in parsed.items(): + for recipient in body.default_recipients: + if recipient.new_issues is None: + continue + if hardware_file: + subscribers.append( + NewIssueSubscriber( + email=recipient.email, + issue_types=frozenset(recipient.new_issues.issue_types), + hardware=name, + origin=body.origin or DEFAULT_ORIGIN, + ) + ) + else: + subscribers.append( + NewIssueSubscriber( + email=recipient.email, + issue_types=frozenset(recipient.new_issues.issue_types), + tree_url=body.url, + ) + ) + return subscribers + + +def issues_for_subscriber( + subscriber: NewIssueSubscriber, issues: list[dict] +) -> list[dict]: + """Issues in this subscriber's scope whose type is in issue_types.""" + matched = [] + for issue in issues: + if issue.get("type") not in subscriber.issue_types: + continue + if subscriber.tree_url is not None: + if issue.get("git_repository_url") != subscriber.tree_url: + continue + elif ( + issue.get("platform") != subscriber.hardware + or issue.get("origin") != subscriber.origin + ): + continue + matched.append(issue) + return matched + + +class TreeSubscription(BaseModel): + model_config = ConfigDict(extra="allow") + + url: str + default_recipients: list[Recipient] = [] + + +class HardwareSubscription(BaseModel): + model_config = ConfigDict(extra="allow") + + default_recipients: list[Recipient] = [] + origin: str | None = None + + +class MetricsRule(BaseModel): + model_config = ConfigDict(extra="forbid") + + recipients: list[Address] = Field(min_length=1) + hardware: list[str] = [] + trees: list[str] = [] + origins: list[str] = [] + period_days: int = Field(gt=0) + + +def is_hardware_filename(name: str) -> bool: + return name.endswith("_hardware.yaml") or name.endswith("_hardware.yml") + + +def _yaml_files(directory: Path) -> list[Path]: + if not directory.is_dir(): + return [] + return sorted( + path + for path in directory.iterdir() + if path.is_file() and path.suffix in {".yaml", ".yml"} + ) + + +def _load_mapping(path: Path) -> dict: + try: + data = yaml.safe_load(path.read_text()) + except yaml.YAMLError as exc: + raise NotificationConfigError( + path.name, "", f"invalid YAML: {exc}" + ) from exc + if not isinstance(data, dict) or not data: + raise NotificationConfigError(path.name, "", "expected a mapping") + return data + + +def _validation_error( + path: Path, exc: ValidationError, *, prefix: str = "" +) -> NotificationConfigError: + err = exc.errors()[-1] + field = ".".join(str(part) for part in err["loc"]) or "" + if prefix: + field = f"{prefix}.{field}" + return NotificationConfigError(path.name, field, err["msg"]) + + +def load_subscriptions(directory: Path) -> tuple[set[str], set[str]]: + """Validate every subscription file. Returns tree names and hardware names.""" + trees: set[str] = set() + hardware: set[str] = set() + for path in _yaml_files(directory): + data = _load_mapping(path) + hardware_file = is_hardware_filename(path.name) + model = TypeAdapter( + dict[str, HardwareSubscription] + if hardware_file + else dict[str, TreeSubscription] + ) + try: + parsed = model.validate_python(data) + except ValidationError as exc: + raise _validation_error(path, exc) from exc + names = hardware if hardware_file else trees + names.update(parsed) + return trees, hardware + + +def load_metrics_rules(directory: Path, *, trees: set[str], hardware: set[str]) -> None: + """Validate metrics rules against subscription names. Does not send mail.""" + seen: dict[str, tuple[str, MetricsRule]] = {} + for path in _yaml_files(directory): + data = _load_mapping(path) + for name, body in data.items(): + if name in seen: + other = seen[name][0] + raise NotificationConfigError( + path.name, + name, + f"duplicate rule name, also in {other}", + ) + try: + rule = MetricsRule.model_validate(body) + except ValidationError as exc: + raise _validation_error(path, exc, prefix=name) from exc + _check_rule_names(path, name, rule, trees=trees, hardware=hardware) + _check_overlap(path, name, rule, seen) + seen[name] = (path.name, rule) + + +def _check_rule_names( + path: Path, + name: str, + rule: MetricsRule, + *, + trees: set[str], + hardware: set[str], +) -> None: + for platform in rule.hardware: + if platform not in hardware: + raise NotificationConfigError( + path.name, + f"{name}.hardware", + f"unknown hardware {platform}", + ) + for tree in rule.trees: + if tree not in trees: + raise NotificationConfigError( + path.name, + f"{name}.trees", + f"unknown tree {tree}", + ) + + +def _check_overlap( + path: Path, + name: str, + rule: MetricsRule, + seen: dict[str, tuple[str, MetricsRule]], +) -> None: + for other_name, (other_file, other) in seen.items(): + shared_recipients = set(rule.recipients) & set(other.recipients) + shared_hardware = set(rule.hardware) & set(other.hardware) + shared_trees = set(rule.trees) & set(other.trees) + if not shared_recipients or not (shared_hardware or shared_trees): + continue + scope = ", ".join(sorted(shared_hardware | shared_trees)) + people = ", ".join(sorted(shared_recipients)) + raise NotificationConfigError( + path.name, + f"{name}.recipients", + f"overlaps {other_name} in {other_file} for {people} on {scope}", + ) diff --git a/docs/notifications.md b/docs/notifications.md index 9dd741196..961a71e07 100644 --- a/docs/notifications.md +++ b/docs/notifications.md @@ -55,6 +55,8 @@ These options are available for all actions and are always optional: | `--update-storage` / `-u` | `issue_report` | Optional | Update JSON storage while generating/sending reports | | `--summary-signup-folder` | `summary` | Optional | Alternative signup folder under `/backend/data` | | `--summary-origins` | `summary` | Optional | Comma-separated list to limit to specific origins | +| `--issue-report-origins` | `new_issues` | Optional | Comma-separated origins for the automatic regression report. Default: `maestro` | +| `--issue-report-types` | `new_issues` | Optional | Comma-separated issue types (`build`, `boot`, `test`) for the automatic regression report. Default: `build` | | `--skip-sent-reports` | `summary` | Optional | Skip reports that have already been sent | | `--hardware-origins` | `hardware_summary` | Optional | Comma-separated list to limit to specific origins | | `--tree` | `fake_report` | Optional | Add recipients for the given tree name | @@ -107,6 +109,75 @@ Edit the corresponding tree file in the [subscriptions folder](../backend/data/n Use the git tree name reported by the Web Dashboard. +Every address in `default_recipients` is Cc on the mail that file already sends. A tree file sends the `[STATUS]` checkout summary, one per entry under `reports`, and the `[REGRESSION]` mail, one message per new build issue on that git URL. A `*_hardware.yaml` file sends the Monday `hardware summary`. The hourly `new issues summary` still goes to `kernelci-results@groups.io` with every new build and boot issue. A `new_issues` block on a recipient adds a second copy for that address. + +An entry is either an address string, including `Name `, or a mapping with `email` and an optional `issues` list. `issues` names the issue kinds that address is notified about on the mails above: `build`, `boot`, or `test`. It does not subscribe anyone to the hourly summary. + +```yaml +broonie-sound: + url: https://git.kernel.org/pub/scm/linux/kernel/git/broonie/sound.git + default_recipients: + - email: someone-else@kernel.org + issues: [build] + - email: broonie@kernel.org + issues: [build, boot, test] +``` + +`build` is the kind with a `[REGRESSION]` sender today. Every tree file lists `[build]` on each address, which states the mail those addresses already receive. Adding `boot` or `test` to `issues` records a request that the regression mail does not cover yet. Full regression reports prefer `build` when a checkout has both build and test incidents. + +### Hourly new-issues summary + +The summary runs once an hour, the same run as the community mail. There is no per-address schedule. + +Add `new_issues` to opt an address into a filtered copy. `issue_types` is `build`, `boot`, `test`, or a combination. The copy contains only issues of those types in that file's scope: + +- a tree file matches `git_repository_url` +- a hardware file matches the platform and its `origin` (`maestro` when omitted) + +An issue is tied to its first incident, the same row the community summary prints. The copy is skipped when nothing matches. Someone on `kernelci-results@groups.io` who also has a block may get two mails. `--ignore-recipients` skips the copies. With `--send` left off, the command prints `To` and the issue ids for the community mail and for each copy. + +```yaml +default_recipients: + - someone-else@kernel.org + - email: broonie@kernel.org + issues: [build] + new_issues: + issue_types: [boot] +``` + +`test` issues are not part of the community mail. They are included only in copies whose `issue_types` list `test`. + +The same run can also send a `[REGRESSION]` report. Without `--issue-report-origins` or `--issue-report-types`, that report stays limited to build issues from `maestro`. Boot issues and other origins are sent only when a flag lists them. The flags do not change who is on To or Cc: the list stays on To, the tree's `default_recipients` stay on Cc, and `regressions@lists.linux.dev` plus regzbot stay on mainline, next, and stable. + +Hardware files keep plain address strings. Those addresses receive the Monday board summary and no issue mail, so there is no kind to list: + +```yaml +qcs6490-rb3gen2: + origin: maestro + labs: + lava-kci-qualcomm: https://lava-oss.qualcomm.com + default_recipients: + - Trilok Soni + - Shiraz Hashim +``` + +A hardware recipient can add the same `new_issues` block. The copy is limited to that platform. + +Editing a file in that folder runs these checks in CI. A malformed file, an unknown issue kind, an unknown `issue_types` value, or an address without a single `@` fails with the file name and the field. + +## Metrics rules + +`backend/data/notifications/metrics/` holds one YAML file per rule group. A rule name is the top-level key. `hardware` and `trees` name keys in the subscriptions folder. `period_days` is a positive integer. Two rules that share a recipient and a hardware or tree name are rejected. These files are validated only; they do not send mail. + +```yaml +qualcomm-weekly: + recipients: + - tsoni@quicinc.com + hardware: [qcs6490-rb3gen2] + origins: [maestro] + period_days: 7 +``` + ## Signup tree for checkout summary