diff --git a/cmd/release/cmd/generate.go b/cmd/release/cmd/generate.go index 54015aaf..af83a49c 100644 --- a/cmd/release/cmd/generate.go +++ b/cmd/release/cmd/generate.go @@ -11,8 +11,10 @@ import ( "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/google/go-github/v39/github" "github.com/rancher/ecm-distro-tools/release" "github.com/rancher/ecm-distro-tools/release/k3s" + "github.com/rancher/ecm-distro-tools/release/metrics" "github.com/rancher/ecm-distro-tools/release/rancher" "github.com/rancher/ecm-distro-tools/repository" "github.com/spf13/cobra" @@ -25,25 +27,28 @@ var ( dashboardPrevMilestone string dashboardMilestone string - concurrencyLimit int - imagesListURL string - ignoreImages []string - checkImages []string - registry string - username string - password string - rancherMissingImagesJSONOutput bool - rke2PrevMilestone string - rke2Milestone string - rancherArtifactsIndexWriteToPath string - rancherArtifactsIndexIgnoreVersions []string - rancherImagesDigestsOutputFile string - rancherImagesDigestsRegistry string - rancherImagesDigestsImagesURL string - rancherSyncImages []string - rancherSourceRegistry string - rancherTargetRegistry string - rancherSyncConfigOutputPath string + concurrencyLimit int + imagesListURL string + ignoreImages []string + checkImages []string + registry string + username string + password string + rancherMissingImagesJSONOutput bool + rke2PrevMilestone string + rke2Milestone string + rancherArtifactsIndexWriteToPath string + rancherArtifactsIndexIgnoreVersions []string + rancherImagesDigestsOutputFile string + rancherImagesDigestsRegistry string + rancherImagesDigestsImagesURL string + rancherSyncImages []string + rancherSourceRegistry string + rancherTargetRegistry string + rancherSyncConfigOutputPath string + rancherMetricsRancherReleasesFilePath string + rancherMetricsWorkflowsFilePath string + rancherMetricsPrimeReleasesFilePath string ) // generateCmd represents the generate command @@ -177,6 +182,54 @@ var rancherGenerateImagesSyncConfigSubCmd = &cobra.Command{ }, } +var rancherGenerateMetricsSubCmd = &cobra.Command{ + Use: "metrics", + Short: "Generate rancher release metrics", + RunE: func(cmd *cobra.Command, args []string) error { + var rancherReleases []github.RepositoryRelease + var primeReleases []github.RepositoryRelease + var workflows []github.WorkflowRun + + rancherReleasesFile, err := os.ReadFile(rancherMetricsRancherReleasesFilePath) + if err != nil { + return err + } + if err := json.Unmarshal(rancherReleasesFile, &rancherReleases); err != nil { + return err + } + + primeReleasesFile, err := os.ReadFile(rancherMetricsPrimeReleasesFilePath) + if err != nil { + return err + } + if err := json.Unmarshal(primeReleasesFile, &primeReleases); err != nil { + return err + } + + workflowsFile, err := os.ReadFile(rancherMetricsWorkflowsFilePath) + if err != nil { + return err + } + if err := json.Unmarshal(workflowsFile, &workflows); err != nil { + return err + } + + metrics, err := metrics.ExtractMetrics(rancherReleases, primeReleases, workflows) + if err != nil { + return err + } + + b, err := json.MarshalIndent(metrics, "", " ") + if err != nil { + return err + } + + fmt.Println(string(b)) + + return nil + }, +} + var uiGenerateSubCmd = &cobra.Command{ Use: "ui", Short: "Generate ui related artifacts", @@ -233,6 +286,7 @@ func init() { rancherGenerateSubCmd.AddCommand(rancherGenerateMissingImagesListSubCmd) rancherGenerateSubCmd.AddCommand(rancherGenerateDockerImagesDigestsSubCmd) rancherGenerateSubCmd.AddCommand(rancherGenerateImagesSyncConfigSubCmd) + rancherGenerateSubCmd.AddCommand(rancherGenerateMetricsSubCmd) uiGenerateSubCmd.AddCommand(uiGenerateReleaseNotesSubCmd) dashboardGenerateSubCmd.AddCommand(dashboardGenerateReleaseNotesSubCmd) @@ -343,4 +397,21 @@ func init() { fmt.Println(err.Error()) os.Exit(1) } + + // rancher generate metrics + rancherGenerateMetricsSubCmd.Flags().StringVarP(&rancherMetricsRancherReleasesFilePath, "rancher-releases-file", "r", "", "Path to the releases file") + rancherGenerateMetricsSubCmd.Flags().StringVarP(&rancherMetricsWorkflowsFilePath, "workflows-file", "w", "", "Path to the workflows file") + rancherGenerateMetricsSubCmd.Flags().StringVarP(&rancherMetricsPrimeReleasesFilePath, "prime-releases-file", "p", "", "Path to the prime releases file") + if err := rancherGenerateMetricsSubCmd.MarkFlagRequired("rancher-releases-file"); err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + if err := rancherGenerateMetricsSubCmd.MarkFlagRequired("workflows-file"); err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + if err := rancherGenerateMetricsSubCmd.MarkFlagRequired("prime-releases-file"); err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } } diff --git a/release/metrics/metrics.go b/release/metrics/metrics.go new file mode 100644 index 00000000..1f9b50da --- /dev/null +++ b/release/metrics/metrics.go @@ -0,0 +1,138 @@ +package metrics + +import ( + "strings" + + "github.com/google/go-github/v39/github" +) + +type ( + yearMonthMap map[int]*[12]int + yearMap map[int]int +) + +type Metrics struct { + Rancher ReleaseMetrics `json:"rancher"` + RancherPrime ReleaseMetrics `json:"rancher_prime"` + Workflows WorkflowsMetrics `json:"actions"` +} + +type ReleaseMetrics struct { + // Number of GA releases per year and month + // Value: Number of releases per month (Jan: 0, Feb: 1, ..., Dec: 11) + GAReleasesPerMonth yearMonthMap `json:"ga_releases_per_month"` + // Number of Pre-releases per year and month (any release with a suffix that starts with a dash '-*') + // Value: Number of releases per month (Jan: 0, Feb: 1, ..., Dec: 11) + PreReleasesPerMonth yearMonthMap `json:"pre_releases_per_month"` + // Number of GA releases per year + // Value: Number of releases per year + GAReleasesPerYear yearMap `json:"ga_releases_per_year"` + // Number of Pre-releases per year + // Value: Number of releases per year + PreReleasesPerYear yearMap `json:"pre_releases_per_year"` +} + +type WorkflowsMetrics struct { + // Number of successful actions per year and month + // Key: Year + // Value: Number of successful actions per month (Jan: 0, Feb: 1, ..., Dec: 11) + SuccessfulWorkflowsPerMonth yearMonthMap `json:"successful_actions_per_month"` + // Number of failed actions per year and month + // Key: Year + // Value: Number of failed actions per month (Jan: 0, Feb: 1, ..., Dec: 11) + FailedWorkflowsPerMonth yearMonthMap `json:"failed_actions_per_month"` +} + +func ExtractMetrics(rancherReleases []github.RepositoryRelease, primeReleases []github.RepositoryRelease, workflows []github.WorkflowRun) (Metrics, error) { + var metrics Metrics + + rancher, err := extractReleaseMetrics(rancherReleases) + if err != nil { + return metrics, err + } + + rancherPrimeReleases, err := extractReleaseMetrics(primeReleases) + + workflowsMetrics, err := extractWorkflowsMetrics(workflows) + if err != nil { + return metrics, err + } + + metrics.Rancher = rancher + metrics.RancherPrime = rancherPrimeReleases + metrics.Workflows = workflowsMetrics + + return metrics, nil +} + +func extractReleaseMetrics(releases []github.RepositoryRelease) (ReleaseMetrics, error) { + var metrics ReleaseMetrics + + metrics.GAReleasesPerMonth = make(yearMonthMap) + metrics.PreReleasesPerMonth = make(yearMonthMap) + metrics.GAReleasesPerYear = make(yearMap) + metrics.PreReleasesPerYear = make(yearMap) + + for _, release := range releases { + releaseDate := release.GetCreatedAt().Time + + monthIndex := int(releaseDate.Month() - 1) + year := releaseDate.Year() + + if _, ok := metrics.GAReleasesPerMonth[year]; !ok { + metrics.GAReleasesPerMonth[year] = &[12]int{} + } + if _, ok := metrics.PreReleasesPerMonth[year]; !ok { + metrics.PreReleasesPerMonth[year] = &[12]int{} + } + + // is pre-release + if strings.Contains(release.GetTagName(), "-") { + metrics.PreReleasesPerMonth[year][monthIndex]++ + } else { + metrics.GAReleasesPerMonth[year][monthIndex]++ + } + } + + for year, releases := range metrics.GAReleasesPerMonth { + for _, count := range releases { + metrics.GAReleasesPerYear[year] += count + } + } + + for year, releases := range metrics.PreReleasesPerMonth { + for _, count := range releases { + metrics.PreReleasesPerYear[year] += count + } + } + + return metrics, nil +} + +func extractWorkflowsMetrics(workflows []github.WorkflowRun) (WorkflowsMetrics, error) { + var metrics WorkflowsMetrics + + metrics.SuccessfulWorkflowsPerMonth = make(yearMonthMap) + metrics.FailedWorkflowsPerMonth = make(yearMonthMap) + + for _, workflow := range workflows { + workflowDate := workflow.GetCreatedAt().Time + + monthIndex := int(workflowDate.Month() - 1) + year := workflowDate.Year() + + if _, ok := metrics.SuccessfulWorkflowsPerMonth[year]; !ok { + metrics.SuccessfulWorkflowsPerMonth[year] = &[12]int{} + } + if _, ok := metrics.FailedWorkflowsPerMonth[year]; !ok { + metrics.FailedWorkflowsPerMonth[year] = &[12]int{} + } + + if workflow.GetConclusion() == "success" { + metrics.SuccessfulWorkflowsPerMonth[year][monthIndex]++ + } else { + metrics.FailedWorkflowsPerMonth[year][monthIndex]++ + } + } + return metrics, nil +}