From dbdd586cf29d281c44f19e2c34ecc8dce29614b5 Mon Sep 17 00:00:00 2001 From: Rafal Golarz Date: Wed, 19 Aug 2026 14:08:58 +0200 Subject: [PATCH 1/3] TT-17742: scope eol-notifier distro tracking to policy-approved cycles dependencies.yaml named specific RHEL/Ubuntu/Debian versions in comments and entry names, but the notifier has no concept of a release cycle filter, so each entry alerted on every cycle its product publishes (e.g. RHEL 6 and 10, not just the approved 7/8/9). The EE-FIPS validated set (RHEL 7/8/9, Ubuntu 24.04 only) had no tracking entries at all. Add an optional `cycles` field to a dependency so it can restrict tracking to named release cycles, and wire it through new-version and EOL detection. Update dependencies.yaml to use it and add the missing EE-FIPS entries. --- .github/eol-notifier/dependencies.yaml | 26 ++++++++++++++++ eol-notifier/README.md | 4 +++ eol-notifier/cmd/notifier/config.go | 32 +++++++++++++++++++ eol-notifier/cmd/notifier/config_test.go | 29 ++++++++++++++++++ eol-notifier/cmd/notifier/lifecycle.go | 34 ++++++++++++--------- eol-notifier/cmd/notifier/lifecycle_test.go | 31 +++++++++++++++++++ 6 files changed, 142 insertions(+), 14 deletions(-) diff --git a/.github/eol-notifier/dependencies.yaml b/.github/eol-notifier/dependencies.yaml index 7c90d74..bf93229 100644 --- a/.github/eol-notifier/dependencies.yaml +++ b/.github/eol-notifier/dependencies.yaml @@ -27,6 +27,32 @@ dependencies: product: mariadb track: [eol, eoes] + # RPM: RHEL 7, 8, and 9. + - name: RPM (RHEL 7, 8, 9) + product: rhel + track: [eol] + cycles: ["7", "8", "9"] + # DEB: Ubuntu 22.04 LTS, 24.04 LTS, 26.04 LTS, and Debian 12 (Bookworm), + # 13 (Trixie). + - name: DEB (Ubuntu 22.04 LTS, 24.04 LTS, 26.04 LTS) + product: ubuntu + track: [eol] + cycles: ["22.04", "24.04", "26.04"] + - name: DEB (Debian 12 Bookworm, 13 Trixie) + product: debian + track: [eol] + cycles: ["12", "13"] + + # EE-FIPS validated set: RHEL 7, 8, and 9, and Ubuntu 24.04 only. + - name: EE-FIPS (RHEL 7, 8, 9) + product: rhel + track: [eol] + cycles: ["7", "8", "9"] + - name: EE-FIPS (Ubuntu 24.04) + product: ubuntu + track: [eol] + cycles: ["24.04"] + # HashiCorp tooling. A version gets its end-of-life date on the day a newer # release drops it out of support, so the date is never in the future: expect # the ended alert from these, not the 12/6/1 month countdown. diff --git a/eol-notifier/README.md b/eol-notifier/README.md index 842dd27..b10e6c4 100644 --- a/eol-notifier/README.md +++ b/eol-notifier/README.md @@ -69,6 +69,10 @@ Each entry under `dependencies` takes these keys: this phase, but a version keeps no date until the vendor announces one. - `eoas` is the end of active support. For example `redis` and `valkey`. - `eoes` is the end of extended support. For example `amazon-rds-postgresql`. +- `cycles` restricts tracking to the named release cycles, using the same names + endoflife.date does, e.g. `["7", "8", "9"]` for RHEL or `["22.04", "24.04"]` + for Ubuntu. Omit it to track every cycle the product publishes, including + ones added after this config was written. - `upstream_proxy` is for a service that `endoflife.date` does not track. The action then uses the dates of the open source engine under it. GCP MemoryStore uses `redis`, GCP Cloud SQL uses `postgresql`, and Azure DocumentDB uses diff --git a/eol-notifier/cmd/notifier/config.go b/eol-notifier/cmd/notifier/config.go index a53d7cc..2bee9ed 100644 --- a/eol-notifier/cmd/notifier/config.go +++ b/eol-notifier/cmd/notifier/config.go @@ -39,6 +39,9 @@ type Dependency struct { Product string `yaml:"product"` // Track lists the lifecycle phases to alert on. Defaults to [eol]. Track []string `yaml:"track"` + // Cycles restricts tracking to the named release cycles, e.g. ["7", "8", "9"] + // for RHEL. Empty means every cycle the product publishes. + Cycles []string `yaml:"cycles"` // UpstreamProxy marks a dependency whose lifecycle is not published by its // vendor, so the upstream OSS engine is used as a stand-in. Its dates are // indicative only and are labelled as such in the alert. @@ -142,6 +145,19 @@ func (c *DependencyConfig) validate() error { } seenPhase[phase] = true } + + seenCycle := make(map[string]bool, len(dep.Cycles)) + for i := range dep.Cycles { + dep.Cycles[i] = strings.TrimSpace(dep.Cycles[i]) + cycle := dep.Cycles[i] + if cycle == "" { + return fmt.Errorf("dependency %q has an empty cycle", dep.Name) + } + if seenCycle[cycle] { + return fmt.Errorf("dependency %q lists cycle %q more than once", dep.Name, cycle) + } + seenCycle[cycle] = true + } } return nil @@ -158,6 +174,22 @@ func (d Dependency) tracks(phase string) bool { return false } +// tracksCycle reports whether the dependency covers the given release cycle. +// An empty Cycles list means every cycle the product publishes. +func (d Dependency) tracksCycle(cycle string) bool { + if len(d.Cycles) == 0 { + return true + } + + for _, tracked := range d.Cycles { + if tracked == cycle { + return true + } + } + + return false +} + func isKnownPhase(phase string) bool { for _, known := range phaseOrder { if phase == known { diff --git a/eol-notifier/cmd/notifier/config_test.go b/eol-notifier/cmd/notifier/config_test.go index 9ab4b2e..bbe8425 100644 --- a/eol-notifier/cmd/notifier/config_test.go +++ b/eol-notifier/cmd/notifier/config_test.go @@ -117,6 +117,35 @@ dependencies: - name: Redis product: redis upstream-proxy: true +`, + wantErr: true, + }, + { + name: "cycles restricts tracking to named release cycles", + contents: ` +dependencies: + - name: RPM (RHEL 7, 8, 9) + product: rhel + cycles: ["7", "8", "9"] +`, + }, + { + name: "empty cycle is rejected", + contents: ` +dependencies: + - name: Redis + product: redis + cycles: ["7", ""] +`, + wantErr: true, + }, + { + name: "duplicate cycle is rejected", + contents: ` +dependencies: + - name: Redis + product: redis + cycles: ["7", "7"] `, wantErr: true, }, diff --git a/eol-notifier/cmd/notifier/lifecycle.go b/eol-notifier/cmd/notifier/lifecycle.go index 03d1d34..304fee7 100644 --- a/eol-notifier/cmd/notifier/lifecycle.go +++ b/eol-notifier/cmd/notifier/lifecycle.go @@ -150,19 +150,21 @@ func detectAlerts(config *DependencyConfig, products map[string]*Product, state for _, release := range product.Releases { if !baseline && !seen[release.Name] && !release.IsEOL { - report.NewVersions = append(report.NewVersions, NewVersionAlert{ - Product: name, - ProductLabel: product.Label, - ProductURL: product.Links.HTML, - Release: releaseLabel(release), - ReleaseDate: release.ReleaseDate, - IsLTS: release.IsLTS, - Dependencies: dependenciesFor(config, name, ""), - }) + if dependencies := dependenciesFor(config, name, "", release.Name); len(dependencies) > 0 { + report.NewVersions = append(report.NewVersions, NewVersionAlert{ + Product: name, + ProductLabel: product.Label, + ProductURL: product.Links.HTML, + Release: releaseLabel(release), + ReleaseDate: release.ReleaseDate, + IsLTS: release.IsLTS, + Dependencies: dependencies, + }) + } } for _, phase := range phaseOrder { - dependencies := dependenciesFor(config, name, phase) + dependencies := dependenciesFor(config, name, phase, release.Name) if len(dependencies) == 0 { continue } @@ -342,10 +344,11 @@ func productOrder(config *DependencyConfig) []string { return order } -// dependenciesFor returns the dependencies backed by a product. An empty phase -// matches every dependency, which is what new-version alerts want; otherwise -// only the dependencies configured to track that phase are returned. -func dependenciesFor(config *DependencyConfig, product, phase string) []DependencyRef { +// dependenciesFor returns the dependencies backed by a product and covering the +// given release cycle. An empty phase matches every dependency regardless of +// tracked phase, which is what new-version alerts want; otherwise only the +// dependencies configured to track that phase are returned. +func dependenciesFor(config *DependencyConfig, product, phase, cycle string) []DependencyRef { var refs []DependencyRef for _, dependency := range config.Dependencies { @@ -355,6 +358,9 @@ func dependenciesFor(config *DependencyConfig, product, phase string) []Dependen if phase != "" && !dependency.tracks(phase) { continue } + if !dependency.tracksCycle(cycle) { + continue + } refs = append(refs, DependencyRef{Name: dependency.Name, UpstreamProxy: dependency.UpstreamProxy}) } diff --git a/eol-notifier/cmd/notifier/lifecycle_test.go b/eol-notifier/cmd/notifier/lifecycle_test.go index f16fdd8..c91a762 100644 --- a/eol-notifier/cmd/notifier/lifecycle_test.go +++ b/eol-notifier/cmd/notifier/lifecycle_test.go @@ -447,6 +447,37 @@ func TestDetectAlertsTracksConfiguredPhasesOnly(t *testing.T) { } } +// TestDetectAlertsTracksConfiguredCyclesOnly covers restricting a dependency to +// specific release cycles, e.g. RHEL 7/8/9 out of every cycle rhel publishes. +func TestDetectAlertsTracksConfiguredCyclesOnly(t *testing.T) { + products := map[string]*Product{ + "rhel": { + Name: "rhel", + Label: "Red Hat Enterprise Linux", + Releases: []Release{ + {Name: "6", EOLFrom: strPtr("2027-02-28")}, + {Name: "9", EOLFrom: strPtr("2027-02-28")}, + }, + }, + } + + config := testConfig(t, Dependency{ + Name: "RPM (RHEL 7, 8, 9)", + Product: "rhel", + Cycles: []string{"7", "8", "9"}, + }) + state := tracked("rhel", "6", "9") + + report, _ := detectAlerts(config, products, state, mustDate(t, "2027-02-28"), false) + + if len(report.EOL) != 1 { + t.Fatalf("got %d alert(s), want 1 for cycle 9 only", len(report.EOL)) + } + if report.EOL[0].Release != "9" { + t.Errorf("release = %q, want 9", report.EOL[0].Release) + } +} + // TestDetectAlertsGroupsSharedProduct covers the upstream-fallback mapping: a // managed service the API does not track rides on the upstream engine, and both // dependencies must appear on one alert rather than producing two. From 1518d2c9edaa5a43a0fc244f0d41a2ad4b2aca7d Mon Sep 17 00:00:00 2001 From: Rafal Golarz Date: Wed, 19 Aug 2026 15:31:52 +0200 Subject: [PATCH 2/3] TT-17742 address code review --- .github/eol-notifier/dependencies.yaml | 18 +++++---- eol-notifier/cmd/notifier/lifecycle_test.go | 44 +++++++++++++++++++++ 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/.github/eol-notifier/dependencies.yaml b/.github/eol-notifier/dependencies.yaml index bf93229..de9de31 100644 --- a/.github/eol-notifier/dependencies.yaml +++ b/.github/eol-notifier/dependencies.yaml @@ -27,30 +27,34 @@ dependencies: product: mariadb track: [eol, eoes] - # RPM: RHEL 7, 8, and 9. + # RPM: RHEL 7, 8, and 9. eoas (Full Support) ends years before eol + # (Maintenance Support), and eoes (Extended Life Cycle Support) years after; + # tracking eol alone would miss the Full Support cliff and call a version + # dead long before ELS actually ends it. - name: RPM (RHEL 7, 8, 9) product: rhel - track: [eol] + track: [eoas, eol, eoes] cycles: ["7", "8", "9"] # DEB: Ubuntu 22.04 LTS, 24.04 LTS, 26.04 LTS, and Debian 12 (Bookworm), - # 13 (Trixie). + # 13 (Trixie). Debian's eoes (Debian LTS) runs well past its eol (Debian + # Security Support), so eol alone would call a release dead years early. - name: DEB (Ubuntu 22.04 LTS, 24.04 LTS, 26.04 LTS) product: ubuntu - track: [eol] + track: [eol, eoes] cycles: ["22.04", "24.04", "26.04"] - name: DEB (Debian 12 Bookworm, 13 Trixie) product: debian - track: [eol] + track: [eol, eoes] cycles: ["12", "13"] # EE-FIPS validated set: RHEL 7, 8, and 9, and Ubuntu 24.04 only. - name: EE-FIPS (RHEL 7, 8, 9) product: rhel - track: [eol] + track: [eoas, eol, eoes] cycles: ["7", "8", "9"] - name: EE-FIPS (Ubuntu 24.04) product: ubuntu - track: [eol] + track: [eol, eoes] cycles: ["24.04"] # HashiCorp tooling. A version gets its end-of-life date on the day a newer diff --git a/eol-notifier/cmd/notifier/lifecycle_test.go b/eol-notifier/cmd/notifier/lifecycle_test.go index c91a762..d6a2bd3 100644 --- a/eol-notifier/cmd/notifier/lifecycle_test.go +++ b/eol-notifier/cmd/notifier/lifecycle_test.go @@ -478,6 +478,50 @@ func TestDetectAlertsTracksConfiguredCyclesOnly(t *testing.T) { } } +// TestDetectAlertsDistroTracksEveryPhase guards against a distro looking dead +// years early: RHEL and Debian both publish eoas/eol well before eoes, and a +// config tracking eol alone would report the eol date as if it were the end, +// silently missing the eoas warning and skipping the eoes date entirely. +func TestDetectAlertsDistroTracksEveryPhase(t *testing.T) { + products := map[string]*Product{ + "rhel": { + Name: "rhel", + Label: "Red Hat Enterprise Linux", + Releases: []Release{{ + Name: "9", + EOASFrom: strPtr("2027-05-31"), // Full Support + EOLFrom: strPtr("2032-05-31"), // Maintenance Support + EOESFrom: strPtr("2036-05-31"), // Extended Life Cycle Support + }}, + }, + } + + config := testConfig(t, Dependency{ + Name: "RPM (RHEL 7, 8, 9)", + Product: "rhel", + Track: []string{phaseEOAS, phaseEOL, phaseEOES}, + Cycles: []string{"9"}, + }) + state := tracked("rhel", "9") + + report, _ := detectAlerts(config, products, state, mustDate(t, "2026-05-31"), false) + + got := map[string]int{} + for _, alert := range report.EOL { + got[alert.Phase] = alert.MonthsLeft + } + + want := map[string]int{phaseEOAS: 12} + for phase, months := range want { + if got[phase] != months { + t.Errorf("phase %q months left = %d, want %d (got alerts: %+v)", phase, got[phase], months, got) + } + } + if _, ok := got[phaseEOL]; ok { + t.Errorf("eol fired 6 years early at %v, want no alert yet", got) + } +} + // TestDetectAlertsGroupsSharedProduct covers the upstream-fallback mapping: a // managed service the API does not track rides on the upstream engine, and both // dependencies must appear on one alert rather than producing two. From 3affbca84fb6b393c4db243b2caea199b0a79d3d Mon Sep 17 00:00:00 2001 From: Rafal Golarz Date: Thu, 20 Aug 2026 15:49:02 +0200 Subject: [PATCH 3/3] TT-17742 address code review --- .github/eol-notifier/dependencies.yaml | 14 ++- eol-notifier/cmd/notifier/lifecycle.go | 15 ++- eol-notifier/cmd/notifier/lifecycle_test.go | 119 +++++++++++++++----- 3 files changed, 108 insertions(+), 40 deletions(-) diff --git a/.github/eol-notifier/dependencies.yaml b/.github/eol-notifier/dependencies.yaml index de9de31..1cdec68 100644 --- a/.github/eol-notifier/dependencies.yaml +++ b/.github/eol-notifier/dependencies.yaml @@ -27,14 +27,16 @@ dependencies: product: mariadb track: [eol, eoes] - # RPM: RHEL 7, 8, and 9. eoas (Full Support) ends years before eol - # (Maintenance Support), and eoes (Extended Life Cycle Support) years after; - # tracking eol alone would miss the Full Support cliff and call a version - # dead long before ELS actually ends it. - - name: RPM (RHEL 7, 8, 9) + # RPM: RHEL 7, 8, 9, and 10. Per policy, a new major LTS is added to the + # validated matrix once the vendor releases it, without waiting for a Tyk + # LTS. eoas (Full Support) ends years before eol (Maintenance Support), and + # eoes (Extended Life Cycle Support) years after; tracking eol alone would + # miss the Full Support cliff and call a version dead long before ELS + # actually ends it. + - name: RPM (RHEL 7, 8, 9, 10) product: rhel track: [eoas, eol, eoes] - cycles: ["7", "8", "9"] + cycles: ["7", "8", "9", "10"] # DEB: Ubuntu 22.04 LTS, 24.04 LTS, 26.04 LTS, and Debian 12 (Bookworm), # 13 (Trixie). Debian's eoes (Debian LTS) runs well past its eol (Debian # Security Support), so eol alone would call a release dead years early. diff --git a/eol-notifier/cmd/notifier/lifecycle.go b/eol-notifier/cmd/notifier/lifecycle.go index 304fee7..3d896ce 100644 --- a/eol-notifier/cmd/notifier/lifecycle.go +++ b/eol-notifier/cmd/notifier/lifecycle.go @@ -150,7 +150,7 @@ func detectAlerts(config *DependencyConfig, products map[string]*Product, state for _, release := range product.Releases { if !baseline && !seen[release.Name] && !release.IsEOL { - if dependencies := dependenciesFor(config, name, "", release.Name); len(dependencies) > 0 { + if dependencies := dependenciesFor(config, name, "", ""); len(dependencies) > 0 { report.NewVersions = append(report.NewVersions, NewVersionAlert{ Product: name, ProductLabel: product.Label, @@ -344,10 +344,13 @@ func productOrder(config *DependencyConfig) []string { return order } -// dependenciesFor returns the dependencies backed by a product and covering the -// given release cycle. An empty phase matches every dependency regardless of -// tracked phase, which is what new-version alerts want; otherwise only the -// dependencies configured to track that phase are returned. +// dependenciesFor returns the dependencies backed by a product. An empty phase +// matches every dependency regardless of tracked phase, and an empty cycle +// matches every dependency regardless of tracked cycles; both are empty for +// new-version alerts, which must fire for a cycle a dependency's `cycles` +// filter excludes, so the vendor shipping something the config doesn't know +// about yet is never silently absorbed into "seen" without ever being +// announced. func dependenciesFor(config *DependencyConfig, product, phase, cycle string) []DependencyRef { var refs []DependencyRef @@ -358,7 +361,7 @@ func dependenciesFor(config *DependencyConfig, product, phase, cycle string) []D if phase != "" && !dependency.tracks(phase) { continue } - if !dependency.tracksCycle(cycle) { + if cycle != "" && !dependency.tracksCycle(cycle) { continue } diff --git a/eol-notifier/cmd/notifier/lifecycle_test.go b/eol-notifier/cmd/notifier/lifecycle_test.go index d6a2bd3..69d4a9e 100644 --- a/eol-notifier/cmd/notifier/lifecycle_test.go +++ b/eol-notifier/cmd/notifier/lifecycle_test.go @@ -479,21 +479,17 @@ func TestDetectAlertsTracksConfiguredCyclesOnly(t *testing.T) { } // TestDetectAlertsDistroTracksEveryPhase guards against a distro looking dead -// years early: RHEL and Debian both publish eoas/eol well before eoes, and a -// config tracking eol alone would report the eol date as if it were the end, -// silently missing the eoas warning and skipping the eoes date entirely. +// years early: RHEL publishes eoas well before eol, and eol well before eoes, +// and a config tracking eol alone would report the eol date as if it were the +// end, silently missing the eoas warning and skipping the eoes date entirely. +// Each phase is checked at its own 12-month mark, so a leak from one phase's +// window into another's would show up as an unexpected second alert. func TestDetectAlertsDistroTracksEveryPhase(t *testing.T) { - products := map[string]*Product{ - "rhel": { - Name: "rhel", - Label: "Red Hat Enterprise Linux", - Releases: []Release{{ - Name: "9", - EOASFrom: strPtr("2027-05-31"), // Full Support - EOLFrom: strPtr("2032-05-31"), // Maintenance Support - EOESFrom: strPtr("2036-05-31"), // Extended Life Cycle Support - }}, - }, + release := Release{ + Name: "9", + EOASFrom: strPtr("2027-05-31"), // Full Support + EOLFrom: strPtr("2032-05-31"), // Maintenance Support + EOESFrom: strPtr("2036-05-31"), // Extended Life Cycle Support } config := testConfig(t, Dependency{ @@ -502,23 +498,55 @@ func TestDetectAlertsDistroTracksEveryPhase(t *testing.T) { Track: []string{phaseEOAS, phaseEOL, phaseEOES}, Cycles: []string{"9"}, }) - state := tracked("rhel", "9") - - report, _ := detectAlerts(config, products, state, mustDate(t, "2026-05-31"), false) - got := map[string]int{} - for _, alert := range report.EOL { - got[alert.Phase] = alert.MonthsLeft + tests := []struct { + name string + today string + wantPhase string + wantMonths int + }{ + {"eoas 12 months out", "2026-05-31", phaseEOAS, 12}, + {"eol 12 months out", "2031-05-31", phaseEOL, 12}, + {"eoes 12 months out", "2035-05-31", phaseEOES, 12}, } - want := map[string]int{phaseEOAS: 12} - for phase, months := range want { - if got[phase] != months { - t.Errorf("phase %q months left = %d, want %d (got alerts: %+v)", phase, got[phase], months, got) - } - } - if _, ok := got[phaseEOL]; ok { - t.Errorf("eol fired 6 years early at %v, want no alert yet", got) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + products := map[string]*Product{ + "rhel": {Name: "rhel", Label: "Red Hat Enterprise Linux", Releases: []Release{release}}, + } + state := tracked("rhel", "9") + + report, _ := detectAlerts(config, products, state, mustDate(t, tt.today), false) + + var got *EOLAlert + for i := range report.EOL { + if report.EOL[i].Phase == tt.wantPhase { + got = &report.EOL[i] + } + } + if got == nil { + t.Fatalf("no alert for phase %q, got: %+v", tt.wantPhase, report.EOL) + } + if got.Ended { + t.Errorf("phase %q already ended by %s, want a 12-month countdown", tt.wantPhase, tt.today) + } + if got.MonthsLeft != tt.wantMonths { + t.Errorf("months left = %d, want %d", got.MonthsLeft, tt.wantMonths) + } + + // Every phase whose date has already passed by today correctly + // reports as ended - only the phase under test must still be + // counting down, so no other alert may fire at the same date. + for _, alert := range report.EOL { + if alert.Phase == tt.wantPhase { + continue + } + if !alert.Ended { + t.Errorf("unexpected live countdown for phase %q at %s: %+v", alert.Phase, tt.today, alert) + } + } + }) } } @@ -634,6 +662,41 @@ func TestDetectAlertsNewVersions(t *testing.T) { } } +// TestDetectAlertsNewVersionsIgnoreCycleFilter guards against a new-version +// alert going silent forever: a dependency's `cycles` scopes which cycles it +// tracks for EOL, but the vendor shipping a cycle outside that scope is still +// news the first time it is seen. If the new-version check were filtered by +// cycles too, recordRun would mark the release seen the same run and it could +// never be announced later, even after cycles was updated to include it. +func TestDetectAlertsNewVersionsIgnoreCycleFilter(t *testing.T) { + products := map[string]*Product{ + "rhel": { + Name: "rhel", + Label: "Red Hat Enterprise Linux", + Releases: []Release{ + {Name: "9", ReleaseDate: "2022-05-17", EOLFrom: strPtr("2032-05-31")}, + {Name: "10", ReleaseDate: "2025-05-20", EOLFrom: strPtr("2035-05-31")}, + }, + }, + } + + config := testConfig(t, Dependency{ + Name: "RPM (RHEL 7, 8, 9)", + Product: "rhel", + Cycles: []string{"7", "8", "9"}, + }) + state := tracked("rhel", "9") + + report, _ := detectAlerts(config, products, state, mustDate(t, "2026-01-01"), false) + + if len(report.NewVersions) != 1 || report.NewVersions[0].Release != "10" { + t.Fatalf("new versions = %+v, want a single alert for cycle 10", report.NewVersions) + } + if len(report.NewVersions[0].Dependencies) != 1 || report.NewVersions[0].Dependencies[0].Name != "RPM (RHEL 7, 8, 9)" { + t.Errorf("dependencies = %+v, want RPM (RHEL 7, 8, 9) named even though cycle 10 is outside its cycles", report.NewVersions[0].Dependencies) + } +} + func TestProductOrderDeduplicates(t *testing.T) { config := testConfig(t, Dependency{Name: "Redis", Product: "redis"},