diff --git a/deploy/endoflife-override/api/amazon-aurora-mysql.json b/deploy/endoflife-override/api/amazon-aurora-mysql.json index 9ba7063..bbf80fa 100644 --- a/deploy/endoflife-override/api/amazon-aurora-mysql.json +++ b/deploy/endoflife-override/api/amazon-aurora-mysql.json @@ -1,5 +1,164 @@ [ - {"cycle":"3","releaseDate":"2021-11-23","eol":"2028-04-30","latest":"3.12.0","latestReleaseDate":"2026-02-17","lts":false,"extendedSupport":"2029-07-31"}, - {"cycle":"2","releaseDate":"2018-02-06","eol":"2024-10-31","latest":"2.12.5","latestReleaseDate":"2025-04-09","lts":false,"extendedSupport":"2027-02-28"}, - {"cycle":"1","releaseDate":"2015-07-27","eol":"2023-02-28","latest":"1.23.4","latestReleaseDate":"2022-08-11","lts":false,"extendedSupport":false} + { + "cycle": "8.4.8", + "releaseDate": "2026-09-03", + "eol": "2028-03-31", + "latest": "8.4.8", + "latestReleaseDate": "2026-09-03", + "lts": false, + "extendedSupport": false + }, + { + "cycle": "8.4.7", + "releaseDate": "2026-05-21", + "eol": "2027-11-30", + "latest": "8.4.7", + "latestReleaseDate": "2026-05-21", + "lts": false, + "extendedSupport": false + }, + { + "cycle": "8.4", + "releaseDate": "2026-05-21", + "eol": "2032-04-30", + "latest": "8.4.8", + "latestReleaseDate": "2026-09-03", + "lts": false, + "extendedSupport": true + }, + { + "cycle": "3.13", + "releaseDate": "2026-08-27", + "eol": "2027-08-27", + "latest": "3.13.0", + "latestReleaseDate": "2026-08-27", + "lts": false, + "extendedSupport": false + }, + { + "cycle": "3.12", + "releaseDate": "2026-02-17", + "eol": "2027-02-17", + "latest": "3.12.0", + "latestReleaseDate": "2026-02-17", + "lts": false, + "extendedSupport": false + }, + { + "cycle": "3.11", + "releaseDate": "2025-11-13", + "eol": "2026-11-13", + "latest": "3.11.1", + "latestReleaseDate": "2025-12-16", + "lts": false, + "extendedSupport": false + }, + { + "cycle": "3.10", + "releaseDate": "2025-07-31", + "eol": "2028-04-30", + "latest": "3.10.5", + "latestReleaseDate": "2026-07-30", + "lts": true, + "extendedSupport": false + }, + { + "cycle": "3.09", + "releaseDate": "2025-05-14", + "eol": "2026-08-31", + "latest": "3.09.0", + "latestReleaseDate": "2025-05-14", + "lts": false, + "extendedSupport": false + }, + { + "cycle": "3.08", + "releaseDate": "2024-11-18", + "eol": "2026-08-31", + "latest": "3.08.2", + "latestReleaseDate": "2025-04-07", + "lts": false, + "extendedSupport": false + }, + { + "cycle": "3.07", + "releaseDate": "2024-06-04", + "eol": "2025-08-31", + "latest": "3.07.1", + "latestReleaseDate": "2024-07-31", + "lts": false, + "extendedSupport": false + }, + { + "cycle": "3.06", + "releaseDate": "2024-03-07", + "eol": "2025-08-31", + "latest": "3.06.1", + "latestReleaseDate": "2024-07-31", + "lts": false, + "extendedSupport": false + }, + { + "cycle": "3.05", + "releaseDate": "2023-10-25", + "eol": "2025-08-31", + "latest": "3.05.2", + "latestReleaseDate": "2024-01-31", + "lts": false, + "extendedSupport": false + }, + { + "cycle": "3.04", + "releaseDate": "2023-07-31", + "eol": "2026-10-31", + "latest": "3.04.6", + "latestReleaseDate": "2026-01-02", + "lts": true, + "extendedSupport": false + }, + { + "cycle": "3", + "releaseDate": "2021-11-23", + "eol": "2028-04-30", + "latest": "3.13.0", + "latestReleaseDate": "2026-08-27", + "lts": false, + "extendedSupport": "2029-07-31" + }, + { + "cycle": "2.12", + "releaseDate": "2023-07-25", + "eol": "2024-10-31", + "latest": "2.12.5", + "latestReleaseDate": "2025-04-09", + "lts": false, + "extendedSupport": "2029-06-30" + }, + { + "cycle": "2.11", + "releaseDate": "2022-10-25", + "eol": "2024-10-31", + "latest": "2.11.6", + "latestReleaseDate": "2024-07-19", + "lts": false, + "extendedSupport": "2029-06-30" + }, + { + "cycle": "2", + "releaseDate": "2018-02-06", + "eol": "2024-10-31", + "latest": "2.12.5", + "latestReleaseDate": "2025-04-09", + "lts": false, + "extendedSupport": "2029-06-30" + }, + { + "cycle": "1", + "releaseDate": "2015-07-27", + "eol": "2023-02-28", + "latest": "1.23.4", + "latestReleaseDate": "2022-08-11", + "lts": false, + "extendedSupport": false + } ] diff --git a/deploy/endoflife-override/aurora_mysql_test.go b/deploy/endoflife-override/aurora_mysql_test.go new file mode 100644 index 0000000..7697f9d --- /dev/null +++ b/deploy/endoflife-override/aurora_mysql_test.go @@ -0,0 +1,87 @@ +package override + +import ( + "context" + "encoding/json" + "os" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/block/Version-Guard/pkg/eol/endoflife" + "github.com/block/Version-Guard/pkg/types" +) + +// Aurora MySQL minor versions carry their own end-of-standard-support dates +// that arrive well before the major cycle's date. Before per-minor cycles +// existed, every 3.x cluster resolved to cycle "3" (EOL 2028-04-30) and a +// fleet on 3.04 (EOL 2026-10-31) reported GREEN. This test loads the +// repository override through the real provider and checks that the bare +// version strings Wiz reports resolve to the minor cycle, and that a minor +// without paid extended support terminates on its own date rather than +// inheriting the major's. +func TestAuroraMySQLOverrideResolvesMinorCycles(t *testing.T) { + provider := auroraMySQLOverrideProvider(t) + + tests := []struct { + version string + wantCycle string + wantEOL string // terminal EOLDate + wantStandardEnd string // DeprecationDate; empty when standard end == EOL + }{ + {version: "3.04.2", wantCycle: "3.04", wantEOL: "2026-10-31"}, + {version: "3.04.6", wantCycle: "3.04", wantEOL: "2026-10-31"}, + // Deprecated non-LTS minors must not fall back to the major's 2028 date. + {version: "3.06.0", wantCycle: "3.06", wantEOL: "2025-08-31"}, + {version: "3.07.1", wantCycle: "3.07", wantEOL: "2025-08-31"}, + {version: "3.08.2", wantCycle: "3.08", wantEOL: "2026-08-31"}, + {version: "3.10.1", wantCycle: "3.10", wantEOL: "2028-04-30"}, + {version: "3.11.1", wantCycle: "3.11", wantEOL: "2026-11-13"}, + {version: "8.4.7", wantCycle: "8.4.7", wantEOL: "2027-11-30"}, + // 2.11/2.12 stay available under RDS Extended Support. + {version: "2.11.5", wantCycle: "2.11", wantEOL: "2029-06-30", wantStandardEnd: "2024-10-31"}, + {version: "2.12.5", wantCycle: "2.12", wantEOL: "2029-06-30", wantStandardEnd: "2024-10-31"}, + // Minor without its own cycle falls back to the major. + {version: "3.99.0", wantCycle: "3", wantEOL: "2029-07-31", wantStandardEnd: "2028-04-30"}, + } + + for _, tt := range tests { + t.Run(tt.version, func(t *testing.T) { + lifecycle, err := provider.GetVersionLifecycle(context.Background(), "aurora-mysql", tt.version) + require.NoError(t, err) + require.Equal(t, tt.wantCycle, lifecycle.Version, "matched cycle") + require.NotNil(t, lifecycle.EOLDate) + require.Equal(t, tt.wantEOL, lifecycle.EOLDate.Format("2006-01-02"), "EOLDate") + if tt.wantStandardEnd == "" { + require.Nil(t, lifecycle.DeprecationDate, "no separate standard-support end") + require.Nil(t, lifecycle.ExtendedSupportEnd, "no extended support") + return + } + require.NotNil(t, lifecycle.DeprecationDate) + require.Equal(t, tt.wantStandardEnd, lifecycle.DeprecationDate.Format("2006-01-02"), "DeprecationDate") + }) + } +} + +func auroraMySQLOverrideProvider(t *testing.T) *endoflife.Provider { + t.Helper() + data, err := os.ReadFile("api/amazon-aurora-mysql.json") + require.NoError(t, err) + var cycles []*endoflife.ProductCycle + require.NoError(t, json.Unmarshal(data, &cycles)) + + client := &endoflife.MockClient{ + GetProductCyclesFunc: func(_ context.Context, product string) (endoflife.ProductCyclesResult, error) { + require.Equal(t, "amazon-aurora-mysql", product) + return endoflife.ProductCyclesResult{ + Cycles: cycles, + FetchedAt: time.Now(), + DataSource: types.LifecycleDataSourceLocalOverride, + }, nil + }, + } + provider, err := endoflife.NewProvider(client, "amazon-aurora-mysql", "standard", time.Hour, nil) + require.NoError(t, err) + return provider +} diff --git a/deploy/endoflife-override/manifest.json b/deploy/endoflife-override/manifest.json index 0f6d8b5..43acfcf 100644 --- a/deploy/endoflife-override/manifest.json +++ b/deploy/endoflife-override/manifest.json @@ -4,11 +4,11 @@ { "product": "amazon-aurora-mysql", "path": "api/amazon-aurora-mysql.json", - "reason": "Product pending upstream inclusion", + "reason": "Upstream only publishes major cycles; Aurora MySQL minors (3.04, 3.08, ...) reach end of standard support years before the major, so per-minor cycles from the AWS release calendar are added here", "owner": "@block/block-platform", - "source_url": "https://github.com/endoflife-date/endoflife.date/pull/9534", - "reviewed_on": "2026-08-05", - "review_due_on": "2026-09-04" + "source_url": "https://docs.aws.amazon.com/AmazonRDS/latest/AuroraMySQLReleaseNotes/AuroraMySQL.release-calendars.html", + "reviewed_on": "2026-09-30", + "review_due_on": "2026-10-30" }, { "product": "amazon-opensearch", diff --git a/deploy/endoflife-override/manifest_test.go b/deploy/endoflife-override/manifest_test.go index df365d8..d061d3c 100644 --- a/deploy/endoflife-override/manifest_test.go +++ b/deploy/endoflife-override/manifest_test.go @@ -40,8 +40,8 @@ func TestValidateManifest(t *testing.T) { {name: "invalid source URL", mutate: mutateManifest(func(m *manifest) { m.Overrides[0].SourceURL = "http://example.com/source" }), wantErr: "must use https"}, {name: "malformed source URL", mutate: mutateManifest(func(m *manifest) { m.Overrides[0].SourceURL = "https://[invalid" }), wantErr: "valid https URL"}, {name: "invalid review date", mutate: mutateManifest(func(m *manifest) { m.Overrides[0].ReviewedOn = "August 5" }), wantErr: "YYYY-MM-DD"}, - {name: "review due before reviewed", mutate: mutateManifest(func(m *manifest) { m.Overrides[0].ReviewDueOn = "2026-08-04" }), wantErr: "before reviewed_on"}, - {name: "review interval over 30 days", mutate: mutateManifest(func(m *manifest) { m.Overrides[0].ReviewDueOn = "2026-09-05" }), wantErr: "exceeds 30 days"}, + {name: "review due before reviewed", mutate: mutateManifest(func(m *manifest) { m.Overrides[0].ReviewDueOn = shiftDate(t, m.Overrides[0].ReviewedOn, -1) }), wantErr: "before reviewed_on"}, + {name: "review interval over 30 days", mutate: mutateManifest(func(m *manifest) { m.Overrides[0].ReviewDueOn = shiftDate(t, m.Overrides[0].ReviewedOn, 31) }), wantErr: "exceeds 30 days"}, {name: "path escapes API directory", mutate: mutateManifest(func(m *manifest) { m.Overrides[0].Path = "api/../manifest.json" }), wantErr: "direct api/"}, {name: "nested API path", mutate: func(t *testing.T, root string) { nested := filepath.Join(root, "api", "nested", "amazon-aurora-mysql.json") @@ -99,9 +99,14 @@ func TestValidateManifest(t *testing.T) { data = append(data, []byte("\n{}")...) require.NoError(t, os.WriteFile(path, data, 0o600)) }, wantErr: "multiple JSON values"}, - {name: "overdue review warns", mutate: mutateManifest(func(m *manifest) { m.Overrides[0].ReviewDueOn = "2026-08-06" }), wantWarning: "review overdue"}, + {name: "overdue review warns", mutate: mutateManifest(func(m *manifest) { m.Overrides[0].ReviewDueOn = shiftDate(t, m.Overrides[0].ReviewedOn, 1) }), wantWarning: "review overdue"}, } + // Two days after the first override was reviewed: inside its review window, + // but past a due date moved to the following day. + reviewedOn, _ := repositoryReviewDates(t) + now := reviewedOn.AddDate(0, 0, 2) + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { root := copyRepositoryFixture(t) @@ -109,7 +114,7 @@ func TestValidateManifest(t *testing.T) { tt.mutate(t, root) } var warnings bytes.Buffer - err := validateManifest(root, time.Date(2026, 8, 7, 0, 0, 0, 0, time.UTC), &warnings) + err := validateManifest(root, now, &warnings) if tt.wantErr != "" { require.ErrorContains(t, err, tt.wantErr) return @@ -121,13 +126,15 @@ func TestValidateManifest(t *testing.T) { } func TestValidateManifestReviewDueDateBoundary(t *testing.T) { + // The earliest due date is the first moment any override can be overdue. + reviewDueOn := earliestReviewDueDate(t) tests := []struct { name string now time.Time wantWarning string }{ - {name: "due date remains valid for full UTC day", now: time.Date(2026, 9, 4, 23, 59, 59, 0, time.UTC)}, - {name: "following UTC day warns", now: time.Date(2026, 9, 5, 0, 0, 0, 0, time.UTC), wantWarning: "review overdue"}, + {name: "due date remains valid for full UTC day", now: reviewDueOn.Add(24*time.Hour - time.Second)}, + {name: "following UTC day warns", now: reviewDueOn.AddDate(0, 0, 1), wantWarning: "review overdue"}, } for _, tt := range tests { @@ -161,6 +168,44 @@ func mutateManifest(mutate func(*manifest)) func(*testing.T, string) { } } +// repositoryReviewDates returns the first override's reviewed_on and +// review_due_on so date-sensitive cases track the manifest as it is reviewed. +func repositoryReviewDates(t *testing.T) (reviewedOn, reviewDueOn time.Time) { + t.Helper() + m, err := readManifest("manifest.json") + require.NoError(t, err) + require.NotEmpty(t, m.Overrides) + reviewedOn, err = time.Parse(dateLayout, m.Overrides[0].ReviewedOn) + require.NoError(t, err) + reviewDueOn, err = time.Parse(dateLayout, m.Overrides[0].ReviewDueOn) + require.NoError(t, err) + return reviewedOn, reviewDueOn +} + +// earliestReviewDueDate returns the soonest review_due_on across all overrides. +func earliestReviewDueDate(t *testing.T) time.Time { + t.Helper() + m, err := readManifest("manifest.json") + require.NoError(t, err) + require.NotEmpty(t, m.Overrides) + var earliest time.Time + for _, override := range m.Overrides { + due, err := time.Parse(dateLayout, override.ReviewDueOn) + require.NoError(t, err) + if earliest.IsZero() || due.Before(earliest) { + earliest = due + } + } + return earliest +} + +func shiftDate(t *testing.T, value string, days int) string { + t.Helper() + parsed, err := time.Parse(dateLayout, value) + require.NoError(t, err) + return parsed.AddDate(0, 0, days).Format(dateLayout) +} + func copyRepositoryFixture(t *testing.T) string { t.Helper() root := t.TempDir()