From 9650576ab7cef039c5e4395b37bfb2bc033a7508 Mon Sep 17 00:00:00 2001 From: ehila Date: Tue, 17 Feb 2026 10:39:33 -0500 Subject: [PATCH] feat: update the html for feature promotion small quality of life improvement to the html for feature promotion, this addition will allow sorting by pass rate and make it easier to identify where a feature is lacking validation by color coating the pass rate. Co-Authored-By: Claude Opus 4.6 Signed-off-by: ehila --- .../codegen/cmd/featuregate-test-analyzer.go | 114 +++++++++--- tools/codegen/pkg/utils/html.go | 166 ++++++++++++++++++ 2 files changed, 254 insertions(+), 26 deletions(-) create mode 100644 tools/codegen/pkg/utils/html.go diff --git a/tools/codegen/cmd/featuregate-test-analyzer.go b/tools/codegen/cmd/featuregate-test-analyzer.go index 1de48e66631..a9b40cc82fc 100644 --- a/tools/codegen/cmd/featuregate-test-analyzer.go +++ b/tools/codegen/cmd/featuregate-test-analyzer.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "html/template" "io" "net/http" "net/url" @@ -16,7 +17,6 @@ import ( "strings" "time" - "github.com/russross/blackfriday" "github.com/spf13/cobra" "github.com/spf13/pflag" "k8s.io/apimachinery/pkg/util/sets" @@ -170,6 +170,7 @@ func (o *FeatureGateTestAnalyzerOptions) Run(ctx context.Context) error { fmt.Fprintf(o.Out, "No new Default FeatureGates found.\n") } + featureGateHTMLData := []utils.HTMLFeatureGate{} recentlyEnabledFeatureGates := sets.KeySet(recentlyEnabledFeatureGatesToClusterProfiles) for _, enabledFeatureGate := range sets.List(recentlyEnabledFeatureGates) { clusterProfiles := recentlyEnabledFeatureGatesToClusterProfiles[enabledFeatureGate] @@ -197,6 +198,7 @@ func (o *FeatureGateTestAnalyzerOptions) Run(ctx context.Context) error { fmt.Fprintf(o.Out, "INSUFFICIENT CI testing for %q.\n", enabledFeatureGate) } errs = append(errs, currErrs...) + featureGateHTMLData = append(featureGateHTMLData, buildHTMLFeatureGateData(enabledFeatureGate, testingResults, currErrs)) } @@ -207,12 +209,8 @@ func (o *FeatureGateTestAnalyzerOptions) Run(ctx context.Context) error { errs = append(errs, err) } - htmlContent := blackfriday.Run(summaryMarkdown) - htmlBytes := []byte{} - htmlBytes = append(htmlBytes, []byte(htmlHeader)...) - htmlBytes = append(htmlBytes, htmlContent...) htmlFilename := filepath.Join(o.OutputDir, "feature-promotion-summary.html") - if err := os.WriteFile(htmlFilename, htmlBytes, 0644); err != nil { + if err := writeHTMLFromTemplate(htmlFilename, featureGateHTMLData); err != nil { errs = append(errs, err) } } @@ -220,26 +218,90 @@ func (o *FeatureGateTestAnalyzerOptions) Run(ctx context.Context) error { return errors.Join(errs...) } -const htmlHeader = ` - FeatureGate Promotion Summary - - - - - - -` +func buildHTMLFeatureGateData(name string, testingResults map[JobVariant]*TestingResults, errs []error) utils.HTMLFeatureGate { + jobVariantsSet := sets.KeySet(testingResults) + jobVariants := jobVariantsSet.UnsortedList() + sort.Sort(OrderedJobVariants(jobVariants)) + + variants := make([]utils.HTMLVariantColumn, 0, len(jobVariants)) + for i, jv := range jobVariants { + variants = append(variants, utils.HTMLVariantColumn{ + Topology: jv.Topology, + Cloud: jv.Cloud, + Architecture: jv.Architecture, + NetworkStack: jv.NetworkStack, + ColIndex: i + 1, + }) + } + + allTests := sets.Set[string]{} + for _, variantTestingResults := range testingResults { + for _, currTestingResult := range variantTestingResults.TestResults { + allTests.Insert(currTestingResult.TestName) + } + } + + tests := make([]utils.HTMLTestRow, 0, len(allTests)) + for _, testName := range sets.List(allTests) { + row := utils.HTMLTestRow{ + TestName: testName, + Cells: make([]utils.HTMLTestCell, len(jobVariants)), + } + for i, jobVariant := range jobVariants { + allTesting := testingResults[jobVariant] + testResults := testResultByName(allTesting.TestResults, testName) + cell := utils.HTMLTestCell{} + if testResults == nil { + cell.Failed = true + } else { + var passPercent float32 + if testResults.TotalRuns > 0 { + passPercent = float32(testResults.SuccessfulRuns) / float32(testResults.TotalRuns) + } + cell.PassPercent = int(passPercent * 100) + cell.SuccessfulRuns = testResults.SuccessfulRuns + cell.TotalRuns = testResults.TotalRuns + cell.FailedRuns = testResults.FailedRuns + if testResults.TotalRuns < requiredNumberOfTestRunsPerVariant || passPercent < requiredPassRateOfTestsPerVariant { + cell.Failed = true + } + } + row.Cells[i] = cell + } + tests = append(tests, row) + } + + return utils.HTMLFeatureGate{ + Name: name, + Sufficient: len(errs) == 0, + Variants: variants, + Tests: tests, + } +} + +func writeHTMLFromTemplate(filename string, featureGateHTMLData []utils.HTMLFeatureGate) error { + + data := utils.HTMLTemplateData{ + FeatureGates: featureGateHTMLData, + } + + tmpl, err := template.New("report").Parse(utils.HTMLTemplateSrc) + if err != nil { + return fmt.Errorf("error parsing HTML template: %w", err) + } + + f, err := os.Create(filename) + if err != nil { + return fmt.Errorf("error creating HTML file: %w", err) + } + defer f.Close() + + if err := tmpl.Execute(f, data); err != nil { + return fmt.Errorf("error executing HTML template: %w", err) + } + + return nil +} func checkIfTestingIsSufficient(featureGate string, testingResults map[JobVariant]*TestingResults) []error { errs := []error{} diff --git a/tools/codegen/pkg/utils/html.go b/tools/codegen/pkg/utils/html.go new file mode 100644 index 00000000000..faa0c8032e1 --- /dev/null +++ b/tools/codegen/pkg/utils/html.go @@ -0,0 +1,166 @@ +package utils + +type HTMLTemplateData struct { + FeatureGates []HTMLFeatureGate +} + +type HTMLFeatureGate struct { + Name string + Sufficient bool + Variants []HTMLVariantColumn + Tests []HTMLTestRow +} + +type HTMLVariantColumn struct { + Topology string + Cloud string + Architecture string + NetworkStack string + ColIndex int +} + +type HTMLTestRow struct { + TestName string + Cells []HTMLTestCell +} + +type HTMLTestCell struct { + PassPercent int + SuccessfulRuns int + TotalRuns int + FailedRuns int + Failed bool +} + +const HTMLTemplateSrc = ` + + + + FeatureGate Promotion Summary + + + + + +
+

FeatureGate Promotion Summary

+ {{if not .FeatureGates}}

No new Default FeatureGates found.

{{end}} + {{range $fgIdx, $fg := .FeatureGates}} +

{{$fg.Name}}

+ {{if $fg.Sufficient}} +
Sufficient CI testing for "{{$fg.Name}}".
+ {{else}} +
+ INSUFFICIENT CI testing for "{{$fg.Name}}". +
    +
  • At least five tests are expected for a feature
  • +
  • Tests must be run on every TechPreview platform (ask for an exception if your feature doesn't support a variant)
  • +
  • All tests must run at least 14 times on every platform
  • +
  • All tests must pass at least 95% of the time
  • +
+
+ {{end}} + {{if $fg.Tests}} + + + + + {{range $v := $fg.Variants}} + + {{end}} + + + + {{range $test := $fg.Tests}} + + + {{range $cell := $test.Cells}} + + {{end}} + + {{end}} + +
Test Name + {{$v.Topology}}
{{$v.Cloud}}
{{$v.Architecture}}{{if $v.NetworkStack}}
{{$v.NetworkStack}}{{end}} +
{{$test.TestName}} + {{if $cell.Failed}}FAIL
{{end}} + {{$cell.PassPercent}}% ({{$cell.SuccessfulRuns}} / {{$cell.TotalRuns}}) + {{if gt $cell.FailedRuns 0}}
{{$cell.FailedRuns}} failed{{end}} +
+ {{end}} + {{end}} +
+ + + +`