diff --git a/cmd/application_deploy.go b/cmd/application_deploy.go index 7e2853d9..95cdf743 100644 --- a/cmd/application_deploy.go +++ b/cmd/application_deploy.go @@ -53,6 +53,6 @@ func init() { applicationDeployCmd.Flags().StringVarP(&environmentName, "environment", "", "", "Environment Name") applicationDeployCmd.Flags().StringVarP(&applicationName, "application", "n", "", "Application Name") applicationDeployCmd.Flags().StringVarP(&applicationNames, "applications", "", "", "Application Names (comma separated) Example: --applications \"app1,app2,app3\"") - applicationDeployCmd.Flags().StringVarP(&applicationCommitID, "commit-id", "c", "", "Application Commit ID") + applicationDeployCmd.Flags().StringVarP(&applicationCommitID, "commit-id", "c", "", "Application Commit ID, or 'latest' for the newest commit of the branch") applicationDeployCmd.Flags().BoolVarP(&watchFlag, "watch", "w", false, "Watch application status until it's ready or an error occurs") } diff --git a/cmd/application_redeploy.go b/cmd/application_redeploy.go index 0e343023..469005c1 100644 --- a/cmd/application_redeploy.go +++ b/cmd/application_redeploy.go @@ -20,9 +20,22 @@ var applicationRedeployCmd = &cobra.Command{ envId := getEnvironmentIdFromContextPanicInCaseOfError(client) applicationList := buildApplicationListFromApplicationNames(client, envId, applicationName, applicationNames) + application := applicationList[0] - _, _, err := client.ApplicationActionsAPI.DeployApplication(context.Background(), applicationList[0].Id). - DeployRequest(qovery.DeployRequest{GitCommitId: *applicationList[0].GitRepository.DeployedCommitId}). + var commitId string + switch { + case utils.IsLatestCommitKeyword(applicationCommitID): + latestCommitId, err := utils.ResolveLatestApplicationCommit(client, application.Id, application.Name) + checkError(err) + commitId = latestCommitId + case applicationCommitID != "": + commitId = applicationCommitID + default: + commitId = application.GitRepository.GetDeployedCommitId() + } + + _, _, err := client.ApplicationActionsAPI.DeployApplication(context.Background(), application.Id). + DeployRequest(qovery.DeployRequest{GitCommitId: commitId}). Execute() checkError(err) utils.Println(fmt.Sprintf("Request to redeploy application(s) %s has been queued..", pterm.FgBlue.Sprintf("%s%s", applicationName, applicationNames))) @@ -36,7 +49,7 @@ func init() { applicationRedeployCmd.Flags().StringVarP(&projectName, "project", "", "", "Project Name") applicationRedeployCmd.Flags().StringVarP(&environmentName, "environment", "", "", "Environment Name") applicationRedeployCmd.Flags().StringVarP(&applicationName, "application", "n", "", "Application Name") - applicationRedeployCmd.Flags().StringVarP(&applicationCommitID, "commit-id", "c", "", "Application Commit ID") + applicationRedeployCmd.Flags().StringVarP(&applicationCommitID, "commit-id", "c", "", "Application Commit ID, or 'latest' for the newest commit of the branch") applicationRedeployCmd.Flags().BoolVarP(&watchFlag, "watch", "w", false, "Watch application status until it's ready or an error occurs") _ = applicationRedeployCmd.MarkFlagRequired("application") diff --git a/cmd/cronjob_deploy.go b/cmd/cronjob_deploy.go index 86fdfbf9..17885464 100644 --- a/cmd/cronjob_deploy.go +++ b/cmd/cronjob_deploy.go @@ -59,7 +59,7 @@ func init() { cronjobDeployCmd.Flags().StringVarP(&environmentName, "environment", "", "", "Environment Name") cronjobDeployCmd.Flags().StringVarP(&cronjobName, "cronjob", "n", "", "Cronjob Name") cronjobDeployCmd.Flags().StringVarP(&cronjobNames, "cronjobs", "", "", "Cronjob Names (comma separated) (ex: --cronjobs \"cron1,cron2\")") - cronjobDeployCmd.Flags().StringVarP(&cronjobCommitId, "commit-id", "c", "", "Cronjob Commit ID") + cronjobDeployCmd.Flags().StringVarP(&cronjobCommitId, "commit-id", "c", "", "Cronjob Commit ID, or 'latest' for the newest commit of the branch") cronjobDeployCmd.Flags().StringVarP(&cronjobTag, "tag", "t", "", "Cronjob Tag") cronjobDeployCmd.Flags().BoolVarP(&watchFlag, "watch", "w", false, "Watch cronjob status until it's ready or an error occurs") } diff --git a/cmd/environment_deploy.go b/cmd/environment_deploy.go index aa594b3c..c0b040e9 100644 --- a/cmd/environment_deploy.go +++ b/cmd/environment_deploy.go @@ -134,6 +134,16 @@ func getDeploymentRequestForMultipleServices( panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011 } + if isLatestVersion(version) { + latestCommitId, err := utils.ResolveLatestApplicationCommit(client, app.Id, app.Name) + if err != nil { + utils.PrintlnError(err) + os.Exit(1) + panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011 + } + version = &latestCommitId + } + request.Applications = append(request.Applications, qovery.DeployAllRequestApplicationsInner{ApplicationId: app.Id, GitCommitId: version}) } } @@ -169,7 +179,7 @@ func getDeploymentRequestForMultipleServices( // Adding lifecycle to be deployed for _, nameAndVersion := range strings.Split(lifecycleNames, ",") { name, version := splitServiceNameAndVersion(nameAndVersion) - job, gitCommitId, imageTag := getLifecycleJobGitCommitAndImageTag(jobs.GetResults(), name) + job := utils.FindByJobName(jobs.GetResults(), name) if job == nil { utils.PrintlnError(fmt.Errorf("lifecycle %s not found", name)) @@ -178,9 +188,9 @@ func getDeploymentRequestForMultipleServices( } req := qovery.DeployAllRequestJobsInner{Id: &job.LifecycleJobResponse.Id} - if gitCommitId != nil { - req.GitCommitId = version - } else if imageTag != nil { + if utils.GetJobDocker(job) != nil { + req.GitCommitId = resolveLatestJobVersion(client, job, version) + } else if utils.GetJobImage(job) != nil { req.ImageTag = version } @@ -193,7 +203,7 @@ func getDeploymentRequestForMultipleServices( for _, nameAndVersion := range strings.Split(cronjobNames, ",") { name, version := splitServiceNameAndVersion(nameAndVersion) - job, gitCommitId, imageTag := getCronjobGitCommitAndImageTag(jobs.GetResults(), name) + job := utils.FindByJobName(jobs.GetResults(), name) if job == nil { utils.PrintlnError(fmt.Errorf("cronjob %s not found", name)) @@ -202,9 +212,9 @@ func getDeploymentRequestForMultipleServices( } req := qovery.DeployAllRequestJobsInner{Id: &job.CronJobResponse.Id} - if gitCommitId != nil { - req.GitCommitId = version - } else if imageTag != nil { + if utils.GetJobDocker(job) != nil { + req.GitCommitId = resolveLatestJobVersion(client, job, version) + } else if utils.GetJobImage(job) != nil { req.ImageTag = version } @@ -234,12 +244,19 @@ func getDeploymentRequestForMultipleServices( panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011 } - gitCommitId, chartVersion := getHelmCommitAndChartVersion(client, name) - req := qovery.DeployAllRequestHelmsInner{Id: &helm.Id} - if gitCommitId != nil { + if utils.GetGitSource(helm) != nil { + if isLatestVersion(version) { + latestCommitId, err := utils.ResolveLatestHelmChartCommit(client, helm.Id, helm.Name) + if err != nil { + utils.PrintlnError(err) + os.Exit(1) + panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011 + } + version = &latestCommitId + } req.GitCommitId = version - } else if chartVersion != nil { + } else if utils.GetHelmRepository(helm) != nil { req.ChartVersion = version } @@ -313,73 +330,23 @@ func splitServiceNameAndVersion(service string) (string, *string) { return split[0], &split[1] } -func getLifecycleJobGitCommitAndImageTag(jobs []qovery.JobResponse, jobName string) (*qovery.JobResponse, *string, *string) { - var commitId, imageTag *string - - job := utils.FindByJobName(jobs, jobName) - - if job == nil { - return nil, nil, nil - } - - if job.LifecycleJobResponse.Source.BaseJobResponseAllOfSourceOneOf != nil { - // image tag - image := job.LifecycleJobResponse.Source.BaseJobResponseAllOfSourceOneOf.GetImage() - tag := image.GetTag() - imageTag = &tag - } else if job.LifecycleJobResponse.Source.BaseJobResponseAllOfSourceOneOf1 != nil { - // commit id - docker := job.LifecycleJobResponse.Source.BaseJobResponseAllOfSourceOneOf1.GetDocker() - commitId = docker.GitRepository.DeployedCommitId - } - - return job, commitId, imageTag +func isLatestVersion(version *string) bool { + return version != nil && utils.IsLatestCommitKeyword(*version) } -func getCronjobGitCommitAndImageTag(jobs []qovery.JobResponse, jobName string) (*qovery.JobResponse, *string, *string) { - var commitId, imageTag *string - - job := utils.FindByJobName(jobs, jobName) - - if job == nil { - return nil, nil, nil - } - - if job.CronJobResponse.Source.BaseJobResponseAllOfSourceOneOf != nil { - // image tag - image := job.CronJobResponse.Source.BaseJobResponseAllOfSourceOneOf.GetImage() - tag := image.GetTag() - imageTag = &tag - } else if job.CronJobResponse.Source.BaseJobResponseAllOfSourceOneOf1 != nil { - // commit id - docker := job.CronJobResponse.Source.BaseJobResponseAllOfSourceOneOf1.GetDocker() - commitId = docker.GitRepository.DeployedCommitId +func resolveLatestJobVersion(client *qovery.APIClient, job *qovery.JobResponse, version *string) *string { + if !isLatestVersion(version) { + return version } - return job, commitId, imageTag -} - -func getHelmCommitAndChartVersion(client *qovery.APIClient, helmId string) (*string, *string) { - var commitId, chartVersion *string - - // check if the helm version is a chart version or a commit id - helm, _, err := client.HelmMainCallsAPI.GetHelm(context.Background(), helmId).Execute() + latestCommitId, err := utils.ResolveLatestJobCommit(client, utils.GetJobId(job), utils.GetJobName(job)) if err != nil { utils.PrintlnError(err) os.Exit(1) + panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011 } - if helm.Source.HelmResponseAllOfSourceOneOf != nil { - // chart version - git := helm.Source.HelmResponseAllOfSourceOneOf.GetGit() - commitId = git.GitRepository.DeployedCommitId - } else if helm.Source.HelmResponseAllOfSourceOneOf1 != nil { - // commit id - git := helm.Source.HelmResponseAllOfSourceOneOf1.GetRepository() - chartVersion = &git.ChartVersion - } - - return commitId, chartVersion + return &latestCommitId } func init() { @@ -388,11 +355,11 @@ func init() { environmentDeployCmd.Flags().StringVarP(&projectName, "project", "", "", "Project Name") environmentDeployCmd.Flags().StringVarP(&environmentName, "environment", "", "", "Environment Name") environmentDeployCmd.Flags().StringVarP(&servicesJson, "services", "", "", "Services to deploy (JSON Format: https://api-doc.qovery.com/#tag/Environment-Actions/operation/deployAllServices)") - environmentDeployCmd.Flags().StringVarP(&applicationNames, "applications", "", "", "Applications to deploy E.g. --applications app1:commit_id,app2:commit_id). If you omit the commit id, the same commit will be used") + environmentDeployCmd.Flags().StringVarP(&applicationNames, "applications", "", "", "Applications to deploy E.g. --applications app1:commit_id,app2:latest). If you omit the commit id, the same commit will be used. 'latest' deploys the newest commit of the branch") environmentDeployCmd.Flags().StringVarP(&containerNames, "containers", "", "", "Containers to deploy E.g. --containers container1:image_tag,container2:image_tag). If you omit the image tag, the same image tag will be used") - environmentDeployCmd.Flags().StringVarP(&lifecycleNames, "lifecycles", "", "", "Lifecycle to deploy E.g. --lifecycles job1:image_tag|git_commit_id,job2:image_tag|git_commit_id). If you omit the git commit id or image tag, the same version will be used") - environmentDeployCmd.Flags().StringVarP(&cronjobNames, "cronjobs", "", "", "Cronjobs to deploy E.g. --cronjobs cronjob1:git_commit_id,cronjob2:git_commit_id). If you omit the git commit id, the same version will be used") - environmentDeployCmd.Flags().StringVarP(&helmNames, "helms", "", "", "Helms to deploy E.g. --helms helm1:chart_version|git_commit_id,helm2:chart_version|git_commit_id). If you omit the chart version or git commit id, the same version will be used") + environmentDeployCmd.Flags().StringVarP(&lifecycleNames, "lifecycles", "", "", "Lifecycle to deploy E.g. --lifecycles job1:image_tag|git_commit_id,job2:image_tag|git_commit_id). If you omit the git commit id or image tag, the same version will be used. 'latest' deploys the newest commit of the branch for a git-sourced job") + environmentDeployCmd.Flags().StringVarP(&cronjobNames, "cronjobs", "", "", "Cronjobs to deploy E.g. --cronjobs cronjob1:git_commit_id,cronjob2:latest). If you omit the git commit id, the same version will be used. 'latest' deploys the newest commit of the branch for a git-sourced cronjob") + environmentDeployCmd.Flags().StringVarP(&helmNames, "helms", "", "", "Helms to deploy E.g. --helms helm1:chart_version|git_commit_id,helm2:chart_version|git_commit_id). If you omit the chart version or git commit id, the same version will be used. 'latest' deploys the newest commit of the branch for a git-sourced chart") environmentDeployCmd.Flags().BoolVarP(&watchFlag, "watch", "w", false, "Watch environment status until it's ready or an error occurs") environmentDeployCmd.Flags().BoolVarP(&skipPausedServicesFlag, "skip-paused-services", "", false, "Skip paused services: paused services won't be started / deployed") } diff --git a/cmd/helm_deploy.go b/cmd/helm_deploy.go index ab332486..a05e63d7 100644 --- a/cmd/helm_deploy.go +++ b/cmd/helm_deploy.go @@ -52,7 +52,7 @@ func init() { helmDeployCmd.Flags().StringVarP(&helmName, "helm", "n", "", "helm Name") helmDeployCmd.Flags().StringVarP(&helmNames, "helms", "", "", "helm Names (comma separated) (ex: --helms \"helm1,helm2\")") helmDeployCmd.Flags().StringVarP(&chartVersion, "chart_version", "", "", "helm chart version") - helmDeployCmd.Flags().StringVarP(&chartGitCommitId, "chart_git_commit_id", "", "", "helm chart git commit id") + helmDeployCmd.Flags().StringVarP(&chartGitCommitId, "chart_git_commit_id", "", "", "helm chart git commit id, or 'latest' for the newest commit of the branch") helmDeployCmd.Flags().StringVarP(&valuesOverrideCommitId, "values_override_git_commit_id", "", "", "helm values override git commit id") helmDeployCmd.Flags().BoolVarP(&watchFlag, "watch", "w", false, "Watch helm status until it's ready or an error occurs") } diff --git a/cmd/lifecycle_deploy.go b/cmd/lifecycle_deploy.go index 418aa321..5d59ea0c 100644 --- a/cmd/lifecycle_deploy.go +++ b/cmd/lifecycle_deploy.go @@ -42,7 +42,7 @@ func init() { lifecycleDeployCmd.Flags().StringVarP(&environmentName, "environment", "", "", "Environment Name") lifecycleDeployCmd.Flags().StringVarP(&lifecycleName, "lifecycle", "n", "", "Lifecycle Job Name") lifecycleDeployCmd.Flags().StringVarP(&lifecycleNames, "lifecycles", "", "", "Lifecycle Job Names") - lifecycleDeployCmd.Flags().StringVarP(&lifecycleCommitId, "commit-id", "c", "", "Lifecycle Commit ID") + lifecycleDeployCmd.Flags().StringVarP(&lifecycleCommitId, "commit-id", "c", "", "Lifecycle Commit ID, or 'latest' for the newest commit of the branch") lifecycleDeployCmd.Flags().StringVarP(&lifecycleTag, "tag", "t", "", "Lifecycle Tag") lifecycleDeployCmd.Flags().BoolVarP(&watchFlag, "watch", "w", false, "Watch lifecycle status until it's ready or an error occurs") } diff --git a/cmd/terraform_force_unlock.go b/cmd/terraform_force_unlock.go index 5fee2445..b92af822 100644 --- a/cmd/terraform_force_unlock.go +++ b/cmd/terraform_force_unlock.go @@ -41,6 +41,6 @@ func init() { terraformForceUnlockCmd.Flags().StringVarP(&environmentName, "environment", "", "", "Environment Name") terraformForceUnlockCmd.Flags().StringVarP(&terraformName, "terraform", "n", "", "Terraform Name") terraformForceUnlockCmd.Flags().StringVarP(&terraformNames, "terraforms", "", "", "Terraform Names (comma separated) Example: --terraforms \"tf1,tf2,tf3\"") - terraformForceUnlockCmd.Flags().StringVarP(&terraformCommitId, "commit-id", "c", "", "Git Commit ID (optional, defaults to deployed commit)") + terraformForceUnlockCmd.Flags().StringVarP(&terraformCommitId, "commit-id", "c", "", "Git Commit ID, or 'latest' for the newest commit of the branch (optional, defaults to deployed commit)") terraformForceUnlockCmd.Flags().BoolVarP(&watchFlag, "watch", "w", false, "Watch terraform status until it's ready or an error occurs") } diff --git a/cmd/terraform_migrate_state.go b/cmd/terraform_migrate_state.go index f4cd660a..00960d87 100644 --- a/cmd/terraform_migrate_state.go +++ b/cmd/terraform_migrate_state.go @@ -44,6 +44,6 @@ func init() { terraformMigrateStateCmd.Flags().StringVarP(&environmentName, "environment", "", "", "Environment Name") terraformMigrateStateCmd.Flags().StringVarP(&terraformName, "terraform", "n", "", "Terraform Name") terraformMigrateStateCmd.Flags().StringVarP(&terraformNames, "terraforms", "", "", "Terraform Names (comma separated) Example: --terraforms \"tf1,tf2,tf3\"") - terraformMigrateStateCmd.Flags().StringVarP(&terraformCommitId, "commit-id", "c", "", "Git Commit ID (optional, defaults to deployed commit)") + terraformMigrateStateCmd.Flags().StringVarP(&terraformCommitId, "commit-id", "c", "", "Git Commit ID, or 'latest' for the newest commit of the branch (optional, defaults to deployed commit)") terraformMigrateStateCmd.Flags().BoolVarP(&watchFlag, "watch", "w", false, "Watch terraform status until it's ready or an error occurs") } diff --git a/cmd/terraform_plan.go b/cmd/terraform_plan.go index e12da102..836151c6 100644 --- a/cmd/terraform_plan.go +++ b/cmd/terraform_plan.go @@ -36,6 +36,6 @@ func init() { terraformPlanCmd.Flags().StringVarP(&environmentName, "environment", "", "", "Environment Name") terraformPlanCmd.Flags().StringVarP(&terraformName, "terraform", "n", "", "Terraform Name") terraformPlanCmd.Flags().StringVarP(&terraformNames, "terraforms", "", "", "Terraform Names (comma separated) Example: --terraforms \"tf1,tf2,tf3\"") - terraformPlanCmd.Flags().StringVarP(&terraformCommitId, "commit-id", "c", "", "Git Commit ID (optional, defaults to deployed commit)") + terraformPlanCmd.Flags().StringVarP(&terraformCommitId, "commit-id", "c", "", "Git Commit ID, or 'latest' for the newest commit of the branch (optional, defaults to deployed commit)") terraformPlanCmd.Flags().BoolVarP(&watchFlag, "watch", "w", false, "Watch terraform status until it's ready or an error occurs") } diff --git a/cmd/terraform_plan_and_apply.go b/cmd/terraform_plan_and_apply.go index 2630aec0..613616fd 100644 --- a/cmd/terraform_plan_and_apply.go +++ b/cmd/terraform_plan_and_apply.go @@ -103,6 +103,6 @@ func init() { terraformPlanAndApplyCmd.Flags().StringVarP(&environmentName, "environment", "", "", "Environment Name") terraformPlanAndApplyCmd.Flags().StringVarP(&terraformName, "terraform", "n", "", "Terraform Name") terraformPlanAndApplyCmd.Flags().StringVarP(&terraformNames, "terraforms", "", "", "Terraform Names (comma separated) Example: --terraforms \"tf1,tf2,tf3\"") - terraformPlanAndApplyCmd.Flags().StringVarP(&terraformCommitId, "commit-id", "c", "", "Git Commit ID (optional, defaults to deployed commit)") + terraformPlanAndApplyCmd.Flags().StringVarP(&terraformCommitId, "commit-id", "c", "", "Git Commit ID, or 'latest' for the newest commit of the branch (optional, defaults to deployed commit)") terraformPlanAndApplyCmd.Flags().BoolVarP(&watchFlag, "watch", "w", false, "Watch terraform status until it's ready or an error occurs") } diff --git a/utils/commit_resolver.go b/utils/commit_resolver.go new file mode 100644 index 00000000..a624d2c0 --- /dev/null +++ b/utils/commit_resolver.go @@ -0,0 +1,95 @@ +package utils + +import ( + "context" + "fmt" + "strings" + + "github.com/pterm/pterm" + "github.com/qovery/qovery-client-go" +) + +const LatestCommitKeyword = "latest" + +// IsLatestCommitKeyword reports whether a git commit id input is the reserved +// `latest` keyword instead of a commit SHA. +func IsLatestCommitKeyword(value string) bool { + return strings.EqualFold(strings.TrimSpace(value), LatestCommitKeyword) +} + +// newestCommit returns the commit with the greatest creation date. The API +// documents no ordering, so position in the list is not trusted. +func newestCommit(commits []qovery.Commit) (*qovery.Commit, error) { + if len(commits) == 0 { + return nil, fmt.Errorf("no commit found") + } + + newest := &commits[0] + for i := range commits { + if commits[i].CreatedAt.After(newest.CreatedAt) { + newest = &commits[i] + } + } + + return newest, nil +} + +func ResolveLatestApplicationCommit(client *qovery.APIClient, applicationId string, serviceName string) (string, error) { + commits, _, err := client.ApplicationMainCallsAPI.ListApplicationCommit(context.Background(), applicationId).Execute() + if err != nil { + return "", fmt.Errorf("cannot list commits of application %s: %w", serviceName, err) + } + + return resolveLatestCommit(commits, serviceName) +} + +func ResolveLatestJobCommit(client *qovery.APIClient, jobId string, serviceName string) (string, error) { + commits, _, err := client.JobMainCallsAPI.ListJobCommit(context.Background(), jobId).Execute() + if err != nil { + return "", fmt.Errorf("cannot list commits of job %s: %w", serviceName, err) + } + + return resolveLatestCommit(commits, serviceName) +} + +func ResolveLatestHelmChartCommit(client *qovery.APIClient, helmId string, serviceName string) (string, error) { + commits, _, err := client.HelmMainCallsAPI.ListHelmCommit(context.Background(), helmId).Execute() + if err != nil { + return "", fmt.Errorf("cannot list commits of helm %s: %w", serviceName, err) + } + + return resolveLatestCommit(commits, serviceName) +} + +func ResolveLatestTerraformCommit(client *qovery.APIClient, terraformId string, serviceName string) (string, error) { + commits, _, err := client.TerraformMainCallsAPI.ListTerraformCommit(context.Background(), terraformId).Execute() + if err != nil { + return "", fmt.Errorf("cannot list commits of terraform %s: %w", serviceName, err) + } + + return resolveLatestCommit(commits, serviceName) +} + +func resolveLatestCommit(commits *qovery.CommitResponseList, serviceName string) (string, error) { + if commits == nil { + return "", fmt.Errorf("no commit found for %s", serviceName) + } + + commit, err := newestCommit(commits.GetResults()) + if err != nil { + return "", fmt.Errorf("%w for %s", err, serviceName) + } + + Println(fmt.Sprintf("Resolved latest commit for %s: %s (%s)", + pterm.FgBlue.Sprintf("%s", serviceName), + pterm.FgBlue.Sprintf("%s", commit.GitCommitId), + firstLine(commit.Message), + )) + + return commit.GitCommitId, nil +} + +func firstLine(message string) string { + line, _, _ := strings.Cut(message, "\n") + return strings.TrimSpace(line) +} diff --git a/utils/commit_resolver_test.go b/utils/commit_resolver_test.go new file mode 100644 index 00000000..f02141f9 --- /dev/null +++ b/utils/commit_resolver_test.go @@ -0,0 +1,114 @@ +package utils + +import ( + "testing" + "time" + + "github.com/qovery/qovery-client-go" +) + +func TestIsLatestCommitKeyword(t *testing.T) { + tests := []struct { + value string + expected bool + }{ + {"latest", true}, + {"LATEST", true}, + {"Latest", true}, + {" latest ", true}, + {"\tlatest\n", true}, + {"", false}, + {"a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0", false}, + {"latest-branch", false}, + {"v1.0.0", false}, + } + + for _, test := range tests { + if got := IsLatestCommitKeyword(test.value); got != test.expected { + t.Errorf("IsLatestCommitKeyword(%q) = %v, want %v", test.value, got, test.expected) + } + } +} + +func TestNewestCommit(t *testing.T) { + oldest := commitAt("oldest", "2024-01-01T00:00:00Z") + middle := commitAt("middle", "2024-06-01T00:00:00Z") + newest := commitAt("newest", "2024-12-01T00:00:00Z") + + tests := []struct { + name string + commits []qovery.Commit + expected string + }{ + {"newest first", []qovery.Commit{newest, middle, oldest}, "newest"}, + {"oldest first", []qovery.Commit{oldest, middle, newest}, "newest"}, + {"unordered", []qovery.Commit{middle, newest, oldest}, "newest"}, + {"single commit", []qovery.Commit{middle}, "middle"}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + commit, err := newestCommit(test.commits) + if err != nil { + t.Fatalf("newestCommit returned an error: %v", err) + } + if commit.GitCommitId != test.expected { + t.Errorf("newestCommit = %q, want %q", commit.GitCommitId, test.expected) + } + }) + } +} + +func TestNewestCommitEmptyList(t *testing.T) { + if _, err := newestCommit(nil); err == nil { + t.Error("newestCommit(nil) returned no error, want one") + } + + if _, err := newestCommit([]qovery.Commit{}); err == nil { + t.Error("newestCommit([]) returned no error, want one") + } +} + +func TestNewestCommitTieKeepsFirst(t *testing.T) { + first := commitAt("first", "2024-06-01T00:00:00Z") + second := commitAt("second", "2024-06-01T00:00:00Z") + + commit, err := newestCommit([]qovery.Commit{first, second}) + if err != nil { + t.Fatalf("newestCommit returned an error: %v", err) + } + if commit.GitCommitId != "first" { + t.Errorf("newestCommit = %q, want %q", commit.GitCommitId, "first") + } +} + +func TestFirstLine(t *testing.T) { + tests := []struct { + message string + expected string + }{ + {"feat: add thing", "feat: add thing"}, + {"feat: add thing\n\nlong body\nmore body", "feat: add thing"}, + {" feat: add thing \nbody", "feat: add thing"}, + {"", ""}, + } + + for _, test := range tests { + if got := firstLine(test.message); got != test.expected { + t.Errorf("firstLine(%q) = %q, want %q", test.message, got, test.expected) + } + } +} + +func commitAt(gitCommitId string, createdAt string) qovery.Commit { + parsed, err := time.Parse(time.RFC3339, createdAt) + if err != nil { + panic(err) + } + + return qovery.Commit{ + GitCommitId: gitCommitId, + CreatedAt: parsed, + Message: "commit " + gitCommitId, + } +} diff --git a/utils/qovery.go b/utils/qovery.go index d8b21420..5b7b9ed6 100644 --- a/utils/qovery.go +++ b/utils/qovery.go @@ -1640,7 +1640,13 @@ func DeployApplications(client *qovery.APIClient, envId string, applicationList // if commitId is not set, use the deployed commit id applicationCommitId := application.GitRepository.DeployedCommitId - if commitId != "" { + if IsLatestCommitKeyword(commitId) { + latestCommitId, err := ResolveLatestApplicationCommit(client, application.Id, application.Name) + if err != nil { + return err + } + applicationCommitId = &latestCommitId + } else if commitId != "" { // commitId is set, use it applicationCommitId = &commitId } @@ -1709,7 +1715,13 @@ func DeployJobs(client *qovery.APIClient, envId string, jobList []*qovery.JobRes if docker != nil { mCommitId = docker.GitRepository.DeployedCommitId - if commitId != "" { + if IsLatestCommitKeyword(commitId) { + latestCommitId, err := ResolveLatestJobCommit(client, GetJobId(job), GetJobName(job)) + if err != nil { + return err + } + mCommitId = &latestCommitId + } else if commitId != "" { mCommitId = &commitId } @@ -1809,6 +1821,11 @@ func DeployHelms(client *qovery.APIClient, envId string, helmList []*qovery.Helm return nil } + if IsLatestCommitKeyword(valuesOverrideCommitId) { + return fmt.Errorf("`%s` is not supported for --values_override_git_commit_id: the API exposes no commit list for the values-override repository. "+ + "Pass an explicit commit id, or omit the flag to keep the deployed one", LatestCommitKeyword) + } + var helmsToDeploy []qovery.DeployAllRequestHelmsInner for _, helm := range helmList { @@ -1825,7 +1842,13 @@ func DeployHelms(client *qovery.APIClient, envId string, helmList []*qovery.Helm var mValuesOverrideCommitId *string if gitSource != nil { - if chartGitCommitId != "" { + if IsLatestCommitKeyword(chartGitCommitId) { + latestCommitId, err := ResolveLatestHelmChartCommit(client, helm.Id, helm.Name) + if err != nil { + return err + } + mCommitId = &latestCommitId + } else if chartGitCommitId != "" { mCommitId = &chartGitCommitId } } @@ -1883,17 +1906,24 @@ func DeployTerraforms(client *qovery.APIClient, envId string, terraformList []*q // If action is not nil (PLAN, FORCE_UNLOCK, MIGRATE_STATE), use individual API // DeployAllServices only supports PLAN_AND_APPLY if action != nil { + // resolve every commit id first: a failure on a later service must leave no earlier service triggered + requests := make([]qovery.TerraformDeployRequest, 0, len(terraformList)) for _, terraform := range terraformList { req := qovery.TerraformDeployRequest{ Action: *qovery.NewNullableString(action), } - // Set commit ID if provided - if commitId != "" { - req.GitCommitId = &commitId + terraformCommitId, err := terraformGitCommitId(client, terraform, commitId) + if err != nil { + return err } + req.GitCommitId = terraformCommitId - _, _, err := client.TerraformActionsAPI.DeployTerraform(context.Background(), terraform.Id).TerraformDeployRequest(req).Execute() + requests = append(requests, req) + } + + for i, terraform := range terraformList { + _, _, err := client.TerraformActionsAPI.DeployTerraform(context.Background(), terraform.Id).TerraformDeployRequest(requests[i]).Execute() if err != nil { return err } @@ -1909,10 +1939,11 @@ func DeployTerraforms(client *qovery.APIClient, envId string, terraformList []*q Id: *qovery.NewNullableString(&terraform.Id), } - // Set commit ID if provided - if commitId != "" { - req.GitCommitId = &commitId + terraformCommitId, err := terraformGitCommitId(client, terraform, commitId) + if err != nil { + return err } + req.GitCommitId = terraformCommitId terraformsToDeploy = append(terraformsToDeploy, req) } @@ -1929,6 +1960,23 @@ func DeployTerraforms(client *qovery.APIClient, envId string, terraformList []*q return deployAllServices(client, envId, deployReq) } +// terraformGitCommitId returns the commit id to deploy, or nil when none was requested +func terraformGitCommitId(client *qovery.APIClient, terraform *qovery.TerraformResponse, commitId string) (*string, error) { + if IsLatestCommitKeyword(commitId) { + latestCommitId, err := ResolveLatestTerraformCommit(client, terraform.Id, terraform.Name) + if err != nil { + return nil, err + } + return &latestCommitId, nil + } + + if commitId != "" { + return &commitId, nil + } + + return nil, nil +} + func DeleteTerraforms(client *qovery.APIClient, envId string, terraformList []*qovery.TerraformResponse, skipDestroy bool, resourcesOnly bool) error { if len(terraformList) == 0 { return nil