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/version_test.go b/e2e/version_test.go index b37555f..a2df7d6 100644 --- a/e2e/version_test.go +++ b/e2e/version_test.go @@ -3,9 +3,11 @@ package e2e import ( + "bytes" "encoding/json" "fmt" "net/http" + "os" "strings" "testing" "time" @@ -196,6 +198,97 @@ func TestCreateVersion_Draft(t *testing.T) { assert.Equal(t, utils.StatusDraft, versionContent.Status) } +func TestCreateVersion_SkipUnassigned(t *testing.T) { + appKey := utils.GenerateUniqueKey("app-version-skip-unassigned") + utils.CreateBasicApplication(t, appKey) + defer utils.DeleteApplication(t, appKey) + + testPackage := utils.GetTestPackage(t) + + t.Run("auto-promotes when source repo is mapped to first stage", func(t *testing.T) { + version := utils.GenerateUniqueKey("skip-ua-ok") + + 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") + defer utils.DeleteApplicationVersion(t, appKey, version) + + require.NotEmpty(t, output) + + var response struct { + ApplicationKey string `json:"application_key"` + Version string `json:"version"` + Status string `json:"status"` + CurrentStage string `json:"current_stage"` + } + 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) + + versionContent, statusCode, err := utils.GetApplicationVersion(appKey, version) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, statusCode) + require.NotNil(t, versionContent) + assert.Equal(t, utils.StatusCompleted, versionContent.Status) + 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") + + baseURL := os.Getenv("JFROG_APPTRUST_CLI_TESTS_JFROG_URL") + token := os.Getenv("JFROG_APPTRUST_CLI_TESTS_JFROG_ACCESS_TOKEN") + projectKey := utils.GetTestProjectKey(t) + repoKey := projectKey + "-prod-only-local" + + repoPayload := fmt.Sprintf(`{"key":"%s","rclass":"local","packageType":"generic","environments":["PROD"],"projectKey":"%s"}`, repoKey, projectKey) + req, err := http.NewRequest(http.MethodPut, baseURL+"artifactory/api/repositories/"+repoKey, bytes.NewBufferString(repoPayload)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + resp.Body.Close() + require.True(t, resp.StatusCode >= 200 && resp.StatusCode < 300, "Failed to create PROD-only repo: %d", resp.StatusCode) + t.Cleanup(func() { + delReq, _ := http.NewRequest(http.MethodDelete, baseURL+"artifactory/api/repositories/"+repoKey, nil) + delReq.Header.Set("Authorization", "Bearer "+token) + delResp, _ := http.DefaultClient.Do(delReq) + if delResp != nil { + delResp.Body.Close() + } + }) + + artifactPath := repoKey + "/mismatch-artifact.txt" + uploadReq, err := http.NewRequest(http.MethodPut, baseURL+"artifactory/"+artifactPath, bytes.NewBufferString("mismatch-content")) + require.NoError(t, err) + uploadReq.Header.Set("Authorization", "Bearer "+token) + uploadResp, err := http.DefaultClient.Do(uploadReq) + require.NoError(t, err) + uploadResp.Body.Close() + require.True(t, uploadResp.StatusCode >= 200 && uploadResp.StatusCode < 300, "Failed to upload artifact: %d", uploadResp.StatusCode) + + artifactFlag := fmt.Sprintf("--source-type-artifacts=path=%s", artifactPath) + output := utils.AppTrustCli.RunCliCmdWithOutput(t, "version-create", appKey, version, artifactFlag, "--skip-unassigned") + defer utils.DeleteApplicationVersion(t, appKey, version) + + require.NotEmpty(t, output) + + var response struct { + ApplicationKey string `json:"application_key"` + Version string `json:"version"` + Status string `json:"status"` + Message string `json:"message"` + } + 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.NotEmpty(t, response.Message, "A message should explain why auto-promotion did not occur") + }) +} + func TestCreateVersion_Async(t *testing.T) { appKey := utils.GenerateUniqueKey("app-version-create-async") utils.CreateBasicApplication(t, appKey)