diff --git a/apptrust/commands/flags.go b/apptrust/commands/flags.go index a5fdaa1..8a50937 100644 --- a/apptrust/commands/flags.go +++ b/apptrust/commands/flags.go @@ -52,6 +52,7 @@ const ( OverwriteStrategyFlag = "overwrite-strategy" TagFlag = "tag" DraftFlag = "draft" + SkipUnassignedFlag = "skip-unassigned" SourceTypeBuildsFlag = "source-type-builds" SourceTypeReleaseBundlesFlag = "source-type-release-bundles" SourceTypeApplicationVersionsFlag = "source-type-application-versions" @@ -94,6 +95,7 @@ var flagsMap = map[string]components.Flag{ OverwriteStrategyFlag: components.NewStringFlag(OverwriteStrategyFlag, "Strategy for handling target artifacts with the same path but different checksum. Supported values: "+coreutils.ListToText(model.OverwriteStrategyValues)+".", func(f *components.StringFlag) { f.Mandatory = false }), TagFlag: components.NewStringFlag(TagFlag, "A tag to associate with the version. Must contain only alphanumeric characters, hyphens (-), underscores (_), and dots (.).", func(f *components.StringFlag) { f.Mandatory = false }), DraftFlag: components.NewBoolFlag(DraftFlag, "Create the application version as a draft.", components.WithBoolDefaultValueFalse()), + SkipUnassignedFlag: components.NewBoolFlag(SkipUnassignedFlag, "Automatically promote the new version to the first lifecycle stage when all of its source artifacts reside in repositories mapped to that stage. Otherwise the version is left unassigned and a message explaining why is returned.", components.WithBoolDefaultValueFalse()), SourceTypeBuildsFlag: components.NewStringFlag(SourceTypeBuildsFlag, "List of semicolon-separated (;) builds in the form of 'name=buildName1, id=runID1[, include-deps=true][, repo-key=repo1][, started=2023-01-01T12:34:56.789+0100]; name=buildName2, id=runID2[, include-deps=true][, repo-key=repo2][, started=2023-01-01T12:34:56.789+0100]' to be included in the new version.", func(f *components.StringFlag) { f.Mandatory = false }), SourceTypeReleaseBundlesFlag: components.NewStringFlag(SourceTypeReleaseBundlesFlag, "List of semicolon-separated (;) release bundles in the form of 'name=releaseBundleName1, version=version1[, project-key=project1][, repo-key=repo1]; name=releaseBundleName2, version=version2[, project-key=project2][, repo-key=repo2]' to be included in the new version.", func(f *components.StringFlag) { f.Mandatory = false }), SourceTypeApplicationVersionsFlag: components.NewStringFlag(SourceTypeApplicationVersionsFlag, "List of semicolon-separated (;) application versions in the form of 'application-key=app1, version=version1; application-key=app2, version=version2' to be included in the new version.", func(f *components.StringFlag) { f.Mandatory = false }), @@ -114,6 +116,7 @@ var commandFlags = map[string][]string{ SyncFlag, TagFlag, DraftFlag, + SkipUnassignedFlag, SourceTypeBuildsFlag, SourceTypeReleaseBundlesFlag, SourceTypeApplicationVersionsFlag, diff --git a/apptrust/commands/version/create_app_version_cmd.go b/apptrust/commands/version/create_app_version_cmd.go index 4c5a93d..eab94ff 100644 --- a/apptrust/commands/version/create_app_version_cmd.go +++ b/apptrust/commands/version/create_app_version_cmd.go @@ -86,6 +86,7 @@ func (cv *createAppVersionCommand) buildRequestPayload(ctx *components.Context) Sources: sources, Tag: ctx.GetStringFlagValue(commands.TagFlag), Draft: ctx.GetBoolFlagValue(commands.DraftFlag), + SkipUnassigned: ctx.GetBoolFlagValue(commands.SkipUnassignedFlag), Filters: filters, }, nil } diff --git a/apptrust/commands/version/create_app_version_cmd_test.go b/apptrust/commands/version/create_app_version_cmd_test.go index 42d91ac..c6366b0 100644 --- a/apptrust/commands/version/create_app_version_cmd_test.go +++ b/apptrust/commands/version/create_app_version_cmd_test.go @@ -173,6 +173,24 @@ func TestCreateAppVersionCommand_FlagsSuite(t *testing.T) { }, }, }, + { + name: "skip-unassigned flag", + ctxSetup: func(ctx *components.Context) { + ctx.Arguments = []string{"app-key", "1.0.0"} + ctx.AddBoolFlag(commands.SkipUnassignedFlag, true) + ctx.AddStringFlag(commands.SourceTypePackagesFlag, "type=npm,name=pkg1,version=1.0.0,repo-key=repo1") + }, + expectsPayload: &model.CreateAppVersionRequest{ + ApplicationKey: "app-key", + Version: "1.0.0", + SkipUnassigned: true, + Sources: &model.CreateVersionSources{ + Packages: []model.CreateVersionPackage{ + {Type: "npm", Name: "pkg1", Version: "1.0.0", Repository: "repo1"}, + }, + }, + }, + }, { name: "spec only", ctxSetup: func(ctx *components.Context) { diff --git a/apptrust/common/keys.go b/apptrust/common/keys.go index 0b03ed6..18025d5 100644 --- a/apptrust/common/keys.go +++ b/apptrust/common/keys.go @@ -8,6 +8,7 @@ var OrderedAppVersionKeys = []string{ "status", "current_stage", "tag", + "message", } // OrderedAppKeys defines the display order for application table output diff --git a/apptrust/model/create_app_version_request.go b/apptrust/model/create_app_version_request.go index e12e24a..b0dcbee 100644 --- a/apptrust/model/create_app_version_request.go +++ b/apptrust/model/create_app_version_request.go @@ -6,6 +6,7 @@ type CreateAppVersionRequest struct { Sources *CreateVersionSources `json:"sources,omitempty"` Tag string `json:"tag,omitempty"` Draft bool `json:"draft,omitempty"` + SkipUnassigned bool `json:"skip_unassigned,omitempty"` Filters *CreateVersionFilters `json:"filters,omitempty"` } diff --git a/e2e/utils/artifactory_utils.go b/e2e/utils/artifactory_utils.go index 10d9ff1..f6989bb 100644 --- a/e2e/utils/artifactory_utils.go +++ b/e2e/utils/artifactory_utils.go @@ -125,6 +125,23 @@ func uploadPackageToArtifactory(t *testing.T, repoKey, buildName, buildNumber st return artifactDetails.Checksums.Sha256 } +func CreateGenericRepoWithEnv(t *testing.T, suffix string, environments []string) string { + servicesManager := getArtifactoryServicesManager(t) + repoKey := GetTestProjectKey(t) + "-" + suffix + localRepoConfig := services.NewGenericLocalRepositoryParams() + localRepoConfig.ProjectKey = GetTestProjectKey(t) + localRepoConfig.Key = repoKey + localRepoConfig.Environments = environments + err := servicesManager.CreateLocalRepository().Generic(localRepoConfig) + require.NoError(t, err) + t.Cleanup(func() { _ = servicesManager.DeleteRepository(repoKey) }) + return repoKey +} + +func UploadTestArtifact(t *testing.T, repoKey, fileName string) string { + return uploadSimpleFileToArtifactory(t, repoKey, fileName) +} + func uploadSimpleFileToArtifactory(t *testing.T, repoKey, targetFileName string) string { tmpFile, err := os.CreateTemp("", "e2e-artifact-*.txt") require.NoError(t, err) @@ -147,6 +164,8 @@ func uploadSimpleFileToArtifactory(t *testing.T, repoKey, targetFileName string) err = summary.Close() require.NoError(t, err) + reindexRepo(t, repoKey) + return targetPath } diff --git a/e2e/version_test.go b/e2e/version_test.go index b37555f..02be0bb 100644 --- a/e2e/version_test.go +++ b/e2e/version_test.go @@ -196,6 +196,66 @@ func TestCreateVersion_Draft(t *testing.T) { assert.Equal(t, utils.StatusDraft, versionContent.Status) } +type skipUnassignedResponse struct { + ApplicationKey string `json:"application_key"` + Version string `json:"version"` + Status string `json:"status"` + CurrentStage string `json:"current_stage"` + Message string `json:"message"` +} + +func TestCreateVersion_SkipUnassigned(t *testing.T) { + appKey := utils.GenerateUniqueKey("app-version-skip-unassigned") + utils.CreateBasicApplication(t, appKey) + defer utils.DeleteApplication(t, appKey) + + t.Run("auto-promotes when source repo is mapped to first stage", func(t *testing.T) { + version := utils.GenerateUniqueKey("skip-ua-ok") + + testPackage := utils.GetTestPackage(t) + packageFlag := fmt.Sprintf("--source-type-packages=type=%s, name=%s, version=%s, repo-key=%s", + testPackage.PackageType, testPackage.PackageName, testPackage.PackageVersion, testPackage.RepoKey) + output := utils.AppTrustCli.RunCliCmdWithOutput(t, "version-create", appKey, version, packageFlag, "--skip-unassigned", "--sync") + defer utils.DeleteApplicationVersion(t, appKey, version) + + require.NotEmpty(t, output) + + var response skipUnassignedResponse + err := json.Unmarshal([]byte(output), &response) + require.NoError(t, err, "failed to parse CLI output as JSON: %s", output) + assert.Equal(t, appKey, response.ApplicationKey) + assert.Equal(t, version, response.Version) + assert.Empty(t, response.Message, "No message means auto-promote succeeded") + + versionContent, statusCode, err := utils.GetApplicationVersion(appKey, version) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, statusCode) + require.NotNil(t, versionContent) + assert.Equal(t, "DEV", versionContent.CurrentStage, "Version should be auto-promoted to DEV stage") + }) + + t.Run("stays unassigned with message when artifact not in first stage", func(t *testing.T) { + version := utils.GenerateUniqueKey("skip-ua-fail") + + repoKey := utils.CreateGenericRepoWithEnv(t, "prod-only-local", []string{"PROD"}) + artifactPath := utils.UploadTestArtifact(t, repoKey, "mismatch-artifact.txt") + + artifactFlag := fmt.Sprintf("--source-type-artifacts=path=%s", artifactPath) + output := utils.AppTrustCli.RunCliCmdWithOutput(t, "version-create", appKey, version, artifactFlag, "--skip-unassigned", "--sync") + defer utils.DeleteApplicationVersion(t, appKey, version) + + require.NotEmpty(t, output) + + var response skipUnassignedResponse + err := json.Unmarshal([]byte(output), &response) + require.NoError(t, err, "failed to parse CLI output as JSON: %s", output) + assert.Equal(t, appKey, response.ApplicationKey) + assert.Equal(t, version, response.Version) + require.NotEmpty(t, response.Message, "A message should explain why auto-promotion did not occur") + assert.Contains(t, response.Message, "not all source artifacts are in repositories mapped to the first stage") + }) +} + func TestCreateVersion_Async(t *testing.T) { appKey := utils.GenerateUniqueKey("app-version-create-async") utils.CreateBasicApplication(t, appKey)