From 292f184d104eb53cf7e044d44b61e288da8fbb7d Mon Sep 17 00:00:00 2001 From: Kanishk Date: Thu, 4 Jun 2026 14:39:47 +0530 Subject: [PATCH 1/2] RTECO-1021 - Enhance metrics visibility by including package manager context --- buildtools/cli.go | 52 ++++----- go.mod | 4 +- go.sum | 8 +- metrics_visibility_test.go | 218 +++++++++++++++++++++++++++++++++++++ 4 files changed, 250 insertions(+), 32 deletions(-) diff --git a/buildtools/cli.go b/buildtools/cli.go index eaaa9ba2d..14da41a45 100644 --- a/buildtools/cli.go +++ b/buildtools/cli.go @@ -676,7 +676,7 @@ func MvnCmd(c *cli.Context) (err error) { } // Create Maven command with empty config for FlexPack mvnCmd := mvn.NewMvnCommand().SetConfigPath("").SetGoals(filteredMavenArgs).SetConfiguration(buildConfiguration) - return commands.Exec(mvnCmd) + return commands.ExecWithPackageManager(mvnCmd, project.Maven.String()) } // If config file is missing and not in native mode, return the standard missing-config error. @@ -729,7 +729,7 @@ func MvnCmd(c *cli.Context) (err error) { } } mvnCmd := mvn.NewMvnCommand().SetConfiguration(buildConfiguration).SetConfigPath(configFilePath).SetGoals(filteredMavenArgs).SetThreads(threads).SetInsecureTls(insecureTls).SetDetailedSummary(detailedSummary || printDeploymentView).SetXrayScan(xrayScan).SetScanOutputFormat(scanOutputFormat) - err = commands.Exec(mvnCmd) + err = commands.ExecWithPackageManager(mvnCmd, project.Maven.String()) result := mvnCmd.Result() defer cliutils.CleanupResult(result, &err) err = cliutils.PrintCommandSummary(mvnCmd.Result(), detailedSummary, printDeploymentView, false, err) @@ -789,7 +789,7 @@ func GradleCmd(c *cli.Context) (err error) { // Create Gradle command with FlexPack (no config file needed) gradleCmd := gradle.NewGradleCommand().SetConfiguration(buildConfiguration).SetTasks(filteredGradleArgs).SetConfigPath("").SetServerDetails(serverDetails) - return commands.Exec(gradleCmd) + return commands.ExecWithPackageManager(gradleCmd, project.Gradle.String()) } // If config file is missing and not in native mode, return the standard missing-config error. @@ -841,7 +841,7 @@ func GradleCmd(c *cli.Context) (err error) { } printDeploymentView := log.IsStdErrTerminal() gradleCmd := gradle.NewGradleCommand().SetConfiguration(buildConfiguration).SetTasks(filteredGradleArgs).SetConfigPath(configFilePath).SetThreads(threads).SetDetailedSummary(detailedSummary || printDeploymentView).SetXrayScan(xrayScan).SetScanOutputFormat(scanOutputFormat) - err = commands.Exec(gradleCmd) + err = commands.ExecWithPackageManager(gradleCmd, project.Gradle.String()) result := gradleCmd.Result() defer cliutils.CleanupResult(result, &err) err = cliutils.PrintCommandSummary(gradleCmd.Result(), detailedSummary, printDeploymentView, false, err) @@ -863,7 +863,7 @@ func YarnCmd(c *cli.Context) error { } yarnCmd := yarn.NewYarnCommand().SetConfigFilePath(configFilePath).SetArgs(c.Args()) - return commands.Exec(yarnCmd) + return commands.ExecWithPackageManager(yarnCmd, project.Yarn.String()) } func pnpmCmd(c *cli.Context) error { @@ -889,7 +889,7 @@ func pnpmCmd(c *cli.Context) error { if err != nil { return err } - return commands.Exec(pnpmCommand) + return commands.ExecWithPackageManager(pnpmCommand, project.Pnpm.String()) default: return runNativePackageManagerCmd("pnpm", append([]string{cmdName}, cleanArgs...)) } @@ -964,7 +964,7 @@ func NugetCmd(c *cli.Context) error { if len(filteredNugetArgs) > 1 { nugetCmd.SetArgAndFlags(filteredNugetArgs[1:]) } - return commands.Exec(nugetCmd) + return commands.ExecWithPackageManager(nugetCmd, project.Nuget.String()) } func DotnetCmd(c *cli.Context) error { @@ -1008,7 +1008,7 @@ func DotnetCmd(c *cli.Context) error { if len(filteredDotnetArgs) > 1 { dotnetCmd.SetArgAndFlags(filteredDotnetArgs[1:]) } - return commands.Exec(dotnetCmd) + return commands.ExecWithPackageManager(dotnetCmd, project.Dotnet.String()) } func getNugetAndDotnetConfigFields(configFilePath string) (rtDetails *coreConfig.ServerDetails, targetRepo string, useNugetV2 bool, err error) { @@ -1058,7 +1058,7 @@ func GoCmd(c *cli.Context) error { args := cliutils.ExtractCommand(c) goCommand := golang.NewGoCommand() goCommand.SetConfigFilePath(configFilePath).SetGoArg(args) - return commands.Exec(goCommand) + return commands.ExecWithPackageManager(goCommand, project.Go.String()) } func GoPublishCmd(c *cli.Context) (err error) { @@ -1077,7 +1077,7 @@ func GoPublishCmd(c *cli.Context) (err error) { printDeploymentView, detailedSummary := log.IsStdErrTerminal(), c.Bool("detailed-summary") goPublishCmd := golang.NewGoPublishCommand() goPublishCmd.SetConfigFilePath(configFilePath).SetBuildConfiguration(buildConfiguration).SetVersion(version).SetDetailedSummary(detailedSummary || printDeploymentView).SetExcludedPatterns(cliutils.GetStringsArrFlagValue(c, "exclusions")) - err = commands.Exec(goPublishCmd) + err = commands.ExecWithPackageManager(goPublishCmd, project.Go.String()) result := goPublishCmd.Result() defer cliutils.CleanupResult(result, &err) err = cliutils.PrintCommandSummary(goPublishCmd.Result(), detailedSummary, printDeploymentView, false, err) @@ -1204,7 +1204,7 @@ func pullCmd(c *cli.Context, image string) error { if !supported { return cliutils.NotSupportedNativeDockerCommand("docker-pull") } - return commands.Exec(PullCommand) + return commands.ExecWithPackageManager(PullCommand, project.Docker.String()) } func pushCmd(c *cli.Context, image string) (err error) { @@ -1228,7 +1228,7 @@ func pushCmd(c *cli.Context, image string) (err error) { if !supported { return cliutils.NotSupportedNativeDockerCommand("docker-push") } - err = commands.Exec(pushCommand) + err = commands.ExecWithPackageManager(pushCommand, project.Docker.String()) result := pushCommand.Result() defer cliutils.CleanupResult(result, &err) err = cliutils.PrintCommandSummary(pushCommand.Result(), detailedSummary, printDeploymentView, false, err) @@ -1266,7 +1266,7 @@ func buildCmd(c *cli.Context) error { buildCommand := container.NewBuildCommand(cleanArgs).SetDockerBuildOptions(dockerOptions).SetBuildConfiguration(buildConfiguration) buildCommand.SetServerDetails(rtDetails) - return commands.Exec(buildCommand) + return commands.ExecWithPackageManager(buildCommand, project.Docker.String()) } func loginCmd(c *cli.Context) error { @@ -1382,7 +1382,7 @@ func huggingFaceUploadCmd(c *cli.Context) error { SetRevision(revision). SetServerDetails(serverDetails). SetBuildConfiguration(buildConfiguration) - return commands.Exec(cmd) + return commands.ExecWithPackageManager(cmd, "huggingface") } func huggingFaceDownloadCmd(c *cli.Context) error { @@ -1432,7 +1432,7 @@ func huggingFaceDownloadCmd(c *cli.Context) error { SetEtagTimeout(etagTimeout). SetServerDetails(serverDetails). SetBuildConfiguration(buildConfiguration) - return commands.Exec(cmd) + return commands.ExecWithPackageManager(cmd, "huggingface") } // validateFolderHasUploadableFiles walks the folder recursively and returns an error @@ -1738,7 +1738,7 @@ func npmGenericCmd(c *cli.Context, cmdName string, collectBuildInfoIfRequested b if err = npmCmd.Init(); err != nil { return err } - return commands.Exec(npmCmd) + return commands.ExecWithPackageManager(npmCmd, project.Npm.String()) } func NpmPublishCmd(c *cli.Context) (err error) { @@ -1764,7 +1764,7 @@ func NpmPublishCmd(c *cli.Context) (err error) { if !detailedSummary { npmCmd.SetDetailedSummary(printDeploymentView) } - err = commands.Exec(npmCmd) + err = commands.ExecWithPackageManager(npmCmd, project.Npm.String()) result := npmCmd.Result() defer cliutils.CleanupResult(result, &err) err = cliutils.PrintCommandSummary(npmCmd.Result(), detailedSummary, printDeploymentView, false, err) @@ -1803,7 +1803,7 @@ func setupCmd(c *cli.Context) (err error) { } } setupCmd.SetServerDetails(artDetails).SetRepoName(repoName).SetProjectKey(cliutils.GetProject(c)) - return commands.Exec(setupCmd) + return commands.ExecWithPackageManager(setupCmd, packageManager.String()) } // validateRepoExists checks if the specified repository exists in Artifactory. @@ -1893,7 +1893,7 @@ func UvCmd(c *cli.Context) error { if cmdName == "help" || cmdName == "" { return uvCommand.Run() } - return commands.Exec(uvCommand) + return commands.ExecWithPackageManager(uvCommand, project.UV.String()) } // HelmCmd executes Helm commands with build info collection support @@ -1938,7 +1938,7 @@ func HelmCmd(c *cli.Context) error { SetWorkingDirectory(workingDir). SetHelmCmdName(cmdName) - return commands.Exec(helmCmd) + return commands.ExecWithPackageManager(helmCmd, project.Helm.String()) } // extractRepositoryCacheFromArgs extracts the --repository-cache flag value from Helm command arguments @@ -2022,7 +2022,7 @@ func ConanCmd(c *cli.Context) error { // Use jfrog-cli-artifactory Conan command with build info support conanCommand := conancommand.NewConanCommand().SetCommandName(cmdName).SetArgs(conanArgs).SetBuildConfiguration(buildConfiguration) - return commands.Exec(conanCommand) + return commands.ExecWithPackageManager(conanCommand, project.Conan.String()) } func NixCmd(c *cli.Context) error { @@ -2069,7 +2069,7 @@ func NixCmd(c *cli.Context) error { cmd.SetServerDetails(serverDetails) } - return commands.Exec(cmd) + return commands.ExecWithPackageManager(cmd, "nix") } func pythonCmd(c *cli.Context, projectType project.ProjectType) error { @@ -2165,15 +2165,15 @@ func pythonCmd(c *cli.Context, projectType project.ProjectType) error { case project.Pip: pipCommand := python.NewPipCommand() pipCommand.SetServerDetails(rtDetails).SetRepo(pythonConfig.TargetRepo()).SetCommandName(cmdName).SetArgs(filteredArgs) - return commands.Exec(pipCommand) + return commands.ExecWithPackageManager(pipCommand, project.Pip.String()) case project.Pipenv: pipenvCommand := python.NewPipenvCommand() pipenvCommand.SetServerDetails(rtDetails).SetRepo(pythonConfig.TargetRepo()).SetCommandName(cmdName).SetArgs(filteredArgs) - return commands.Exec(pipenvCommand) + return commands.ExecWithPackageManager(pipenvCommand, project.Pipenv.String()) case project.Poetry: poetryCommand := python.NewPoetryCommand() poetryCommand.SetServerDetails(rtDetails).SetRepo(pythonConfig.TargetRepo()).SetCommandName(cmdName).SetArgs(filteredArgs) - return commands.Exec(poetryCommand) + return commands.ExecWithPackageManager(poetryCommand, project.Poetry.String()) default: return errorutils.CheckErrorf("%s is not supported", projectType) } @@ -2212,7 +2212,7 @@ func terraformPublishCmd(configFilePath string, args []string, c *cli.Context) e if err := terraformCmd.Init(); err != nil { return err } - err := commands.Exec(terraformCmd) + err := commands.ExecWithPackageManager(terraformCmd, project.Terraform.String()) result := terraformCmd.Result() return cliutils.PrintBriefSummaryReport(result.SuccessCount(), result.FailCount(), cliutils.IsFailNoOp(c), err) } diff --git a/go.mod b/go.mod index e10c4379c..27333e6b3 100644 --- a/go.mod +++ b/go.mod @@ -245,10 +245,10 @@ require ( //replace github.com/ktrysmt/go-bitbucket => github.com/ktrysmt/go-bitbucket v0.9.80 -// replace github.com/jfrog/jfrog-cli-artifactory => github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260416054314-f941180fccc3 +replace github.com/jfrog/jfrog-cli-artifactory => github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260604090426-c24f4507e1e6 // replace github.com/jfrog/build-info-go => github.com/jfrog/build-info-go v1.13.1-0.20260428071432-1e9d9a1991ad -// replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260518123155-036d9195c4e9 +replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260604085947-7c110b77b4b4 //replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go v1.54.2-0.20251007084958-5eeaa42c31a6 diff --git a/go.sum b/go.sum index 6a3ee31a0..f88bf5809 100644 --- a/go.sum +++ b/go.sum @@ -412,10 +412,10 @@ github.com/jfrog/jfrog-apps-config v1.0.1 h1:mtv6k7g8A8BVhlHGlSveapqf4mJfonwvXYL github.com/jfrog/jfrog-apps-config v1.0.1/go.mod h1:8AIIr1oY9JuH5dylz2S6f8Ym2MaadPLR6noCBO4C22w= github.com/jfrog/jfrog-cli-application v1.0.2-0.20260511133105-55a0ab56fd64 h1:bxcy1v1LXQV4T0kVU1duWQr3h7vKfHyMD1B+IuFLWUw= github.com/jfrog/jfrog-cli-application v1.0.2-0.20260511133105-55a0ab56fd64/go.mod h1:cKqb/JgN+XuD4RhOxvSZnyGyXw3cJsTZfQT3rk9MCho= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260603105750-3886c0f01286 h1:IF9Fyhfd7hilnuHO2AezV3lE9SF2FSxRxs4gfcU3f1U= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260603105750-3886c0f01286/go.mod h1:GQEGVW3wT1XPykXNsEiPQrF8/+01JvDVcGGYb5vqJuE= -github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260601130310-8d52a530da18 h1:tPv7XscDFAZaijVwMQNb+HmuucUMYQdjuA5frdGzhF0= -github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260601130310-8d52a530da18/go.mod h1:9R90mhbczGXwW5EGlDs7F08ejQU/xdoDhYHMvzBiqgE= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260604090426-c24f4507e1e6 h1:c2LVapX1OJk9+qV3/sLkVMQTgfa7q6e9+eHQbNy4rf0= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260604090426-c24f4507e1e6/go.mod h1:9T+Ifxc1TtfX2rvMn+QPHnmTrr99aXp7/Hlr9EvFsNE= +github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260604085947-7c110b77b4b4 h1:10kXhD3aWj4Jd6o7BBLzC/eQDelyZjcWEWnbXbL5+eI= +github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260604085947-7c110b77b4b4/go.mod h1:9R90mhbczGXwW5EGlDs7F08ejQU/xdoDhYHMvzBiqgE= github.com/jfrog/jfrog-cli-evidence v0.9.5-0.20260601141509-8df6c9a4bc9b h1:V0FxnU3xh29y8yJHWymm6rPr1MrjG1DdPQlr3ckImwk= github.com/jfrog/jfrog-cli-evidence v0.9.5-0.20260601141509-8df6c9a4bc9b/go.mod h1:t2luv7YHtrKe/Yf1xLZgLOkkiPtk1DsKj0OLXL2GwYo= github.com/jfrog/jfrog-cli-platform-services v1.10.1-0.20260601140139-4cefb6add7b7 h1:wqOk6luH2NFVhK+3p7DyHCa0e5Prie4QQpo5hPyCFwI= diff --git a/metrics_visibility_test.go b/metrics_visibility_test.go index 9a93feca0..2f0058aa5 100644 --- a/metrics_visibility_test.go +++ b/metrics_visibility_test.go @@ -422,6 +422,224 @@ func TestVisibility_GoBuild_Flags(t *testing.T) { } } +// TestVisibility_WirePayload_RtPing prints the full JSON wire payload for a plain +// `jf rt ping` (no package manager). Run with -v to see the logged payload. +// Asserts that package_alias and package_manager are absent from the wire. +func TestVisibility_WirePayload_RtPing(t *testing.T) { + srv, ch := startVisMockServer(t) + defer srv.Close() + + home := t.TempDir() + t.Setenv("JFROG_CLI_HOME_DIR", home) + t.Setenv("JFROG_CLI_REPORT_USAGE", "true") + + jf := coreTests.NewJfrogCli(execMain, "jf", "").WithoutCredentials() + platformURL := srv.URL + "/" + artURL := srv.URL + "/artifactory/" + if err := jf.Exec("c", "add", "mock", "--url", platformURL, "--artifactory-url", artURL, + "--access-token", "dummy", "--interactive=false", "--enc-password=false"); err != nil { + t.Fatalf("config add: %v", err) + } + _ = jf.Exec("c", "use", "mock") + _ = jf.Exec("rt", "ping", "--server-id", "mock") + + select { + case req := <-ch: + t.Logf("WIRE PAYLOAD (rt ping):\n%s", string(req.Body)) + var full map[string]interface{} + if err := json.Unmarshal(req.Body, &full); err != nil { + t.Fatalf("bad JSON: %v", err) + } + labels, _ := full["labels"].(map[string]interface{}) + if v, ok := labels["package_alias"]; ok { + t.Errorf("unexpected package_alias=%q in rt ping payload", v) + } + if v, ok := labels["package_manager"]; ok { + t.Errorf("unexpected package_manager=%q in rt ping payload", v) + } + case <-time.After(10 * time.Second): + t.Fatal("timeout waiting for metric") + } +} + +// TestVisibility_WirePayload_NonPMCommand captures and logs the full JSON wire +// payload for a non-package-manager command (`jf rt curl`). +// Asserts that package_manager and package_alias are absent from the wire. +// This is the negative-scenario counterpart of TestVisibility_WirePayload_GoBuild. +func TestVisibility_WirePayload_NonPMCommand(t *testing.T) { + srv, ch := startVisMockServer(t) + defer srv.Close() + + home := t.TempDir() + t.Setenv("JFROG_CLI_HOME_DIR", home) + t.Setenv("JFROG_CLI_REPORT_USAGE", "true") + + jf := coreTests.NewJfrogCli(execMain, "jf", "").WithoutCredentials() + platformURL := srv.URL + "/" + artURL := srv.URL + "/artifactory/" + if err := jf.Exec("c", "add", "mock", "--url", platformURL, "--artifactory-url", artURL, + "--access-token", "dummy", "--interactive=false", "--enc-password=false"); err != nil { + t.Fatalf("config add: %v", err) + } + _ = jf.Exec("c", "use", "mock") + + // rt curl is a generic RT command — not associated with any package manager + _ = jf.Exec("rt", "curl", "-X", "POST", "/api/system/ping", "--server-id", "mock") + + select { + case req := <-ch: + t.Logf("WIRE PAYLOAD (rt curl — non-PM command):\n%s", string(req.Body)) + var full map[string]interface{} + if err := json.Unmarshal(req.Body, &full); err != nil { + t.Fatalf("bad JSON: %v", err) + } + labels, _ := full["labels"].(map[string]interface{}) + + // package_manager must be absent for non-PM commands (omitempty) + if v, ok := labels["package_manager"]; ok { + t.Errorf("non-PM command sent unexpected package_manager=%q", v) + } + // package_alias must also be absent + if v, ok := labels["package_alias"]; ok { + t.Errorf("non-PM command sent unexpected package_alias=%q", v) + } + // feature_id must be rt_curl + if featureID, _ := labels["feature_id"].(string); featureID != "rt_curl" { + t.Errorf("feature_id: got %q want %q", featureID, "rt_curl") + } + case <-time.After(10 * time.Second): + t.Fatal("timeout waiting for metric") + } +} + +// TestVisibility_WirePayload_GoBuild prints the full JSON wire payload for +// `jf go build` and asserts package_manager=go, package_alias absent. +func TestVisibility_WirePayload_GoBuild(t *testing.T) { + srv, ch := startVisMockServer(t) + defer srv.Close() + + home := t.TempDir() + t.Setenv("JFROG_CLI_HOME_DIR", home) + t.Setenv("JFROG_CLI_REPORT_USAGE", "true") + + jf := coreTests.NewJfrogCli(execMain, "jf", "").WithoutCredentials() + platformURL := srv.URL + "/" + artURL := srv.URL + "/artifactory/" + if err := jf.Exec("c", "add", "mock", "--url", platformURL, "--artifactory-url", artURL, + "--access-token", "dummy", "--interactive=false", "--enc-password=false"); err != nil { + t.Fatalf("config add: %v", err) + } + _ = jf.Exec("c", "use", "mock") + + projDir := t.TempDir() + _ = os.WriteFile(projDir+"/go.mod", []byte("module example.com/jfvis\n\ngo 1.21\n"), 0o644) + _ = os.WriteFile(projDir+"/main.go", []byte("package main\nfunc main(){}\n"), 0o644) + _ = os.MkdirAll(projDir+"/.jfrog/projects", 0o755) + goYaml := []byte("version: 1\ntype: go\nresolver:\n repo: go-virtual\n serverId: mock\ndeployer:\n repo: go-virtual\n serverId: mock\n") + _ = os.WriteFile(projDir+"/.jfrog/projects/go.yaml", goYaml, 0o644) + cwd, _ := os.Getwd() + defer func() { _ = os.Chdir(cwd) }() + _ = os.Chdir(projDir) + + _ = jf.Exec("go", "build", "--server-id", "mock") + + select { + case req := <-ch: + t.Logf("WIRE PAYLOAD (go build):\n%s", string(req.Body)) + var full map[string]interface{} + if err := json.Unmarshal(req.Body, &full); err != nil { + t.Fatalf("bad JSON: %v", err) + } + labels, _ := full["labels"].(map[string]interface{}) + if pm, _ := labels["package_manager"].(string); pm != "go" { + t.Errorf("package_manager: got %q want \"go\" (full: %s)", pm, string(req.Body)) + } + if pa, ok := labels["package_alias"]; ok { + t.Errorf("unexpected package_alias=%q in go build payload", pa) + } + case <-time.After(15 * time.Second): + t.Fatal("timeout waiting for metric") + } +} + +// TestVisibility_GoBuild_PackageManager verifies that direct (non-alias) +// buildtool invocations stamp the package_manager label on the wire payload. +func TestVisibility_GoBuild_PackageManager(t *testing.T) { + srv, ch := startVisMockServer(t) + defer srv.Close() + + home := t.TempDir() + _ = os.Setenv("JFROG_CLI_HOME_DIR", home) + _ = os.Setenv("JFROG_CLI_REPORT_USAGE", "true") + + jf := coreTests.NewJfrogCli(execMain, "jf", "").WithoutCredentials() + + platformURL := srv.URL + "/" + artURL := srv.URL + "/artifactory/" + if err := jf.Exec("c", "add", "mock", "--url", platformURL, "--artifactory-url", artURL, "--access-token", "dummy", "--interactive=false", "--enc-password=false"); err != nil { + t.Fatalf("config add failed: %v", err) + } + if err := jf.Exec("c", "use", "mock"); err != nil { + t.Fatalf("config use failed: %v", err) + } + + projDir := t.TempDir() + if err := os.WriteFile(projDir+"/go.mod", []byte("module example.com/jfvis\n\ngo 1.21\n"), 0o644); err != nil { + t.Fatalf("write go.mod: %v", err) + } + if err := os.WriteFile(projDir+"/main.go", []byte("package main\nfunc main(){}\n"), 0o644); err != nil { + t.Fatalf("write main.go: %v", err) + } + if err := os.MkdirAll(projDir+"/.jfrog/projects", 0o755); err != nil { + t.Fatalf("mkdir .jfrog/projects: %v", err) + } + goYaml := []byte("version: 1\n" + + "type: go\n" + + "resolver:\n" + + " repo: go-virtual\n" + + " serverId: mock\n" + + "deployer:\n" + + " repo: go-virtual\n" + + " serverId: mock\n") + if err := os.WriteFile(projDir+"/.jfrog/projects/go.yaml", goYaml, 0o644); err != nil { + t.Fatalf("write go.yaml: %v", err) + } + cwd, _ := os.Getwd() + defer func(dir string) { + if err := os.Chdir(dir); err != nil { + t.Fatalf("Failed to restore working directory: %v", err) + } + }(cwd) + _ = os.Chdir(projDir) + + _ = jf.Exec("go", "build", "--server-id", "mock") + + select { + case req := <-ch: + if req.Path != "/jfconnect/api/v1/backoffice/metrics/log" { + t.Fatalf("unexpected path: %s", req.Path) + } + var p struct { + Labels struct { + PackageAlias string `json:"package_alias"` + PackageManager string `json:"package_manager"` + } `json:"labels"` + } + if err := json.Unmarshal(req.Body, &p); err != nil { + t.Fatalf("bad JSON: %v\nbody: %s", err, string(req.Body)) + } + if p.Labels.PackageManager != "go" { + t.Errorf("package_manager: got %q want %q (body: %s)", p.Labels.PackageManager, "go", string(req.Body)) + } + // Direct invocation must not set package_alias. + if p.Labels.PackageAlias != "" { + t.Errorf("package_alias: got %q want empty (body: %s)", p.Labels.PackageAlias, string(req.Body)) + } + case <-time.After(15 * time.Second): + t.Fatal("timeout waiting for metric") + } +} + func TestVisibility_PackageAlias_Metrics(t *testing.T) { // This test requires the ghost frog binary and alias setup. homeDir := initGhostFrogTest(t) From 9e719c07f08a8a1eff83003c1fef84f0b1d2b2d4 Mon Sep 17 00:00:00 2001 From: Kanishk Date: Mon, 8 Jun 2026 18:59:41 +0530 Subject: [PATCH 2/2] RTECO-1021 - Enhance commands to support server ID for usage reporting in native mode --- buildtools/cli.go | 81 ++++++++++++++--- go.mod | 2 +- go.sum | 4 +- npm_test.go | 148 +++++++++++++------------------- utils/cliutils/commandsflags.go | 9 +- 5 files changed, 141 insertions(+), 103 deletions(-) diff --git a/buildtools/cli.go b/buildtools/cli.go index 14da41a45..c942e9ce0 100644 --- a/buildtools/cli.go +++ b/buildtools/cli.go @@ -674,8 +674,12 @@ func MvnCmd(c *cli.Context) (err error) { if err != nil { return err } - // Create Maven command with empty config for FlexPack - mvnCmd := mvn.NewMvnCommand().SetConfigPath("").SetGoals(filteredMavenArgs).SetConfiguration(buildConfiguration) + // Maven does not accept --server-id; use the default configured server for usage reporting. + serverDetails, err := coreConfig.GetDefaultServerConf() + if err != nil { + return err + } + mvnCmd := mvn.NewMvnCommand().SetConfigPath("").SetGoals(filteredMavenArgs).SetConfiguration(buildConfiguration).SetServerDetails(serverDetails) return commands.ExecWithPackageManager(mvnCmd, project.Maven.String()) } @@ -1166,7 +1170,7 @@ func dockerCmd(c *cli.Context) error { case "push": err = pushCmd(c, cmdArg) case "login": - err = loginCmd(c) + err = loginCmd(c, true) case "scan": return dockerScanCmd(c, cmdArg) case "build": @@ -1251,7 +1255,7 @@ func buildCmd(c *cli.Context) error { } if !skipLogin { - err = loginCmd(c) + err = loginCmd(c, false) if err != nil { return err } @@ -1269,7 +1273,7 @@ func buildCmd(c *cli.Context) error { return commands.ExecWithPackageManager(buildCommand, project.Docker.String()) } -func loginCmd(c *cli.Context) error { +func loginCmd(c *cli.Context, reportMetrics bool) error { if show, err := cliutils.ShowGenericCmdHelpIfNeeded(c, c.Args(), "dockerloginhelp"); show || err != nil { return err } @@ -1280,6 +1284,19 @@ func loginCmd(c *cli.Context) error { return err } + // Report usage metrics only when invoked as a standalone command. + // When called from buildCmd, metrics are already reported for the build itself. + if reportMetrics { + metricCmdName := "rt_docker_login" + commands.SetPackageManagerContext(project.Docker.String()) + commands.CollectMetrics(metricCmdName, nil) + defer func() { + if rtDetails != nil { + commands.ReportUsage(metricCmdName, rtDetails, nil) + } + }() + } + // extract login specific options user, password, err := extractDockerLoginOptionsFromArgs(c.Args()) if err != nil { @@ -1730,11 +1747,11 @@ func npmGenericCmd(c *cli.Context, cmdName string, collectBuildInfoIfRequested b // Run generic npm command. npmCmd := npm.NewNpmCommand(cmdName, collectBuildInfoIfRequested) - configFilePath, args, err := GetNpmConfigAndArgs(c) + configFilePath, args, useNative, err := GetNpmConfigAndArgs(c) if err != nil { return err } - npmCmd.SetConfigFilePath(configFilePath).SetNpmArgs(args) + npmCmd.SetConfigFilePath(configFilePath).SetNpmArgs(args).SetUseNative(useNative) if err = npmCmd.Init(); err != nil { return err } @@ -1746,13 +1763,13 @@ func NpmPublishCmd(c *cli.Context) (err error) { return err } - configFilePath, args, err := GetNpmConfigAndArgs(c) + configFilePath, args, useNative, err := GetNpmConfigAndArgs(c) if err != nil { return err } npmCmd := npm.NewNpmPublishCommand() - npmCmd.SetConfigFilePath(configFilePath).SetArgs(args) + npmCmd.SetConfigFilePath(configFilePath).SetArgs(args).SetUseNative(useNative) if err = npmCmd.Init(); err != nil { return err } @@ -1828,12 +1845,22 @@ func selectPackageManagerInteractively() (selectedPackageManager project.Project return } -func GetNpmConfigAndArgs(c *cli.Context) (configFilePath string, args []string, err error) { - configFilePath, err = getProjectConfigPathOrThrow(project.Npm, "npm", "npm-config") +func GetNpmConfigAndArgs(c *cli.Context) (configFilePath string, args []string, useNative bool, err error) { + var configExists bool + configFilePath, configExists, err = project.GetProjectConfFilePath(project.Npm) if err != nil { return } _, args = getCommandName(c.Args()) + useNative, args, err = npm.CheckIsNativeAndFetchFilteredArgs(args) + if err != nil { + return + } + if !configExists && !useNative { + configFilePath, err = getProjectConfigPathOrThrow(project.Npm, "npm", "npm-config") + } else if !configExists { + configFilePath = "" + } return } @@ -2011,6 +2038,16 @@ func ConanCmd(c *cli.Context) error { args := cliutils.ExtractCommand(c) + // Extract --server-id (or fall back to default) so usage metrics can be reported. + args, serverID, err := coreutils.ExtractServerIdFromCommand(args) + if err != nil { + return fmt.Errorf("failed to extract server ID: %w", err) + } + serverDetails, err := coreConfig.GetSpecificConfig(serverID, true, false) + if err != nil { + return err + } + // Extract build flags (--build-name, --build-number) before passing to Conan filteredArgs, buildConfiguration, err := build.ExtractBuildDetailsFromArgs(args) if err != nil { @@ -2020,7 +2057,7 @@ func ConanCmd(c *cli.Context) error { cmdName, conanArgs := getCommandName(filteredArgs) // Use jfrog-cli-artifactory Conan command with build info support - conanCommand := conancommand.NewConanCommand().SetCommandName(cmdName).SetArgs(conanArgs).SetBuildConfiguration(buildConfiguration) + conanCommand := conancommand.NewConanCommand().SetCommandName(cmdName).SetArgs(conanArgs).SetBuildConfiguration(buildConfiguration).SetServerDetails(serverDetails) return commands.ExecWithPackageManager(conanCommand, project.Conan.String()) } @@ -2084,12 +2121,32 @@ func pythonCmd(c *cli.Context, projectType project.ProjectType) error { if artutils.ShouldRunNative("") && projectType == project.Poetry { log.Debug("Routing to Poetry native implementation") args := cliutils.ExtractCommand(c) + // Extract --server-id so usage metrics can be reported in native mode. + args, serverID, err := coreutils.ExtractServerIdFromCommand(args) + if err != nil { + return fmt.Errorf("failed to extract server ID: %w", err) + } + serverDetails, err := coreConfig.GetSpecificConfig(serverID, true, false) + if err != nil { + log.Debug("Failed to resolve server for usage reporting:", err.Error()) + } filteredArgs, buildConfiguration, err := build.ExtractBuildDetailsFromArgs(args) if err != nil { return err } cmdName, poetryArgs := getCommandName(filteredArgs) + // Tag usage metric with package_manager=poetry and collect. + // Defer reporting so metrics are sent even when poetry fails. + metricCmdName := "rt_poetry_" + cmdName + commands.SetPackageManagerContext(project.Poetry.String()) + commands.CollectMetrics(metricCmdName, nil) + defer func() { + if serverDetails != nil { + commands.ReportUsage(metricCmdName, serverDetails, nil) + } + }() + // Extract --repository flag for artifact collection (if publishing) deployerRepo := "" for i, arg := range poetryArgs { diff --git a/go.mod b/go.mod index 27333e6b3..4f9dc4c66 100644 --- a/go.mod +++ b/go.mod @@ -245,7 +245,7 @@ require ( //replace github.com/ktrysmt/go-bitbucket => github.com/ktrysmt/go-bitbucket v0.9.80 -replace github.com/jfrog/jfrog-cli-artifactory => github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260604090426-c24f4507e1e6 +replace github.com/jfrog/jfrog-cli-artifactory => github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260608132618-003a2af3b8a3 // replace github.com/jfrog/build-info-go => github.com/jfrog/build-info-go v1.13.1-0.20260428071432-1e9d9a1991ad diff --git a/go.sum b/go.sum index f88bf5809..625cb8924 100644 --- a/go.sum +++ b/go.sum @@ -412,8 +412,8 @@ github.com/jfrog/jfrog-apps-config v1.0.1 h1:mtv6k7g8A8BVhlHGlSveapqf4mJfonwvXYL github.com/jfrog/jfrog-apps-config v1.0.1/go.mod h1:8AIIr1oY9JuH5dylz2S6f8Ym2MaadPLR6noCBO4C22w= github.com/jfrog/jfrog-cli-application v1.0.2-0.20260511133105-55a0ab56fd64 h1:bxcy1v1LXQV4T0kVU1duWQr3h7vKfHyMD1B+IuFLWUw= github.com/jfrog/jfrog-cli-application v1.0.2-0.20260511133105-55a0ab56fd64/go.mod h1:cKqb/JgN+XuD4RhOxvSZnyGyXw3cJsTZfQT3rk9MCho= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260604090426-c24f4507e1e6 h1:c2LVapX1OJk9+qV3/sLkVMQTgfa7q6e9+eHQbNy4rf0= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260604090426-c24f4507e1e6/go.mod h1:9T+Ifxc1TtfX2rvMn+QPHnmTrr99aXp7/Hlr9EvFsNE= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260608132618-003a2af3b8a3 h1:ZZ+TWp1eSmUuS6JMTP6iZuh27ACvMVYz6yE7/p+H1wo= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260608132618-003a2af3b8a3/go.mod h1:9T+Ifxc1TtfX2rvMn+QPHnmTrr99aXp7/Hlr9EvFsNE= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260604085947-7c110b77b4b4 h1:10kXhD3aWj4Jd6o7BBLzC/eQDelyZjcWEWnbXbL5+eI= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260604085947-7c110b77b4b4/go.mod h1:9R90mhbczGXwW5EGlDs7F08ejQU/xdoDhYHMvzBiqgE= github.com/jfrog/jfrog-cli-evidence v0.9.5-0.20260601141509-8df6c9a4bc9b h1:V0FxnU3xh29y8yJHWymm6rPr1MrjG1DdPQlr3ckImwk= diff --git a/npm_test.go b/npm_test.go index ef23067b6..49871fe80 100644 --- a/npm_test.go +++ b/npm_test.go @@ -171,26 +171,24 @@ func testNpmPublishWithNpmrc(t *testing.T, validationFunc func(t *testing.T, npm return } - // Init npm project & npmp command for testing - npmProjectPath := initNpmPublishRcProjectTest(t, projectName) - configFilePath := filepath.Join(npmProjectPath, ".jfrog", "projects", "npm.yaml") + // Create project without a JFrog config file — native mode must not require it. + // npm publish does not require node_modules, so no prepareArtifactoryForNpmBuild needed. + npmProjectPath := filepath.Dir(createNpmProject(t, projectName)) + clientTestUtils.ChangeDirAndAssert(t, npmProjectPath) // fetch module id - packageJsonPath := npmProjectPath + "/package.json" + packageJsonPath := filepath.Join(npmProjectPath, "package.json") moduleName := readModuleId(t, packageJsonPath, npmVersion) - err = createNpmrcForTesting(t, configFilePath) - assert.NoError(t, err) + // Write .npmrc directly — no npm.yaml config file. + assert.NoError(t, writeNpmrcForNativeMode(t, tests.NpmRepo)) if isScoped { addNpmScopeRegistryToNpmRc(t, npmProjectPath, packageJsonPath, npmVersion) } - npmpCmd, err := publishUsingNpmrc(configFilePath, buildNumber) - assert.NoError(t, err) - - result := npmpCmd.Result() - assert.NotNil(t, result) + // Use deprecated --run-native flag to exercise backward-compat path through the CLI layer. + runJfrogCli(t, "npm", "publish", "--run-native=true", "--build-name="+tests.NpmBuildName, "--build-number="+buildNumber) validateNpmLocalBuildInfo(t, tests.NpmBuildName, buildNumber, moduleName) assert.NoError(t, artifactoryCli.Exec("bp", tests.NpmBuildName, buildNumber)) @@ -208,9 +206,15 @@ func testNpmPublishWithNpmrc(t *testing.T, validationFunc func(t *testing.T, npm validationFunc(t, testParams, false) } +// TestNpmInstallClientNative verifies that in native mode the user's .npmrc is +// never modified — no backup/restore, no temp file, mod-time unchanged. +// The test intentionally has no JFrog config file so it exercises the no-config +// native path introduced alongside JFROG_RUN_NATIVE support. func TestNpmInstallClientNative(t *testing.T) { initNpmTest(t) defer cleanNpmTest(t) + defer enableNativeMode(t)() + wd, err := os.Getwd() assert.NoError(t, err, "Failed to get current dir") defer clientTestUtils.ChangeDirAndAssert(t, wd) @@ -222,22 +226,18 @@ func TestNpmInstallClientNative(t *testing.T) { } buildNumber := "1" - npmProjectDirectory := initNpmProjectTest(t) - configFilePath := filepath.Join(npmProjectDirectory, ".jfrog", "projects", "npm.yaml") - err = createNpmrcForTesting(t, configFilePath) - assert.NoError(t, err) - + // Create project without a JFrog config file — native mode must not require it. + npmProjectDirectory := filepath.Dir(createNpmProject(t, "npmproject")) + prepareArtifactoryForNpmBuild(t, npmProjectDirectory) clientTestUtils.ChangeDirAndAssert(t, npmProjectDirectory) + + // Write .npmrc directly; this is the file native mode must leave untouched. + assert.NoError(t, writeNpmrcForNativeMode(t, tests.NpmRemoteRepo)) npmrcFileInfo, err := os.Stat(".npmrc") - if err != nil && os.IsNotExist(err) { - assert.Fail(t, err.Error()) - } + assert.NoError(t, err) - packageJsonPath := npmProjectDirectory + "/package.json" + packageJsonPath := filepath.Join(npmProjectDirectory, "package.json") moduleName := readModuleId(t, packageJsonPath, npmVersion) - // Set JFROG_RUN_NATIVE=true for native npm mode - clientTestUtils.SetEnvAndAssert(t, "JFROG_RUN_NATIVE", "true") - defer clientTestUtils.UnSetEnvAndAssert(t, "JFROG_RUN_NATIVE") runJfrogCli(t, "npm", "i", "--build-name="+tests.NpmBuildName, "--build-number="+buildNumber) validateNpmLocalBuildInfo(t, tests.NpmBuildName, buildNumber, moduleName) @@ -246,7 +246,6 @@ func TestNpmInstallClientNative(t *testing.T) { npmTest := npmTestParams{ testName: "npm with run-native (JFROG_RUN_NATIVE=true)", buildNumber: buildNumber, - npmArgs: "", } validateNpmInstall(t, npmTest, isNpm7(npmVersion)) @@ -255,30 +254,27 @@ func TestNpmInstallClientNative(t *testing.T) { validateIfFileWasEverModified(t, npmrcFileInfo, postTestFileInfo) } -func createNpmrcForTesting(t *testing.T, configFilePath string) (err error) { - npmCommand := npm.NewNpmCommand("install", true) - npmCommand.SetConfigFilePath(configFilePath) - npmCommand.SetServerDetails(serverDetails) - err = npmCommand.Init() - assert.NoError(t, err) - err = npmCommand.PreparePrerequisites(tests.NpmRepo) - assert.NoError(t, err) - err = npmCommand.CreateTempNpmrc() - if err != nil { - return - } - return appendRegistryAuthToNpmrc(t, "") +// writeNpmrcForNativeMode creates a .npmrc in the current directory pointing to +// the given Artifactory npm repo, using serverDetails for auth. +// This simulates a project configured for Artifactory without a JFrog CLI config file. +func writeNpmrcForNativeMode(t *testing.T, repo string) error { + repoURL := utils2.GetNpmRepositoryUrl(repo, serverDetails.ArtifactoryUrl) + // Ensure trailing slash so GetNpmAuthKeyValue produces "//host/path/:_auth", + // matching what npm's nerfDart() generates when looking up credentials. + if !strings.HasSuffix(repoURL, "/") { + repoURL += "/" + } + key, value := utils2.GetNpmAuthKeyValue(serverDetails, repoURL) + require.NotEmpty(t, key, "no auth credentials available in serverDetails") + content := fmt.Sprintf("registry = %s\n%s=%s\n", repoURL, key, value) + return os.WriteFile(".npmrc", []byte(content), 0644) } -// appendRegistryAuthToNpmrc writes a file-based _authToken entry directly into the -// project .npmrc. npm 10 lowercases env-var config keys (_authToken → _authtoken), -// which breaks the scoped auth env var that CreateTempNpmrc sets. +// appendRegistryAuthToNpmrc appends auth credentials for registryURL to the current .npmrc. // If registryURL is empty, reads it from the "registry = " line in the current .npmrc. +// Handles both access token and user/password auth. Ensures trailing slash in the registry +// URL so the nerf-dart key matches what npm's nerfDart() generates during credential lookup. func appendRegistryAuthToNpmrc(t *testing.T, registryURL string) error { - token := serverDetails.AccessToken - if token == "" { - return nil - } if registryURL == "" { data, err := os.ReadFile(".npmrc") if err != nil { @@ -294,16 +290,14 @@ func appendRegistryAuthToNpmrc(t *testing.T, registryURL string) error { if registryURL == "" { return nil } - protocolIdx := strings.Index(registryURL, "://") - if protocolIdx == -1 { - return nil + // Ensure trailing slash so the nerf-dart key matches npm's nerfDart() output. + if !strings.HasSuffix(registryURL, "/") { + registryURL += "/" } - nerfDart := registryURL[protocolIdx+1:] - if !strings.HasSuffix(nerfDart, "/") { - nerfDart += "/" + key, value := utils2.GetNpmAuthKeyValue(serverDetails, registryURL) + if key == "" { + return nil } - authLine := fmt.Sprintf("%s:_authToken=%s\nalways-auth=true\n", nerfDart, token) - f, err := os.OpenFile(".npmrc", os.O_APPEND|os.O_WRONLY, 0644) if err != nil { return err @@ -311,25 +305,10 @@ func appendRegistryAuthToNpmrc(t *testing.T, registryURL string) error { defer func() { assert.NoError(t, f.Close()) }() - _, err = f.WriteString(authLine) + _, err = f.WriteString(fmt.Sprintf("%s=%s\n", key, value)) return err } -func publishUsingNpmrc(configFilePath string, buildNumber string) (npm.NpmPublishCommand, error) { - // Use deprecated --run-native flag to test backward compatibility (shows deprecation warning) - args := []string{"--run-native=true", "--build-name=" + tests.NpmBuildName, "--build-number=" + buildNumber} - npmpCmd := npm.NewNpmPublishCommand() - npmpCmd.SetConfigFilePath(configFilePath).SetArgs(args) - err := npmpCmd.Init() - if err != nil { - return *npmpCmd, err - } - err = commands.Exec(npmpCmd) - if err != nil { - return *npmpCmd, err - } - return *npmpCmd, err -} func readModuleId(t *testing.T, wd string, npmVersion *version.Version) string { packageInfo, err := buildutils.ReadPackageInfoFromPackageJsonIfExists(filepath.Dir(wd), npmVersion) @@ -343,6 +322,10 @@ func addNpmScopeRegistryToNpmRc(t *testing.T, projectPath string, packageJsonPat assert.NoError(t, err) _, registry, err := utils2.GetArtifactoryNpmRepoDetails(tests.NpmScopedRepo, authConfig, false) assert.NoError(t, err) + // Ensure trailing slash so npm's nerfDart() finds the matching auth key. + if !strings.HasSuffix(registry, "/") { + registry += "/" + } scopedRegistry := scope + ":registry=" + registry npmrcFilePath := filepath.Join(projectPath, ".npmrc") func() { @@ -500,13 +483,6 @@ func initNpmProjectTest(t *testing.T) (npmProjectPath string) { return } -func initNpmPublishRcProjectTest(t *testing.T, projectName string) (npmProjectPath string) { - npmProjectPath = filepath.Dir(createNpmProject(t, projectName)) - err := createConfigFileForTest([]string{npmProjectPath}, tests.NpmRemoteRepo, tests.NpmRepo, t, project.Npm, false) - assert.NoError(t, err) - prepareArtifactoryForNpmBuild(t, npmProjectPath) - return -} func initNpmWorkspacesProjectTest(t *testing.T) (npmProjectPath string) { npmProjectPath = filepath.Dir(createNpmProject(t, "npmworkspaces")) @@ -909,21 +885,21 @@ func TestNpmPublishWithWorkspacesRunNative(t *testing.T) { initNpmTest(t) defer cleanNpmTest(t) + defer enableNativeMode(t)() + wd, err := os.Getwd() assert.NoError(t, err, "Failed to get current dir") defer clientTestUtils.ChangeDirAndAssert(t, wd) - // Init npm project & npmp command for testing - npmProjectPath := initNpmWorkspacesProjectTest(t) - configFilePath := filepath.Join(npmProjectPath, ".jfrog", "projects", "npm.yaml") - - // Create npmrc for native functionality - err = createNpmrcForTesting(t, configFilePath) - assert.NoError(t, err) + // Init npm workspaces project without a JFrog config file — native mode must not require it. + npmProjectPath := filepath.Dir(createNpmProject(t, "npmworkspaces")) + testFolder := filepath.Join(filepath.FromSlash(tests.GetTestResourcesPath()), "npm", "npmworkspaces") + assert.NoError(t, biutils.CopyDir(testFolder, npmProjectPath, true, []string{})) + prepareArtifactoryForNpmBuild(t, npmProjectPath) + clientTestUtils.ChangeDirAndAssert(t, npmProjectPath) - // Set JFROG_RUN_NATIVE=true for native npm mode - clientTestUtils.SetEnvAndAssert(t, "JFROG_RUN_NATIVE", "true") - defer clientTestUtils.UnSetEnvAndAssert(t, "JFROG_RUN_NATIVE") + // Write .npmrc directly — no npm.yaml config file. + assert.NoError(t, writeNpmrcForNativeMode(t, tests.NpmRepo)) // Add build info parameters buildName := tests.NpmBuildName + "-workspaces-native" @@ -931,7 +907,7 @@ func TestNpmPublishWithWorkspacesRunNative(t *testing.T) { args := []string{"--workspaces", "--build-name=" + buildName, "--build-number=" + buildNumber} npmpCmd := npm.NewNpmPublishCommand() - npmpCmd.SetConfigFilePath(configFilePath).SetArgs(args) + npmpCmd.SetArgs(args).SetUseNative(true) assert.NoError(t, npmpCmd.Init()) err = commands.Exec(npmpCmd) assert.NoError(t, err) diff --git a/utils/cliutils/commandsflags.go b/utils/cliutils/commandsflags.go index 95ded3d1c..c84421f23 100644 --- a/utils/cliutils/commandsflags.go +++ b/utils/cliutils/commandsflags.go @@ -150,6 +150,7 @@ const ( accessToken = "access-token" serverId = "server-id" serverIdYarn = "server-id-yarn" + serverIdNpm = "server-id-npm" disableTokenRefresh = "disable-token-refresh" passwordStdin = "password-stdin" @@ -753,6 +754,10 @@ var flagsMap = map[string]cli.Flag{ Name: serverId, Usage: "[Optional] Server ID configured using the 'jf config' command. Supported from yarn v4+ in native mode (JFROG_RUN_NATIVE=true).` `", }, + serverIdNpm: cli.StringFlag{ + Name: serverId, + Usage: "[Optional] Server ID configured using the 'jf config' command. Used in native mode (JFROG_RUN_NATIVE=true) to identify the JFrog server for usage reporting.` `", + }, passwordStdin: cli.BoolFlag{ Name: passwordStdin, Usage: "[Default: false] Set to true to provide the password via stdin.` `", @@ -2082,10 +2087,10 @@ var commandFlags = map[string][]string{ global, serverIdResolve, serverIdDeploy, repoResolve, repoDeploy, }, NpmInstallCi: { - BuildName, BuildNumber, module, Project, runNative, + BuildName, BuildNumber, module, Project, runNative, serverIdNpm, }, NpmPublish: { - BuildName, BuildNumber, module, Project, npmDetailedSummary, xrayScan, XrFormat, runNative, npmWorkspaces, + BuildName, BuildNumber, module, Project, npmDetailedSummary, xrayScan, XrFormat, runNative, npmWorkspaces, serverIdNpm, }, PnpmConfig: { global, serverIdResolve, repoResolve,