github.com/redhat-appstudio/e2e-tests@v0.0.0-20230619105049-9a422b2094d7/magefiles/magefile.go (about)

     1  package main
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"net/http"
     7  	"os"
     8  	"regexp"
     9  	"strconv"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/devfile/library/pkg/util"
    14  	"k8s.io/apimachinery/pkg/runtime"
    15  
    16  	"golang.org/x/text/cases"
    17  	"golang.org/x/text/language"
    18  
    19  	"k8s.io/klog/v2"
    20  
    21  	"sigs.k8s.io/yaml"
    22  
    23  	"github.com/google/go-containerregistry/pkg/authn"
    24  	"github.com/google/go-containerregistry/pkg/name"
    25  	remoteimg "github.com/google/go-containerregistry/pkg/v1/remote"
    26  	gh "github.com/google/go-github/v44/github"
    27  	"github.com/magefile/mage/sh"
    28  	"github.com/redhat-appstudio/e2e-tests/magefiles/installation"
    29  	"github.com/redhat-appstudio/e2e-tests/pkg/apis/github"
    30  	"github.com/redhat-appstudio/e2e-tests/pkg/constants"
    31  	"github.com/redhat-appstudio/e2e-tests/pkg/utils"
    32  	"github.com/redhat-appstudio/image-controller/pkg/quay"
    33  	tektonapi "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
    34  )
    35  
    36  var (
    37  	requiredBinaries = []string{"jq", "kubectl", "oc", "yq", "git", "helm"}
    38  	artifactDir      = utils.GetEnv("ARTIFACT_DIR", ".")
    39  	openshiftJobSpec = &OpenshiftJobSpec{}
    40  	pr               = &PullRequestMetadata{}
    41  	jobName          = utils.GetEnv("JOB_NAME", "")
    42  	// can be periodic, presubmit or postsubmit
    43  	jobType                    = utils.GetEnv("JOB_TYPE", "")
    44  	reposToDeleteDefaultRegexp = "jvm-build|e2e-dotnet|build-suite|e2e|pet-clinic-e2e|test-app|e2e-quayio|petclinic|test-app|integ-app|^dockerfile-|new-|^python|my-app|^test-|^multi-component"
    45  	repositoriesWithWebhooks   = []string{"devfile-sample-hello-world", "hacbs-test-project"}
    46  )
    47  
    48  func (CI) parseJobSpec() error {
    49  	jobSpecEnvVarData := os.Getenv("JOB_SPEC")
    50  
    51  	if err := json.Unmarshal([]byte(jobSpecEnvVarData), openshiftJobSpec); err != nil {
    52  		return fmt.Errorf("error when parsing openshift job spec data: %v", err)
    53  	}
    54  	return nil
    55  }
    56  
    57  func (ci CI) init() error {
    58  	var err error
    59  
    60  	if jobType == "periodic" || strings.Contains(jobName, "rehearse") {
    61  		return nil
    62  	}
    63  
    64  	if err = ci.parseJobSpec(); err != nil {
    65  		return err
    66  	}
    67  
    68  	pr.Organization = openshiftJobSpec.Refs.Organization
    69  	pr.RepoName = openshiftJobSpec.Refs.Repo
    70  	pr.CommitSHA = openshiftJobSpec.Refs.Pulls[0].SHA
    71  	pr.Number = openshiftJobSpec.Refs.Pulls[0].Number
    72  
    73  	prUrl := fmt.Sprintf("https://api.github.com/repos/%s/%s/pulls/%d", pr.Organization, pr.RepoName, pr.Number)
    74  	pr.RemoteName, pr.BranchName, err = getRemoteAndBranchNameFromPRLink(prUrl)
    75  	if err != nil {
    76  		return err
    77  	}
    78  
    79  	return nil
    80  }
    81  
    82  func (ci CI) PrepareE2EBranch() error {
    83  	if jobType == "periodic" || strings.Contains(jobName, "rehearse") {
    84  		return nil
    85  	}
    86  
    87  	if err := ci.init(); err != nil {
    88  		return err
    89  	}
    90  
    91  	if openshiftJobSpec.Refs.Repo == "e2e-tests" {
    92  		if err := gitCheckoutRemoteBranch(pr.RemoteName, pr.CommitSHA); err != nil {
    93  			return err
    94  		}
    95  	} else {
    96  		if ci.isPRPairingRequired("e2e-tests") {
    97  			if err := gitCheckoutRemoteBranch(pr.RemoteName, pr.BranchName); err != nil {
    98  				return err
    99  			}
   100  		}
   101  	}
   102  
   103  	return nil
   104  }
   105  
   106  func (Local) PrepareCluster() error {
   107  	if err := PreflightChecks(); err != nil {
   108  		return fmt.Errorf("error when running preflight checks: %v", err)
   109  	}
   110  	if err := BootstrapCluster(); err != nil {
   111  		return fmt.Errorf("error when bootstrapping cluster: %v", err)
   112  	}
   113  
   114  	return nil
   115  }
   116  
   117  func (Local) TestE2E() error {
   118  	return RunE2ETests()
   119  }
   120  
   121  // Deletes autogenerated repositories from redhat-appstudio-qe Github org.
   122  // Env vars to configure this target: REPO_REGEX (optional), DRY_RUN (optional) - defaults to false
   123  // Remove all repos which with 1 day lifetime. By default will delete gitops repositories from redhat-appstudio-qe
   124  func (Local) CleanupGithubOrg() error {
   125  	githubToken := os.Getenv("GITHUB_TOKEN")
   126  	if githubToken == "" {
   127  		return fmt.Errorf("env var GITHUB_TOKEN is not set")
   128  	}
   129  	dryRun, err := strconv.ParseBool(utils.GetEnv("DRY_RUN", "true"))
   130  	if err != nil {
   131  		return fmt.Errorf("unable to parse DRY_RUN env var\n\t%s", err)
   132  	}
   133  
   134  	// Get all repos
   135  	githubOrgName := utils.GetEnv(constants.GITHUB_E2E_ORGANIZATION_ENV, "redhat-appstudio-qe")
   136  	ghClient, err := github.NewGithubClient(githubToken, githubOrgName)
   137  	if err != nil {
   138  		return err
   139  	}
   140  	repos, err := ghClient.GetAllRepositories()
   141  	if err != nil {
   142  		return err
   143  	}
   144  	var reposToDelete []*gh.Repository
   145  
   146  	// Filter repos by regex & time check
   147  	r, err := regexp.Compile(utils.GetEnv("REPO_REGEX", reposToDeleteDefaultRegexp))
   148  	if err != nil {
   149  		return fmt.Errorf("unable to compile regex: %s", err)
   150  	}
   151  	for _, repo := range repos {
   152  		// Add only repos older than 24 hours
   153  		dayDuration, _ := time.ParseDuration("24h")
   154  		if time.Since(repo.GetCreatedAt().Time) > dayDuration {
   155  			// Add only repos matching the regex
   156  			if r.MatchString(*repo.Name) {
   157  				reposToDelete = append(reposToDelete, repo)
   158  			}
   159  		}
   160  	}
   161  
   162  	if dryRun {
   163  		klog.Info("Dry run enabled. Listing repositories that would be deleted:")
   164  	}
   165  
   166  	// Delete repos
   167  	for _, repo := range reposToDelete {
   168  		if dryRun {
   169  			klog.Infof("\t%s", repo.GetName())
   170  		} else {
   171  			err := ghClient.DeleteRepository(repo)
   172  			if err != nil {
   173  				klog.Warningf("error deleting repository: %s\n", err)
   174  			}
   175  		}
   176  	}
   177  	if dryRun {
   178  		klog.Info("If you really want to delete these repositories, run `DRY_RUN=false [REGEXP=<regexp>] mage local:cleanupGithubOrg`")
   179  	}
   180  	return nil
   181  }
   182  
   183  // Deletes Quay repos and robot accounts older than 24 hours with prefixes `has-e2e` and `e2e-demos`, uses env vars DEFAULT_QUAY_ORG and DEFAULT_QUAY_ORG_TOKEN
   184  func (Local) CleanupQuayReposAndRobots() error {
   185  	quayOrgToken := os.Getenv("DEFAULT_QUAY_ORG_TOKEN")
   186  	if quayOrgToken == "" {
   187  		return fmt.Errorf("DEFAULT_QUAY_ORG_TOKEN env var was not found")
   188  	}
   189  	quayOrg := utils.GetEnv("DEFAULT_QUAY_ORG", "redhat-appstudio-qe")
   190  
   191  	quayClient := quay.NewQuayClient(&http.Client{Transport: &http.Transport{}}, quayOrgToken, "https://quay.io/api/v1")
   192  	return cleanupQuayReposAndRobots(&quayClient, quayOrg)
   193  }
   194  
   195  // Deletes Quay Tags older than 7 days in `test-images` repository
   196  func (Local) CleanupQuayTags() error {
   197  	quayOrgToken := os.Getenv("DEFAULT_QUAY_ORG_TOKEN")
   198  	if quayOrgToken == "" {
   199  		return fmt.Errorf("DEFAULT_QUAY_ORG_TOKEN env var was not found")
   200  	}
   201  	quayOrg := utils.GetEnv("DEFAULT_QUAY_ORG", "redhat-appstudio-qe")
   202  
   203  	quayClient := quay.NewQuayClient(&http.Client{Transport: &http.Transport{}}, quayOrgToken, "https://quay.io/api/v1")
   204  	return cleanupQuayTags(&quayClient, quayOrg, "test-images")
   205  }
   206  
   207  func (ci CI) TestE2E() error {
   208  	var testFailure bool
   209  
   210  	if err := ci.init(); err != nil {
   211  		return fmt.Errorf("error when running ci init: %v", err)
   212  	}
   213  
   214  	if err := PreflightChecks(); err != nil {
   215  		return fmt.Errorf("error when running preflight checks: %v", err)
   216  	}
   217  
   218  	if err := ci.setRequiredEnvVars(); err != nil {
   219  		return fmt.Errorf("error when setting up required env vars: %v", err)
   220  	}
   221  
   222  	if err := retry(BootstrapCluster, 2, 10*time.Second); err != nil {
   223  		return fmt.Errorf("error when bootstrapping cluster: %v", err)
   224  	}
   225  
   226  	if err := RunE2ETests(); err != nil {
   227  		testFailure = true
   228  	}
   229  
   230  	if err := ci.sendWebhook(); err != nil {
   231  		klog.Infof("error when sending webhook: %v", err)
   232  	}
   233  
   234  	if testFailure {
   235  		return fmt.Errorf("error when running e2e tests - see the log above for more details")
   236  	}
   237  
   238  	return nil
   239  }
   240  
   241  func RunE2ETests() error {
   242  	cwd, _ := os.Getwd()
   243  
   244  	// added --output-interceptor-mode=none to mitigate RHTAPBUGS-34
   245  	return sh.RunV("ginkgo", "-p", "--output-interceptor-mode=none", "--timeout=90m", fmt.Sprintf("--output-dir=%s", artifactDir), "--junit-report=e2e-report.xml", "--label-filter=$E2E_TEST_SUITE_LABEL", "./cmd", "--", fmt.Sprintf("--config-suites=%s/tests/e2e-demos/config/default.yaml", cwd), "--generate-rppreproc-report=true", fmt.Sprintf("--rp-preproc-dir=%s", artifactDir))
   246  }
   247  
   248  func PreflightChecks() error {
   249  	requiredEnv := []string{
   250  		"GITHUB_TOKEN",
   251  		"QUAY_TOKEN",
   252  		"DEFAULT_QUAY_ORG",
   253  		"DEFAULT_QUAY_ORG_TOKEN",
   254  	}
   255  	missingEnv := []string{}
   256  	for _, env := range requiredEnv {
   257  		if os.Getenv(env) == "" {
   258  			missingEnv = append(missingEnv, env)
   259  		}
   260  	}
   261  	if len(missingEnv) != 0 {
   262  		return fmt.Errorf("required env vars containing secrets (%s) not defined or empty", strings.Join(missingEnv, ","))
   263  	}
   264  
   265  	for _, binaryName := range requiredBinaries {
   266  		if err := sh.Run("which", binaryName); err != nil {
   267  			return fmt.Errorf("binary %s not found in PATH - please install it first", binaryName)
   268  		}
   269  	}
   270  
   271  	if err := sh.RunV("go", "install", "-mod=mod", "github.com/onsi/ginkgo/v2/ginkgo"); err != nil {
   272  		return err
   273  	}
   274  
   275  	return nil
   276  }
   277  
   278  func (ci CI) setRequiredEnvVars() error {
   279  
   280  	if strings.Contains(jobName, "hacbs-e2e-periodic") {
   281  		os.Setenv("E2E_TEST_SUITE_LABEL", "HACBS")
   282  		return nil
   283  	} else if strings.Contains(jobName, "appstudio-e2e-deployment-periodic") {
   284  		os.Setenv("E2E_TEST_SUITE_LABEL", "!HACBS")
   285  		return nil
   286  	}
   287  
   288  	if openshiftJobSpec.Refs.Repo != "e2e-tests" {
   289  
   290  		if strings.Contains(jobName, "-service") || strings.Contains(jobName, "image-controller") {
   291  			var envVarPrefix, imageTagSuffix, testSuiteLabel string
   292  			sp := strings.Split(os.Getenv("COMPONENT_IMAGE"), "@")
   293  
   294  			switch {
   295  			case strings.Contains(jobName, "application-service"):
   296  				envVarPrefix = "HAS"
   297  				imageTagSuffix = "has-image"
   298  				testSuiteLabel = "e2e-demo,byoc"
   299  			case strings.Contains(jobName, "release-service"):
   300  				envVarPrefix = "RELEASE_SERVICE"
   301  				imageTagSuffix = "release-service-image"
   302  				testSuiteLabel = "release"
   303  			case strings.Contains(jobName, "integration-service"):
   304  				envVarPrefix = "INTEGRATION_SERVICE"
   305  				imageTagSuffix = "integration-service-image"
   306  				testSuiteLabel = "integration-service"
   307  			case strings.Contains(jobName, "jvm-build-service"):
   308  				envVarPrefix = "JVM_BUILD_SERVICE"
   309  				imageTagSuffix = "jvm-build-service-image"
   310  				testSuiteLabel = "jvm-build"
   311  				// Since CI requires to have default values for dependency images
   312  				// (https://github.com/openshift/release/blob/master/ci-operator/step-registry/redhat-appstudio/e2e/redhat-appstudio-e2e-ref.yaml#L15)
   313  				// we cannot let these env vars to have identical names in CI as those env vars used in tests
   314  				// e.g. JVM_BUILD_SERVICE_REQPROCESSOR_IMAGE, otherwise those images they are referencing wouldn't
   315  				// be always relevant for tests and tests would be failing
   316  				os.Setenv(fmt.Sprintf("%s_REQPROCESSOR_IMAGE", envVarPrefix), os.Getenv("CI_JBS_REQPROCESSOR_IMAGE"))
   317  				os.Setenv(fmt.Sprintf("%s_CACHE_IMAGE", envVarPrefix), os.Getenv("CI_JBS_CACHE_IMAGE"))
   318  
   319  				klog.Infof("going to override default Tekton bundle for the purpose of testing jvm-build-service PR")
   320  				var err error
   321  				var defaultBundleRef string
   322  				var tektonObj runtime.Object
   323  
   324  				tag := fmt.Sprintf("%d-%s", time.Now().Unix(), util.GenerateRandomString(4))
   325  				var newS2iJavaTaskRef, _ = name.ParseReference(fmt.Sprintf("%s:task-bundle-%s", constants.DefaultImagePushRepo, tag))
   326  				var newJavaBuilderPipelineRef, _ = name.ParseReference(fmt.Sprintf("%s:pipeline-bundle-%s", constants.DefaultImagePushRepo, tag))
   327  				var newReqprocessorImage = os.Getenv("JVM_BUILD_SERVICE_REQPROCESSOR_IMAGE")
   328  				var newTaskYaml, newPipelineYaml []byte
   329  
   330  				if err = utils.CreateDockerConfigFile(os.Getenv("QUAY_TOKEN")); err != nil {
   331  					return fmt.Errorf("failed to create docker config file: %+v", err)
   332  				}
   333  				if defaultBundleRef, err = utils.GetDefaultPipelineBundleRef(constants.BuildPipelineSelectorYamlURL, "Java"); err != nil {
   334  					return fmt.Errorf("failed to get the pipeline bundle ref: %+v", err)
   335  				}
   336  				if tektonObj, err = utils.ExtractTektonObjectFromBundle(defaultBundleRef, "pipeline", "java-builder"); err != nil {
   337  					return fmt.Errorf("failed to extract the Tekton Pipeline from bundle: %+v", err)
   338  				}
   339  				javaPipelineObj := tektonObj.(tektonapi.PipelineObject)
   340  
   341  				var currentS2iJavaTaskRef string
   342  				for _, t := range javaPipelineObj.PipelineSpec().Tasks {
   343  					if t.TaskRef.Name == "s2i-java" {
   344  						currentS2iJavaTaskRef = t.TaskRef.Bundle
   345  						t.TaskRef.Bundle = newS2iJavaTaskRef.String()
   346  					}
   347  				}
   348  				if tektonObj, err = utils.ExtractTektonObjectFromBundle(currentS2iJavaTaskRef, "task", "s2i-java"); err != nil {
   349  					return fmt.Errorf("failed to extract the Tekton Task from bundle: %+v", err)
   350  				}
   351  				taskObj := tektonObj.(tektonapi.TaskObject)
   352  
   353  				for i, s := range taskObj.TaskSpec().Steps {
   354  					if s.Name == "analyse-dependencies-java-sbom" {
   355  						taskObj.TaskSpec().Steps[i].Image = newReqprocessorImage
   356  					}
   357  				}
   358  
   359  				if newTaskYaml, err = yaml.Marshal(taskObj); err != nil {
   360  					return fmt.Errorf("error when marshalling a new task to YAML: %v", err)
   361  				}
   362  				if newPipelineYaml, err = yaml.Marshal(javaPipelineObj); err != nil {
   363  					return fmt.Errorf("error when marshalling a new pipeline to YAML: %v", err)
   364  				}
   365  
   366  				keychain := authn.NewMultiKeychain(authn.DefaultKeychain)
   367  				authOption := remoteimg.WithAuthFromKeychain(keychain)
   368  
   369  				if err = utils.BuildAndPushTektonBundle(newTaskYaml, newS2iJavaTaskRef, authOption); err != nil {
   370  					return fmt.Errorf("error when building/pushing a tekton task bundle: %v", err)
   371  				}
   372  				if err = utils.BuildAndPushTektonBundle(newPipelineYaml, newJavaBuilderPipelineRef, authOption); err != nil {
   373  					return fmt.Errorf("error when building/pushing a tekton pipeline bundle: %v", err)
   374  				}
   375  				os.Setenv(constants.CUSTOM_JAVA_PIPELINE_BUILD_BUNDLE_ENV, newJavaBuilderPipelineRef.String())
   376  			case strings.Contains(jobName, "build-service"):
   377  				envVarPrefix = "BUILD_SERVICE"
   378  				imageTagSuffix = "build-service-image"
   379  				testSuiteLabel = "build"
   380  			case strings.Contains(jobName, "image-controller"):
   381  				envVarPrefix = "IMAGE_CONTROLLER"
   382  				imageTagSuffix = "image-controller-image"
   383  				testSuiteLabel = "image-controller"
   384  			}
   385  
   386  			os.Setenv(fmt.Sprintf("%s_IMAGE_REPO", envVarPrefix), sp[0])
   387  			os.Setenv(fmt.Sprintf("%s_IMAGE_TAG", envVarPrefix), fmt.Sprintf("redhat-appstudio-%s", imageTagSuffix))
   388  			// "rehearse" jobs metadata are not relevant for testing
   389  			if !strings.Contains(jobName, "rehearse") {
   390  				os.Setenv(fmt.Sprintf("%s_PR_OWNER", envVarPrefix), pr.RemoteName)
   391  				os.Setenv(fmt.Sprintf("%s_PR_SHA", envVarPrefix), pr.CommitSHA)
   392  			}
   393  
   394  			os.Setenv("E2E_TEST_SUITE_LABEL", testSuiteLabel)
   395  
   396  		} else if openshiftJobSpec.Refs.Repo == "infra-deployments" {
   397  			os.Setenv("INFRA_DEPLOYMENTS_ORG", pr.RemoteName)
   398  			os.Setenv("INFRA_DEPLOYMENTS_BRANCH", pr.BranchName)
   399  			os.Setenv("E2E_TEST_SUITE_LABEL", "e2e-demo,mvp-demo,spi-suite,integration-service,o11y,ec,byoc")
   400  		}
   401  	} else {
   402  		if ci.isPRPairingRequired("infra-deployments") {
   403  			os.Setenv("INFRA_DEPLOYMENTS_ORG", pr.RemoteName)
   404  			os.Setenv("INFRA_DEPLOYMENTS_BRANCH", pr.BranchName)
   405  		}
   406  	}
   407  
   408  	return nil
   409  }
   410  
   411  func BootstrapCluster() error {
   412  	envVars := map[string]string{}
   413  
   414  	if os.Getenv("CI") == "true" && os.Getenv("REPO_NAME") == "e2e-tests" {
   415  		// Some scripts in infra-deployments repo are referencing scripts/utils in e2e-tests repo
   416  		// This env var allows to test changes introduced in "e2e-tests" repo PRs in CI
   417  		envVars["E2E_TESTS_COMMIT_SHA"] = os.Getenv("PULL_PULL_SHA")
   418  	}
   419  
   420  	ic, err := installation.NewAppStudioInstallController()
   421  	if err != nil {
   422  		return fmt.Errorf("failed to initialize installation controller: %+v", err)
   423  	}
   424  
   425  	return ic.InstallAppStudioPreviewMode()
   426  }
   427  
   428  func (CI) isPRPairingRequired(repoForPairing string) bool {
   429  	var pullRequests []gh.PullRequest
   430  
   431  	url := fmt.Sprintf("https://api.github.com/repos/redhat-appstudio/%s/pulls?per_page=100", repoForPairing)
   432  	if err := sendHttpRequestAndParseResponse(url, "GET", &pullRequests); err != nil {
   433  		klog.Infof("cannot determine %s Github branches for author %s: %v. will stick with the redhat-appstudio/%s main branch for running tests", repoForPairing, pr.RemoteName, err, repoForPairing)
   434  		return false
   435  	}
   436  
   437  	for _, pull := range pullRequests {
   438  		if pull.GetHead().GetRef() == pr.BranchName && pull.GetUser().GetLogin() == pr.RemoteName {
   439  			return true
   440  		}
   441  	}
   442  
   443  	return false
   444  }
   445  
   446  func (CI) sendWebhook() error {
   447  	// AppStudio QE webhook configuration values will be used by default (if none are provided via env vars)
   448  	const appstudioQESaltSecret = "123456789"
   449  	const appstudioQEWebhookTargetURL = "https://hook.pipelinesascode.com/EyFYTakxEgEy"
   450  
   451  	var repoURL string
   452  
   453  	var repoOwner = os.Getenv("REPO_OWNER")
   454  	var repoName = os.Getenv("REPO_NAME")
   455  	var prNumber = os.Getenv("PULL_NUMBER")
   456  	var saltSecret = utils.GetEnv("WEBHOOK_SALT_SECRET", appstudioQESaltSecret)
   457  	var webhookTargetURL = utils.GetEnv("WEBHOOK_TARGET_URL", appstudioQEWebhookTargetURL)
   458  
   459  	if strings.Contains(jobName, "hacbs-e2e-periodic") {
   460  		// TODO configure webhook channel for sending HACBS test results
   461  		klog.Infof("not sending webhook for HACBS periodic job yet")
   462  		return nil
   463  	}
   464  
   465  	if jobType == "periodic" {
   466  		repoURL = "https://github.com/redhat-appstudio/infra-deployments"
   467  		repoOwner = "redhat-appstudio"
   468  		repoName = "infra-deployments"
   469  		prNumber = "periodic"
   470  	} else if repoName == "e2e-tests" || repoName == "infra-deployments" {
   471  		repoURL = openshiftJobSpec.Refs.RepoLink
   472  	} else {
   473  		klog.Infof("sending webhook for jobType %s, jobName %s is not supported", jobType, jobName)
   474  		return nil
   475  	}
   476  
   477  	path, err := os.Executable()
   478  	if err != nil {
   479  		return fmt.Errorf("error when sending webhook: %+v", err)
   480  	}
   481  
   482  	wh := Webhook{
   483  		Path: path,
   484  		Repository: Repository{
   485  			FullName:   fmt.Sprintf("%s/%s", repoOwner, repoName),
   486  			PullNumber: prNumber,
   487  		},
   488  		RepositoryURL: repoURL,
   489  	}
   490  	resp, err := wh.CreateAndSend(saltSecret, webhookTargetURL)
   491  	if err != nil {
   492  		return fmt.Errorf("error sending webhook: %+v", err)
   493  	}
   494  	klog.Infof("webhook response: %+v", resp)
   495  
   496  	return nil
   497  }
   498  
   499  // Generates test cases for Polarion(polarion.xml) from test files for AppStudio project.
   500  func GenerateTestCasesAppStudio() error {
   501  	return sh.RunV("ginkgo", "--dry-run", "--label-filter=$E2E_TEST_SUITE_LABEL", "./cmd", "--", "--polarion-output-file=polarion.xml", "--generate-test-cases=true")
   502  }
   503  
   504  // I've attached to the Local struct for now since it felt like it fit but it can be decoupled later as a standalone func.
   505  func (Local) GenerateTestSuiteFile() error {
   506  
   507  	var templatePackageName = utils.GetEnv("TEMPLATE_SUITE_PACKAGE_NAME", "")
   508  	var templatePath = "templates/test_suite_cmd.tmpl"
   509  	var err error
   510  
   511  	if !utils.CheckIfEnvironmentExists("TEMPLATE_SUITE_PACKAGE_NAME") {
   512  		klog.Exitf("TEMPLATE_SUITE_PACKAGE_NAME not set or is empty")
   513  	}
   514  
   515  	var templatePackageFile = fmt.Sprintf("cmd/%s_test.go", templatePackageName)
   516  	klog.Infof("Creating new test suite file %s.\n", templatePackageFile)
   517  	data := TemplateData{SuiteName: templatePackageName}
   518  	err = renderTemplate(templatePackageFile, templatePath, data, false)
   519  
   520  	if err != nil {
   521  		klog.Errorf("failed to render template with: %s", err)
   522  		return err
   523  	}
   524  
   525  	err = goFmt(templatePackageFile)
   526  	if err != nil {
   527  
   528  		klog.Errorf("%s", err)
   529  		return err
   530  	}
   531  
   532  	return nil
   533  }
   534  
   535  // Remove all webhooks which with 1 day lifetime. By default will delete webooks from redhat-appstudio-qe
   536  func CleanWebHooks() error {
   537  	token := utils.GetEnv(constants.GITHUB_TOKEN_ENV, "")
   538  	if token == "" {
   539  		return fmt.Errorf("empty GITHUB_TOKEN env. Please provide a valid github token")
   540  	}
   541  
   542  	githubOrg := utils.GetEnv(constants.GITHUB_E2E_ORGANIZATION_ENV, "redhat-appstudio-qe")
   543  	gh, err := github.NewGithubClient(token, githubOrg)
   544  	if err != nil {
   545  		return err
   546  	}
   547  	for _, repo := range repositoriesWithWebhooks {
   548  		webhookList, err := gh.ListRepoWebhooks(repo)
   549  		if err != nil {
   550  			return err
   551  		}
   552  		for _, wh := range webhookList {
   553  			dayDuration, _ := time.ParseDuration("24h")
   554  			if time.Since(wh.GetCreatedAt()) > dayDuration {
   555  				klog.Infof("removing webhook: %s, git_organization: %s, git_repository: %s", wh.GetName(), githubOrg, repo)
   556  				if err := gh.DeleteWebhook(repo, wh.GetID()); err != nil {
   557  					return fmt.Errorf("failed to delete webhook: %v, repo: %s", wh.Name, repo)
   558  				}
   559  			}
   560  		}
   561  	}
   562  	return nil
   563  }
   564  
   565  // I've attached to the Local struct for now since it felt like it fit but it can be decoupled later as a standalone func.
   566  func (Local) GenerateTestSpecFile() error {
   567  
   568  	var templateDirName = utils.GetEnv("TEMPLATE_SUITE_PACKAGE_NAME", "")
   569  	var templateSpecName = utils.GetEnv("TEMPLATE_SPEC_FILE_NAME", templateDirName)
   570  	var templateAppendFrameworkDescribeBool = utils.GetEnv("TEMPLATE_APPEND_FRAMEWORK_DESCRIBE_FILE", "true")
   571  	var templateJoinSuiteSpecNamesBool = utils.GetEnv("TEMPLATE_JOIN_SUITE_SPEC_FILE_NAMES", "false")
   572  	var templatePath = "templates/test_spec_file.tmpl"
   573  	var templateDirPath string
   574  	var templateSpecFile string
   575  	var err error
   576  	var caser = cases.Title(language.English)
   577  
   578  	if !utils.CheckIfEnvironmentExists("TEMPLATE_SUITE_PACKAGE_NAME") {
   579  		klog.Exitf("TEMPLATE_SUITE_PACKAGE_NAME not set or is empty")
   580  	}
   581  
   582  	if !utils.CheckIfEnvironmentExists("TEMPLATE_SPEC_FILE_NAME") {
   583  		klog.Infof("TEMPLATE_SPEC_FILE_NAME not set. Defaulting test spec file to value of `%s`.\n", templateSpecName)
   584  	}
   585  
   586  	if !utils.CheckIfEnvironmentExists("TEMPLATE_APPEND_FRAMEWORK_DESCRIBE_FILE") {
   587  		klog.Infof("TEMPLATE_APPEND_FRAMEWORK_DESCRIBE_FILE not set. Defaulting to `%s` which will update the pkg/framework/describe.go after generating the template.\n", templateAppendFrameworkDescribeBool)
   588  	}
   589  
   590  	if utils.CheckIfEnvironmentExists("TEMPLATE_JOIN_SUITE_SPEC_FILE_NAMES") {
   591  		klog.Infof("TEMPLATE_JOIN_SUITE_SPEC_FILE_NAMES is set to %s. Will join the suite package and spec file name to be used in the Describe of suites.\n", templateJoinSuiteSpecNamesBool)
   592  	}
   593  
   594  	templateDirPath = fmt.Sprintf("tests/%s", templateDirName)
   595  	err = os.MkdirAll(templateDirPath, 0775)
   596  	if err != nil {
   597  		klog.Errorf("failed to create package directory, %s, template with: %v", templateDirPath, err)
   598  		return err
   599  	}
   600  	templateSpecFile = fmt.Sprintf("%s/%s.go", templateDirPath, templateSpecName)
   601  
   602  	klog.Infof("Creating new test package directory and spec file %s.\n", templateSpecFile)
   603  	if templateJoinSuiteSpecNamesBool == "true" {
   604  		templateSpecName = fmt.Sprintf("%s%v", caser.String(templateDirName), caser.String(templateSpecName))
   605  	} else {
   606  		templateSpecName = caser.String(templateSpecName)
   607  	}
   608  
   609  	data := TemplateData{SuiteName: templateDirName,
   610  		TestSpecName: templateSpecName}
   611  	err = renderTemplate(templateSpecFile, templatePath, data, false)
   612  	if err != nil {
   613  		klog.Errorf("failed to render template with: %s", err)
   614  		return err
   615  	}
   616  
   617  	err = goFmt(templateSpecFile)
   618  	if err != nil {
   619  
   620  		klog.Errorf("%s", err)
   621  		return err
   622  	}
   623  
   624  	if templateAppendFrameworkDescribeBool == "true" {
   625  
   626  		err = appendFrameworkDescribeFile(templateSpecName)
   627  
   628  		if err != nil {
   629  			return err
   630  		}
   631  
   632  	}
   633  
   634  	return nil
   635  }
   636  
   637  func appendFrameworkDescribeFile(packageName string) error {
   638  
   639  	var templatePath = "templates/framework_describe_func.tmpl"
   640  	var describeFile = "pkg/framework/describe.go"
   641  	var err error
   642  	var caser = cases.Title(language.English)
   643  
   644  	data := TemplateData{TestSpecName: caser.String(packageName)}
   645  	err = renderTemplate(describeFile, templatePath, data, true)
   646  	if err != nil {
   647  		klog.Errorf("failed to append to pkg/framework/describe.go with : %s", err)
   648  		return err
   649  	}
   650  	err = goFmt(describeFile)
   651  
   652  	if err != nil {
   653  
   654  		klog.Errorf("%s", err)
   655  		return err
   656  	}
   657  
   658  	return nil
   659  
   660  }