diff --git a/.github/workflows/build-gate.yml b/.github/workflows/build-gate.yml index c1035934b..b823511e3 100644 --- a/.github/workflows/build-gate.yml +++ b/.github/workflows/build-gate.yml @@ -61,6 +61,10 @@ jobs: needs: gate uses: ./.github/workflows/artifactoryTests.yml secrets: inherit + choco: + needs: gate + uses: ./.github/workflows/chocoTests.yml + secrets: inherit conan: needs: gate uses: ./.github/workflows/conanTests.yml diff --git a/.github/workflows/chocoTests.yml b/.github/workflows/chocoTests.yml new file mode 100644 index 000000000..93c5a58fc --- /dev/null +++ b/.github/workflows/chocoTests.yml @@ -0,0 +1,75 @@ +name: Chocolatey Tests + +on: + workflow_call: + workflow_dispatch: + +jobs: + Choco-Tests: + name: Chocolatey tests (windows) + # Chocolatey is a Windows-only package manager, so unlike the other package + # manager suites this one does not run on a matrix. + runs-on: windows-2022 + steps: + - name: Checkout code + uses: actions/checkout@v7 + with: + ref: ${{ github.event.pull_request.head.sha || github.ref }} + # Safe: this workflow only runs after human approval via the build-gate environment. + allow-unsafe-pr-checkout: true + + - name: Setup FastCI + uses: jfrog-fastci/fastci@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + fastci_otel_token: ${{ secrets.FASTCI_TOKEN }} + + # Chocolatey is pre-installed on GitHub-hosted Windows runners (2.7.4 on windows-2022). + # 'jf setup choco' needs 2.0+ for the 'apikey add' verb - on 1.x, a bare 'choco apikey' + # only lists keys - so fail here with a clear message rather than letting every setup + # test fail later on an argument-parsing error. + - name: Verify Chocolatey + shell: pwsh + run: | + $version = (choco --version).Trim() + Write-Host "Chocolatey version: $version" + if ([int]($version -split '\.')[0] -lt 2) { + Write-Error "'jf setup choco' requires Chocolatey 2.0+ for the 'apikey add' verb; found $version" + exit 1 + } + + - name: Setup Go with cache + uses: jfrog/.github/actions/install-go-with-cache@main + + - name: Install local Artifactory + uses: jfrog/.github/actions/install-local-artifactory@main + with: + RTLIC: ${{ secrets.RTLIC }} + RT_CONNECTION_TIMEOUT_SECONDS: ${{ env.RT_CONNECTION_TIMEOUT_SECONDS || '1200' }} + + - name: Run Chocolatey tests + run: >- + go test -v github.com/jfrog/jfrog-cli --timeout 0 --test.choco + ${{ env.JFROG_TESTS_IS_EXTERNAL == 'true' && format('--jfrog.url={0} --jfrog.adminToken={1}', env.JFROG_TESTS_URL, env.JFROG_TESTS_LOCAL_ACCESS_TOKEN) || '' }} + + # The OS gate is the one behaviour that can only be observed where Chocolatey cannot run, so it + # needs a non-Windows job. Deliberately cheap: no Artifactory and no Chocolatey, just the two + # tests asserting that 'jf choco' refuses to run and that 'jf choco --help' still works anyway. + Choco-OS-Gate: + name: Chocolatey OS gate (linux) + runs-on: ubuntu-24.04 + steps: + - name: Checkout code + uses: actions/checkout@v7 + with: + ref: ${{ github.event.pull_request.head.sha || github.ref }} + # Safe: this workflow only runs after human approval via the build-gate environment. + allow-unsafe-pr-checkout: true + + - name: Setup Go with cache + uses: jfrog/.github/actions/install-go-with-cache@main + + - name: Assert jf choco refuses to run on a non-Windows host + run: >- + go test -v github.com/jfrog/jfrog-cli --timeout 0 --test.choco + -run 'TestChocoNonWindowsGate|TestChocoHelpWorksOnAllPlatforms' diff --git a/buildtools/cli.go b/buildtools/cli.go index 9e60d7e8b..5b70f280d 100644 --- a/buildtools/cli.go +++ b/buildtools/cli.go @@ -16,6 +16,7 @@ import ( alpinecommand "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/alpine" aptcommand "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/apt" cargocommand "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/cargo" + chococommand "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/choco" conancommand "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/conan" nixcommand "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/nix" nugetcommand "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/nuget" @@ -64,6 +65,7 @@ import ( "github.com/jfrog/jfrog-cli/docs/buildtools/apkcommand" aptdocs "github.com/jfrog/jfrog-cli/docs/buildtools/apt" "github.com/jfrog/jfrog-cli/docs/buildtools/cargo" + chocodocs "github.com/jfrog/jfrog-cli/docs/buildtools/choco" "github.com/jfrog/jfrog-cli/docs/buildtools/conan" "github.com/jfrog/jfrog-cli/docs/buildtools/conanconfig" "github.com/jfrog/jfrog-cli/docs/buildtools/docker" @@ -248,6 +250,21 @@ func GetCommands() []cli.Command { Category: buildToolsCategory, Action: NugetCmd, }, + { + Name: "choco", + Flags: cliutils.GetCommandFlags(cliutils.Choco), + Usage: corecommon.ResolveDescription(chocodocs.GetDescription(), chocodocs.GetAIDescription()), + HelpName: corecommon.CreateUsage("choco", corecommon.ResolveDescription(chocodocs.GetDescription(), chocodocs.GetAIDescription()), chocodocs.Usage), + UsageText: chocodocs.GetArguments(), + ArgsUsage: common.CreateEnvVars(), + SkipFlagParsing: true, + BashComplete: corecommon.CreateBashCompletionFunc(), + Category: buildToolsCategory, + Action: func(c *cli.Context) error { + cmdName, _ := getCommandName(c.Args()) + return securityCLI.WrapCmdWithCurationPostFailureRun(c, ChocoCmd, techutils.Nuget, cmdName) + }, + }, { Name: "dotnet-config", Flags: cliutils.GetCommandFlags(cliutils.DotnetConfig), @@ -1094,6 +1111,52 @@ func NugetCmd(c *cli.Context) error { return commands.ExecWithPackageManager(nugetCmd, project.Nuget.String()) } +func ChocoCmd(c *cli.Context) error { + if show, err := cliutils.ShowGenericCmdHelpIfNeeded(c, c.Args(), c.Command.Name); show || err != nil { + return err + } + if c.NArg() < 1 { + return cliutils.WrongNumberOfArgumentsHandler(c) + } + args := cliutils.ExtractCommand(c) + args, serverID, err := coreutils.ExtractServerIdFromCommand(args) + if err != nil { + return fmt.Errorf("extract server ID: %w", err) + } + filteredArgs, buildConfiguration, err := build.ExtractBuildDetailsFromArgs(args) + if err != nil { + return err + } + filteredArgs, repoResolve, err := coreutils.ExtractStringOptionFromArgs(filteredArgs, "repo-resolve") + if err != nil { + return fmt.Errorf("extract --repo-resolve: %w", err) + } + filteredArgs, repoDeploy, err := coreutils.ExtractStringOptionFromArgs(filteredArgs, "repo") + if err != nil { + return fmt.Errorf("extract --repo: %w", err) + } + commandName, commandArgs := getCommandName(filteredArgs) + workingDirectory, err := filepath.Abs(".") + if err != nil { + return err + } + command := chococommand.NewChocoFlexPackCommand(). + SetSubCommand(commandName). + SetArgs(commandArgs). + SetRepoResolve(repoResolve). + SetRepoDeploy(repoDeploy). + SetBuildConfiguration(buildConfiguration). + SetWorkingDirectory(workingDirectory) + serverDetails, err := coreConfig.GetSpecificConfig(serverID, true, false) + if err != nil && serverID != "" { + return fmt.Errorf("server-id %q not found: %w", serverID, err) + } + if err == nil { + command.SetServerDetails(serverDetails) + } + return commands.ExecWithPackageManager(command, "choco") +} + func DotnetCmd(c *cli.Context) error { if show, err := cliutils.ShowCmdHelpIfNeeded(c, c.Args()); show || err != nil { return err @@ -1946,6 +2009,11 @@ func setupCmd(c *cli.Context) (err error) { return } } + if packageManager == project.Choco { + if err = setup.ValidateChocoPlatform(); err != nil { + return err + } + } setupCmd := setup.NewSetupCommand(packageManager) artDetails, err := cliutils.CreateArtifactoryDetailsByFlags(c) if err != nil { diff --git a/choco_test.go b/choco_test.go new file mode 100644 index 000000000..405e862f1 --- /dev/null +++ b/choco_test.go @@ -0,0 +1,781 @@ +package main + +import ( + "fmt" + "net/http" + "net/url" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" + "time" + + buildInfo "github.com/jfrog/build-info-go/entities" + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + coreTests "github.com/jfrog/jfrog-cli-core/v2/utils/tests" + "github.com/jfrog/jfrog-cli/inttestutils" + "github.com/jfrog/jfrog-cli/utils/tests" + "github.com/jfrog/jfrog-client-go/http/httpclient" + clientTestUtils "github.com/jfrog/jfrog-client-go/utils/tests" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// chocoCommandProperty is the single module property that 'jf choco' records, holding the +// executed Chocolatey command line with any API key redacted. +const chocoCommandProperty = buildInfo.BuildInfoEnvPrefix + "CHOCO_COMMAND" + +// initChocoTest gates every Chocolatey test. Chocolatey is a Windows-only package manager, and +// both 'jf choco' and 'jf setup choco' fail fast on any other OS, so the tests are skipped +// unless a real 'choco' executable is available. +func initChocoTest(t *testing.T) { + if !*tests.TestChoco { + t.Skip("Skipping Chocolatey test. To run Choco test add the '-test.choco=true' option.") + } + if runtime.GOOS != "windows" { + t.Skipf("Skipping Chocolatey test. Chocolatey runs on Windows only. Detected OS: %s", runtime.GOOS) + } + if _, err := exec.LookPath("choco"); err != nil { + t.Skip("Skipping Chocolatey test. The 'choco' executable was not found in PATH.") + } + createJfrogHomeConfig(t, true) +} + +// runChoco runs a 'jf choco' command. Unlike 'jf nuget', the Chocolatey command has no +// JFROG_RUN_NATIVE gate - it always delegates to the native client - and its flag set has no +// --allow-insecure-connections, so neither is set here. +func runChoco(t *testing.T, args ...string) error { + t.Helper() + jfrogCli := coreTests.NewJfrogCli(execMain, "jfrog", "") + return jfrogCli.Exec(args...) +} + +// createChocoPackageSource writes a minimal but valid Chocolatey package layout into a fresh +// temp directory and returns that directory. A nuspec with neither dependencies nor content is +// rejected (NU5017), so a tools script is packed as content - which is also the conventional +// Chocolatey layout and makes the package safe to install. +func createChocoPackageSource(t *testing.T, id, version string) (packageDir, nuspecName string) { + t.Helper() + packageDir = t.TempDir() + toolsDir := filepath.Join(packageDir, "tools") + require.NoError(t, os.MkdirAll(toolsDir, 0o700)) + installScript := "Write-Host 'jfrog-cli-tests Chocolatey package installed'\n" + require.NoError(t, os.WriteFile(filepath.Join(toolsDir, "chocolateyinstall.ps1"), []byte(installScript), 0o600)) + uninstallScript := "Write-Host 'jfrog-cli-tests Chocolatey package uninstalled'\n" + require.NoError(t, os.WriteFile(filepath.Join(toolsDir, "chocolateyuninstall.ps1"), []byte(uninstallScript), 0o600)) + + nuspecName = id + ".nuspec" + nuspecContent := fmt.Sprintf(` + + + %s + %s + jfrog-cli-tests + jfrog-cli-tests + Test package for jf choco integration tests. + + + + +`, id, version) + require.NoError(t, os.WriteFile(filepath.Join(packageDir, nuspecName), []byte(nuspecContent), 0o600)) + return packageDir, nuspecName +} + +// packChocoPackage runs 'jf choco pack' from inside a generated package directory and returns the +// absolute path of the produced .nupkg. 'choco pack' writes the package into the current working +// directory, and the command snapshots that same directory to discover what it produced, so the +// test must run from there. +func packChocoPackage(t *testing.T, id, version string, extraArgs ...string) (nupkgPath string) { + t.Helper() + packageDir, nuspecName := createChocoPackageSource(t, id, version) + packChocoPackageIn(t, packageDir, nuspecName, extraArgs...) + nupkgPath = filepath.Join(packageDir, id+"."+version+".nupkg") + require.FileExists(t, nupkgPath) + return nupkgPath +} + +// packChocoPackageIn runs 'jf choco pack' with packageDir as the working directory. +func packChocoPackageIn(t *testing.T, packageDir, nuspecName string, extraArgs ...string) { + t.Helper() + workingDirectory, err := os.Getwd() + require.NoError(t, err) + defer clientTestUtils.ChangeDirWithCallback(t, workingDirectory, packageDir)() + args := append([]string{"choco", "pack", nuspecName}, extraArgs...) + require.NoError(t, runChoco(t, args...), "'jf choco pack' should succeed") +} + +// pushChocoPackage runs 'jf choco push' for an already packed .nupkg. +func pushChocoPackage(t *testing.T, nupkgPath, repo string, extraArgs ...string) error { + t.Helper() + args := append([]string{"choco", "push", nupkgPath, "--repo=" + repo}, extraArgs...) + return runChoco(t, args...) +} + +// chocoArtifactPath is the repository-relative path a Chocolatey package lands on. The layout is +// flat: the package sits directly under the repository root, with no version directories. +func chocoArtifactPath(repo, id, version string) string { + return repo + "/" + id + "." + version + ".nupkg" +} + +// assertChocoArtifactExists verifies that a pushed package is retrievable from Artifactory. +func assertChocoArtifactExists(t *testing.T, repoRelativePath string) { + t.Helper() + client, err := httpclient.ClientBuilder().Build() + require.NoError(t, err) + _, res, err := client.GetRemoteFileDetails(serverDetails.ArtifactoryUrl+repoRelativePath, artHttpDetails) + require.NoError(t, err, "pushed Chocolatey package should exist at %s", repoRelativePath) + assert.Equal(t, http.StatusOK, res.StatusCode) +} + +// chocoSourceName mirrors the source name that 'jf setup choco' derives from the Artifactory +// hostname and repository key, so tests can clean the machine-wide source up afterwards. +func chocoSourceName(t *testing.T, repo string) string { + t.Helper() + parsedURL, err := url.Parse(serverDetails.ArtifactoryUrl) + require.NoError(t, err) + return "jfrt-" + sanitizeChocoSourceComponent(parsedURL.Hostname()) + "-" + repo +} + +func sanitizeChocoSourceComponent(value string) string { + var builder strings.Builder + for _, character := range strings.ToLower(value) { + if (character >= 'a' && character <= 'z') || (character >= '0' && character <= '9') || character == '.' || character == '_' || character == '-' { + builder.WriteRune(character) + } else { + builder.WriteByte('-') + } + } + return strings.Trim(builder.String(), "-") +} + +// cleanupChocoSource removes a machine-wide Chocolatey source. Removal is best effort: the source +// may never have been created if the test failed early. +func cleanupChocoSource(t *testing.T, sourceName string) { + t.Helper() + t.Cleanup(func() { + if output, err := exec.Command("choco", "source", "remove", "-n="+sourceName).CombinedOutput(); err != nil { + t.Logf("cleanup: 'choco source remove -n=%s' returned %v: %s", sourceName, err, string(output)) + } + }) +} + +// cleanupChocoInstalledPackage uninstalls a package that a test installed machine-wide. +func cleanupChocoInstalledPackage(t *testing.T, packageID string) { + t.Helper() + t.Cleanup(func() { + if output, err := exec.Command("choco", "uninstall", packageID, "-y").CombinedOutput(); err != nil { + t.Logf("cleanup: 'choco uninstall %s -y' returned %v: %s", packageID, err, string(output)) + } + }) +} + +// getPublishedChocoBuildInfo publishes and then reads back the build info of a Chocolatey build. +func getPublishedChocoBuildInfo(t *testing.T, buildName, buildNumber string) buildInfo.BuildInfo { + t.Helper() + require.NoError(t, artifactoryCli.Exec("bp", buildName, buildNumber)) + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, buildName, buildNumber) + require.NoError(t, err) + require.True(t, found, "build info %s/%s should have been published", buildName, buildNumber) + return publishedBuildInfo.BuildInfo +} + +// getChocoCommandProperty reads the recorded Chocolatey command line out of a module. Module +// properties are typed as interface{} in build-info, so after a publish/fetch round trip they +// arrive as a generic map. +func getChocoCommandProperty(t *testing.T, module buildInfo.Module) string { + t.Helper() + switch properties := module.Properties.(type) { + case nil: + return "" + case map[string]string: + return properties[chocoCommandProperty] + case map[string]interface{}: + value, ok := properties[chocoCommandProperty].(string) + if !ok { + return "" + } + return value + default: + t.Fatalf("unexpected module properties type %T", module.Properties) + return "" + } +} + +// TestChocoPackCollectsArtifacts covers the 'jf choco pack' happy path: the packed .nupkg is +// discovered in the working directory and recorded as a build-info artifact. +func TestChocoPackCollectsArtifacts(t *testing.T) { + initChocoTest(t) + defer cleanTestsHomeEnv() + + id, version := "ChocoPackPkg", "1.0.0" + buildName := tests.ChocoBuildName + "-pack" + buildNumber := "1" + defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + + packChocoPackage(t, id, version, "--build-name="+buildName, "--build-number="+buildNumber) + + collectedBuildInfo := getPublishedChocoBuildInfo(t, buildName, buildNumber) + require.Len(t, collectedBuildInfo.Modules, 1) + module := collectedBuildInfo.Modules[0] + assert.Equal(t, id+":"+version, module.Id, "pack module id should be ':'") + assert.Equal(t, buildInfo.Nuget, module.Type, "Chocolatey packages are NuGet packages") + require.Len(t, module.Artifacts, 1) + artifact := module.Artifacts[0] + assert.Equal(t, id+"."+version+".nupkg", artifact.Name) + assert.Equal(t, "nupkg", artifact.Type) + assert.Equal(t, artifact.Name, artifact.Path, "the Chocolatey layout is flat, so path equals name") + assert.NotEmpty(t, artifact.Sha1) + assert.NotEmpty(t, artifact.Sha256) + assert.NotEmpty(t, artifact.Md5) + assert.Contains(t, getChocoCommandProperty(t, module), "pack", "the executed choco command should be recorded") +} + +// TestChocoPushBuildInfoAndProperties covers the primary end-to-end flow - pack, then push to a +// local repository - and asserts the artifact layout, the published build info and the build +// properties stamped on the uploaded package. +func TestChocoPushBuildInfoAndProperties(t *testing.T) { + initChocoTest(t) + defer cleanTestsHomeEnv() + + id, version := "ChocoPushPkg", "1.0.0" + buildName := tests.ChocoBuildName + "-push" + buildNumber := "1" + defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + + nupkgPath := packChocoPackage(t, id, version) + require.NoError(t, pushChocoPackage(t, nupkgPath, tests.NugetLocalRepo, + "--build-name="+buildName, "--build-number="+buildNumber)) + + artifactPath := chocoArtifactPath(tests.NugetLocalRepo, id, version) + assertChocoArtifactExists(t, artifactPath) + + pushedBuildInfo := getPublishedChocoBuildInfo(t, buildName, buildNumber) + require.Len(t, pushedBuildInfo.Modules, 1) + module := pushedBuildInfo.Modules[0] + assert.Equal(t, id+":"+version, module.Id) + assert.Equal(t, buildInfo.Nuget, module.Type) + require.Len(t, module.Artifacts, 1) + artifact := module.Artifacts[0] + assert.Equal(t, id+"."+version+".nupkg", artifact.Name) + assert.Equal(t, "nupkg", artifact.Type) + assert.Equal(t, tests.NugetLocalRepo, artifact.OriginalDeploymentRepo) + assert.NotEmpty(t, artifact.Sha1) + assert.NotEmpty(t, artifact.Sha256) + assert.NotEmpty(t, artifact.Md5) + assert.Contains(t, getChocoCommandProperty(t, module), "push") + + // Build properties are stamped on the uploaded package so it can be traced back to its build. + properties := getFlexPackItemProps(t, artifactPath) + assert.Equal(t, []string{buildName}, properties["build.name"]) + assert.Equal(t, []string{buildNumber}, properties["build.number"]) + assert.NotEmpty(t, properties["build.timestamp"]) +} + +// TestChocoPushToRemoteRejected verifies that a remote repository is refused as a push target +// before anything is uploaded. +func TestChocoPushToRemoteRejected(t *testing.T) { + initChocoTest(t) + defer cleanTestsHomeEnv() + + nupkgPath := packChocoPackage(t, "ChocoPushRemotePkg", "1.0.0") + err := pushChocoPackage(t, nupkgPath, tests.NugetRemoteRepo) + require.Error(t, err, "pushing to a remote repository must be rejected") + assert.Contains(t, err.Error(), "cannot be used as a Chocolatey push target") +} + +// TestChocoPushSourceRepoMismatchRejected verifies that an explicit --source pointing at one +// repository while --repo names another is rejected, instead of silently pushing to one and +// recording build info against the other. +func TestChocoPushSourceRepoMismatchRejected(t *testing.T) { + initChocoTest(t) + defer cleanTestsHomeEnv() + + nupkgPath := packChocoPackage(t, "ChocoSourceMismatchPkg", "1.0.0") + mismatchedSource := serverDetails.ArtifactoryUrl + "api/nuget/" + tests.NugetVirtualRepo + err := pushChocoPackage(t, nupkgPath, tests.NugetLocalRepo, "-s="+mismatchedSource) + require.Error(t, err, "a --source that disagrees with --repo must be rejected") + assert.Contains(t, err.Error(), "use matching source and --repo values") +} + +// TestChocoPushToVirtualRepoConvention verifies that pushing to a virtual repository forwards to +// its default deployment repository, and that build info records the resolved local repository +// rather than the virtual repository key. +func TestChocoPushToVirtualRepoConvention(t *testing.T) { + initChocoTest(t) + defer cleanTestsHomeEnv() + + id, version := "ChocoVirtualPushPkg", "1.0.0" + buildName := tests.ChocoBuildName + "-virtual-push" + buildNumber := "1" + defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + + nupkgPath := packChocoPackage(t, id, version) + require.NoError(t, pushChocoPackage(t, nupkgPath, tests.NugetVirtualRepo, + "--build-name="+buildName, "--build-number="+buildNumber)) + + // The package must land in the virtual repository's default deployment (local) repository. + assertChocoArtifactExists(t, chocoArtifactPath(tests.NugetLocalRepo, id, version)) + + virtualPushBuildInfo := getPublishedChocoBuildInfo(t, buildName, buildNumber) + require.Len(t, virtualPushBuildInfo.Modules, 1) + require.NotEmpty(t, virtualPushBuildInfo.Modules[0].Artifacts) + for _, artifact := range virtualPushBuildInfo.Modules[0].Artifacts { + assert.Equal(t, tests.NugetLocalRepo, artifact.OriginalDeploymentRepo, + "build info must record the resolved local repository, not the virtual repository key") + } +} + +// TestChocoBuildFlagsValidation covers the two partial build-flag cases. They behave differently: +// --build-name alone is accepted and simply collects no build info, while --build-number without +// --build-name is rejected by jf's CLI-wide flag-pair validation. +func TestChocoBuildFlagsValidation(t *testing.T) { + initChocoTest(t) + defer cleanTestsHomeEnv() + + // Build-info is collected when, and only when, both --build-name and --build-number are given. + // Neither flag is a plain passthrough that must still succeed; half of the pair is a mistake + // worth reporting rather than silently ignoring, and it is reported before choco runs at all. + testCases := []struct { + name string + packageID string + buildName string + extraArgs []string + expectError bool + }{ + { + name: "neither build flag pushes without collecting build info", + packageID: "ChocoNoBuildFlagsPkg", + }, + { + name: "build name without build number is rejected", + packageID: "ChocoNameOnlyPkg", + buildName: tests.ChocoBuildName + "-name-only", + expectError: true, + }, + { + name: "build number without build name is rejected", + packageID: "ChocoNumberOnlyPkg", + extraArgs: []string{"--build-number=1"}, + expectError: true, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + extraArgs := testCase.extraArgs + if testCase.buildName != "" { + extraArgs = append(extraArgs, "--build-name="+testCase.buildName) + t.Cleanup(func() { + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, testCase.buildName, artHttpDetails) + }) + } + nupkgPath := packChocoPackage(t, testCase.packageID, "1.0.0") + err := pushChocoPackage(t, nupkgPath, tests.NugetLocalRepo, extraArgs...) + if testCase.expectError { + require.Error(t, err, "one build flag without the other must be rejected") + assert.Contains(t, err.Error(), "cannot be provided separately") + // The rejection happens before the native command, so nothing was published. + return + } + // A bare passthrough is legal: the package is pushed, there is just no build to publish. + require.NoError(t, err) + assertChocoArtifactExists(t, chocoArtifactPath(tests.NugetLocalRepo, testCase.packageID, "1.0.0")) + }) + } +} + +// TestChocoBuildInfoFromEnvVars verifies that the build name and number can come from the +// standard JFrog CLI environment variables instead of command-line flags. +func TestChocoBuildInfoFromEnvVars(t *testing.T) { + initChocoTest(t) + defer cleanTestsHomeEnv() + + buildName := tests.ChocoBuildName + "-envvars" + buildNumber := "7" + clientTestUtils.SetEnvAndAssert(t, "JFROG_CLI_BUILD_NAME", buildName) + clientTestUtils.SetEnvAndAssert(t, "JFROG_CLI_BUILD_NUMBER", buildNumber) + defer clientTestUtils.UnSetEnvAndAssert(t, "JFROG_CLI_BUILD_NAME") + defer clientTestUtils.UnSetEnvAndAssert(t, "JFROG_CLI_BUILD_NUMBER") + defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + + nupkgPath := packChocoPackage(t, "ChocoEnvVarPkg", "1.0.0") + require.NoError(t, pushChocoPackage(t, nupkgPath, tests.NugetLocalRepo)) + + envVarBuildInfo := getPublishedChocoBuildInfo(t, buildName, buildNumber) + require.Len(t, envVarBuildInfo.Modules, 1) + assert.NotEmpty(t, envVarBuildInfo.Modules[0].Artifacts, + "build info must be collected from JFROG_CLI_BUILD_NAME/NUMBER alone") +} + +// TestChocoModuleOverride verifies that --module replaces the default ':' +// module id with a caller-chosen name. +func TestChocoModuleOverride(t *testing.T) { + initChocoTest(t) + defer cleanTestsHomeEnv() + + buildName := tests.ChocoBuildName + "-module-override" + buildNumber := "1" + defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + + nupkgPath := packChocoPackage(t, "ChocoModuleOverridePkg", "1.0.0") + require.NoError(t, pushChocoPackage(t, nupkgPath, tests.NugetLocalRepo, + "--build-name="+buildName, "--build-number="+buildNumber, "--module=my-service")) + + moduleOverrideBuildInfo := getPublishedChocoBuildInfo(t, buildName, buildNumber) + require.Len(t, moduleOverrideBuildInfo.Modules, 1) + assert.Equal(t, "my-service", moduleOverrideBuildInfo.Modules[0].Id, + "--module must override the default ':' module id") +} + +// TestChocoCommandPropertyRedactsApiKey verifies that a Chocolatey API key passed through to the +// native client is never recorded in build info. +func TestChocoCommandPropertyRedactsApiKey(t *testing.T) { + initChocoTest(t) + defer cleanTestsHomeEnv() + + id, version := "ChocoRedactPkg", "1.0.0" + buildName := tests.ChocoBuildName + "-redact" + buildNumber := "1" + defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + + sourceURL := serverDetails.ArtifactoryUrl + "api/nuget/" + tests.NugetLocalRepo + apiKey := chocoPushApiKey(t) + nupkgPath := packChocoPackage(t, id, version) + // An explicit source and API key suppress the credentials the command would otherwise inject, + // so the native push is driven entirely by these arguments. + require.NoError(t, pushChocoPackage(t, nupkgPath, tests.NugetLocalRepo, + "--build-name="+buildName, "--build-number="+buildNumber, + "-s="+sourceURL, "-k="+apiKey)) + + redactedBuildInfo := getPublishedChocoBuildInfo(t, buildName, buildNumber) + require.Len(t, redactedBuildInfo.Modules, 1) + recordedCommand := getChocoCommandProperty(t, redactedBuildInfo.Modules[0]) + require.NotEmpty(t, recordedCommand) + assert.Contains(t, recordedCommand, "-k=***", "the API key must be redacted in build info") + assert.NotContains(t, recordedCommand, apiKey, "the real credential must never be recorded") +} + +// chocoPushApiKey builds the composite ':' key that Artifactory NuGet endpoints +// expect for authenticated pushes. +func chocoPushApiKey(t *testing.T) string { + t.Helper() + if serverDetails.AccessToken != "" { + return serverDetails.User + ":" + serverDetails.AccessToken + } + return serverDetails.User + ":" + serverDetails.Password +} + +// TestSetupChocoConfiguresSource covers the 'jf setup choco' happy path: the machine-wide +// Chocolatey source for the repository is created and authenticated. +func TestSetupChocoConfiguresSource(t *testing.T) { + initChocoTest(t) + defer cleanTestsHomeEnv() + + sourceName := chocoSourceName(t, tests.NugetVirtualRepo) + cleanupChocoSource(t, sourceName) + + require.NoError(t, runChoco(t, "setup", "choco", "--repo="+tests.NugetVirtualRepo), + "'jf setup choco' should configure the Chocolatey source") + + output, err := exec.Command("choco", "source", "list").CombinedOutput() + require.NoError(t, err, "'choco source list' failed: %s", string(output)) + assert.Contains(t, string(output), sourceName, "the JFrog Chocolatey source should be configured") + // The V2 NuGet endpoint is what Chocolatey speaks; a V3 URL would not work. + assert.Contains(t, string(output), "api/nuget/"+tests.NugetVirtualRepo, + "the source should point at the Artifactory NuGet V2 endpoint") +} + +// TestChocoInstallCollectsDependencies covers the 'jf choco install' happy path end to end: a +// package is packed, pushed, resolved from Artifactory and recorded as a build-info dependency. +func TestChocoInstallCollectsDependencies(t *testing.T) { + initChocoTest(t) + defer cleanTestsHomeEnv() + + id, version := "ChocoInstallPkg", "1.0.0" + buildName := tests.ChocoBuildName + "-install" + buildNumber := "1" + defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + + nupkgPath := packChocoPackage(t, id, version) + require.NoError(t, pushChocoPackage(t, nupkgPath, tests.NugetLocalRepo)) + + sourceName := chocoSourceName(t, tests.NugetLocalRepo) + cleanupChocoSource(t, sourceName) + require.NoError(t, runChoco(t, "setup", "choco", "--repo="+tests.NugetLocalRepo)) + + cleanupChocoInstalledPackage(t, id) + requireChocoInstall(t, id, version, sourceName, buildName, buildNumber) + + installBuildInfo := getPublishedChocoBuildInfo(t, buildName, buildNumber) + require.Len(t, installBuildInfo.Modules, 1) + module := installBuildInfo.Modules[0] + assert.Equal(t, buildInfo.Nuget, module.Type) + require.NotEmpty(t, module.Dependencies, "the installed package must be recorded as a dependency") + dependency := module.Dependencies[0] + assert.Equal(t, id+":"+version, dependency.Id) + assert.Equal(t, "nupkg", dependency.Type) + assert.Equal(t, tests.NugetLocalRepo, dependency.Repository, + "--repo-resolve should be recorded as the resolution repository") + assert.Contains(t, getChocoCommandProperty(t, module), "install") +} + +// requireChocoInstall runs 'jf choco install' with a short retry, because a freshly pushed +// package is not immediately searchable through the NuGet endpoint. +func requireChocoInstall(t *testing.T, id, version, sourceName, buildName, buildNumber string) { + t.Helper() + args := []string{"choco", "install", id, "--version=" + version, "--repo-resolve=" + tests.NugetLocalRepo, + "--build-name=" + buildName, "--build-number=" + buildNumber, "-y", "-s=" + sourceName} + var lastErr error + for attempt := 0; attempt < 5; attempt++ { + if attempt > 0 { + time.Sleep(time.Duration(attempt) * 2 * time.Second) + } + if lastErr = runChoco(t, args...); lastErr == nil { + return + } + t.Logf("'jf choco install' attempt %d failed, retrying: %v", attempt+1, lastErr) + } + require.NoError(t, lastErr, "'jf choco install' should succeed once the pushed package is indexed") +} + +// --------------------------------------------------------------------------------------------- +// Regression tests for three behaviours Chocolatey has that the first implementation pass assumed +// away. Each was verified against Chocolatey's own documentation or source, and each failed before +// the accompanying build-info-go fix. +// --------------------------------------------------------------------------------------------- + +// TestChocoPackRespectsOutputDirectory covers 'choco pack --output-directory'. +// +// The flag exists (--out / --outdir / --outputdirectory / --output-directory), so snapshotting only +// the working directory finds nothing the pack produced. That failure is silent: the command +// succeeds, the package is on disk, and the build info simply has no artifacts. +func TestChocoPackRespectsOutputDirectory(t *testing.T) { + initChocoTest(t) + + const id, version = "JfChocoOutDir", "1.0.0" + buildName := tests.ChocoBuildName + "-outdir" + const buildNumber = "1" + defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + + packageDir, nuspecName := createChocoPackageSource(t, id, version) + outputDir := filepath.Join(packageDir, "build-output") + require.NoError(t, os.MkdirAll(outputDir, 0o700)) + + packChocoPackageIn(t, packageDir, nuspecName, + "--output-directory="+outputDir, + "--build-name="+buildName, "--build-number="+buildNumber) + + require.FileExists(t, filepath.Join(outputDir, id+"."+version+".nupkg"), + "'choco pack' should have written the package into --output-directory") + + publishedBuildInfo := getPublishedChocoBuildInfo(t, buildName, buildNumber) + require.Len(t, publishedBuildInfo.Modules, 1) + require.Len(t, publishedBuildInfo.Modules[0].Artifacts, 1, + "a package written to --output-directory must still be collected; collecting zero artifacts "+ + "here is the silent failure this test exists to catch") + assert.Equal(t, id+"."+version+".nupkg", publishedBuildInfo.Modules[0].Artifacts[0].Name) +} + +// TestChocoPushWithoutPositionalPath covers 'choco push' with no path argument. +// +// The path is optional: with exactly one .nupkg in the folder Chocolatey pushes it. Requiring an +// explicit positional means the package uploads but no artifact is recorded, so it is never +// stamped and never appears in the build info. +func TestChocoPushWithoutPositionalPath(t *testing.T) { + initChocoTest(t) + + const id, version = "JfChocoBarePush", "1.0.0" + buildName := tests.ChocoBuildName + "-bare-push" + const buildNumber = "1" + defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + + nupkgPath := packChocoPackage(t, id, version) + + workingDirectory, err := os.Getwd() + require.NoError(t, err) + defer clientTestUtils.ChangeDirWithCallback(t, workingDirectory, filepath.Dir(nupkgPath))() + + require.NoError(t, runChoco(t, "choco", "push", + "--repo="+tests.NugetLocalRepo, + "--build-name="+buildName, "--build-number="+buildNumber), + "'choco push' with no positional path must succeed") + + publishedBuildInfo := getPublishedChocoBuildInfo(t, buildName, buildNumber) + require.Len(t, publishedBuildInfo.Modules, 1) + require.Len(t, publishedBuildInfo.Modules[0].Artifacts, 1, + "a push with no positional path must still record the package Chocolatey found in the folder") + assert.Equal(t, id+"."+version+".nupkg", publishedBuildInfo.Modules[0].Artifacts[0].Name) + assertChocoArtifactExists(t, chocoArtifactPath(tests.NugetLocalRepo, id, version)) +} + +// TestChocoInstallRecordsVersionFromInstalledPackage is the end-to-end counterpart to +// resolveInstalledPackage's unit tests: Chocolatey stores lib\\.nupkg with no version in +// the file name, so a version read off that name is always empty and every dependency is dropped. +// Asserting the resolved version here proves the version came from the installed .nuspec. +func TestChocoInstallRecordsVersionFromInstalledPackage(t *testing.T) { + initChocoTest(t) + + const id, version = "JfChocoInstalledVersion", "3.4.5" + buildName := tests.ChocoBuildName + "-installed-version" + const buildNumber = "1" + defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + cleanupChocoInstalledPackage(t, id) + + nupkgPath := packChocoPackage(t, id, version) + require.NoError(t, pushChocoPackage(t, nupkgPath, tests.NugetLocalRepo)) + // Confirm the fixture really is published at 3.4.5 before asserting what the install recorded, + // so a wrong recorded version cannot be blamed on a bad fixture. + assertChocoArtifactExists(t, chocoArtifactPath(tests.NugetLocalRepo, id, version)) + + sourceName := chocoSourceName(t, tests.NugetLocalRepo) + cleanupChocoSource(t, sourceName) + require.NoError(t, runChoco(t, "setup", "choco", "--repo="+tests.NugetLocalRepo)) + + // Deliberately no --version: a bare install resolves latest, so the recorded version cannot + // have come from the command line either. + requireChocoInstall(t, id, "", sourceName, buildName, buildNumber) + + publishedBuildInfo := getPublishedChocoBuildInfo(t, buildName, buildNumber) + require.Len(t, publishedBuildInfo.Modules, 1) + require.Len(t, publishedBuildInfo.Modules[0].Dependencies, 1) + assert.Equal(t, id+":"+version, publishedBuildInfo.Modules[0].Dependencies[0].Id, + "the installed version must be resolved from the package's .nuspec, not from its file name") +} + +// --------------------------------------------------------------------------------------------- +// Pass-through, statelessness and the platform gate. +// --------------------------------------------------------------------------------------------- + +// initChocoTestAnyPlatform gates only on the feature flag, for the two scenarios that must be +// observed where Chocolatey cannot run. It deliberately configures no JFrog server: the OS gate +// fires before any server interaction, and requiring one here would hide a regression that moved +// the gate behind server resolution. +func initChocoTestAnyPlatform(t *testing.T) { + t.Helper() + if !*tests.TestChoco { + t.Skip("Skipping Chocolatey test. To run Choco test add the '-test.choco=true' option.") + } +} + +// TestChocoNonWindowsGate asserts the command refuses to run off Windows, naming the detected OS +// instead of surfacing a bare "choco: executable file not found". +func TestChocoNonWindowsGate(t *testing.T) { + initChocoTestAnyPlatform(t) + if runtime.GOOS == "windows" { + t.Skip("The OS gate only rejects non-Windows hosts; nothing to assert on Windows.") + } + + err := runChoco(t, "choco", "install", "some-package") + require.Error(t, err, "'jf choco' must refuse to run on a non-Windows host") + assert.Contains(t, err.Error(), "Windows only") + assert.Containsf(t, err.Error(), runtime.GOOS, "the error must name the detected OS (%s)", runtime.GOOS) +} + +// TestChocoHelpWorksOnAllPlatforms asserts the gate lives in the command's Run(), not in its +// registration, so help stays reachable on the machines that cannot run the tool. +func TestChocoHelpWorksOnAllPlatforms(t *testing.T) { + initChocoTestAnyPlatform(t) + assert.NoError(t, runChoco(t, "choco", "--help"), "'jf choco --help' must work on every OS") +} + +// TestChocoPassThroughCollectsNoBuildInfo covers the subcommands that are not build events. The +// config-mutating ones matter most: 'source', 'apikey' and 'config' change machine state, and a +// wrapper whose contract is that it writes no configuration must not treat them as builds. +func TestChocoPassThroughCollectsNoBuildInfo(t *testing.T) { + initChocoTest(t) + + buildName := tests.ChocoBuildName + "-passthrough" + const buildNumber = "1" + defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + + for _, testCase := range []struct { + name string + args []string + }{ + {"list", []string{"choco", "list", "-r"}}, + {"outdated", []string{"choco", "outdated", "-r"}}, + {"source-list", []string{"choco", "source", "list", "-r"}}, + {"config-list", []string{"choco", "config", "list"}}, + {"feature-list", []string{"choco", "feature", "list"}}, + } { + t.Run(testCase.name, func(t *testing.T) { + // Built as a fresh slice rather than appended onto testCase.args, which would be free + // to reuse that slice's backing array and leak the build flags into the next case. + args := make([]string, 0, len(testCase.args)+2) + args = append(args, testCase.args...) + args = append(args, "--build-name="+buildName, "--build-number="+buildNumber) + // The native exit code is passed through; a query returning non-zero is not this + // test's concern. What matters is that nothing was collected. + _ = runChoco(t, args...) + + _, found, err := tests.GetBuildInfo(serverDetails, buildName, buildNumber) + assert.NoError(t, err) + assert.Falsef(t, found, "'choco %s' is not a build event and must not produce build info", testCase.name) + }) + } +} + +// TestChocoPassThroughWithoutServerConfigured asserts a pass-through command does not require a +// configured JFrog server. Requiring one would make 'jf choco list' fail on any unconfigured +// machine, leaving the wrapper strictly worse than the tool it wraps. +func TestChocoPassThroughWithoutServerConfigured(t *testing.T) { + initChocoTest(t) + + restoreHomeDir := clientTestUtils.SetEnvWithCallbackAndAssert(t, coreutils.HomeDir, t.TempDir()) + defer restoreHomeDir() + + assert.NoError(t, runChoco(t, "choco", "--version"), + "pass-through must work with no JFrog server configured") +} + +// TestChocoDoesNotMutateChocolateyConfig covers the statelessness contract, and the clearest line +// between 'jf choco' and 'jf setup choco'. chocolatey.config is machine-wide, so an accidental +// write by the wrapper would change behaviour for every user on the machine. +func TestChocoDoesNotMutateChocolateyConfig(t *testing.T) { + initChocoTest(t) + + const id, version = "JfChocoStateless", "1.0.0" + buildName := tests.ChocoBuildName + "-stateless" + const buildNumber = "1" + defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + + sourcesBefore, err := exec.Command("choco", "source", "list", "-r").CombinedOutput() + require.NoError(t, err) + + packageDir, nuspecName := createChocoPackageSource(t, id, version) + packChocoPackageIn(t, packageDir, nuspecName, "--build-name="+buildName, "--build-number="+buildNumber) + + assert.NoDirExists(t, filepath.Join(packageDir, ".jfrog", "projects"), + "FlexPack is stateless: 'jf choco' must not create a .jfrog/projects directory") + + sourcesAfter, err := exec.Command("choco", "source", "list", "-r").CombinedOutput() + require.NoError(t, err) + assert.Equal(t, string(sourcesBefore), string(sourcesAfter), + "'jf choco' must not modify Chocolatey's machine-wide source list; that is 'jf setup choco's job") +} + +// TestChocoNoBuildFlagsCollectsNothing asserts that without both build coordinates the command is a +// transparent native run: the package is produced, and nothing is published. +func TestChocoNoBuildFlagsCollectsNothing(t *testing.T) { + initChocoTest(t) + + const id, version = "JfChocoNoFlags", "1.0.0" + nupkgPath := packChocoPackage(t, id, version) + assert.FileExists(t, nupkgPath, "the native pack must still run and produce the package") + + _, found, err := tests.GetBuildInfo(serverDetails, tests.ChocoBuildName+"-noflags", "1") + assert.NoError(t, err) + assert.False(t, found, "no build info may be published when the build flags are absent") +} + +// TestChocoPushToNonexistentRepoRejected asserts an unknown repository fails clearly rather than +// surfacing a raw Artifactory 404 from deep inside the push. +func TestChocoPushToNonexistentRepoRejected(t *testing.T) { + initChocoTest(t) + + nupkgPath := packChocoPackage(t, "JfChocoBadRepo", "1.0.0") + require.Error(t, pushChocoPackage(t, nupkgPath, "cli-choco-nonexistent-repo")) +} diff --git a/docs/buildtools/choco/help.go b/docs/buildtools/choco/help.go new file mode 100644 index 000000000..26d7d3671 --- /dev/null +++ b/docs/buildtools/choco/help.go @@ -0,0 +1,32 @@ +package choco + +var Usage = []string{"choco [command options]"} + +func GetDescription() string { + return "Run Chocolatey with optional JFrog build-info collection." +} + +func GetArguments() string { + return ` choco sub-command + Arguments and options for the native Chocolatey command.` +} + +func GetAIDescription() string { + return `Run a native Chocolatey command through JFrog. The command forwards Chocolatey arguments unchanged and optionally records build-info for pack, push, install, and upgrade. + +Prerequisites: +- Chocolatey installed on Windows. +- Run 'jf setup choco' to add authenticated Artifactory sources, or pass the native Chocolatey source/authentication options yourself. + +Examples: + $ jf choco install git -s=jfrt-acme.jfrog.io-choco-virtual --build-name=image --build-number=1 + $ jf choco push tool.1.0.0.nupkg -s=jfrt-acme.jfrog.io-choco-local --repo=choco-local --build-name=tool --build-number=1 + +Gotchas: +- Chocolatey runs on Windows only. +- Build-info is collected only when both '--build-name' and '--build-number' are given; supplying just one is an error. +- The native '-s' source and JFrog '--repo' must identify the same Artifactory endpoint for push build-info. +- Install and upgrade record the requested packages and their transitive dependencies, read from Chocolatey's lib directory. A dependency served by a Chocolatey special source (ruby, cygwin, python, windowsfeatures) never lands there, so it is reported as not found and left out of the build-info. +- In CI, set '--execution-timeout' explicitly. Chocolatey's default of 2700 seconds is mishandled by Chocolatey itself and becomes a five-hour timeout, so a hung install stalls the build instead of failing it. '--execution-timeout=2700' is enough to avoid this. +- This command never writes Chocolatey configuration. Use 'jf setup choco' for that.` +} diff --git a/docs/buildtools/setup/help.go b/docs/buildtools/setup/help.go index 0a8d7eaca..bc989c2f9 100644 --- a/docs/buildtools/setup/help.go +++ b/docs/buildtools/setup/help.go @@ -34,6 +34,7 @@ Not the same command as jf npm-config / jf mvn-config / jf pip-config, which loo Prerequisites: - A configured server (jf c add or jf login), or pass --url/--user/--password/--access-token directly. - The Artifactory repository name for the package manager (a virtual repo where supported). +- For Chocolatey, select a NuGet virtual, local, or remote repository when prompted. Setup creates a named native source ('jfrt--') for each repository, so use -s to choose a configured resolve or publish endpoint. Common patterns: $ jf setup npm @@ -49,6 +50,7 @@ Gotchas: - pnpm and npm can end up on different repositories without any warning. pnpm reads its own configuration first and ~/.npmrc only as a fallback, so a machine with no pnpm setup follows "jf setup npm", but once "jf setup pnpm" has run, a later "jf setup npm --repo b" moves npm alone and pnpm keeps resolving from the repository it was given. If both are in use, run "jf setup" for both. - maven and gradle do not need their client installed: their setup writes settings.xml and a Gradle init script directly, so it works on a machine that only has ./mvnw or ./gradlew. Every other package manager's setup runs its client. helm additionally needs 3.8.0 or newer, because its login targets an OCI registry. - docker/podman authenticate directly against the registry and skip the repository prompt entirely (no --repo needed); helm still goes through repository selection like the other package managers even though its login step doesn't end up using the repo name. +- Chocolatey runs on Windows only and updates machine-wide chocolatey.config, so use an elevated shell. Re-running 'jf setup choco' for one repository refreshes only that repository's 'jfrt--' source; it preserves other Chocolatey sources. Related: jf npm-config, jf go-config, jf pip-config, jf c add` } diff --git a/go.mod b/go.mod index 8d2b4d8b0..f9a664c7f 100644 --- a/go.mod +++ b/go.mod @@ -19,11 +19,11 @@ require ( github.com/buger/jsonparser v1.3.0 github.com/gocarina/gocsv v0.0.0-20260607070740-0735908c6461 github.com/jfrog/archiver/v3 v3.6.5 - github.com/jfrog/build-info-go v1.13.1-0.20260902120316-b325d342b210 + github.com/jfrog/build-info-go v1.13.1-0.20260910033343-40d564cbd202 github.com/jfrog/gofrog v1.7.7 github.com/jfrog/jfrog-cli-application v1.0.2-0.20260820134442-c8629258ff3a - github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260908123315-10cfe381853a - github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260909093400-32a7208a18bd + github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260910035002-9e5eaa824782 + github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260910032555-f560dade8e04 github.com/jfrog/jfrog-cli-evidence v0.11.1-0.20260824063609-79b735ec565e github.com/jfrog/jfrog-cli-platform-services v1.10.1-0.20260618062042-6053ab368cab github.com/jfrog/jfrog-cli-security v1.37.0 @@ -221,14 +221,14 @@ require ( go.opentelemetry.io/otel/trace v1.45.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect - golang.org/x/crypto v0.54.0 // indirect - golang.org/x/mod v0.38.0 // indirect + golang.org/x/crypto v0.56.0 // indirect + golang.org/x/mod v0.40.0 // indirect golang.org/x/net v0.57.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.22.0 // indirect golang.org/x/sys v0.47.0 // indirect golang.org/x/term v0.45.0 // indirect - golang.org/x/text v0.40.0 // indirect + golang.org/x/text v0.41.0 // indirect golang.org/x/time v0.15.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d // indirect diff --git a/go.sum b/go.sum index 7320371b7..648984699 100644 --- a/go.sum +++ b/go.sum @@ -390,8 +390,8 @@ github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4= github.com/jfrog/archiver/v3 v3.6.5 h1:AiNXJoe8jYDOtyykfVuwh26aM4rk/ei+YzBpfBukdzU= github.com/jfrog/archiver/v3 v3.6.5/go.mod h1:5V9l+Fte30Y4qe9dUOAd3yNTf8lmtVNuhKNrvI8PMhg= -github.com/jfrog/build-info-go v1.13.1-0.20260902120316-b325d342b210 h1:u1Ijj6fOX9hCzz27L3IpqFqdSsjOlg6Td7URtmNFVR8= -github.com/jfrog/build-info-go v1.13.1-0.20260902120316-b325d342b210/go.mod h1:CYRUCvLKfyARjoJXLWAxce1qNUxTEtbRKAARkV42vpE= +github.com/jfrog/build-info-go v1.13.1-0.20260910033343-40d564cbd202 h1:XD2+J0bYh6ZaEJYV6uRvfrrDIFFzdCGSEnp4CKvrJbA= +github.com/jfrog/build-info-go v1.13.1-0.20260910033343-40d564cbd202/go.mod h1:CYRUCvLKfyARjoJXLWAxce1qNUxTEtbRKAARkV42vpE= github.com/jfrog/froggit-go v1.23.1 h1:4wmaHeuptxVINbovMaeITzVhi3+VQoc/FFIjF4axzu0= github.com/jfrog/froggit-go v1.23.1/go.mod h1:wRDryqyp3oe+eHgME2mpnEQmO8XBECIPagFwj0nHmdI= github.com/jfrog/go-mockhttp v0.3.1 h1:/wac8v4GMZx62viZmv4wazB5GNKs+GxawuS1u3maJH8= @@ -402,10 +402,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.20260820134442-c8629258ff3a h1:7GhcPfi+k9oOAJdCsKWjymnqH0e7DQZ1soVhbneYnGY= github.com/jfrog/jfrog-cli-application v1.0.2-0.20260820134442-c8629258ff3a/go.mod h1:p8yLtbmCxxQucIbLZKnWu0F+EDtj6NLXbRQCEK/nb6o= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260908123315-10cfe381853a h1:Bp/vFrvUHdy4NEKcE3cZ5Dkz4S3IJtSIOv8PtUPbrTQ= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260908123315-10cfe381853a/go.mod h1:Oiq1Gc1RtmaDBmpVMQuG3XlmRAWXkA5bfRraHLwbZF0= -github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260909093400-32a7208a18bd h1:tfC6CtOpqWoU/1V4ymBL0GnhM2kbq5JTGSaAk9eOybg= -github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260909093400-32a7208a18bd/go.mod h1:SwV+DNLBnWLxBeNeZpJk+xxAbqJ8ywq1va56up+AGu4= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260910035002-9e5eaa824782 h1:fgHYLhpVRQFYd8Uoo9Lv9lfMMwxr7flph8v7m6ulzZY= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260910035002-9e5eaa824782/go.mod h1:rCQHDC+q2hak/Jerz5ptrxCK3P+ONqFnJRW6eX0yNXg= +github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260910032555-f560dade8e04 h1:2SZDAl1CRqNaXB6iecU1Z+aPiFLLXNS60seZgZPY0hI= +github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260910032555-f560dade8e04/go.mod h1:SwV+DNLBnWLxBeNeZpJk+xxAbqJ8ywq1va56up+AGu4= github.com/jfrog/jfrog-cli-evidence v0.11.1-0.20260824063609-79b735ec565e h1:+QYbewvK+PZKbfPpxYmy0bewhqMFtJPk/tUbCICjf8U= github.com/jfrog/jfrog-cli-evidence v0.11.1-0.20260824063609-79b735ec565e/go.mod h1:I83k7IH/cmMh00LyZOp13jctj8/pPKVHSAlu7GMJgwI= github.com/jfrog/jfrog-cli-platform-services v1.10.1-0.20260618062042-6053ab368cab h1:Zn/qB8LYhSu82YDtbqXwErN1RPHTHe/a3gQY6Ti/OBE= @@ -758,14 +758,14 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= -golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= +golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y= +golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I= golang.org/x/exp v0.0.0-20260727155853-b88d891fe743 h1:ex206bKw+v3K0dm3andkrIF+ijyQKJG1pLgwQ2PYdQM= golang.org/x/exp v0.0.0-20260727155853-b88d891fe743/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= -golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= +golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= +golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -834,16 +834,16 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= -golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= -golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= +golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= +golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= diff --git a/main_test.go b/main_test.go index 640018f10..3d6c32e7a 100644 --- a/main_test.go +++ b/main_test.go @@ -77,7 +77,7 @@ func setupIntegrationTests() { InitArtifactoryTests() } - if *tests.TestNpm || *tests.TestPnpm || *tests.TestCargo || *tests.TestGradle || *tests.TestMaven || *tests.TestGo || *tests.TestNuget || *tests.TestPip || *tests.TestPipenv || *tests.TestPoetry || *tests.TestConan || *tests.TestHelm || *tests.TestUv || *tests.TestNix || *tests.TestApt || *tests.TestAlpine || *tests.TestApm || *tests.TestRuby || (*tests.TestArtifactory && !*tests.TestArtifactoryProxy) || *tests.TestArtifactoryProject { + if *tests.TestNpm || *tests.TestPnpm || *tests.TestCargo || *tests.TestGradle || *tests.TestMaven || *tests.TestGo || *tests.TestNuget || *tests.TestChoco || *tests.TestPip || *tests.TestPipenv || *tests.TestPoetry || *tests.TestConan || *tests.TestHelm || *tests.TestUv || *tests.TestNix || *tests.TestApt || *tests.TestAlpine || *tests.TestApm || *tests.TestRuby || (*tests.TestArtifactory && !*tests.TestArtifactoryProxy) || *tests.TestArtifactoryProject { InitBuildToolsTests() } if *tests.TestDocker || *tests.TestPodman || *tests.TestDockerScan { @@ -125,7 +125,7 @@ func tearDownIntegrationTests() { if (*tests.TestArtifactory && !*tests.TestArtifactoryProxy) || *tests.TestArtifactoryProject { CleanArtifactoryTests() } - if *tests.TestNpm || *tests.TestPnpm || *tests.TestGradle || *tests.TestMaven || *tests.TestGo || *tests.TestNuget || *tests.TestPip || *tests.TestPipenv || *tests.TestPoetry || *tests.TestConan || *tests.TestHelm || *tests.TestNix || *tests.TestCargo || *tests.TestApt || *tests.TestAlpine || *tests.TestApm || *tests.TestRuby || *tests.TestDocker || *tests.TestPodman || *tests.TestDockerScan || (*tests.TestArtifactory && !*tests.TestArtifactoryProxy) || *tests.TestArtifactoryProject { + if *tests.TestNpm || *tests.TestPnpm || *tests.TestGradle || *tests.TestMaven || *tests.TestGo || *tests.TestNuget || *tests.TestChoco || *tests.TestPip || *tests.TestPipenv || *tests.TestPoetry || *tests.TestConan || *tests.TestHelm || *tests.TestNix || *tests.TestCargo || *tests.TestApt || *tests.TestAlpine || *tests.TestApm || *tests.TestRuby || *tests.TestDocker || *tests.TestPodman || *tests.TestDockerScan || (*tests.TestArtifactory && !*tests.TestArtifactoryProxy) || *tests.TestArtifactoryProject { CleanBuildToolsTests() } if *tests.TestDistribution { diff --git a/utils/cliutils/commandsflags.go b/utils/cliutils/commandsflags.go index 131210556..2f498ca84 100644 --- a/utils/cliutils/commandsflags.go +++ b/utils/cliutils/commandsflags.go @@ -64,6 +64,7 @@ const ( Yarn = "yarn" NugetConfig = "nuget-config" Nuget = "nuget" + Choco = "choco" Dotnet = "dotnet" DotnetConfig = "dotnet-config" Go = "go" @@ -2259,6 +2260,9 @@ var commandFlags = map[string][]string{ Nuget: { BuildName, BuildNumber, module, Project, allowInsecureConnections, serverId, repoResolve, repo, nugetV2, }, + Choco: { + BuildName, BuildNumber, module, Project, serverId, repoResolve, repo, + }, DotnetConfig: { global, serverIdResolve, repoResolve, nugetV2, }, diff --git a/utils/tests/consts.go b/utils/tests/consts.go index 12f9928f2..a6875cedf 100644 --- a/utils/tests/consts.go +++ b/utils/tests/consts.go @@ -292,6 +292,7 @@ var ( PnpmBuildName = "cli-pnpm-build" YarnBuildName = "cli-yarn-build" NuGetBuildName = "cli-nuget-build" + ChocoBuildName = "cli-choco-build" PipBuildName = "cli-pip-build" PipenvBuildName = "cli-pipenv-build" PoetryBuildName = "cli-poetry-build" diff --git a/utils/tests/utils.go b/utils/tests/utils.go index f4acd5408..915601f90 100644 --- a/utils/tests/utils.go +++ b/utils/tests/utils.go @@ -66,6 +66,7 @@ var ( TestGradle *bool TestMaven *bool TestNuget *bool + TestChoco *bool TestPip *bool TestPipenv *bool TestPoetry *bool @@ -141,6 +142,7 @@ func init() { TestGradle = flag.Bool("test.gradle", false, "Test Gradle") TestMaven = flag.Bool("test.maven", false, "Test Maven") TestNuget = flag.Bool("test.nuget", false, "Test Nuget") + TestChoco = flag.Bool("test.choco", false, "Test Chocolatey") TestPip = flag.Bool("test.pip", false, "Test Pip") TestPipenv = flag.Bool("test.pipenv", false, "Test Pipenv") TestPoetry = flag.Bool("test.poetry", false, "Test Poetry") @@ -491,6 +493,7 @@ func GetNonVirtualRepositories() map[*string]string { TestNpm: {&NpmRepo, &NpmScopedRepo, &NpmRemoteRepo}, TestPnpm: {&NpmRepo, &NpmScopedRepo, &NpmRemoteRepo}, TestNuget: {&NugetRemoteRepo, &NugetLocalRepo}, + TestChoco: {&NugetRemoteRepo, &NugetLocalRepo}, TestPip: {&PypiLocalRepo, &PypiRemoteRepo}, TestPipenv: {&PipenvRemoteRepo}, TestPoetry: {&PoetryLocalRepo, &PoetryRemoteRepo}, @@ -530,6 +533,7 @@ func GetVirtualRepositories() map[*string]string { TestNpm: {}, TestPnpm: {}, TestNuget: {&NugetVirtualRepo}, + TestChoco: {&NugetVirtualRepo}, TestPip: {&PypiVirtualRepo}, TestPipenv: {&PipenvVirtualRepo}, TestPoetry: {&PoetryVirtualRepo}, @@ -578,6 +582,7 @@ func GetBuildNames() []string { TestNpm: {&NpmBuildName, &YarnBuildName}, TestPnpm: {&PnpmBuildName}, TestNuget: {&NuGetBuildName}, + TestChoco: {&ChocoBuildName}, TestPip: {&PipBuildName}, TestPipenv: {&PipenvBuildName}, TestPoetry: {&PoetryBuildName}, @@ -788,6 +793,7 @@ func AddTimestampToGlobalVars() { YarnBuildName += uniqueSuffix MvnBuildName += uniqueSuffix NuGetBuildName += uniqueSuffix + ChocoBuildName += uniqueSuffix PipBuildName += uniqueSuffix PipenvBuildName += uniqueSuffix PoetryBuildName += uniqueSuffix