func Run()

in tools/version-tracker/pkg/commands/upgrade/upgrade.go [36:520]


func Run(upgradeOptions *types.UpgradeOptions) error {
	var currentRevision, latestRevision, patchesWarningComment string
	var isTrackedByCommitHash, patchApplySucceeded bool
	var totalPatchCount int
	var updatedFiles []string
	var pullRequest *gogithub.PullRequest
	failedSteps := map[string]error{}

	projectName := upgradeOptions.ProjectName

	// Get org and repository name from project name.
	projectOrg := strings.Split(projectName, "/")[0]
	projectRepo := strings.Split(projectName, "/")[1]

	// Check if branch name environment variable has been set.
	branchName, ok := os.LookupEnv(constants.BranchNameEnvVar)
	if !ok {
		branchName = constants.MainBranchName
	}

	// Check if base repository owner environment variable has been set.
	baseRepoOwner, ok := os.LookupEnv(constants.BaseRepoOwnerEnvvar)
	if !ok {
		return fmt.Errorf("BASE_REPO_OWNER environment variable is not set")
	}

	// Check if head repository owner environment variable has been set.
	headRepoOwner, ok := os.LookupEnv(constants.HeadRepoOwnerEnvvar)
	if !ok {
		return fmt.Errorf("HEAD_REPO_OWNER environment variable is not set")
	}

	// Check if GitHub token environment variable has been set.
	githubToken, ok := os.LookupEnv(constants.GitHubTokenEnvvar)
	if !ok {
		return fmt.Errorf("GITHUB_TOKEN environment variable is not set")
	}
	client := gogithub.NewTokenClient(context.Background(), githubToken)

	// Skip project upgrade if it is in the ProjectsUpgradedOnlyOnMainBranch list and branch is not main
	if branchName != constants.MainBranchName && slices.Contains(constants.ProjectsUpgradedOnlyOnMainBranch, projectName) {
		logger.Info(fmt.Sprintf("Skipping upgrade for project %s on %s branch", projectName, branchName))
		return nil
	}

	cwd, err := os.Getwd()
	if err != nil {
		return fmt.Errorf("retrieving current working directory: %v", err)
	}

	skippedProjectsFilepath := filepath.Join(cwd, constants.SkippedProjectsFile)
	contents, err := os.ReadFile(skippedProjectsFilepath)
	if err != nil {
		return fmt.Errorf("reading skipped projects file: %v", err)
	}
	skippedProjects := strings.Split(string(contents), "\n")
	if slices.Contains(skippedProjects, projectName) {
		logger.Info("Project is in SKIPPED_PROJECTS list. Skipping upgrade")
		return nil
	}

	// Clone the eks-anywhere-build-tooling repository.
	buildToolingRepoPath := filepath.Join(cwd, constants.BuildToolingRepoName)
	repo, headCommit, err := git.CloneRepo(fmt.Sprintf(constants.BuildToolingRepoURL, baseRepoOwner), buildToolingRepoPath, headRepoOwner, branchName)
	if err != nil {
		return fmt.Errorf("cloning build-tooling repo: %v", err)
	}

	// Get the worktree corresponding to the cloned repository.
	worktree, err := repo.Worktree()
	if err != nil {
		return fmt.Errorf("getting repo's current worktree: %v", err)
	}

	// Checkout the eks-anywhere-build-tooling repo at the provided branch name.
	createBranch := (branchName != constants.MainBranchName)
	err = git.Checkout(worktree, branchName, createBranch)
	if err != nil {
		return fmt.Errorf("checking out worktree at branch %s: %v", branchName, err)
	}

	// Reset current worktree to get a clean index.
	err = git.ResetToHEAD(worktree, headCommit)
	if err != nil {
		return fmt.Errorf("resetting new branch to [origin/%s] HEAD: %v", branchName, err)
	}

	var headBranchName, baseBranchName, commitMessage, pullRequestBody string
	if isEKSDistroUpgrade(projectName) {
		headBranchName = fmt.Sprintf("update-eks-distro-latest-releases-%s", branchName)
		baseBranchName = branchName
		commitMessage = "Bump EKS Distro releases to latest"
		pullRequestBody = constants.EKSDistroUpgradePullRequestBody

		// Checkout a new branch to keep track of version upgrade chaneges.
		err = git.Checkout(worktree, headBranchName, true)
		if err != nil {
			return fmt.Errorf("checking out worktree at branch %s: %v", headBranchName, err)
		}

		// Reset current worktree to get a clean index.
		err = git.ResetToHEAD(worktree, headCommit)
		if err != nil {
			return fmt.Errorf("resetting new branch to [origin/%s] HEAD: %v", branchName, err)
		}

		isUpdated, err := updateEKSDistroReleasesFile(buildToolingRepoPath)
		if err != nil {
			return fmt.Errorf("updating EKS Distro releases file: %v", err)
		}
		if isUpdated {
			updatedFiles = append(updatedFiles, constants.EKSDistroLatestReleasesFile)
		}
	} else if isEKSDistroBuildToolingUpgrade(projectName) {
		headBranchName = fmt.Sprintf("update-eks-distro-base-image-tag-files-%s", branchName)
		baseBranchName = branchName
		commitMessage = "Bump EKS Distro base image tag files to latest"

		// Checkout a new branch to keep track of version upgrade chaneges.
		err = git.Checkout(worktree, headBranchName, true)
		if err != nil {
			return fmt.Errorf("checking out worktree at branch %s: %v", headBranchName, err)
		}

		// Reset current worktree to get a clean index.
		err = git.ResetToHEAD(worktree, headCommit)
		if err != nil {
			return fmt.Errorf("resetting new branch to [origin/%s] HEAD: %v", branchName, err)
		}

		eksDistroBaseTagFilesGlobPattern := filepath.Join(buildToolingRepoPath, constants.EKSDistroBaseTagFilesPattern)
		eksDistroBaseTagFilesGlob, err := filepath.Glob(eksDistroBaseTagFilesGlobPattern)
		if err != nil {
			return fmt.Errorf("finding filenames matching EKS Distro Base tag file pattern [%s]: %v", constants.EKSDistroBaseTagFilesPattern, err)
		}

		updatedPackages, isUpdated, err := updateEKSDistroBaseImageTagFiles(client, buildToolingRepoPath, eksDistroBaseTagFilesGlob)
		if err != nil {
			return fmt.Errorf("updating EKS Distro base tag files: %v", err)
		}
		if isUpdated {
			pullRequestBody = fmt.Sprintf(constants.EKSDistroBuildToolingUpgradePullRequestBody, updatedPackages)
			for _, tagFile := range eksDistroBaseTagFilesGlob {
				tagFileRelativePath, err := filepath.Rel(buildToolingRepoPath, tagFile)
				if err != nil {
					return fmt.Errorf("getting relative path for tag file: %v", err)
				}
				updatedFiles = append(updatedFiles, tagFileRelativePath)
			}
		}
	} else {
		// Validate if the project name provided exists in the repository.
		projectPath := filepath.Join("projects", projectName)
		projectRootFilepath := filepath.Join(buildToolingRepoPath, projectPath)
		if _, err := os.Stat(projectRootFilepath); os.IsNotExist(err) {
			return fmt.Errorf("invalid project name %s", projectName)
		}

		// Load upstream projects tracker file.
		upstreamProjectsTrackerFilePath := filepath.Join(buildToolingRepoPath, constants.UpstreamProjectsTrackerFile)
		_, targetRepo, err := loadUpstreamProjectsTrackerFile(upstreamProjectsTrackerFilePath, projectOrg, projectRepo)
		if err != nil {
			return fmt.Errorf("loading upstream projects tracker file: %v", err)
		}

		// Validate whether the given project is release-branched.
		var isReleaseBranched bool
		var currentVersion types.Version
		var versionIndex int
		if len(targetRepo.Versions) > 1 {
			isReleaseBranched = true
		}
		releaseBranch := os.Getenv(constants.ReleaseBranchEnvvar)
		if releaseBranch == "" {
			releaseBranch, err = getDefaultReleaseBranch(buildToolingRepoPath)
			if err != nil {
				return fmt.Errorf("getting default EKS Distro release branch: %v", err)
			}
			os.Setenv(constants.ReleaseBranchEnvvar, releaseBranch)
		}
		if isReleaseBranched {
			supportedReleaseBranches, err := getSupportedReleaseBranches(buildToolingRepoPath)
			if err != nil {
				return fmt.Errorf("getting supported EKS Distro release branches: %v", err)
			}

			versionIndex = slices.Index(supportedReleaseBranches, releaseBranch)
		} else {
			versionIndex = 0
		}
		currentVersion = targetRepo.Versions[versionIndex]

		if currentVersion.Tag != "" {
			currentRevision = currentVersion.Tag
		} else if currentVersion.Commit != "" {
			currentRevision = currentVersion.Commit
			isTrackedByCommitHash = true
		}

		// Check if project to be upgraded has patches
		projectHasPatches := false
		patchesDirectory := filepath.Join(projectRootFilepath, constants.PatchesDirectory)
		if isReleaseBranched {
			patchesDirectory = filepath.Join(projectRootFilepath, releaseBranch, constants.PatchesDirectory)
		}
		if _, err := os.Stat(patchesDirectory); err == nil {
			projectHasPatches = true
			patchFiles, err := os.ReadDir(patchesDirectory)
			if err != nil {
				return fmt.Errorf("reading patches directory: %v", err)
			}
			totalPatchCount = len(patchFiles)
		}

		headBranchName = fmt.Sprintf("update-%s-%s-%s", projectOrg, projectRepo, branchName)
		baseBranchName = branchName
		commitMessage = fmt.Sprintf("Bump %s to latest release", projectName)
		if isReleaseBranched {
			headBranchName = fmt.Sprintf("update-%s-%s-%s-%s", projectOrg, projectRepo, releaseBranch, branchName)
			commitMessage = fmt.Sprintf("Bump %s %s release branch to latest release", projectName, releaseBranch)
		}

		var latestRevision string
		var needsUpgrade bool
		if projectName == "cilium/cilium" {
			latestRevision, needsUpgrade, err = ecrpublic.GetLatestRevision(constants.CiliumImageRepository, currentRevision, branchName)
			if err != nil {
				return fmt.Errorf("getting latest revision from ECR Public: %v", err)
			}
		} else {
			// Get latest revision for the project from GitHub.
			latestRevision, needsUpgrade, err = github.GetLatestRevision(client, projectOrg, projectRepo, currentRevision, branchName, isTrackedByCommitHash, isReleaseBranched)
			if err != nil {
				return fmt.Errorf("getting latest revision from GitHub: %v", err)
			}
		}

		prLabels := constants.DefaultProjectUpgradePRLabels
		if slices.Contains(constants.CuratedPackagesProjects, projectName) {
			prLabels = constants.PackagesProjectUpgradePRLabels
		}
		pullRequestBody = fmt.Sprintf(constants.DefaultUpgradePullRequestBody, projectOrg, projectRepo, currentRevision, latestRevision, strings.Join(prLabels, "\n"))

		// Upgrade project if latest commit was made after current commit and the semver of the latest revision is
		// greater than the semver of the current version.
		if needsUpgrade || slices.Contains(constants.ProjectsWithUnconventionalUpgradeFlows, projectName) {
			// Checkout a new branch to keep track of version upgrade chaneges.
			err = git.Checkout(worktree, headBranchName, true)
			if err != nil {
				return fmt.Errorf("checking out worktree at branch %s: %v", headBranchName, err)
			}

			// Reset current worktree to get a clean index.
			err = git.ResetToHEAD(worktree, headCommit)
			if err != nil {
				return fmt.Errorf("resetting new branch to [origin/%s] HEAD: %v", branchName, err)
			}

			if needsUpgrade {
				logger.Info("Project is out of date.", "Current version", currentRevision, "Latest version", latestRevision)

				// Reload upstream projects tracker file to get its original value instead of
				// the updated one from another project's previous upgrade
				projectsList, targetRepo, err := loadUpstreamProjectsTrackerFile(upstreamProjectsTrackerFilePath, projectOrg, projectRepo)
				if err != nil {
					return fmt.Errorf("reloading upstream projects tracker file: %v", err)
				}
				if isTrackedByCommitHash {
					targetRepo.Versions[versionIndex].Commit = latestRevision
				} else {
					targetRepo.Versions[versionIndex].Tag = latestRevision
				}

				// Update the Git tag file corresponding to the project
				logger.Info("Updating Git tag file corresponding to the project")
				projectGitTagRelativePath, err := updateProjectVersionFile(buildToolingRepoPath, constants.GitTagFile, projectName, latestRevision, releaseBranch, isReleaseBranched)
				if err != nil {
					return fmt.Errorf("updating project GIT_TAG file: %v", err)
				}
				updatedFiles = append(updatedFiles, projectGitTagRelativePath)

				var latestGoVersion string
				if currentVersion.GoVersion != "N/A" {
					currentGoVersion := currentVersion.GoVersion
					// Get Go version corresponding to the latest revision of the project.
					latestGoVersion, err := github.GetGoVersionForLatestRevision(client, projectOrg, projectRepo, latestRevision)
					if err != nil {
						return fmt.Errorf("getting latest Go version for release %s: %v", latestRevision, err)
					}

					// Get the minor version for the current revision's Go version.
					currentGoMinorVersion, err := strconv.Atoi(strings.Split(currentGoVersion, ".")[1])
					if err != nil {
						return fmt.Errorf("getting current Go minor version: %v", err)
					}

					// Get the major version for the latest revision's Go version.
					latestGoMinorVersion, err := strconv.Atoi(strings.Split(latestGoVersion, ".")[1])
					if err != nil {
						return fmt.Errorf("getting latest Go minor version: %v", err)
					}

					// If the Go version has been updated in the latest revision, then update the Go version file corresponding to the project.
					if latestGoMinorVersion > currentGoMinorVersion {
						logger.Info("Project Go version needs to be updated.", "Current Go version", currentGoVersion, "Latest Go version", latestGoVersion)
						targetRepo.Versions[versionIndex].GoVersion = latestGoVersion

						logger.Info("Updating Go version file corresponding to the project")
						projectGoVersionRelativePath, err := updateProjectVersionFile(buildToolingRepoPath, constants.GoVersionFile, projectName, latestGoVersion, releaseBranch, isReleaseBranched)
						if err != nil {
							return fmt.Errorf("updating project GOLANG_VERSION file: %v", err)
						}
						updatedFiles = append(updatedFiles, projectGoVersionRelativePath)
					}
				} else {
					latestGoVersion = "N/A"
					targetRepo.Versions[versionIndex].GoVersion = latestGoVersion
				}

				// Update the tag and Go version in the section of the upstream projects tracker file corresponding to the given project.
				logger.Info("Updating Git tag and Go version in upstream projects tracker file")
				err = updateUpstreamProjectsTrackerFile(&projectsList, buildToolingRepoPath, upstreamProjectsTrackerFilePath)
				if err != nil {
					return fmt.Errorf("updating upstream projects tracker file: %v", err)
				}
				updatedFiles = append(updatedFiles, constants.UpstreamProjectsTrackerFile)

				// Update the version in the project's README file.
				logger.Info("Updating project README file")
				projectReadmePath := filepath.Join(projectPath, constants.ReadmeFile)
				err = updateProjectReadmeVersion(buildToolingRepoPath, projectOrg, projectRepo)
				if err != nil {
					return fmt.Errorf("updating version in project README: %v", err)
				}
				updatedFiles = append(updatedFiles, projectReadmePath)

				// If project has patches, attempt to apply them. Track failed patches and files that failed to apply, if any.
				if projectHasPatches {
					appliedPatchesCount, failedPatch, applyFailedFiles, err := applyPatchesToRepo(projectRootFilepath, projectRepo, totalPatchCount)
					if appliedPatchesCount == totalPatchCount {
						patchApplySucceeded = true
					}
					if !patchApplySucceeded {
						failedSteps["Patch application"] = err
						patchesWarningComment = fmt.Sprintf(constants.FailedPatchesCommentBody, appliedPatchesCount, totalPatchCount, failedPatch, applyFailedFiles)
					}
				}

				// If project doesn't have patches, or it does and they were applied successfully, then update the checksums file
				// and attribution file(s) corresponding to the project.
				if !projectHasPatches || patchApplySucceeded {
					projectChecksumsFile := filepath.Join(projectRootFilepath, constants.ChecksumsFile)
					projectChecksumsFileRelativePath := filepath.Join(projectPath, constants.ChecksumsFile)
					projectAttributionFileGlob := filepath.Join(projectRootFilepath, constants.AttributionsFilePattern)
					if isReleaseBranched {
						projectChecksumsFile = filepath.Join(projectRootFilepath, releaseBranch, constants.ChecksumsFile)
						projectChecksumsFileRelativePath = filepath.Join(projectPath, releaseBranch, constants.ChecksumsFile)
						projectAttributionFileGlob = filepath.Join(projectRootFilepath, releaseBranch, constants.AttributionsFilePattern)
					}
					if _, err := os.Stat(projectChecksumsFile); err == nil {
						logger.Info("Updating project checksums and attribution files")
						err = updateChecksumsAttributionFiles(projectRootFilepath)
						if err != nil {
							failedSteps["Checksums and attribution generation"] = err
						} else {
							updatedFiles = append(updatedFiles, projectChecksumsFileRelativePath)

							// Attribution files can have a binary name prefix so we use a common prefix regular expression
							// and glob them to cover all possibilities.
							projectAttributionFileGlob, err := filepath.Glob(projectAttributionFileGlob)
							if err != nil {
								return fmt.Errorf("finding filenames matching attribution file pattern [%s]: %v", constants.AttributionsFilePattern, err)
							}
							for _, attributionFile := range projectAttributionFileGlob {
								attributionFileRelativePath, err := filepath.Rel(buildToolingRepoPath, attributionFile)
								if err != nil {
									return fmt.Errorf("getting relative path for attribution file: %v", err)
								}
								updatedFiles = append(updatedFiles, attributionFileRelativePath)
							}
						}
					}
				}

				op, message := getProjectSpecificUpdateOperation(projectName)
				if op != nil {
					updatedProjectFiles, err := op(projectRootFilepath, projectPath)
					if err != nil {
						failedSteps[message] = err
					} else {
						updatedFiles = append(updatedFiles, updatedProjectFiles...)
					}
				}
			}

			if projectName == "kubernetes-sigs/image-builder" {
				currentBottlerocketVersion, latestBottlerocketVersion, updatedBRFiles, err := updateBottlerocketVersionFiles(client, projectRootFilepath, projectPath, branchName)
				if err != nil {
					failedSteps["Bottlerocket version upgrade"] = err
				} else {
					if len(updatedBRFiles) > 0 {
						updatedFiles = append(updatedFiles, updatedBRFiles...)
						if len(updatedFiles) == len(updatedBRFiles) {
							headBranchName = fmt.Sprintf("update-bottlerocket-releases-%s", branchName)
							commitMessage = "Bump Bottlerocket versions to latest release"
							pullRequestBody = fmt.Sprintf(constants.BottlerocketUpgradePullRequestBody, currentBottlerocketVersion, latestBottlerocketVersion)
						} else {
							headBranchName = fmt.Sprintf("update-%s-%s-and-bottlerocket-%s", projectOrg, projectRepo, branchName)
							commitMessage = fmt.Sprintf("Bump %s and Bottlerocket versions to latest release", projectName)
							pullRequestBody = fmt.Sprintf(constants.CombinedImageBuilderBottlerocketUpgradePullRequestBody, currentRevision, latestRevision, currentBottlerocketVersion, latestBottlerocketVersion)
						}

						err = git.Checkout(worktree, headBranchName, true)
						if err != nil {
							return fmt.Errorf("checking out worktree at branch %s: %v", headBranchName, err)
						}
					}
				}
			}
		} else if latestRevision == currentRevision {
			logger.Info("Project is at the latest available version.", "Current version", currentRevision, "Latest version", latestRevision)
		}
	}

	if len(updatedFiles) > 0 {
		// Add all the updated files to the index.
		err = git.Add(worktree, updatedFiles)
		if err != nil {
			return fmt.Errorf("adding updated files to index: %v", err)
		}

		// Create a new commit including the updated files, with an appropriate commit message.
		err = git.Commit(worktree, commitMessage)
		if err != nil {
			return fmt.Errorf("committing updated project version files for [%s] project: %v", projectName, err)
		}

		if !upgradeOptions.DryRun {
			// Push the changes to the target branch in the head repository.
			err = git.Push(repo, headRepoOwner, headBranchName, githubToken)
			if err != nil {
				return fmt.Errorf("pushing updated project version files for [%s] project: %v", projectName, err)
			}

			// Update the title of the pull request depending on the base branch name.
			title := commitMessage
			if baseBranchName != constants.MainBranchName {
				title = fmt.Sprintf("[%s] %s", baseBranchName, title)
			}

			// Create a pull request from the branch in the head repository to the target branch in the aws/eks-anywhere-build-tooling repository.
			logger.Info("Creating pull request with updated files")
			pullRequest, err = github.CreatePullRequest(client, projectOrg, projectRepo, title, pullRequestBody, baseRepoOwner, baseBranchName, headRepoOwner, headBranchName, currentRevision, latestRevision)
			if err != nil {
				return fmt.Errorf("creating pull request to %s repository: %v", constants.BuildToolingRepoName, err)
			}
		} else {
			logger.Info(fmt.Sprintf("Completed dry run of upgrade for project %s", projectName))
		}
	}

	if len(failedSteps) > 0 {
		var failedStepsList []string
		var errorsList []string
		for step, err := range failedSteps {
			if step == "Patch application" {
				step = fmt.Sprintf("%s\n%s", step, patchesWarningComment)
			}
			failedStepsList = append(failedStepsList, fmt.Sprintf("* %s", step))
			errorsList = append(errorsList, fmt.Sprintf("Error occured in %s step: %v", step, err))
		}
		failedUpgradeComment := fmt.Sprintf(constants.FailedUpgradeCommentBody, strings.Join(failedStepsList, "\n"))

		if !upgradeOptions.DryRun {
			err = github.AddCommentOnPR(client, baseRepoOwner, failedUpgradeComment, pullRequest)
			if err != nil {
				return fmt.Errorf("commenting failed upgrade comment on pull request [%s]: %v", *pullRequest.HTMLURL, err)
			}
		}

		return errors.New(strings.Join(errorsList, "\n"))
	}

	return nil
}