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
}