diff --git a/.github/config/commitlint/commitlint.config.js b/.github/config/commitlint/commitlint.config.js new file mode 100644 index 0000000..f6346c5 --- /dev/null +++ b/.github/config/commitlint/commitlint.config.js @@ -0,0 +1,4 @@ +module.exports = { + extends: ['@commitlint/config-conventional'], + ignores: [(message) => /^feat(deps): bump \[.+]\(.+\) from .+ to .+\.$/m.test(message)], +} diff --git a/.github/workflows/coffee.yml b/.github/workflows/coffee.yml index fbe742b..6964f25 100644 --- a/.github/workflows/coffee.yml +++ b/.github/workflows/coffee.yml @@ -3,34 +3,48 @@ name: Coffee-break on: schedule: - cron: '0 0 5 * *' # This runs at 00:00 UTC on the 5th of every month + workflow_dispatch: jobs: - build: + coffee-break: runs-on: ubuntu-latest - container: - image: golang:1.19 + container: + image: quay.io/redhat-appstudio-qe/qe-tools:latest steps: - - name: Check out code - uses: actions/checkout@v4 - with: - repository: 'redhat-appstudio/qe-tools' + - name: Check out code + uses: actions/checkout@v4 + with: + repository: 'redhat-appstudio/qe-tools' + - name: Setup Go environment + uses: actions/setup-go@v5 - - name: Setup Go environment - uses: actions/setup-go@v4 + - name: Run Test and Send Slack Message + run: qe-tools coffee-break + env: + SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }} + HACBS_CHANNEL_ID: ${{ secrets.HACBS_CHANNEL_ID }} - - name: Run Test and Send Slack Message - run: go run main.go coffee-break - env: - SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }} - HACBS_CHANNEL_ID: ${{ secrets.HACBS_CHANNEL_ID }} + - name: Commit and push + run: | + git config user.name "GitHub Action" + git config user.email "action@github.com" + git switch -c coffee-break + git add config/coffee-break/last_week.txt + git commit -m "chore: update config/coffee-break/last_week.txt" + git push -u origin HEAD - - name: Commit and push if it's not a Pull Request - run: | - git config --global --add safe.directory /__w/qe-tools/qe-tools - git init /__w/qe-tools/qe-tools - git config user.name "GitHub Action" - git config user.email "action@github.com" - git add config/coffee-break/last_week.txt - git commit -m "Update config/coffee-break/last_week.txt" - git push origin main -f + create-pr: + runs-on: ubuntu-latest + needs: coffee-break + steps: + - name: Create PR + run: | + gh pr create \ + -t "chore: Add last Coffee Break" \ + -b "Created automatically by a GitHub Action." \ + -H coffee-break \ + -B main \ + -R ${{ github.repository }} \ + env: + GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml index c65fdea..ff56b08 100644 --- a/.github/workflows/commitlint.yml +++ b/.github/workflows/commitlint.yml @@ -12,4 +12,6 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: wagoid/commitlint-github-action@v5 \ No newline at end of file + - uses: wagoid/commitlint-github-action@v5 + with: + configFile: .github/config/commitlint/commitlint.config.js diff --git a/.github/workflows/estimate-review.yml b/.github/workflows/estimate-review.yml new file mode 100644 index 0000000..f224ba3 --- /dev/null +++ b/.github/workflows/estimate-review.yml @@ -0,0 +1,35 @@ +name: Estimate-review + +on: pull_request_target + +permissions: + pull-requests: write + +jobs: + estimate: + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v4 + with: + repository: 'redhat-appstudio/qe-tools' + + - name: Setup Go environment + uses: actions/setup-go@v5 + + - name: Build qe-tools + run: make build + + - name: Run estimate time needed for PR review + run: | + ./qe-tools estimate-review \ + --owner ${{ github.repository_owner }} \ + --repository ${{ github.event.repository.name }} \ + --number ${{ github.event.pull_request.number }} \ + --config ${{ env.CONFIG_PATH }} \ + --token ${{ github.token }} \ + --add-label \ + --human + env: + CONFIG_PATH: 'config/estimate/config.yaml' \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 307ef63..1ddbab9 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,12 +14,12 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: - go-version: '1.19' + go-version: '1.20' - name: golangci-lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v4 with: version: v1.54.2 args: -c .golang-ci.yml -v --timeout=5m \ No newline at end of file diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..c714757 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,24 @@ +name: Pre-commit checks + +on: + pull_request: + +jobs: + pre-commit: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go environment + uses: actions/setup-go@v5 + + - name: Install required tools + run: | + pip install pre-commit + make bootstrap + go mod tidy + + - name: Run pre-commit checks + run: make pre-commit \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e870def --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,23 @@ +name: release + +on: + release: + types: [ created ] + +permissions: + contents: write + +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: wangyoucao577/go-release-action@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + goos: linux + goarch: amd64 + goversion: "1.20" + binary_name: qe-tools + extra_files: config diff --git a/.github/workflows/slack-message.yml b/.github/workflows/slack-message.yml index dc69f5f..862b81c 100644 --- a/.github/workflows/slack-message.yml +++ b/.github/workflows/slack-message.yml @@ -3,24 +3,44 @@ name: Prow-CI on: schedule: - cron: '30 3 * * *' # Runs every day at 3:30 AM UTC (9 AM IST) + workflow_dispatch: jobs: build: runs-on: ubuntu-latest container: - image: golang:1.19 + image: golang:1.20 steps: - - name: Check out code - uses: actions/checkout@v4 + - name: Check out code + uses: actions/checkout@v4 - - name: Setup Go environment - uses: actions/setup-go@v4 + - name: Setup Go environment + uses: actions/setup-go@v5 + with: + go-version: '1.20' - - name: Run Test and Send Slack Message - run: go run main.go prowjob periodic-slack-report - env: - SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }} - URL: ${{ secrets.URL }} - PROW_URL: ${{ secrets.PROW_URL }} - CHANNEL_ID: ${{ secrets.CHANNEL_ID }} + - name: Fetch URL Content + run: | + latest=$(curl -s ${{ secrets.URL }}) + echo "LATEST=$latest" >> $GITHUB_ENV + + - name: Run Test and Store Output + run: | + go run main.go prowjob periodic-report > prowjob-output.txt + echo $PROW_URL + env: + PROW_URL: ${{ secrets.PROW_URL }}${{ env.LATEST }} + + - name: Conditional Slack Message + run: | + output=$(cat prowjob-output.txt) + if [ "$output" != "Job Succeeded" ]; then + echo "Job Failed, sending Slack message." + go run main.go send-slack-message -m "$output" + else + echo "Job Succeeded, not sending Slack message." + fi + env: + SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }} + CHANNEL_ID: ${{ secrets.CHANNEL_ID }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e44f979..7241a26 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: test: strategy: matrix: - go-version: [1.19.x] + go-version: [1.20.x] os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} @@ -22,7 +22,7 @@ jobs: with: fetch-depth: 2 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} @@ -30,4 +30,7 @@ jobs: run: go get ./... - name: go mod tidy - run: go mod tidy \ No newline at end of file + run: go mod tidy + + - name: Run tests + run: make test diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 15f465e..bbd156b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: # # StructSlop # - - id: go-structslop-mod + #- id: go-structslop-mod # # Formatters # @@ -39,4 +39,4 @@ repos: # Style Checkers # - id: go-lint - - id: go-critic \ No newline at end of file + - id: go-critic diff --git a/.sonarcloud.properties b/.sonarcloud.properties new file mode 100644 index 0000000..63ca9c3 --- /dev/null +++ b/.sonarcloud.properties @@ -0,0 +1,5 @@ +# Path to sources +sonar.sources=. +sonar.exclusions=**/*test*, **/vendor/**, **/zz_generated.*, +# Source encoding +sonar.sourceEncoding=UTF-8 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..10f778c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM registry.access.redhat.com/ubi9/go-toolset:1.20.10 AS builder + +COPY go.mod go.mod +COPY go.sum go.sum + +RUN go mod download + +COPY main.go main.go +COPY config/ config/ +COPY tools/ tools/ +COPY pkg/ pkg/ +COPY cmd/ cmd/ + +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o qe-tools main.go + +FROM registry.access.redhat.com/ubi9/ubi-minimal:9.3 + +USER 65532:65532 + +WORKDIR /qe-tools + +COPY --from=builder /opt/app-root/src/qe-tools /usr/bin/ +COPY --from=builder /opt/app-root/src/config config + + diff --git a/cmd/estimate/reviewTime.go b/cmd/estimate/reviewTime.go new file mode 100644 index 0000000..88d5328 --- /dev/null +++ b/cmd/estimate/reviewTime.go @@ -0,0 +1,251 @@ +package estimate + +import ( + "context" + "errors" + "fmt" + "regexp" + + "github.com/google/go-github/v56/github" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "k8s.io/klog/v2" +) + +const ( + defaultBaseWeight = 1.0 + defaultDeletionWeight = 0.5 + + defaultExtensionWeight = 2.0 + + defaultCommitWeight = 0.05 + defaultCommitCeiling = 2 + + defaultFileChangeWeight = 0.1 + defaultFileChangeCeiling = 2 + + defaultConfigPath = "./config/estimate" +) + +// TimeLabel represents a label describing estimated time to review a PR +type TimeLabel struct { + Name string `yaml:"name"` + Time int `yaml:"time"` +} + +// CoefficientConfig represents coefficients used in estimation of time required for a PR review +type CoefficientConfig struct { + Weight float64 `yaml:"weight"` + Ceiling float64 `yaml:"ceiling"` +} + +type configFile struct { + Base float64 `yaml:"base"` + Deletion float64 `yaml:"deletion"` + Commit CoefficientConfig `yaml:"commit"` + Files CoefficientConfig `yaml:"files"` + Extensions map[string]float64 `yaml:"extensions"` + Labels []TimeLabel `yaml:"labels"` +} + +var ( + config = configFile{ + Base: defaultBaseWeight, + Deletion: defaultDeletionWeight, + Commit: CoefficientConfig{ + Weight: defaultCommitWeight, + Ceiling: defaultCommitCeiling, + }, + Files: CoefficientConfig{ + Weight: defaultFileChangeWeight, + Ceiling: defaultFileChangeCeiling, + }, + } + owner string + repository string + prNumber int + ghToken string + + human bool + addLabel bool + + errEmptyLabels = errors.New("zero labels specified in config, make sure there is a non-empty 'labels' list") +) + +// EstimateTimeToReviewCmd is a cobra command that estimates time needed to review a PR +var EstimateTimeToReviewCmd = &cobra.Command{ + Use: "estimate-review", + Short: "Estimate time needed to review a PR in seconds", + PreRunE: func(cmd *cobra.Command, args []string) error { + if addLabel && ghToken == "" { + return fmt.Errorf("github token needs to be specified to add a label") + } + viper.AddConfigPath(defaultConfigPath) + if err := viper.ReadInConfig(); err != nil { + return fmt.Errorf("error reading in config: %+v", err) + } + if err := viper.Unmarshal(&config); err != nil { + return fmt.Errorf("failed to parse config: %+v", err) + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + client := github.NewClient(nil) + if ghToken != "" { + client.WithAuthToken(ghToken) + } + + review, err := TimeToReview(client, owner, repository, prNumber) + if err != nil { + return err + } + + if human { + fmt.Printf("Estimated time to review %s/%s#%d is %d seconds (~%d minutes)\n", owner, repository, prNumber, review, review/60) + } else { + fmt.Println(review) + } + if addLabel { + err := addLabelToPR(client, review) + if err != nil { + return err + } + } + return nil + }, +} + +// TimeToReview estimates time needed to review a PR +func TimeToReview(client *github.Client, owner, repository string, number int) (int, error) { + files, err := getChangedFiles(client, owner, repository, number) + if err != nil { + return -1, err + } + + commitCount, err := countCommits(client, owner, repository, number) + if err != nil { + return -1, err + } + + commitCoefficient := 1 + config.Commit.Weight*(float64(commitCount)-1) + fileCoefficient := 1 + config.Files.Weight*(float64(len(files))-1) + + if commitCoefficient > config.Commit.Ceiling { + commitCoefficient = config.Commit.Ceiling + } + + if fileCoefficient > config.Files.Ceiling { + fileCoefficient = config.Files.Ceiling + } + + result := int(commitCoefficient * fileCoefficient * float64(estimateFileTimes(files))) + return result, nil +} + +func countCommits(client *github.Client, owner, repository string, number int) (int, error) { + commits, _, err := client.PullRequests.ListCommits(context.Background(), owner, repository, number, nil) + return len(commits), err +} + +func getChangedFiles(client *github.Client, owner, repository string, number int) ([]*github.CommitFile, error) { + files, _, err := client.PullRequests.ListFiles(context.Background(), owner, repository, number, nil) + return files, err +} + +func getFileExtension(filename string) string { + regex := regexp.MustCompile(`\.[^.]*$`) + return regex.FindString(filename) +} + +func estimateFileTimes(files []*github.CommitFile) int { + var result float64 = 0 + for _, file := range files { + extension := getFileExtension(file.GetFilename()) + if len(extension) > 0 { + extension = extension[1:] + } + estimate, included := config.Extensions[extension] + if !included { + var defaultIncluded bool + estimate, defaultIncluded = config.Extensions["default"] + if !defaultIncluded { + estimate = defaultExtensionWeight + } + klog.Warningf("Weight for '%s' extension not specified. Using default weight '%.1f'.\n", extension, estimate) + } + + result += float64(file.GetAdditions()) * estimate * config.Base + result += float64(file.GetDeletions()) * estimate * config.Deletion + } + return int(result) +} + +func addLabelToPR(client *github.Client, reviewTime int) error { + existingLabels, err := listLabels(client) + if err != nil { + return err + } + calculatedLabel, err := getLabelBasedOnTime(reviewTime) + if err != nil { + return err + } + klog.Infof("Calculated label '%s'\n", calculatedLabel.Name) + + for _, existingLabel := range existingLabels { + if *existingLabel.Name == calculatedLabel.Name { + klog.Infof("The issue already has the same label '%s'. Skipping addition.\n", *existingLabel.Name) + return nil + } + // Remove outdated label if the estimation changed + for _, timeLabel := range config.Labels { + if timeLabel.Name == *existingLabel.Name { + _, err := client.Issues.RemoveLabelForIssue(context.Background(), owner, repository, prNumber, timeLabel.Name) + if err != nil { + return err + } + klog.Infof("Removed outdated label '%s'", timeLabel.Name) + } + } + } + _, _, err = client.Issues.AddLabelsToIssue(context.Background(), owner, repository, prNumber, []string{calculatedLabel.Name}) + if err != nil { + return err + } + klog.Infof("Added label '%s'\n", calculatedLabel.Name) + return nil +} + +func listLabels(client *github.Client) ([]*github.Label, error) { + labels, _, err := client.Issues.ListLabelsByIssue(context.Background(), owner, repository, prNumber, nil) + if err != nil { + return nil, err + } + return labels, nil +} + +func getLabelBasedOnTime(reviewTime int) (*TimeLabel, error) { + if len(config.Labels) == 0 { + return nil, errEmptyLabels + } + maxLabel := TimeLabel{Time: -1} + for _, label := range config.Labels { + if label.Time <= reviewTime && label.Time > maxLabel.Time { + maxLabel = label + } + } + return &maxLabel, nil +} + +func init() { + EstimateTimeToReviewCmd.Flags().StringVar(&owner, "owner", "redhat-appstudio", "owner of the repository") + EstimateTimeToReviewCmd.Flags().StringVar(&repository, "repository", "e2e-tests", "name of the repository") + EstimateTimeToReviewCmd.Flags().IntVar(&prNumber, "number", 1, "number of the pull request") + err := EstimateTimeToReviewCmd.MarkFlagRequired("number") + if err != nil { // silence golangci-lint + return + } + EstimateTimeToReviewCmd.Flags().StringVar(&ghToken, "token", "", "GitHub token") + + EstimateTimeToReviewCmd.Flags().BoolVar(&addLabel, "add-label", false, "add label to the GitHub PR") + EstimateTimeToReviewCmd.Flags().BoolVar(&human, "human", false, "human readable form") +} diff --git a/cmd/prowjob/createReport.go b/cmd/prowjob/createReport.go index c717984..d2feda9 100644 --- a/cmd/prowjob/createReport.go +++ b/cmd/prowjob/createReport.go @@ -4,9 +4,14 @@ import ( "bufio" "encoding/xml" "fmt" + "io" "os" "path/filepath" "strings" + "time" + + "github.com/redhat-appstudio/qe-tools/pkg/customjunit" + "github.com/redhat-appstudio/qe-tools/pkg/types" "github.com/GoogleCloudPlatform/testgrid/metadata" "github.com/redhat-appstudio/qe-tools/pkg/prow" @@ -23,12 +28,20 @@ import ( "github.com/spf13/viper" ) +var ( + formatReportPortal bool + stepsToSkip []string +) + const ( buildLogFilename = "build-log.txt" finishedFilename = "finished.json" - junitFilename = `/(j?unit|e2e).*\.xml` - gcsBrowserURLPrefix = "https://gcsweb-ci.apps.ci.l2s4.p1.openshiftapps.com/gcs/origin-ci-test/" + gcsBrowserURLPrefix = "https://gcsweb-ci.apps.ci.l2s4.p1.openshiftapps.com/gcs/test-platform-results/" + + reportPortalFormatParamName = "report-portal-format" + stepsToSkipParamName = "skip-ci-steps" + openshiftCITestSuiteName = "openshift-ci job" ) // createReportCmd represents the createReport command @@ -36,19 +49,21 @@ var createReportCmd = &cobra.Command{ Use: "create-report", Short: "Analyze specified prow job and create a report in junit/html format", PreRunE: func(cmd *cobra.Command, _ []string) error { - if viper.GetString(prowJobIDParamName) == "" { + if viper.GetString(types.ProwJobIDParamName) == "" { _ = cmd.Usage() - return fmt.Errorf("parameter %q not provided, neither %s env var was set", prowJobIDParamName, prowJobIDEnv) + return fmt.Errorf("parameter %q not provided, neither %s env var was set", types.ProwJobIDParamName, types.ProwJobIDEnv) } return nil }, SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { - jobID := viper.GetString(prowJobIDParamName) + prowJobID = viper.GetString(types.ProwJobIDParamName) + stepsToSkip = viper.GetStringSlice(stepsToSkipParamName) cfg := prow.ScannerConfig{ - ProwJobID: jobID, - FileNameFilter: []string{finishedFilename, buildLogFilename, junitFilename}, + ProwJobID: prowJobID, + FileNameFilter: []string{finishedFilename, buildLogFilename, types.JunitFilename}, + StepsToSkip: stepsToSkip, } scanner, err := prow.NewArtifactScanner(cfg) @@ -57,13 +72,13 @@ var createReportCmd = &cobra.Command{ } if err := scanner.Run(); err != nil { - return fmt.Errorf("failed to scan artifacts for prow job %s: %+v", jobID, err) + return fmt.Errorf("failed to scan artifacts for prow job %s: %+v", prowJobID, err) } overallJUnitSuites := &reporters.JUnitTestSuites{} - openshiftCiJunit := reporters.JUnitTestSuite{Name: "openshift-ci job", Properties: reporters.JUnitProperties{Properties: []reporters.JUnitProperty{}}} + openshiftCiJunit := reporters.JUnitTestSuite{Name: openshiftCITestSuiteName, Properties: reporters.JUnitProperties{Properties: []reporters.JUnitProperty{}}} - htmlReportLink := gcsBrowserURLPrefix + scanner.ObjectPrefix + "redhat-appstudio-report/artifacts/junit-summary.html" + htmlReportLink := gcsBrowserURLPrefix + scanner.ArtifactDirectoryPrefix + "redhat-appstudio-report/artifacts/junit-summary.html" openshiftCiJunit.Properties.Properties = append(openshiftCiJunit.Properties.Properties, reporters.JUnitProperty{Name: "html-report-link", Value: htmlReportLink}) for stepName, artifactsFilenameMap := range scanner.ArtifactStepMap { @@ -101,9 +116,9 @@ var createReportCmd = &cobra.Command{ } } - artifactDir := viper.GetString(artifactDirParamName) + artifactDir := viper.GetString(types.ArtifactDirParamName) if artifactDir == "" { - artifactDir = "./tmp/" + jobID + artifactDir = "./tmp/" + prowJobID klog.Warningf("path to artifact dir was not provided - using default %q\n", artifactDir) } @@ -111,11 +126,28 @@ var createReportCmd = &cobra.Command{ return fmt.Errorf("failed to create directory for results '%s': %+v", artifactDir, err) } + // Add timestamp to openshift-ci job + if len(overallJUnitSuites.TestSuites) > 0 { + openshiftCiJunit.Timestamp = overallJUnitSuites.TestSuites[0].Timestamp + } else { + openshiftCiJunit.Timestamp = time.Now().Format("2006-01-02T15:04:05") + } + overallJUnitSuites.TestSuites = append(overallJUnitSuites.TestSuites, openshiftCiJunit) overallJUnitSuites.Failures += openshiftCiJunit.Failures overallJUnitSuites.Errors += openshiftCiJunit.Errors overallJUnitSuites.Tests += openshiftCiJunit.Tests + // Omit system-err from passed test cases + for i := range overallJUnitSuites.TestSuites { + for j := range overallJUnitSuites.TestSuites[i].TestCases { + tc := &overallJUnitSuites.TestSuites[i].TestCases[j] + if tc.Status == "passed" { + tc.SystemErr = "" + } + } + } + generatedJunitFilepath := filepath.Clean(artifactDir + "/junit.xml") outFile, err := os.Create(generatedJunitFilepath) if err != nil { @@ -136,17 +168,75 @@ var createReportCmd = &cobra.Command{ klog.Infof("JUnit report saved to: %s/junit.xml", artifactDir) klog.Infof("HTML report saved to: %s/junit-summary.html", artifactDir) + + if formatReportPortal { + reportPortalSuites := &customjunit.TestSuites{} + if err := readXMLFile(fmt.Sprintf("%s/junit.xml", artifactDir), reportPortalSuites); err != nil { + return fmt.Errorf("could not read junit.xml file") + } + + changeDisabledToSkipped(overallJUnitSuites, reportPortalSuites) + + generatedReportPortalFilepath := filepath.Clean(artifactDir + "/junit-rp.xml") + outRPFile, err := os.Create(generatedReportPortalFilepath) + if err != nil { + return fmt.Errorf("cannot create file '%s': %+v", generatedReportPortalFilepath, err) + } + + if err := xml.NewEncoder(bufio.NewWriter(outRPFile)).Encode(reportPortalSuites); err != nil { + return fmt.Errorf("cannot encode JUnit suites struct '%+v' into file located at '%s': %+v", reportPortalSuites, generatedJunitFilepath, err) + } + klog.Infof("JUnit report for Report Portal saved to: %s/junit-rp.xml", artifactDir) + } + return nil }, } -func init() { - createReportCmd.Flags().StringVar(&prowJobID, prowJobIDParamName, "", "Prow job ID to analyze") - createReportCmd.Flags().StringVar(&artifactDir, artifactDirParamName, "", "Path to the folder where to store produced files") +func readXMLFile(xmlPath string, result any) error { + xmlFile, err := os.Open(filepath.Clean(xmlPath)) + if err != nil { + return fmt.Errorf("could not open file '%s', error: %v", xmlPath, err) + } + defer xmlFile.Close() + + xmlBytes, err := io.ReadAll(xmlFile) + if err != nil { + return err + } - _ = viper.BindPFlag(artifactDirParamName, createReportCmd.Flags().Lookup(artifactDirParamName)) - _ = viper.BindPFlag(prowJobIDParamName, createReportCmd.Flags().Lookup(prowJobIDParamName)) + if err = xml.Unmarshal(xmlBytes, &result); err != nil { + klog.Errorf("cannot decode JUnit suite %q into xml: %+v", xmlPath, err) + } + + return nil +} + +func changeDisabledToSkipped(original *reporters.JUnitTestSuites, custom *customjunit.TestSuites) { + totalSkipped := 0 + for _, suite := range original.TestSuites { + if suite.Disabled != 0 { + for i := range custom.TestSuites { + if custom.TestSuites[i].Name == suite.Name { + custom.TestSuites[i].Skipped += suite.Disabled + } + totalSkipped += custom.TestSuites[i].Skipped + } + } + } + custom.Skipped = totalSkipped +} + +func init() { + createReportCmd.Flags().StringVar(&prowJobID, types.ProwJobIDParamName, "", "Prow job ID to analyze") + createReportCmd.Flags().BoolVar(&formatReportPortal, reportPortalFormatParamName, false, "Format for Report Portal") + createReportCmd.Flags().StringArrayVar(&stepsToSkip, stepsToSkipParamName, []string{"redhat-appstudio-report"}, "List of CI steps to skip when gathering artifacts") + + _ = viper.BindPFlag(types.ArtifactDirParamName, createReportCmd.Flags().Lookup(types.ArtifactDirParamName)) + _ = viper.BindPFlag(types.ProwJobIDParamName, createReportCmd.Flags().Lookup(types.ProwJobIDParamName)) + _ = viper.BindPFlag(reportPortalFormatParamName, createReportCmd.Flags().Lookup(reportPortalFormatParamName)) + _ = viper.BindPFlag(stepsToSkipParamName, createReportCmd.Flags().Lookup(stepsToSkipParamName)) // Bind environment variables to viper (in case the associated command's parameter is not provided) - _ = viper.BindEnv(prowJobIDParamName, prowJobIDEnv) - _ = viper.BindEnv(artifactDirParamName, artifactDirEnv) + _ = viper.BindEnv(types.ProwJobIDParamName, types.ProwJobIDEnv) + _ = viper.BindEnv(types.ArtifactDirParamName, types.ArtifactDirEnv) } diff --git a/cmd/prowjob/healthCheck.go b/cmd/prowjob/healthCheck.go new file mode 100644 index 0000000..d2225d8 --- /dev/null +++ b/cmd/prowjob/healthCheck.go @@ -0,0 +1,199 @@ +package prowjob + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "os" + "strconv" + "strings" + + "github.com/redhat-appstudio/qe-tools/pkg/types" + + "golang.org/x/exp/slices" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "k8s.io/klog/v2" + prowUtils "k8s.io/test-infra/prow/pod-utils/downwardapi" + + "github.com/google/go-github/v56/github" + "github.com/redhat-appstudio/qe-tools/pkg/status" +) + +const ( + healthCheckDefaultConfigPath = "./config/health-check/config.yaml" + healthCheckCmdLongDescription = `This command checks status of external services provided via config. +The default config is located in ` + healthCheckDefaultConfigPath + `, however user can provide their own config +via --config= option) +` +) + +var ( + healthCheckConfig HealthCheckConfig + healthCheckNotifyRequiredEnvVars = []string{types.GithubTokenEnv, prowUtils.RepoOwnerEnv, prowUtils.RepoNameEnv, prowUtils.PullNumberEnv} +) + +// HealthCheckConfig represents configuration of external services which status will be checked +type HealthCheckConfig struct { + ExternalServices []Service `json:"externalServices"` +} + +// HealthCheckStatus contains specification and current status of external services +// and contains a map of currently unhealthy critical components +type HealthCheckStatus struct { + ExternalServices []Service `json:"externalServices"` + UnhealthyCriticalComponents map[string][]string `json:"unhealthyCriticalComponents"` +} + +// Service contains specification of an external service specified via config +// and holds an information about current status of related components +type Service struct { + Name string `json:"name"` + CriticalComponents []string `json:"criticalComponents"` + StatusPageURL string `json:"statusPageURL"` + CurrentStatus status.Summary `json:"currentStatus"` +} + +// healthCheckCmd represents the createReport command +var healthCheckCmd = &cobra.Command{ + Use: "health-check", + Short: `Perform a health check on dependant services`, + Long: healthCheckCmdLongDescription, + PreRunE: func(cmd *cobra.Command, args []string) error { + if viper.ConfigFileUsed() == "" { + viper.SetConfigFile(healthCheckDefaultConfigPath) + } + if err := viper.ReadInConfig(); err != nil { + return fmt.Errorf("err readinconfig: %+v", err) + } + if err := viper.Unmarshal(&healthCheckConfig); err != nil { + return fmt.Errorf("failed to parse config: %+v", err) + } + if viper.GetBool(notifyOnPRParamName) { + for _, e := range healthCheckNotifyRequiredEnvVars { + if viper.GetString(e) == "" { + return fmt.Errorf("%q flag provided, but %q env var not set", notifyOnPRParamName, strings.ToUpper(e)) + } + } + } + return nil + }, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + hcStatus := &HealthCheckStatus{} + hcStatus.ExternalServices = healthCheckConfig.ExternalServices + hcStatus.UnhealthyCriticalComponents = make(map[string][]string) + + for i, service := range hcStatus.ExternalServices { + r, err := http.Get(service.StatusPageURL) + if err != nil { + return fmt.Errorf("failed to get service %s status page: %+v", service.Name, err) + } + body, err := io.ReadAll(r.Body) + if err != nil { + return fmt.Errorf("failed to read response body for a service %s: %+v", service.Name, err) + } + v := status.Summary{} + if err := json.Unmarshal(body, &v); err != nil { + return fmt.Errorf("failed to unmarshal response body from a service %s: %+v", service.Name, err) + } + hcStatus.ExternalServices[i].CurrentStatus = v + + for _, c := range v.Components { + if c.Status == "major_outage" && isCriticalComponent(service, c) { + hcStatus.UnhealthyCriticalComponents[service.Name] = append(hcStatus.UnhealthyCriticalComponents[service.Name], c.Name) + } + } + } + + artifactDir := viper.GetString(types.ArtifactDirParamName) + if artifactDir == "" { + artifactDir = "./tmp" + klog.Warningf("path to artifact dir was not provided - using default %q\n", artifactDir) + } + if err := os.MkdirAll(artifactDir, 0o750); err != nil { + return fmt.Errorf("failed to create directory for results '%s': %+v", artifactDir, err) + } + o, err := json.MarshalIndent(hcStatus, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal services status: %+v", err) + } + reportFilePath := artifactDir + "/services-status.json" + if err := os.WriteFile(reportFilePath, []byte(o), 0o600); err != nil { + return fmt.Errorf("failed to create file with the status of dependant services: %+v", err) + } + klog.Infof("health check report saved to %s", reportFilePath) + + if len(hcStatus.UnhealthyCriticalComponents) > 0 { + + if viper.GetBool(notifyOnPRParamName) { + prMessage := buildPRMessage(hcStatus, failIfUnhealthy) + githubClient := github.NewClient(http.DefaultClient).WithAuthToken(viper.GetString(types.GithubTokenEnv)) + prNumberInt, _ := strconv.Atoi(viper.GetString(prowUtils.PullNumberEnv)) + comment, _, err := githubClient.Issues.CreateComment( + context.Background(), + viper.GetString(prowUtils.RepoOwnerEnv), + viper.GetString(prowUtils.RepoNameEnv), + prNumberInt, + &github.IssueComment{ + Body: github.String(prMessage), + }) + if err != nil { + klog.Errorf("couldn't report an issue on a PR: %+v", err) + } + klog.Infof("added a report about an outage to %s", comment.GetHTMLURL()) + } + + if viper.GetBool(failIfUnhealthyParamName) { + return fmt.Errorf("detected unhealthy critical components: %+v - see %s for more info", hcStatus.UnhealthyCriticalComponents, reportFilePath) + } + } + + return nil + }, +} + +func isCriticalComponent(service Service, c status.Component) bool { + return slices.Contains(service.CriticalComponents, c.Name) +} + +func buildPRMessage(hcStatus *HealthCheckStatus, failIfUnhealthy bool) string { + prMessage := "❗ Detected an outage of the following critical component(s)❗\n" + for s, components := range hcStatus.UnhealthyCriticalComponents { + prMessage += fmt.Sprintf("- %s: %s\n", s, strings.Join(components, ", ")) + } + var consequence string + if failIfUnhealthy { + consequence = "E2E tests won't run on your PR" + } else { + consequence = "E2E tests will probably fail" + } + prMessage += fmt.Sprintf("\nDue to this issue **%s**. Please keep an eye on the following status pages:\n", consequence) + for _, s := range healthCheckConfig.ExternalServices { + if _, ok := hcStatus.UnhealthyCriticalComponents[s.Name]; ok { + u, err := url.Parse(s.StatusPageURL) + if err != nil { + klog.Errorf("could not parse status page URL %s: %+v", s.StatusPageURL, err) + continue + } + prMessage += fmt.Sprintf("- %s://%s\n", u.Scheme, u.Host) + } + } + prMessage += "\nand add a comment `/retest-required` once the reported issues are solved" + return prMessage +} + +func init() { + healthCheckCmd.Flags().BoolVar(&failIfUnhealthy, failIfUnhealthyParamName, false, "Exit with non-zero code if critical issues were found") + healthCheckCmd.Flags().BoolVar(¬ifyOnPR, notifyOnPRParamName, false, fmt.Sprintf("Create a comment in a related PR if critical issues were found (required env vars: %+v)", strings.Join(healthCheckNotifyRequiredEnvVars, ", "))) + + _ = viper.BindPFlag(types.ArtifactDirParamName, healthCheckCmd.Flags().Lookup(types.ArtifactDirParamName)) + _ = viper.BindPFlag(failIfUnhealthyParamName, healthCheckCmd.Flags().Lookup(failIfUnhealthyParamName)) + _ = viper.BindPFlag(notifyOnPRParamName, healthCheckCmd.Flags().Lookup(notifyOnPRParamName)) + // Bind environment variables to viper (in case the associated command's parameter is not provided) + _ = viper.BindEnv(types.ArtifactDirParamName, types.ArtifactDirEnv) +} diff --git a/cmd/prowjob/periodicSlackReport.go b/cmd/prowjob/periodicSlackReport.go index e73c766..d1175ef 100644 --- a/cmd/prowjob/periodicSlackReport.go +++ b/cmd/prowjob/periodicSlackReport.go @@ -4,7 +4,6 @@ import ( "fmt" "io" "net/http" - "net/url" "os" "regexp" "strings" @@ -13,12 +12,12 @@ import ( "github.com/spf13/viper" ) -// periodicSlackReportCmd returns the periodic-slack-report command -var periodicSlackReportCmd = &cobra.Command{ - Use: "periodic-slack-report", - Short: "Analyzes the build log from latest periodic job and sends a summary of detected issues to dedicated Slack channel", +// periodicReportCmd returns the periodic-report command +var periodicReportCmd = &cobra.Command{ + Use: "periodic-report", + Short: "Analyzes the build log from latest ci jobs and returns a short job summary", PreRunE: func(cmd *cobra.Command, args []string) error { - requiredEnvVars := []string{"slack_token", "channel_id", "url", "prow_url"} + requiredEnvVars := []string{"prow_url"} for _, e := range requiredEnvVars { if viper.GetString(e) == "" { @@ -48,105 +47,79 @@ func fetchTextContent(url string) (string, error) { return "", fmt.Errorf("error reading the webpage content: %w", err) } - return string(bodyBytes), nil -} - -func sendMessageToLatestThread(token, channelID, message string) error { - slackURL := "https://slack.com/api/chat.postMessage" + bodyString := string(bodyBytes) - payload := url.Values{} - payload.Set("channel", channelID) - payload.Set("text", message) + cleanedString := removeANSIEscapeSequences(bodyString) - req, err := http.NewRequest("POST", slackURL, strings.NewReader(payload.Encode())) - if err != nil { - return fmt.Errorf("error creating the request: %w", err) - } + return cleanedString, nil +} - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - req.Header.Add("Authorization", "Bearer "+token) +func constructMessage(bodyString string) (string, bool) { + failureMatches := regexp.MustCompile(`(?s)(Summarizing.*?Test Suite Failed)`).FindStringSubmatch(bodyString) - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return fmt.Errorf("error sending the request: %w", err) + if isJobFailed(bodyString) || failureMatches != nil { + message := "Test Suite Summary:\n" + message += extractTestResultsAndSummary(bodyString) + message += extractDuration(bodyString) + message += formatFailures(failureMatches[1]) + return message, false } - defer resp.Body.Close() + return "Job Succeeded", true +} - if resp.StatusCode != http.StatusOK { - bodyBytes, _ := io.ReadAll(resp.Body) - return fmt.Errorf("request failed with status code %d: %s", resp.StatusCode, string(bodyBytes)) - } +func isJobFailed(body string) bool { + stateRegexp := regexp.MustCompile(`Reporting job state '(\w+)'`) + stateMatches := stateRegexp.FindStringSubmatch(body) - return nil + return len(stateMatches) == 2 && stateMatches[1] == "failed" } -func constructMessage(content, bodyString string) (string, bool) { - var message string - const statePattern = `Reporting job state '(\w+)'` - const failurePattern = `(?s)(Summarizing.*?Test Suite Failed)` - const durationPattern = `Ran for ([\dhms]+)` +func extractTestResultsAndSummary(body string) string { + pattern := `Ran (\d+) of (\d+) Specs in ([\d.]+) seconds\nFAIL! -- (\d+) Passed \| (\d+) Failed \| (\d+) Pending \| (\d+) Skipped` + matches := regexp.MustCompile(pattern).FindStringSubmatch(body) - stateRegexp := regexp.MustCompile(statePattern) - stateMatches := stateRegexp.FindStringSubmatch(bodyString) - - hasFailed := len(stateMatches) == 2 && stateMatches[1] == "failed" - if !hasFailed { - return "", false + if matches == nil { + return "Infrastructure setup issues or failures unrelated to tests were found\n" } - failureRegexp := regexp.MustCompile(failurePattern) - failureMatches := failureRegexp.FindStringSubmatch(bodyString) + return fmt.Sprintf("Test Results: %s Passed | %s Failed | %s Pending | %s Skipped\nRan %s of %s Specs in %s seconds\n", + matches[4], matches[5], matches[6], matches[7], matches[1], matches[2], matches[3]) +} - failureSummary := "" - if failureMatches == nil { - failureSummary = "Infrastructure setup issues or failures unrelated to tests were found. No report of test failures was produced. \n" - } else { - failureSummary = removeANSIEscapeSequences(failureMatches[1]) + "\n" +func extractDuration(body string) string { + matches := regexp.MustCompile(`Ran for ([\dhms]+)`).FindStringSubmatch(body) + if matches == nil { + return "" } + return fmt.Sprintf("Total Duration: %s\n", matches[1]) +} - message += failureSummary - message += fmt.Sprintf("Reporting job state: %s\n", strings.TrimSpace(stateMatches[1])) +func formatFailures(failures string) string { + var formattedFailures strings.Builder + formattedFailures.WriteString("Failures:\n") - durationRegexp := regexp.MustCompile(durationPattern) - durationMatches := durationRegexp.FindStringSubmatch(bodyString) + for _, line := range strings.Split(failures, "\n") { + if strings.Contains(line, "[FAIL]") { + formattedFailures.WriteString("- " + strings.TrimSpace(line) + "\n") + } + } - if len(durationMatches) >= 2 { - message += fmt.Sprintf("Ran for %s\n", durationMatches[1]) + if formattedFailures.String() == "Failures:\n" { + return "No specific failures captured in the report.\n" } - return message, true + return formattedFailures.String() } func run(cmd *cobra.Command, args []string) error { - token := os.Getenv("SLACK_TOKEN") - channelID := os.Getenv("CHANNEL_ID") - - url := os.Getenv("URL") - content, err := fetchTextContent(url) - if err != nil { - return err - } - - prowURL := fmt.Sprintf(os.Getenv("PROW_URL"), content) + // Required GCS build.log PATH for latest build + prowURL := os.Getenv("PROW_URL") + "/build-log.txt" bodyString, err := fetchTextContent(prowURL) if err != nil { return err } - message, sendSlackMessage := constructMessage(content, bodyString) - + message, _ := constructMessage(bodyString) fmt.Println(message) - - if sendSlackMessage { - err = sendMessageToLatestThread(token, channelID, message) - if err != nil { - return err - } - - fmt.Println("Slack message sent successfully!") - } else { - fmt.Println("No test failures found. Slack message not sent.") - } return nil } diff --git a/cmd/prowjob/prowjob.go b/cmd/prowjob/prowjob.go index 1d64b5a..71d08f5 100644 --- a/cmd/prowjob/prowjob.go +++ b/cmd/prowjob/prowjob.go @@ -1,20 +1,20 @@ package prowjob import ( + "github.com/redhat-appstudio/qe-tools/pkg/types" "github.com/spf13/cobra" ) const ( - artifactDirEnv string = "ARTIFACT_DIR" - artifactDirParamName string = "artifact-dir" - - prowJobIDEnv string = "PROW_JOB_ID" - prowJobIDParamName string = "prow-job-id" + failIfUnhealthyParamName string = "fail-if-unhealthy" + notifyOnPRParamName string = "notify-on-pr" ) var ( - artifactDir string - prowJobID string + artifactDir string + failIfUnhealthy bool + notifyOnPR bool + prowJobID string ) // ProwjobCmd represents the prowjob command @@ -24,6 +24,10 @@ var ProwjobCmd = &cobra.Command{ } func init() { - ProwjobCmd.AddCommand(periodicSlackReportCmd) + ProwjobCmd.AddCommand(periodicReportCmd) ProwjobCmd.AddCommand(createReportCmd) + ProwjobCmd.AddCommand(healthCheckCmd) + + createReportCmd.Flags().StringVar(&artifactDir, types.ArtifactDirParamName, "", "Path to the folder where to store produced files") + healthCheckCmd.Flags().StringVar(&artifactDir, types.ArtifactDirParamName, "", "Path to the folder where to store produced files") } diff --git a/cmd/root.go b/cmd/root.go index 25cd3f7..bbbf8cd 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,11 +4,15 @@ import ( "fmt" "os" + "github.com/redhat-appstudio/qe-tools/cmd/estimate" + "github.com/redhat-appstudio/qe-tools/cmd/webhook" + "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/redhat-appstudio/qe-tools/cmd/coffeebreak" "github.com/redhat-appstudio/qe-tools/cmd/prowjob" + "github.com/redhat-appstudio/qe-tools/cmd/sendslackmessage" ) var cfgFile string @@ -43,6 +47,9 @@ func init() { rootCmd.AddCommand(prowjob.ProwjobCmd) rootCmd.AddCommand(coffeebreak.CoffeeBreakCmd) + rootCmd.AddCommand(sendslackmessage.SendSlackMessageCmd) + rootCmd.AddCommand(webhook.WebhookCmd) + rootCmd.AddCommand(estimate.EstimateTimeToReviewCmd) } // initConfig reads in config file and ENV variables if set. @@ -50,15 +57,6 @@ func initConfig() { if cfgFile != "" { // Use config file from the flag. viper.SetConfigFile(cfgFile) - } else { - // Find home directory. - home, err := os.UserHomeDir() - cobra.CheckErr(err) - - // Search config in home directory with name ".qe-tools" (without extension). - viper.AddConfigPath(home) - viper.SetConfigType("yaml") - viper.SetConfigName(".qe-tools") } viper.AutomaticEnv() // read in environment variables that match diff --git a/cmd/sendslackmessage/sendSlackMessage.go b/cmd/sendslackmessage/sendSlackMessage.go new file mode 100644 index 0000000..a2bd95b --- /dev/null +++ b/cmd/sendslackmessage/sendSlackMessage.go @@ -0,0 +1,66 @@ +package sendslackmessage + +import ( + "fmt" + "os" + "strings" + + "github.com/slack-go/slack" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "k8s.io/klog/v2" +) + +var messageText string + +// SendSlackMessageCmd defines to send messages to a Slack channel. +var SendSlackMessageCmd = &cobra.Command{ + Use: "send-slack-message", + Short: "This command will send message to any slack channel.", + PreRunE: func(cmd *cobra.Command, args []string) error { + requiredEnvVars := []string{"slack_token", "channel_id"} + for _, e := range requiredEnvVars { + if viper.GetString(e) == "" { + return fmt.Errorf("%+v env var not set", strings.ToUpper(e)) + } + } + return nil + }, + Run: run, +} + +func sendSlackMessage(token, channelID, message string) error { + api := slack.New(token) + + _, _, err := api.PostMessage( + channelID, + slack.MsgOptionText(message, false), + slack.MsgOptionAsUser(true), + ) + return err +} + +func run(cmd *cobra.Command, args []string) { + slackToken := os.Getenv("SLACK_TOKEN") + slackChannelID := os.Getenv("CHANNEL_ID") + + err := sendSlackMessage(slackToken, slackChannelID, messageText) + if err != nil { + fmt.Printf("Error sending message to Slack: %v\n", err) + } + klog.Info("message was delivered successfully") +} + +func init() { + SendSlackMessageCmd.Flags().StringVarP(&messageText, "message", "m", "", "Message body of the Slack message") + + if err := SendSlackMessageCmd.MarkFlagRequired("message"); err != nil { + fmt.Fprintf(os.Stderr, "Error marking 'message' flag as required: %v\n", err) + os.Exit(1) + } + + if err := viper.BindPFlag("message", SendSlackMessageCmd.Flags().Lookup("message")); err != nil { + fmt.Fprintf(os.Stderr, "Error binding message flag to viper: %v\n", err) + os.Exit(1) + } +} diff --git a/cmd/webhook/reportPortalWebhook.go b/cmd/webhook/reportPortalWebhook.go new file mode 100644 index 0000000..c92df31 --- /dev/null +++ b/cmd/webhook/reportPortalWebhook.go @@ -0,0 +1,91 @@ +package webhook + +import ( + "fmt" + "os" + "strconv" + + "github.com/redhat-appstudio/qe-tools/pkg/prow" + "github.com/redhat-appstudio/qe-tools/pkg/types" + "github.com/redhat-appstudio/qe-tools/pkg/webhook" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "k8s.io/klog/v2" +) + +var ( + openshiftJobSpec *prow.OpenshiftJobSpec + parameters = []*types.CmdParameter[string]{jobSpec, saltSecret, webhookTargetURL} + jobSpec = &types.CmdParameter[string]{ + Name: "job-spec", + Env: "JOB_SPEC", + Usage: "Job spec", + } + saltSecret = &types.CmdParameter[string]{ + Name: "salt-secret", + Env: "SALT_SECRET", + DefaultValue: "123456789", + Usage: "Salt for webhook config", + } + webhookTargetURL = &types.CmdParameter[string]{ + Name: "target-url", + Env: "TARGET_URL", + DefaultValue: "https://hook.pipelinesascode.com/EyFYTakxEgEy", + Usage: "Target URL for webhook", + } +) + +var reportPortalWebhookCmd = &cobra.Command{ + Use: "report-portal", + PreRunE: func(cmd *cobra.Command, args []string) error { + var err error + openshiftJobSpec, err = prow.ParseJobSpec(jobSpec.Value) + if err != nil { + return fmt.Errorf("error parsing openshift job spec: %+v", err) + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + pullNumber := "" + if openshiftJobSpec.Type == "periodic" { + openshiftJobSpec.Refs.RepoLink = "https://github.com/redhat-appstudio/infra-deployments" + pullNumber = "periodic" + } else if (openshiftJobSpec.Refs.Repo == "e2e-tests" || openshiftJobSpec.Refs.Repo == "infra-deployments") && len(openshiftJobSpec.Refs.Pulls) > 0 { + pullNumber = strconv.Itoa(openshiftJobSpec.Refs.Pulls[0].Number) + } else { + klog.Infof("sending webhook for jobType %s, jobName %s is not supported", openshiftJobSpec.Type, openshiftJobSpec.Job) + return nil + } + + path, err := os.Executable() + if err != nil { + return fmt.Errorf("error when sending webhook: %+v", err) + } + + wh := webhook.Webhook{ + Path: path, + Repository: webhook.Repository{ + FullName: fmt.Sprintf("%s/%s", openshiftJobSpec.Refs.Organization, openshiftJobSpec.Refs.Repo), + PullNumber: pullNumber, + }, + RepositoryURL: openshiftJobSpec.Refs.RepoLink, + } + + resp, err := wh.CreateAndSend(saltSecret.Value, webhookTargetURL.Value) + if err != nil { + return fmt.Errorf("error sending webhook: %+v", err) + } + klog.Infof("webhook response: %+v", resp) + + return nil + }, +} + +func init() { + for _, parameter := range parameters { + reportPortalWebhookCmd.Flags().StringVar(¶meter.Value, parameter.Name, parameter.DefaultValue, parameter.Usage) + _ = viper.BindEnv(parameter.Name, parameter.Env) + viper.SetDefault(parameter.Name, parameter.DefaultValue) + parameter.Value = viper.GetString(parameter.Name) + } +} diff --git a/cmd/webhook/webhook.go b/cmd/webhook/webhook.go new file mode 100644 index 0000000..e883cfe --- /dev/null +++ b/cmd/webhook/webhook.go @@ -0,0 +1,13 @@ +package webhook + +import "github.com/spf13/cobra" + +// WebhookCmd is a cobra command for triggering webhooks +var WebhookCmd = &cobra.Command{ + Use: "webhook", + Short: "Command for triggering webhooks", +} + +func init() { + WebhookCmd.AddCommand(reportPortalWebhookCmd) +} diff --git a/config/coffee-break/last_week.txt b/config/coffee-break/last_week.txt index a34bc1d..d8e1459 100644 --- a/config/coffee-break/last_week.txt +++ b/config/coffee-break/last_week.txt @@ -1,4 +1,6 @@ -sselvan, djodha, nshprai -psturc, jinqi, tnevrlka nshprai, kalem, sselvan -jinqi, djodha, psturc \ No newline at end of file +jinqi, djodha, psturc +kalem, nshprai, chuo +sselvan, djodha, psturc +kalem, nshprai, chuo +tnevrlka, jinqi, sselvan \ No newline at end of file diff --git a/config/coffee-break/participants.txt b/config/coffee-break/participants.txt index 4f35177..f04f6cd 100644 --- a/config/coffee-break/participants.txt +++ b/config/coffee-break/participants.txt @@ -5,4 +5,5 @@ kalem psturc sselvan tnevrlka -jinqi \ No newline at end of file +jinqi +ascerra \ No newline at end of file diff --git a/config/estimate/config.yaml b/config/estimate/config.yaml new file mode 100644 index 0000000..0f0dcf8 --- /dev/null +++ b/config/estimate/config.yaml @@ -0,0 +1,43 @@ +base: 1 +deletion: 0.5 + +commit: + weight: 0.05 + ceiling: 2 + +files: + weight: 0.1 + ceiling: 2 + +extensions: + default: 2 + go: 1 + sum: 0.1 + mod: 0.5 + sh: 1.5 + yml: 2 + yaml: 2 + json: 2 + md: 0.2 + +labels: + - name: "> 60 min" + time: 3600 + + - name: "30-60 min" + time: 1800 + + - name: "15-30 min" + time: 900 + + - name: "10-15 min" + time: 600 + + - name: "5-10 min" + time: 300 + + - name: "1-5 min" + time: 60 + + - name: "< 1 min" + time: 0 diff --git a/config/health-check/config.yaml b/config/health-check/config.yaml new file mode 100644 index 0000000..040b27d --- /dev/null +++ b/config/health-check/config.yaml @@ -0,0 +1,20 @@ +externalServices: + - name: redhat + criticalComponents: + - registry.redhat.io + - registry.access.redhat.com + - registry.connect.redhat.com + statusPageURL: https://status.redhat.com/api/v2/summary.json + - name: quay + criticalComponents: + - Registry + - API + - Build System + statusPageURL: https://status.redhat.com/api/v2/summary.json + - name: github + criticalComponents: + - Git Operations + - API Requests + - Webhooks + - Pull Requests + statusPageURL: https://www.githubstatus.com/api/v2/summary.json \ No newline at end of file diff --git a/docs/estimate-review.md b/docs/estimate-review.md new file mode 100644 index 0000000..699cd35 --- /dev/null +++ b/docs/estimate-review.md @@ -0,0 +1,91 @@ +# Estimating time to review a PR + +## Usage +See the [GitHub Action workflow file](../.github/workflows/estimate-review.yml) + +Everything should be already setup, but you might want to change the `CONFIG_PATH` environment variable. + +For setting up more specific workflows, run `./qe-tools estimate-review --help` to see the usage + +``` +Estimate time needed to review a PR in seconds + +Usage: + qe-tools estimate-review [flags] + +Flags: + --add-label add label to the GitHub PR + --config string path to the yaml config file + -h, --help help for estimate-review + --human human readable form + --number int number of the pull request (default 1) + --owner string owner of the repository (default "redhat-appstudio") + --repository string name of the repository (default "e2e-tests") + --token string GitHub token +``` + +## How it works + +The tool takes following parameters into account: +- additions +- deletions +- extensions of changed files +- number of changed files +- number of commits + +It sums additions and deletions considering their weights and the weight of the extension of the changed file. + +`(additions*BASE_COEF + deletions*DELETE_COEF) * EXT_COEF` + +Then it multiplies this equation with coefficients of +- commits `(1 + COMMIT_COEF * (commits-1))` +- number of changed files `(1 + FILE_COEF * (files-1))` + +These coefficients support having a ceiling (won't go above the specified value). + +## Configuration +Configuration is done via a yaml file. + +**Please take a look at the provided [YAML config file](../config/estimate/config.yaml)** + +### Required +#### labels +Having at least one label in the config is required if the `add-label` flag is present. + +A label should have following parameters: +- `name`: Label text that you want on the pull request (string) +- `time`: the lower bound when the label can be applied (in seconds, int) + +e.g. Label that should be applied if the review time takes longer than 60 second: +```yaml +labels: + - name: "> 1 min" + time: 60 +``` + +### Recommended +#### extensions +Weights that should be applied on changes in a particular file extension. + +`default` is a special extension which specifies a weight for extensions not found in the list. + +e.g. Weights for default, py and md extensions where: +- .py files take 1 unit of time to review +- .md files take 0.2 units of time +- all the other files take 2 units of time +```yaml +extensions: + - default: 2 + - py: 1 + - md: 0.2 +``` + +### Optional +- `base`: base weight, used for additions (default 1) +- `deletion`: weight used for deletions (default 0.5) +- `commit`: weight and ceiling used for calculating the commit coefficient + - `weight`: weight of one commit (default 0.05) + - `ceiling`: maximum value of the coefficient (default 2) +- `files`: same idea as commit coefficient but for number of changed files + - `weight`: (default 0.1) + - `ceiling`: (default 2) diff --git a/go.mod b/go.mod index cf3e161..59db583 100644 --- a/go.mod +++ b/go.mod @@ -1,65 +1,69 @@ module github.com/redhat-appstudio/qe-tools -go 1.19 +go 1.20 require ( - cloud.google.com/go/storage v1.35.1 - github.com/GoogleCloudPlatform/testgrid v0.0.166 - github.com/daixiang0/gci v0.11.2 - github.com/go-critic/go-critic v0.9.0 + cloud.google.com/go/storage v1.38.0 + github.com/GoogleCloudPlatform/testgrid v0.0.170 + github.com/daixiang0/gci v0.13.1 + github.com/go-critic/go-critic v0.11.1 + github.com/google/go-github/v56 v56.0.0 github.com/gotesttools/gotestfmt/v2 v2.5.0 - github.com/mgechev/revive v1.3.4 - github.com/onsi/ginkgo/v2 v2.13.1 + github.com/mgechev/revive v1.3.7 + github.com/onsi/ginkgo/v2 v2.15.0 github.com/orijtech/structslop v0.0.8 - github.com/redhat-appstudio-qe/junit2html v0.0.0-20231113100636-5b753bd2ee31 - github.com/securego/gosec/v2 v2.18.2 + github.com/redhat-appstudio-qe/junit2html v0.0.0-20231122104025-4c86e177eec8 + github.com/securego/gosec/v2 v2.19.0 + github.com/slack-go/slack v0.12.5 github.com/spf13/cobra v1.8.0 - github.com/spf13/viper v1.17.0 + github.com/spf13/viper v1.18.2 github.com/sqs/goreturns v0.0.0-20231030191505-16fc3d8edd91 golang.org/x/exp v0.0.0-20230905200255-921286631fa9 golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 - golang.org/x/tools v0.15.0 - google.golang.org/api v0.150.0 - honnef.co/go/tools v0.4.6 - k8s.io/klog/v2 v2.110.1 + golang.org/x/tools v0.19.0 + google.golang.org/api v0.164.0 + honnef.co/go/tools v0.4.7 + k8s.io/klog/v2 v2.120.1 k8s.io/test-infra v0.0.0-20231026093210-34e553baa873 - mvdan.cc/gofumpt v0.5.0 + mvdan.cc/gofumpt v0.6.0 sigs.k8s.io/yaml v1.4.0 ) require ( - cloud.google.com/go v0.110.8 // indirect - cloud.google.com/go/compute v1.23.1 // indirect + cloud.google.com/go v0.112.0 // indirect + cloud.google.com/go/compute v1.23.3 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect - cloud.google.com/go/iam v1.1.3 // indirect + cloud.google.com/go/iam v1.1.6 // indirect contrib.go.opencensus.io/exporter/ocagent v0.7.1-0.20200907061046-05415f1de66d // indirect contrib.go.opencensus.io/exporter/prometheus v0.4.0 // indirect github.com/BurntSushi/toml v1.3.2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blendle/zapdriver v1.3.1 // indirect - github.com/ccojocar/zxcvbn-go v1.0.1 // indirect + github.com/ccojocar/zxcvbn-go v1.0.2 // indirect github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chavacava/garif v0.1.0 // indirect github.com/cjwagner/httpcache v0.0.0-20230907212505-d4841bbad466 // indirect - github.com/cristalhq/acmd v0.11.1 // indirect + github.com/cristalhq/acmd v0.11.2 // indirect github.com/dave/dst v0.27.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 // indirect github.com/emicklei/go-restful/v3 v3.9.0 // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect - github.com/fatih/color v1.15.0 // indirect + github.com/fatih/color v1.16.0 // indirect github.com/fatih/structtag v1.2.0 // indirect - github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-kit/log v0.2.0 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect - github.com/go-logr/logr v1.3.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.1 // indirect github.com/go-openapi/swag v0.22.3 // indirect github.com/go-toolsmith/astcast v1.1.0 // indirect github.com/go-toolsmith/astcopy v1.1.0 // indirect - github.com/go-toolsmith/astequal v1.1.0 // indirect + github.com/go-toolsmith/astequal v1.2.0 // indirect github.com/go-toolsmith/astfmt v1.1.0 // indirect github.com/go-toolsmith/astp v1.1.0 // indirect github.com/go-toolsmith/pkgload v1.2.2 // indirect @@ -73,12 +77,14 @@ require ( github.com/google/gnostic v0.6.9 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-containerregistry v0.15.2 // indirect + github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.1-0.20210504230335-f78f29fc09ea // indirect github.com/google/s2a-go v0.1.7 // indirect - github.com/google/uuid v1.4.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/gax-go/v2 v2.12.0 // indirect github.com/gookit/color v1.5.4 // indirect + github.com/gorilla/websocket v1.4.2 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -90,7 +96,7 @@ require ( github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517 // indirect @@ -112,40 +118,45 @@ require ( github.com/quasilyte/gogrep v0.5.0 // indirect github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect - github.com/sagikazarmark/locafero v0.3.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/shurcooL/githubv4 v0.0.0-20210725200734-83ba7b4c9228 // indirect github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.10.0 // indirect - github.com/spf13/cast v1.5.1 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tektoncd/pipeline v0.45.0 // indirect github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect + go.opentelemetry.io/otel v1.23.0 // indirect + go.opentelemetry.io/otel/metric v1.23.0 // indirect + go.opentelemetry.io/otel/trace v1.23.0 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.9.0 // indirect go.uber.org/zap v1.24.0 // indirect - golang.org/x/crypto v0.15.0 // indirect + go4.org v0.0.0-20201209231011-d4a079459e60 // indirect + golang.org/x/crypto v0.21.0 // indirect golang.org/x/exp/typeparams v0.0.0-20230307190834-24139beb5833 // indirect - golang.org/x/mod v0.14.0 // indirect - golang.org/x/net v0.18.0 // indirect - golang.org/x/oauth2 v0.13.0 // indirect - golang.org/x/sync v0.5.0 // indirect - golang.org/x/sys v0.14.0 // indirect - golang.org/x/term v0.14.0 // indirect + golang.org/x/mod v0.16.0 // indirect + golang.org/x/net v0.22.0 // indirect + golang.org/x/oauth2 v0.17.0 // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/term v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect - golang.org/x/time v0.3.0 // indirect - golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect + golang.org/x/time v0.5.0 // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect - google.golang.org/grpc v1.59.0 // indirect - google.golang.org/protobuf v1.31.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/genproto v0.0.0-20240125205218-1f4bbc51befe // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240205150955-31a09d347014 // indirect + google.golang.org/grpc v1.61.0 // indirect + google.golang.org/protobuf v1.32.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 2e99cce..e2b7234 100644 --- a/go.sum +++ b/go.sum @@ -41,8 +41,8 @@ cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVqux cloud.google.com/go v0.110.4/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI= cloud.google.com/go v0.110.6/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI= cloud.google.com/go v0.110.7/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI= -cloud.google.com/go v0.110.8 h1:tyNdfIxjzaWctIiLYOTalaLKZ17SI44SKFW26QbOhME= -cloud.google.com/go v0.110.8/go.mod h1:Iz8AkXJf1qmxC3Oxoep8R1T36w8B92yU29PcBhHO5fk= +cloud.google.com/go v0.112.0 h1:tpFCD7hpHFlQ8yPwT3x+QeXqc2T6+n6T+hmABHfDUSM= +cloud.google.com/go v0.112.0/go.mod h1:3jEEVwZ/MHU4djK5t5RHuKOA/GbLddgTdVubX1qnPD4= cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= @@ -213,8 +213,8 @@ cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IK cloud.google.com/go/compute v1.19.3/go.mod h1:qxvISKp/gYnXkSAD1ppcSOveRAmzxicEv/JlizULFrI= cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= -cloud.google.com/go/compute v1.23.1 h1:V97tBoDaZHb6leicZ1G6DLK2BAaZLJ/7+9BB/En3hR0= -cloud.google.com/go/compute v1.23.1/go.mod h1:CqB3xpmPKKt3OJpW2ndFIXnA9A4xAy/F3Xp1ixncW78= +cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= +cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU= cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= @@ -400,8 +400,8 @@ cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCta cloud.google.com/go/iam v1.0.1/go.mod h1:yR3tmSL8BcZB4bxByRv2jkSIahVmCtfKZwLYGBalRE8= cloud.google.com/go/iam v1.1.0/go.mod h1:nxdHjaKfCr7fNYx/HJMM8LgiMugmveWlkatear5gVyk= cloud.google.com/go/iam v1.1.1/go.mod h1:A5avdyVL2tCppe4unb0951eI9jreack+RJ0/d+KUZOU= -cloud.google.com/go/iam v1.1.3 h1:18tKG7DzydKWUnLjonWcJO6wjSCAtzh4GcRKlH/Hrzc= -cloud.google.com/go/iam v1.1.3/go.mod h1:3khUlaBXfPKKe7huYgEpDn6FtgRyMEqbkvBxrQyY5SE= +cloud.google.com/go/iam v1.1.6 h1:bEa06k05IO4f4uJonbB5iAgKTPpABy1ayxaIZV/GHVc= +cloud.google.com/go/iam v1.1.6/go.mod h1:O0zxdPeGBoFdWW3HWmBxJsk0pfvNM/p/qa82rWOGTwI= cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc= cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A= cloud.google.com/go/iap v1.6.0/go.mod h1:NSuvI9C/j7UdjGjIde7t7HBz+QTwBcapPE07+sSRcLk= @@ -677,8 +677,8 @@ cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5og cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4= cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E= cloud.google.com/go/storage v1.31.0/go.mod h1:81ams1PrhW16L4kF7qg+4mTq7SRs5HsbDTM0bWvrwJ0= -cloud.google.com/go/storage v1.35.1 h1:B59ahL//eDfx2IIKFBeT5Atm9wnNmj3+8xG/W4WB//w= -cloud.google.com/go/storage v1.35.1/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8= +cloud.google.com/go/storage v1.38.0 h1:Az68ZRGlnNTpIBbLjSMIV2BDcwwXYlRlQzis0llkpJg= +cloud.google.com/go/storage v1.38.0/go.mod h1:tlUADB0mAb9BgYls9lq+8MGkfzOXuLrnHXlpHmvFJoY= cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w= cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4= @@ -773,8 +773,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/GoogleCloudPlatform/testgrid v0.0.166 h1:622GXnl529+eRL4DyPZSiP9zDYgwrYRVHX56wkbKDDg= -github.com/GoogleCloudPlatform/testgrid v0.0.166/go.mod h1:h0oLH3TaQ2UJYPwdFIDhrAP4hzY+GIj/BE8+yQkaIa4= +github.com/GoogleCloudPlatform/testgrid v0.0.170 h1:RI4UFLOijuSW+pFST1ZgGtoqz8RilBtgSVK2mD2Rxxo= +github.com/GoogleCloudPlatform/testgrid v0.0.170/go.mod h1:lOKP2QzzzIDB4D0nJs1BcNMzJErjrlTNqG3vsCddx8c= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -805,8 +805,8 @@ github.com/blendle/zapdriver v1.3.1/go.mod h1:mdXfREi6u5MArG4j9fewC+FGnXaBR+T4Ox github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/ccojocar/zxcvbn-go v1.0.1 h1:+sxrANSCj6CdadkcMnvde/GWU1vZiiXRbqYSCalV4/4= -github.com/ccojocar/zxcvbn-go v1.0.1/go.mod h1:g1qkXtUSvHP8lhHp5GrSmTz6uWALGRMQdw6Qnz/hi60= +github.com/ccojocar/zxcvbn-go v1.0.2 h1:na/czXU8RrhXO4EZme6eQJLR4PzcGsahsBOAwU6I3Vg= +github.com/ccojocar/zxcvbn-go v1.0.2/go.mod h1:g1qkXtUSvHP8lhHp5GrSmTz6uWALGRMQdw6Qnz/hi60= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= @@ -839,12 +839,13 @@ github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230310173818-32f1caf87195/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101 h1:7To3pQ+pZo0i3dsWEbinPNFs5gPSBOsJtx3wTT94VBY= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cristalhq/acmd v0.11.1 h1:DJ4fh2Pv0nPKmqT646IU/0Vh5FNdGblxvF+3/W3NAUI= -github.com/cristalhq/acmd v0.11.1/go.mod h1:LG5oa43pE/BbxtfMoImHCQN++0Su7dzipdgBjMCBVDQ= -github.com/daixiang0/gci v0.11.2 h1:Oji+oPsp3bQ6bNNgX30NBAVT18P4uBH4sRZnlOlTj7Y= -github.com/daixiang0/gci v0.11.2/go.mod h1:xtHP9N7AHdNvtRNfcx9gwTDfw7FRJx4bZUsiEfiNNAI= +github.com/cristalhq/acmd v0.11.2 h1:ITIWtBRiYbmzk+i8xQgH2RzfCVMII+dOd0CtGWVIhaU= +github.com/cristalhq/acmd v0.11.2/go.mod h1:LG5oa43pE/BbxtfMoImHCQN++0Su7dzipdgBjMCBVDQ= +github.com/daixiang0/gci v0.13.1 h1:jkQWCPu7JZhNoO+OMHFxs1KGonWPhXw9txySE8qwAb0= +github.com/daixiang0/gci v0.13.1/go.mod h1:JyUVY/ZKzBjrzLOm2UQDZohEZ2HlfX72jONBV0REVb4= github.com/dave/dst v0.27.2 h1:4Y5VFTkhGLC1oddtNwuxxe36pnyLxMFXT51FOzH8Ekc= github.com/dave/dst v0.27.2/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc= github.com/dave/jennifer v1.5.0 h1:HmgPN93bVDpkQyYbqhCHj5QlgvUkvEOzMyEvKLgCRrg= @@ -877,28 +878,31 @@ github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= +github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= -github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= -github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= -github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= -github.com/go-critic/go-critic v0.9.0 h1:Pmys9qvU3pSML/3GEQ2Xd9RZ/ip+aXHKILuxczKGV/U= -github.com/go-critic/go-critic v0.9.0/go.mod h1:5P8tdXL7m/6qnyG6oRAlYLORvoXH0WDypYgAEmagT40= +github.com/go-critic/go-critic v0.11.1 h1:/zBseUSUMytnRqxjlsYNbDDxpu3R2yH8oLXo/FOE8b8= +github.com/go-critic/go-critic v0.11.1/go.mod h1:aZVQR7+gazH6aDEQx4356SD7d8ez8MipYjXbEl5JAKA= github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= @@ -922,10 +926,13 @@ github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KE github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= -github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonreference v0.20.1 h1:FBLnyygC4/IZZr893oiomc9XaghoveYTrLC1F86HID8= @@ -937,13 +944,16 @@ github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhO github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M= github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8= github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU= github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s= github.com/go-toolsmith/astcopy v1.1.0/go.mod h1:hXM6gan18VA1T/daUEHCFcYiW8Ai1tIwIzHY6srfEAw= github.com/go-toolsmith/astequal v1.0.3/go.mod h1:9Ai4UglvtR+4up+bAD4+hCj7iTo4m/OXVTSLnCyTAx4= -github.com/go-toolsmith/astequal v1.1.0 h1:kHKm1AWqClYn15R0K1KKE4RG614D46n+nqUQ06E1dTw= github.com/go-toolsmith/astequal v1.1.0/go.mod h1:sedf7VIdCL22LD8qIvv7Nn9MuWJruQA/ysswh64lffQ= +github.com/go-toolsmith/astequal v1.2.0 h1:3Fs3CYZ1k9Vo4FzFhwwewC3CHISHDnVUPC4x0bI2+Cw= +github.com/go-toolsmith/astequal v1.2.0/go.mod h1:c8NZ3+kSFtFY/8lPso4v8LuJjdJiUFVnSuU3s0qrrDY= github.com/go-toolsmith/astfmt v1.1.0 h1:iJVPDPp6/7AaeLJEruMsBUlOYCmvg0MoCfJprsOmcco= github.com/go-toolsmith/astfmt v1.1.0/go.mod h1:OrcLlRwu0CuiIBp/8b5PYF9ktGVZUjlNMV634mhwuQ4= github.com/go-toolsmith/astp v1.1.0 h1:dXPuCl6u2llURjdPLLDxJeZInAeZ0/eZwFJmqZMnpQA= @@ -1028,7 +1038,11 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-containerregistry v0.15.2 h1:MMkSh+tjSdnmJZO7ljvEqV1DjfekB6VUEAZgy3a+TQE= github.com/google/go-containerregistry v0.15.2/go.mod h1:wWK+LnOv4jXMM23IT/F1wdYftGWGr47Is8CG+pmHK1Q= +github.com/google/go-github/v56 v56.0.0 h1:TysL7dMa/r7wsQi44BjqlwaHvwlFlqkK8CtBWCX3gb4= +github.com/google/go-github/v56 v56.0.0/go.mod h1:D8cdcX98YWJvi7TLo7zM4/h8ZTx6u6fwGEkCdisopo0= github.com/google/go-pkcs11 v0.2.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -1066,8 +1080,8 @@ github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= -github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= @@ -1097,6 +1111,7 @@ github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+ github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gotesttools/gotestfmt/v2 v2.5.0 h1:fSU3MnR+E+fvuXdw1l8xbufKhDxY3Tfjsjx/I1WerB4= github.com/gotesttools/gotestfmt/v2 v2.5.0/go.mod h1:oQJg2KZ2aGoqEbMC2PDaAeBYm0tOkocgixK9FzsCdp4= @@ -1170,8 +1185,9 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= @@ -1181,8 +1197,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zk github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517 h1:zpIH83+oKzcpryru8ceC6BxnoG8TBrhgAvRg8obzup0= github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517/go.mod h1:KQ7+USdGKfpPjXk4Ga+5XxQM4Lm4e3gAogrreFAYpOg= -github.com/mgechev/revive v1.3.4 h1:k/tO3XTaWY4DEHal9tWBkkUMJYO/dLDVyMmAQxmIMDc= -github.com/mgechev/revive v1.3.4/go.mod h1:W+pZCMu9qj8Uhfs1iJMQsEFLRozUfvwFwqVvRbSNLVw= +github.com/mgechev/revive v1.3.7 h1:502QY0vQGe9KtYJ9FpxMz9rL+Fc/P13CI5POL4uHCcE= +github.com/mgechev/revive v1.3.7/go.mod h1:RJ16jUbF0OWC3co/+XTxmFNgEpUPwnnA0BRllX2aDNA= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= @@ -1220,8 +1236,8 @@ github.com/onsi/ginkgo/v2 v2.7.0/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1L github.com/onsi/ginkgo/v2 v2.8.1/go.mod h1:N1/NbDngAFcSLdyZ+/aYTYGSlq9qMCS/cNKGJjy+csc= github.com/onsi/ginkgo/v2 v2.9.0/go.mod h1:4xkjoL/tZv4SMWeww56BU5kAt19mVB47gTWxmrTcxyk= github.com/onsi/ginkgo/v2 v2.9.1/go.mod h1:FEcmzVcCHl+4o9bQZVab+4dC9+j+91t2FHSzmGAPfuo= -github.com/onsi/ginkgo/v2 v2.13.1 h1:LNGfMbR2OVGBfXjvRZIZ2YCTQdGKtPLvuI1rMCCj3OU= -github.com/onsi/ginkgo/v2 v2.13.1/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM= +github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= +github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= @@ -1235,7 +1251,7 @@ github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdM github.com/onsi/gomega v1.27.1/go.mod h1:aHX5xOykVYzWOV4WqQy0sy8BQptgukenXpCXfadcIAw= github.com/onsi/gomega v1.27.3/go.mod h1:5vG284IBtfDAmDyrK+eGyZmUgUlmi+Wngqo557cZ6Gw= github.com/onsi/gomega v1.27.4/go.mod h1:riYq/GJKh8hhoM01HN6Vmuy93AarCXCBGpvFDK3q3fQ= -github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= +github.com/onsi/gomega v1.31.1 h1:KYppCUK+bUgAZwHOu7EXVBKyQA6ILvOESHkn/tgoqvo= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/orijtech/structslop v0.0.8 h1:xkiOOdE3Obe82vdQvrb4bsjCGAUqQEo6Ln3r4yFMzGw= @@ -1295,26 +1311,28 @@ github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs= github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ= -github.com/redhat-appstudio-qe/junit2html v0.0.0-20231113100636-5b753bd2ee31 h1:Na4j4bGreDGEiCdZBCcDK8Ft5YBk3UoO+vsMduopeg8= -github.com/redhat-appstudio-qe/junit2html v0.0.0-20231113100636-5b753bd2ee31/go.mod h1:6ze5rUwXFKwSGpmFNyfH2+l5L5LiwfKs1jsGqgiol4o= +github.com/redhat-appstudio-qe/junit2html v0.0.0-20231122104025-4c86e177eec8 h1:vJw6swGDd7hx6xOYmMSTcxAutG6jXdD9AKBSNRKFgEk= +github.com/redhat-appstudio-qe/junit2html v0.0.0-20231122104025-4c86e177eec8/go.mod h1:VNpXEDt4XUJHhn8yoZ3sFRFqyCDfD59GDc7Ym8Fnmqo= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= -github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9cJvm4SvQ= -github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= -github.com/securego/gosec/v2 v2.18.2 h1:DkDt3wCiOtAHf1XkiXZBhQ6m6mK/b9T/wD257R3/c+I= -github.com/securego/gosec/v2 v2.18.2/go.mod h1:xUuqSF6i0So56Y2wwohWAmB07EdBkUN6crbLlHwbyJs= +github.com/securego/gosec/v2 v2.19.0 h1:gl5xMkOI0/E6Hxx0XCY2XujA3V7SNSefA8sC+3f1gnk= +github.com/securego/gosec/v2 v2.19.0/go.mod h1:hOkDcHz9J/XIgIlPDXalxjeVYsHxoWUc5zJSHxcB8YM= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sethvargo/go-retry v0.2.4/go.mod h1:1afjQuvh7s4gflMObvjLPaWgluLLyhA1wmVZ6KLpICw= github.com/shurcooL/githubv4 v0.0.0-20210725200734-83ba7b4c9228 h1:N5B+JgvM/DVYIxreItPJMM3yWrNO/GB2q4nESrtBisM= github.com/shurcooL/githubv4 v0.0.0-20210725200734-83ba7b4c9228/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo= github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f h1:tygelZueB1EtXkPI6mQ4o9DQ0+FKW41hTbunoXZCTqk= @@ -1324,6 +1342,8 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/slack-go/slack v0.12.5 h1:ddZ6uz6XVaB+3MTDhoW04gG+Vc/M/X1ctC+wssy2cqs= +github.com/slack-go/slack v0.12.5/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -1331,16 +1351,16 @@ github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTd github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= -github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= -github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= -github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= -github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI= -github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= github.com/sqs/goreturns v0.0.0-20231030191505-16fc3d8edd91 h1:Be13sz3icROd/Drj6KAh/WMXUqTNIFDXAftgD8C7/to= github.com/sqs/goreturns v0.0.0-20231030191505-16fc3d8edd91/go.mod h1:4jmSAe8WpDhqvAX79mpw0yRwbkjk+JxYE5ocIc8kfzk= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= @@ -1388,6 +1408,17 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 h1:UNQQKPfTDe1J81ViolILjTKPr9WetKW6uei2hFgJmFs= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0/go.mod h1:r9vWsPS/3AQItv3OSlEJ/E4mbrhUbbw18meOjArPtKQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 h1:sv9kVfal0MK0wBMCOGr+HeJm9v803BkJxGrk2au7j08= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw= +go.opentelemetry.io/otel v1.23.0 h1:Df0pqjqExIywbMCMTxkAwzjLZtRf+bBKLbUcpxO2C9E= +go.opentelemetry.io/otel v1.23.0/go.mod h1:YCycw9ZeKhcJFrb34iVSkyT0iczq/zYDtZYFufObyB0= +go.opentelemetry.io/otel/metric v1.23.0 h1:pazkx7ss4LFVVYSxYew7L5I6qvLXHA0Ap2pwV+9Cnpo= +go.opentelemetry.io/otel/metric v1.23.0/go.mod h1:MqUW2X2a6Q8RN96E2/nqNoT+z9BSms20Jb7Bbp+HiTo= +go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= +go.opentelemetry.io/otel/trace v1.23.0 h1:37Ik5Ib7xfYVb4V1UtnT97T1jI+AoIYkJyPkuL4iJgI= +go.opentelemetry.io/otel/trace v1.23.0/go.mod h1:GSGTbIClEsuZrGIzoEHqsVfxgn5UkggkflQwDScNUsk= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= @@ -1401,6 +1432,8 @@ go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTV go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +go4.org v0.0.0-20201209231011-d4a079459e60 h1:iqAGo78tVOJXELHQFRjR6TMwItrvXH4hrGJ32I/NFF8= +go4.org v0.0.0-20201209231011-d4a079459e60/go.mod h1:CIiUVy99QCPfoE13bO4EZaz5GZMZXMSBGhxRdsvzbkg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -1412,14 +1445,13 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= -golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= -golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1487,8 +1519,8 @@ golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= +golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1559,8 +1591,8 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= -golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= -golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1592,8 +1624,8 @@ golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= -golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= -golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= +golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ= +golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1612,8 +1644,8 @@ golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1702,7 +1734,6 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1713,8 +1744,8 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1727,8 +1758,8 @@ golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= -golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8= -golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1755,8 +1786,9 @@ golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1827,8 +1859,8 @@ golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= -golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8= -golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= +golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= +golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1836,8 +1868,8 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY= gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= @@ -1913,16 +1945,17 @@ google.golang.org/api v0.125.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvy google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw= google.golang.org/api v0.128.0/go.mod h1:Y611qgqaE92On/7g65MQgxYul3c0rEB894kniWLY750= google.golang.org/api v0.134.0/go.mod h1:sjRL3UnjTx5UqNQS9EWr9N8p7xbHpy1k0XGRLCf3Spk= -google.golang.org/api v0.150.0 h1:Z9k22qD289SZ8gCJrk4DrWXkNjtfvKAUo/l1ma8eBYE= -google.golang.org/api v0.150.0/go.mod h1:ccy+MJ6nrYFgE3WgRx/AMXOxOmU8Q4hSa+jjibzhxcg= +google.golang.org/api v0.164.0 h1:of5G3oE2WRMVb2yoWKME4ZP8y8zpUKC6bMhxDr8ifyk= +google.golang.org/api v0.164.0/go.mod h1:2OatzO7ZDQsoS7IFf3rvsE17/TldiU3F/zxFHeqUB5o= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -2066,8 +2099,8 @@ google.golang.org/genproto v0.0.0-20230629202037-9506855d4529/go.mod h1:xZnkP7mR google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:O9kGHb51iE/nOGvQaDUuadVYqovW56s5emA88lQnj6Y= google.golang.org/genproto v0.0.0-20230726155614-23370e0ffb3e/go.mod h1:0ggbjUrZYpy1q+ANUS30SEoGZ53cdfwtbuG7Ptgy108= google.golang.org/genproto v0.0.0-20230731193218-e0aa005b6bdf/go.mod h1:oH/ZOT02u4kWEp7oYBGYFFkCdKS/uYR9Z7+0/xuuFp8= -google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b h1:+YaDE2r2OG8t/z5qmsh7Y+XXwCbvadxxZ0YY6mTdrVA= -google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:CgAqfJo+Xmu0GwA0411Ht3OU3OntXwsGmrmjI8ioGXI= +google.golang.org/genproto v0.0.0-20240125205218-1f4bbc51befe h1:USL2DhxfgRchafRvt/wYyyQNzwgL7ZiURcozOE/Pkvo= +google.golang.org/genproto v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro= google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a/go.mod h1:ts19tUU+Z0ZShN1y3aPyq2+O3d5FUNNgT6FtOzmrNn8= google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= google.golang.org/genproto/googleapis/api v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= @@ -2076,8 +2109,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20230629202037-9506855d4529/go. google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:mPBs5jNgx2GuQGvFwUvVKqtn6HsUw9nP64BedgvqEsQ= google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ= google.golang.org/genproto/googleapis/api v0.0.0-20230731193218-e0aa005b6bdf/go.mod h1:5DZzOUPCLYL3mNkQ0ms0F3EuUNZ7py1Bqeq6sxzI7/Q= -google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b h1:CIC2YMXmIhYw6evmhPxBKJ4fmLbOFtXQN/GV3XOZR8k= -google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:IBQ646DjkDkvUIsVq/cc03FUFQ9wbZu7yE396YcL870= +google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014 h1:x9PwdEgd11LgK+orcck69WVRo7DezSO4VUMPI4xpc8A= +google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014/go.mod h1:rbHMSEDyoYX62nRVLOCc4Qt1HbsdytAYoVwgjiOhF3I= google.golang.org/genproto/googleapis/bytestream v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:ylj+BE99M198VPbBh6A8d9n3w8fChvyLK3wwBOjXBFA= google.golang.org/genproto/googleapis/bytestream v0.0.0-20230720185612-659f7aaaa771/go.mod h1:3QoBVwTHkXbY1oRGzlhwhOykfcATQN43LJ6iT8Wy8kE= google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234015-3fc162c6f38a/go.mod h1:xURIpW9ES5+/GZhnV6beoEtxQrnkRGIfP5VQG2tCBLc= @@ -2089,8 +2122,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20230720185612-659f7aaaa771/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= google.golang.org/genproto/googleapis/rpc v0.0.0-20230731190214-cbb8c96f2d6d/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= google.golang.org/genproto/googleapis/rpc v0.0.0-20230731193218-e0aa005b6bdf/go.mod h1:zBEcrKX2ZOcEkHWxBPAIvYUWOKKMIhYcmNiUIu2ji3I= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 h1:AB/lmRny7e2pLhFEYIbl5qkDAUt2h0ZRO4wGPhZf+ik= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405/go.mod h1:67X1fPuzjcrkymZzZV1vvkFeTn2Rvc6lYF9MYFGCcwE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240205150955-31a09d347014 h1:FSL3lRCkhaPFxqi0s9o+V4UI2WTzAVOvkgbd4kVV4Wg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240205150955-31a09d347014/go.mod h1:SaPjaZGWb0lPqs6Ittu0spdfrOArqji4ZdeP5IC/9N4= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -2136,8 +2169,8 @@ google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGO google.golang.org/grpc v1.56.1/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= -google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= -google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0= +google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -2156,8 +2189,9 @@ google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -2192,8 +2226,8 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= -honnef.co/go/tools v0.4.6 h1:oFEHCKeID7to/3autwsWfnuv69j3NsfcXbvJKuIcep8= -honnef.co/go/tools v0.4.6/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0= +honnef.co/go/tools v0.4.7 h1:9MDAWxMoSnB6QoSqiVr7P5mtkT9pOc1kSxchzPCnqJs= +honnef.co/go/tools v0.4.7/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0= k8s.io/api v0.27.4 h1:0pCo/AN9hONazBKlNUdhQymmnfLRbSZjd5H5H3f0bSs= k8s.io/api v0.27.4/go.mod h1:O3smaaX15NfxjzILfiln1D8Z3+gEYpjEpiNA/1EVK1Y= k8s.io/apimachinery v0.27.4 h1:CdxflD4AF61yewuid0fLl6bM4a3q04jWel0IlP+aYjs= @@ -2206,8 +2240,8 @@ k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= -k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= +k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= +k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f h1:2kWPakN3i/k81b0gvD5C5FJ2kxm1WrQFanWchyKuqGg= k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f/go.mod h1:byini6yhqGC14c3ebc/QwanvYwhuMWF6yz2F8uwW8eg= k8s.io/test-infra v0.0.0-20231026093210-34e553baa873 h1:0LHs29IMXkaLlNiHncIlBwTGgQBmGAX4M3c+ypWpe1I= @@ -2269,8 +2303,8 @@ modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= -mvdan.cc/gofumpt v0.5.0 h1:0EQ+Z56k8tXjj/6TQD25BFNKQXpCvT0rnansIc7Ug5E= -mvdan.cc/gofumpt v0.5.0/go.mod h1:HBeVDtMKRZpXyxFciAirzdKklDlGu8aAy1wEbH5Y9js= +mvdan.cc/gofumpt v0.6.0 h1:G3QvahNDmpD+Aek/bNOLrFR2XC6ZAdo62dZu65gmwGo= +mvdan.cc/gofumpt v0.6.0/go.mod h1:4L0wf+kgIPZtcCWXynNS2e6bhmj73umwnuXSZarixzA= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= diff --git a/pkg/customjunit/customjunit.go b/pkg/customjunit/customjunit.go new file mode 100644 index 0000000..9f09a30 --- /dev/null +++ b/pkg/customjunit/customjunit.go @@ -0,0 +1,77 @@ +package customjunit + +import ( + "encoding/xml" + + "github.com/onsi/ginkgo/v2/reporters" +) + +/* +These custom structures are copied from https://github.com/onsi/ginkgo/blob/v2.13.1/reporters/junit_report.go +These custom structures are modified to use Skipped field instead of Disabled and the Status field is removed +See RHTAP-1902 for more details. +*/ + +// TestSuites represent JUnit test suites from Ginkgo +type TestSuites struct { + XMLName xml.Name `xml:"testsuites"` + // Tests maps onto the total number of specs in all test suites (this includes any suite nodes such as BeforeSuite) + Tests int `xml:"tests,attr"` + // Skipped maps onto specs that are pending and/or disabled + Skipped int `xml:"skipped,attr"` + // Errors maps onto specs that panicked or were interrupted + Errors int `xml:"errors,attr"` + // Failures maps onto specs that failed + Failures int `xml:"failures,attr"` + // Time is the time in seconds to execute all test suites + Time float64 `xml:"time,attr"` + + // The set of all test suites + TestSuites []TestSuite `xml:"testsuite"` +} + +// TestSuite represents a JUnit test suite from Ginkgo +type TestSuite struct { + // Name maps onto the description of the test suite - maps onto Report.SuiteDescription + Name string `xml:"name,attr"` + // Package maps onto the absolute path to the test suite - maps onto Report.SuitePath + Package string `xml:"package,attr"` + // Tests maps onto the total number of specs in the test suite (this includes any suite nodes such as BeforeSuite) + Tests int `xml:"tests,attr"` + // Skipped maps onto specs that are skipped/pending + Skipped int `xml:"skipped,attr"` + // Errors maps onto specs that panicked or were interrupted + Errors int `xml:"errors,attr"` + // Failures maps onto specs that failed + Failures int `xml:"failures,attr"` + // Time is the time in seconds to execute all the test suite - maps onto Report.RunTime + Time float64 `xml:"time,attr"` + // Timestamp is the ISO 8601 formatted start-time of the suite - maps onto Report.StartTime + Timestamp string `xml:"timestamp,attr"` + + // Properties captures the information stored in the rest of the Report type (including SuiteConfig) as key-value pairs + Properties reporters.JUnitProperties `xml:"properties"` + + // TestCases capture the individual specs + TestCases []TestCase `xml:"testcase"` +} + +// TestCase represents a JUnit test case from Ginkgo +type TestCase struct { + // Name maps onto the full text of the spec - equivalent to "[SpecReport.LeafNodeType] SpecReport.FullText()" + Name string `xml:"name,attr"` + // Classname maps onto the name of the test suite - equivalent to Report.SuiteDescription + Classname string `xml:"classname,attr"` + // Time is the time in seconds to execute the spec - maps onto SpecReport.RunTime + Time float64 `xml:"time,attr"` + // Skipped is populated with a message if the test was skipped or pending + Skipped *reporters.JUnitSkipped `xml:"skipped,omitempty"` + // Error is populated if the test panicked or was interrupted + Error *reporters.JUnitError `xml:"error,omitempty"` + // Failure is populated if the test failed + Failure *reporters.JUnitFailure `xml:"failure,omitempty"` + // SystemOut maps onto any captured stdout/stderr output - maps onto SpecReport.CapturedStdOutErr + SystemOut string `xml:"system-out,omitempty"` + // SystemOut maps onto any captured GinkgoWriter output - maps onto SpecReport.CapturedGinkgoWriterOutput + SystemErr string `xml:"system-err,omitempty"` +} diff --git a/pkg/prow/prow.go b/pkg/prow/prow.go index 3b863dd..e6f77df 100644 --- a/pkg/prow/prow.go +++ b/pkg/prow/prow.go @@ -2,6 +2,8 @@ package prow import ( "context" + "encoding/json" + "errors" "fmt" "io" "net/http" @@ -33,92 +35,190 @@ func NewArtifactScanner(cfg ScannerConfig) (*ArtifactScanner, error) { }, nil } -// Run processes the artifacts associated with the Prow job and store required files -// with their associated openshift-ci step names and their content in ArtifactStepMap +// Run processes the artifacts associated with the Prow job and stores required files +// with their associated openshift-ci step names and their content in ArtifactStepMap. func (as *ArtifactScanner) Run() error { - pjYAML, err := getProwJobYAML(as.config.ProwJobID) + // Determine job target and Prow job URL. + jobTarget, pjURL, err := as.determineJobDetails() if err != nil { - return fmt.Errorf("failed to get prow job yaml: %+v", err) + return fmt.Errorf("failed to determine job details: %+v", err) } - jobTarget, err := determineJobTarget(pjYAML) + artifactDirectoryPrefix, err := getArtifactsDirectoryPrefix(as, pjURL, jobTarget) if err != nil { - return fmt.Errorf("failed to determine job target: %+v", err) + return fmt.Errorf("failed to get artifact directory prefix: %+v", err) } - pjURL := pjYAML.Status.URL - klog.Infof("got the prow job URL: %s", pjURL) - // => e.g. [ "https://prow.ci.openshift.org/view/gs", "pr-logs/pull/redhat-appstudio_infra-deployments/123/pull-ci-redhat-appstudio-infra-deployments-main-appstudio-e2e-tests/123" ] - sp := strings.Split(pjURL, "/"+bucketName+"/") - if len(sp) != 2 { - return fmt.Errorf("failed to determine object prefix - prow job url: '%s', bucket name: '%s'", pjURL, bucketName) - } - // => e.g. "pr-logs/pull/redhat-appstudio_infra-deployments/123/pull-ci-redhat-appstudio-infra-deployments-main-appstudio-e2e-tests/123/artifacts/appstudio-e2e-tests/" - objectPrefix := sp[1] + "/artifacts/" + jobTarget + "/" - as.ObjectPrefix = objectPrefix - + // Iterate over storage objects. ctx, cancel := context.WithTimeout(context.Background(), time.Minute*2) defer cancel() as.bucketHandle = as.Client.Bucket(bucketName) + it := as.bucketHandle.Objects(ctx, &storage.Query{Prefix: artifactDirectoryPrefix}) + + // Process storage objects. + if err := as.processStorageObjects(ctx, it, artifactDirectoryPrefix, pjURL); err != nil { + return fmt.Errorf("failed to process storage objects: %+v", err) + } + + return nil +} + +// Helper function to determine job details. +func (as *ArtifactScanner) determineJobDetails() (jobTarget, pjURL string, err error) { + switch { + case as.config.ProwJobID != "": + pjYAML, err := getProwJobYAML(as.config.ProwJobID) + if err != nil { + return "", "", fmt.Errorf("failed to get Prow job YAML: %+v", err) + } + jobTarget, err = determineJobTargetFromYAML(pjYAML) + if err != nil { + return "", "", fmt.Errorf("failed to determine job target from YAML: %+v", err) + } + pjURL = pjYAML.Status.URL + + case as.config.ProwJobURL != "": + pjURL = as.config.ProwJobURL + jobTarget, err = determineJobTargetFromProwJobURL(pjURL) + if err != nil { + return "", "", fmt.Errorf("failed to determine job target from Prow job URL: %+v", err) + } + + default: + return "", "", fmt.Errorf("ScannerConfig doesn't contain either ProwJobID or ProwJobURL") + } - it := as.bucketHandle.Objects(ctx, &storage.Query{Prefix: objectPrefix}) + return jobTarget, pjURL, nil +} + +// Helper function to process storage objects. +func (as *ArtifactScanner) processStorageObjects(ctx context.Context, it *storage.ObjectIterator, artifactDirectoryPrefix, pjURL string) error { + var objectAttrs *storage.ObjectAttrs + var err error + objectAttrs, err = it.Next() + if errors.Is(err, iterator.Done) { + // No files present within the target directory - get the root build-log.txt instead. + if err := as.handleEmptyDirectory(ctx, pjURL, artifactDirectoryPrefix); err != nil { + return err + } + return nil + } + + // Iterate over storage objects. + for { + if err != nil { + return fmt.Errorf("failed to iterate over storage objects: %+v", err) + } + fullArtifactName := objectAttrs.Name + if as.isRequiredFile(fullArtifactName) { + if err := as.processRequiredFile(fullArtifactName, artifactDirectoryPrefix); err != nil { + return err + } + } + + objectAttrs, err = it.Next() + if errors.Is(err, iterator.Done) { + break + } + } + + return nil +} + +// Helper function to handle an empty directory. +func (as *ArtifactScanner) handleEmptyDirectory(ctx context.Context, pjURL, artifactDirectoryPrefix string) error { + klog.Infof("For the job (%s), there are no files present within the directory with prefix: `%s`", pjURL, artifactDirectoryPrefix) + + // Set up default file name filter. + fileName := "build-log.txt" + as.config.FileNameFilter = []string{fileName} + + // Check for build log file. + sp := strings.Split(pjURL, "/"+bucketName+"/") + if len(sp) != 2 { + return fmt.Errorf("failed to determine artifact directory's prefix - Prow job URL: '%s', bucket name: '%s'", pjURL, bucketName) + } + buildLogPrefix := sp[1] + "/" + fileName + + // Iterate over build log files. + it := as.bucketHandle.Objects(ctx, &storage.Query{Prefix: buildLogPrefix}) for { attrs, err := it.Next() - if err == iterator.Done { + if errors.Is(err, iterator.Done) { break } if err != nil { return fmt.Errorf("failed to iterate over storage objects: %+v", err) } fullArtifactName := attrs.Name - if as.isRequiredFile(fullArtifactName) { - klog.Infof("found required file %s", fullArtifactName) - // => e.g. [ "", "redhat-appstudio-e2e/artifacts/e2e-report.xml" ] - sp := strings.Split(fullArtifactName, objectPrefix) - if len(sp) != 2 { - return fmt.Errorf("cannot determine filepath - object name: %s, object prefix: %s", fullArtifactName, objectPrefix) - } - parentStepFilePath := sp[1] - - // => e.g. [ "redhat-appstudio-e2e", "artifacts", "e2e-report.xml" ] - sp = strings.Split(parentStepFilePath, "/") - parentStepName := sp[0] - // skip artifacts produced by this step that is used in CI - if parentStepName == reportStepName { - continue - } - fileName := sp[len(sp)-1] - - rc, err := as.bucketHandle.Object(fullArtifactName).NewReader(ctx) - if err != nil { - return fmt.Errorf("failed to create objecthandle for %s: %+v", fullArtifactName, err) - } - data, err := io.ReadAll(rc) - if err != nil { - return fmt.Errorf("cannot read from storage reader: %+v", err) - } - artifact := Artifact{Content: string(data), FullName: fullArtifactName} - - // No artifact step map not initialized yet - if as.ArtifactStepMap == nil { - newArtifactMap := ArtifactFilenameMap{ArtifactFilename(fileName): artifact} - as.ArtifactStepMap = map[ArtifactStepName]ArtifactFilenameMap{ArtifactStepName(parentStepName): newArtifactMap} - } else { - // Already have a record of an artifact being mapped to a step name - if afMap, ok := as.ArtifactStepMap[ArtifactStepName(parentStepName)]; ok { - afMap[ArtifactFilename(fileName)] = artifact - as.ArtifactStepMap[ArtifactStepName(parentStepName)] = afMap - } else { // Artifact map initialized, but the artifact filename does not belong to any collected step - as.ArtifactStepMap[ArtifactStepName(parentStepName)] = ArtifactFilenameMap{ArtifactFilename(fileName): artifact} - } - } + if err := as.initArtifactStepMap(ctx, fileName, fullArtifactName, "/"); err != nil { + return err } } + + return nil +} + +// Helper function to process a required file. +func (as *ArtifactScanner) processRequiredFile(fullArtifactName, artifactDirectoryPrefix string) error { + parentStepName, err := getParentStepName(fullArtifactName, artifactDirectoryPrefix) + if err != nil { + return err + } + + if slices.Contains(as.config.StepsToSkip, parentStepName) { + klog.Infof("Skipping step name %s", parentStepName) + return nil + } + + fileName, err := getFileName(fullArtifactName, artifactDirectoryPrefix) + if err != nil { + return err + } + + if err := as.initArtifactStepMap(context.Background(), fileName, fullArtifactName, parentStepName); err != nil { + return err + } + return nil } +// Helper function to initialise/update the ArtifactStepMap with content +// of a file with given 'fileName', within the given 'parentStepName' +func (as *ArtifactScanner) initArtifactStepMap(ctx context.Context, fileName, fullArtifactName, parentStepName string) error { + rc, err := as.bucketHandle.Object(fullArtifactName).NewReader(ctx) + if err != nil { + return fmt.Errorf("failed to create objecthandle for %s: %+v", fullArtifactName, err) + } + data, err := io.ReadAll(rc) + if err != nil { + return fmt.Errorf("cannot read from storage reader: %+v", err) + } + + artifact := Artifact{Content: string(data), FullName: fullArtifactName} + newArtifactMap := ArtifactFilenameMap{ArtifactFilename(fileName): artifact} + + // No artifact step map not initialized yet + if as.ArtifactStepMap == nil { + as.ArtifactStepMap = map[ArtifactStepName]ArtifactFilenameMap{ArtifactStepName(parentStepName): newArtifactMap} + return nil + } + + // Already have a record of an artifact being mapped to a step name + if afMap, ok := as.ArtifactStepMap[ArtifactStepName(parentStepName)]; ok { + afMap[ArtifactFilename(fileName)] = artifact + as.ArtifactStepMap[ArtifactStepName(parentStepName)] = afMap + } else { // Artifact map initialized, but the artifact filename does not belong to any collected step + as.ArtifactStepMap[ArtifactStepName(parentStepName)] = newArtifactMap + } + + return nil +} + +// Helper function to check if a file with given 'fullArtifactName', +// matches the file-name filter(s) defined within ScannerConfig struct func (as *ArtifactScanner) isRequiredFile(fullArtifactName string) bool { return slices.ContainsFunc(as.config.FileNameFilter, func(s string) bool { re := regexp.MustCompile(s) @@ -147,7 +247,7 @@ func getProwJobYAML(jobID string) (*v1.ProwJob, error) { return &pj, nil } -func determineJobTarget(pjYAML *v1.ProwJob) (jobTarget string, err error) { +func determineJobTargetFromYAML(pjYAML *v1.ProwJob) (jobTarget string, err error) { errPrefix := "failed to determine job target:" args := pjYAML.Spec.PodSpec.Containers[0].Args for _, arg := range args { @@ -162,3 +262,73 @@ func determineJobTarget(pjYAML *v1.ProwJob) (jobTarget string, err error) { } return "", fmt.Errorf("%s expected %+v to contain arg --target", errPrefix, args) } + +// ParseJobSpec parses and then returns the openshift job spec data +func ParseJobSpec(jobSpecData string) (*OpenshiftJobSpec, error) { + openshiftJobSpec := &OpenshiftJobSpec{} + + if err := json.Unmarshal([]byte(jobSpecData), openshiftJobSpec); err != nil { + return nil, fmt.Errorf("error occurred when parsing openshift job spec data: %v", err) + } + return openshiftJobSpec, nil +} + +func determineJobTargetFromProwJobURL(prowJobURL string) (jobTarget string, err error) { + switch { + case strings.Contains(prowJobURL, "pull-ci-redhat-appstudio-infra-deployments"): + // prow URL is from infra-deployments repo + jobTarget = "appstudio-e2e-tests" + case strings.Contains(prowJobURL, "pull-ci-redhat-appstudio-e2e-tests"): + // prow URL is from e2e-tests repo + jobTarget = "redhat-appstudio-e2e" + case strings.Contains(prowJobURL, "pull-ci-konflux-ci-integration-service"): + // prow URL is from integration-service repo + jobTarget = "integration-service-e2e" + default: + return "", fmt.Errorf("unable to determine the target from the ProwJobURL: %s", prowJobURL) + } + + return jobTarget, nil +} + +func getArtifactsDirectoryPrefix(artifactScanner *ArtifactScanner, prowJobURL, jobTarget string) (string, error) { + // => e.g. [ "https://prow.ci.openshift.org/view/gs", "pr-logs/pull/redhat-appstudio_infra-deployments/123/pull-ci-redhat-appstudio-infra-deployments-main-appstudio-e2e-tests/123" ] + sp := strings.Split(prowJobURL, "/"+bucketName+"/") + if len(sp) != 2 { + return "", fmt.Errorf("failed to determine artifact directory's prefix - prow job url: '%s', bucket name: '%s'", prowJobURL, bucketName) + } + + // => e.g. "pr-logs/pull/redhat-appstudio_infra-deployments/123/pull-ci-redhat-appstudio-infra-deployments-main-appstudio-e2e-tests/123/artifacts/appstudio-e2e-tests/" + artifactDirectoryPrefix := sp[1] + "/artifacts/" + jobTarget + "/" + artifactScanner.ArtifactDirectoryPrefix = artifactDirectoryPrefix + + return artifactDirectoryPrefix, nil +} + +func getParentStepName(fullArtifactName, artifactDirectoryPrefix string) (string, error) { + // => e.g. [ "", "redhat-appstudio-e2e/artifacts/e2e-report.xml" ] + sp := strings.Split(fullArtifactName, artifactDirectoryPrefix) + if len(sp) != 2 { + return "", fmt.Errorf("cannot determine filepath - object name: %s, object prefix: %s", fullArtifactName, artifactDirectoryPrefix) + } + parentStepFilePath := sp[1] + + // => e.g. [ "redhat-appstudio-e2e", "artifacts", "e2e-report.xml" ] + sp = strings.Split(parentStepFilePath, "/") + parentStepName := sp[0] + + return parentStepName, nil +} + +func getFileName(fullArtifactName, artifactDirectoryPrefix string) (string, error) { + sp := strings.Split(fullArtifactName, artifactDirectoryPrefix) + if len(sp) != 2 { + return "", fmt.Errorf("cannot determine filepath - object name: %s, object prefix: %s", fullArtifactName, artifactDirectoryPrefix) + } + parentStepFilePath := sp[1] + + sp = strings.Split(parentStepFilePath, "/") + fileName := sp[len(sp)-1] + + return fileName, nil +} diff --git a/pkg/prow/types.go b/pkg/prow/types.go index 8635e97..6cc6f17 100644 --- a/pkg/prow/types.go +++ b/pkg/prow/types.go @@ -7,7 +7,7 @@ import ( const ( // The name of the openshift-ci step where the "createReport" command is used reportStepName = "redhat-appstudio-report" - bucketName = "origin-ci-test" + bucketName = "test-platform-results" prowJobYAMLPrefix = "https://prow.ci.openshift.org/prowjob?prowjob=" ) @@ -24,15 +24,17 @@ type ArtifactScanner struct { "e2e-tests": {"build-log.txt": ...}, } */ - ArtifactStepMap map[ArtifactStepName]ArtifactFilenameMap - ObjectPrefix string + ArtifactStepMap map[ArtifactStepName]ArtifactFilenameMap + ArtifactDirectoryPrefix string } // ScannerConfig contains fields required // for scaning files with ArtifactScanner type ScannerConfig struct { - ProwJobID string FileNameFilter []string + ProwJobID string + ProwJobURL string + StepsToSkip []string } // ArtifactStepName represents the openshift-ci step name @@ -49,3 +51,27 @@ type Artifact struct { Content string FullName string } + +// OpenshiftJobSpec represents the Openshift job spec data +type OpenshiftJobSpec struct { + Type string `json:"type"` + Job string `json:"job"` + Refs Refs `json:"refs"` +} + +// Refs represent the refs field of an OpenShift job +type Refs struct { + RepoLink string `json:"repo_link"` + Repo string `json:"repo"` + Organization string `json:"org"` + Pulls []Pull `json:"pulls"` +} + +// Pull represents a GitHub Pull Request +type Pull struct { + Number int `json:"number"` + Author string `json:"author"` + SHA string `json:"sha"` + PRLink string `json:"link"` + AuthorLink string `json:"author_link"` +} diff --git a/pkg/status/types.go b/pkg/status/types.go new file mode 100644 index 0000000..5d9dabb --- /dev/null +++ b/pkg/status/types.go @@ -0,0 +1,77 @@ +package status + +import "time" + +// Summary is the Statuspage API component representation +type Summary struct { + Components []Component `json:"components"` + Incidents []Incident `json:"incidents"` + Status Status `json:"status"` +} + +// Component is the Statuspage API component representation +type Component struct { + CreatedAt time.Time + UpdatedAt time.Time + Name string + GroupID string + PageID string + ID string + Description string + Status string + AutomationEmail string + Position int32 + Group bool + Showcase bool + OnlyShowIfDegraded bool +} + +// Incident entity reflects one single incident +type Incident struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Status string `json:"status,omitempty"` + Message string `json:"message,omitempty"` + Visible int `json:"visible,omitempty"` + ComponentID int `json:"component_id,omitempty"` + ComponentStatus int `json:"component_status,omitempty"` + Notify bool `json:"notify,omitempty"` + Stickied bool `json:"stickied,omitempty"` + OccurredAt string `json:"occurred_at,omitempty"` + Template string `json:"template,omitempty"` + Vars []string `json:"vars,omitempty"` + CreatedAt string `json:"created_at,omitempty"` + UpdatedAt string `json:"updated_at,omitempty"` + DeletedAt string `json:"deleted_at,omitempty"` + IsResolved bool `json:"is_resolved,omitempty"` + Updates []IncidentUpdate `json:"incident_updates,omitempty"` + HumanStatus string `json:"human_status,omitempty"` + LatestUpdateID int `json:"latest_update_id,omitempty"` + LatestStatus int `json:"latest_status,omitempty"` + LatestHumanStatus string `json:"latest_human_status,omitempty"` + LatestIcon string `json:"latest_icon,omitempty"` + Permalink string `json:"permalink,omitempty"` + Duration int `json:"duration,omitempty"` +} + +// IncidentUpdate entity reflects one single incident update +type IncidentUpdate struct { + ID string `json:"id,omitempty"` + Body string `json:"body,omitempty"` + IncidentID string `json:"incident_id,omitempty"` + ComponentID int `json:"component_id,omitempty"` + ComponentStatus int `json:"component_status,omitempty"` + Status string `json:"status,omitempty"` + Message string `json:"message,omitempty"` + UserID int `json:"user_id,omitempty"` + CreatedAt string `json:"created_at,omitempty"` + UpdatedAt string `json:"updated_at,omitempty"` + HumanStatus string `json:"human_status,omitempty"` + Permalink string `json:"permalink,omitempty"` +} + +// Status entity contains the contents of API Response of a /status call. +type Status struct { + Indicator string `json:"indicator,omitempty"` + Description string `json:"description,omitempty"` +} diff --git a/pkg/types/types.go b/pkg/types/types.go new file mode 100644 index 0000000..f0458d7 --- /dev/null +++ b/pkg/types/types.go @@ -0,0 +1,22 @@ +package types + +// Constants common across the whole project +const ( + ArtifactDirEnv string = "ARTIFACT_DIR" + GithubTokenEnv string = "GITHUB_TOKEN" // #nosec G101 + ProwJobIDEnv string = "PROW_JOB_ID" + + ArtifactDirParamName string = "artifact-dir" + ProwJobIDParamName string = "prow-job-id" + + JunitFilename string = `/(j?unit|e2e).*\.xml` +) + +// CmdParameter represents an abstraction for viper parameters +type CmdParameter[T any] struct { + Name string + Env string + DefaultValue T + Value T + Usage string +} diff --git a/pkg/webhook/webhook.go b/pkg/webhook/webhook.go new file mode 100644 index 0000000..e0baa1c --- /dev/null +++ b/pkg/webhook/webhook.go @@ -0,0 +1,173 @@ +/* +Some of the code is copied and refactored from GoHooks library: https://pkg.go.dev/github.com/averageflow/gohooks/v2/gohooks +Original version is available on https://github.com/averageflow/gohooks/blob/v2.2.0/gohooks/GoHook.go + +MIT License: +Copyright (c) 2013-2014 Onsi Fakhouri + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package webhook + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "crypto/tls" + "encoding/hex" + "encoding/json" + "fmt" + "net/http" + "time" + + "k8s.io/klog/v2" +) + +// GoWebHook represents the definition of a GoWebHook. +type GoWebHook struct { + // Data to be sent in the GoWebHook + Payload GoWebHookPayload + // The encrypted SHA resulting with the used salt + ResultingSha string + // Prepared JSON marshaled data + PreparedData []byte + // Choice of signature header to use on sending a GoWebHook + SignatureHeader string + // Should validate SSL certificate + IsSecure bool + // Preferred HTTP method to send the GoWebHook + // Please choose only POST, DELETE, PATCH or PUT + // Any other value will make the send use POST as fallback + PreferredMethod string + // Additional HTTP headers to be added to the hook + AdditionalHeaders map[string]string +} + +// GoWebHookPayload represents the data that will be sent in the GoWebHook. +type GoWebHookPayload struct { + Resource string `json:"resource"` + Data interface{} `json:"data"` +} + +const ( + // DefaultSignatureHeader is used as default signature header + DefaultSignatureHeader = "X-GoWebHooks-Verification" +) + +// Create creates a webhook to be sent to another system, +// with a SHA 256 signature based on its contents. +func (hook *GoWebHook) Create(data interface{}, resource, secret string) { + hook.Payload.Resource = resource + hook.Payload.Data = data + + preparedHookData, err := json.Marshal(hook.Payload) + if err != nil { + klog.Error(err.Error()) + } + + hook.PreparedData = preparedHookData + + h := hmac.New(sha256.New, []byte(secret)) + + _, err = h.Write(preparedHookData) + if err != nil { + klog.Error(err.Error()) + } + + // Get result and encode as hexadecimal string + hook.ResultingSha = hex.EncodeToString(h.Sum(nil)) +} + +// Send sends a GoWebHook to the specified URL, as a UTF-8 JSON payload. +func (hook *GoWebHook) Send(receiverURL string) (*http.Response, error) { + if hook.SignatureHeader == "" { + // Use the DefaultSignatureHeader as default if no custom header is specified + hook.SignatureHeader = DefaultSignatureHeader + } + + if !hook.IsSecure { + // By default do not verify SSL certificate validity + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{ + InsecureSkipVerify: true, // #nosec G402 + } + } + + switch hook.PreferredMethod { + case http.MethodPost, http.MethodPatch, http.MethodPut, http.MethodDelete: + // Valid Methods, do nothing + default: + // By default send GoWebHook using a POST method + hook.PreferredMethod = http.MethodPost + } + + client := &http.Client{Timeout: 30 * time.Second} + + req, err := http.NewRequest( + hook.PreferredMethod, + receiverURL, + bytes.NewBuffer(hook.PreparedData), + ) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Charset", "utf-8") + req.Header.Add(DefaultSignatureHeader, hook.ResultingSha) + + // Add user's additional headers + for i := range hook.AdditionalHeaders { + req.Header.Add(i, hook.AdditionalHeaders[i]) + } + + req.Close = true + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + return resp, nil +} + +// Webhook struct used for sending webhooks to https://smee.io/ +type Webhook struct { + Path string `json:"path"` + RepositoryURL string `json:"repository_url"` + Repository `json:"repository"` +} + +// Repository struct - part of Webhook struct +type Repository struct { + FullName string `json:"full_name"` + PullNumber string `json:"pull_number"` +} + +// CreateAndSend creates and then sends a webhook to the given target +func (w *Webhook) CreateAndSend(saltSecret, webhookTarget string) (*http.Response, error) { + hook := &GoWebHook{} + hook.Create(w, w.Path, saltSecret) + resp, err := hook.Send(webhookTarget) + if err != nil { + return nil, fmt.Errorf("error sending webhook: %+v", err) + } + return resp, nil +}