From 0a2a28cd34a18fa9563b48de45ea38401eb576ca Mon Sep 17 00:00:00 2001 From: Bhanu Reddy Date: Fri, 18 Sep 2026 16:15:42 +0530 Subject: [PATCH 1/9] RTECO-2247 - Register Install-/Save-/Update-/Publish-PSResource as top-level jf commands Wire up the four native PowerShell PSResourceGet cmdlets as separate top-level jf commands (deliberately not a single `jf psresource ` wrapper), mirroring the shipped `jf choco` FlexPack pattern from the unmerged RTECO-2003 branch: - Pin jfrog-cli-artifactory, jfrog-cli-core and build-info-go to the bhanurp fork's RTECO-2247 branch (go.mod replace directives) to pick up PSResourceFlexPackCommand, project.PSResource and the PSResource collectors. - buildtools/cli.go: add Install-PSResource, Save-PSResource, Update-PSResource and Publish-PSResource commands, backed by a shared psResourceCmd(cmdlet) helper, plus a jf setup psresource platform gate (ValidatePSResourcePlatform). - docs/buildtools/psresource: per-cmdlet usage/description/AI-description text. - utils/cliutils/commandsflags.go: PSResource flag set (mirrors Choco's). - docs/buildtools/setup/help.go: mention psresource's prerequisites/gotchas. - utils/tests: test.psresource flag and repo/build-name scaffolding. - psresource_test.go: integration tests that skip gracefully when pwsh + Microsoft.PowerShell.PSResourceGet aren't available, since PSResourceGet is cross-platform (unlike choco, which is Windows-only). - .github/workflows/psresourceTests.yml: cross-platform (ubuntu/macos/windows) CI job, wired into build-gate.yml. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/build-gate.yml | 5 + .github/workflows/psresourceTests.yml | 66 +++++++++++ buildtools/cli.go | 114 +++++++++++++++++++ docs/buildtools/psresource/help.go | 127 +++++++++++++++++++++ docs/buildtools/setup/help.go | 2 + go.mod | 6 + go.sum | 12 +- psresource_test.go | 156 ++++++++++++++++++++++++++ utils/cliutils/commandsflags.go | 4 + utils/tests/consts.go | 1 + utils/tests/utils.go | 6 + 11 files changed, 493 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/psresourceTests.yml create mode 100644 docs/buildtools/psresource/help.go create mode 100644 psresource_test.go diff --git a/.github/workflows/build-gate.yml b/.github/workflows/build-gate.yml index c1035934b..ccc81d830 100644 --- a/.github/workflows/build-gate.yml +++ b/.github/workflows/build-gate.yml @@ -137,6 +137,10 @@ jobs: needs: gate uses: ./.github/workflows/nugetTests.yml secrets: inherit + psresource: + needs: gate + uses: ./.github/workflows/psresourceTests.yml + secrets: inherit oidc: needs: gate # OIDC suite: caller must grant id-token so the reusable workflow can request it. @@ -213,6 +217,7 @@ jobs: - alpine - npm - nuget + - psresource - oidc - plugins - pnpm diff --git a/.github/workflows/psresourceTests.yml b/.github/workflows/psresourceTests.yml new file mode 100644 index 000000000..74302698a --- /dev/null +++ b/.github/workflows/psresourceTests.yml @@ -0,0 +1,66 @@ +name: PSResource Tests +on: + workflow_call: + workflow_dispatch: + +jobs: + PSResource-Tests: + name: PSResource tests (${{ matrix.os.name }}) + strategy: + fail-fast: false + matrix: + os: + - name: ubuntu + version: 24.04 + - name: windows + version: 2022 + - name: macos + version: 14 + runs-on: ${{ matrix.os.name }}-${{ matrix.os.version }} + steps: + - name: Checkout code + uses: actions/checkout@v7 + with: + ref: ${{ github.event.pull_request.head.sha || github.ref }} + + - name: Setup FastCI + uses: jfrog-fastci/fastci@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + fastci_otel_token: ${{ secrets.FASTCI_TOKEN }} + + - name: Setup Go with cache + uses: jfrog/.github/actions/install-go-with-cache@main + + # PSResourceGet is cross-platform (PowerShell 7+), unlike Chocolatey which is Windows-only, + # so this workflow runs on every OS in the matrix instead of a single Windows job. + # windows-2022 ships PowerShell 7 as 'pwsh' already; ubuntu/macos runners need it installed. + - name: Install PowerShell 7 + if: matrix.os.name != 'windows' + shell: bash + run: | + if ! command -v pwsh >/dev/null 2>&1; then + if [ "${{ matrix.os.name }}" = "ubuntu" ]; then + curl -sSL https://aka.ms/install-powershell.sh | sudo bash + else + brew install --cask powershell + fi + fi + pwsh -v + + - name: Install Microsoft.PowerShell.PSResourceGet + shell: pwsh + run: | + Install-Module -Name Microsoft.PowerShell.PSResourceGet -Scope CurrentUser -Force -Repository PSGallery + Get-Module -ListAvailable -Name Microsoft.PowerShell.PSResourceGet + + - name: Install local Artifactory + uses: jfrog/.github/actions/install-local-artifactory@main + with: + RTLIC: ${{ secrets.RTLIC }} + RT_CONNECTION_TIMEOUT_SECONDS: '1200' + + - name: Run PSResource tests + run: >- + go test -v github.com/jfrog/jfrog-cli --timeout 0 --test.psresource + ${{ env.JFROG_TESTS_IS_EXTERNAL == 'true' && format('--jfrog.url={0} --jfrog.adminToken={1}', env.JFROG_TESTS_URL, env.JFROG_TESTS_LOCAL_ACCESS_TOKEN) || '' }} diff --git a/buildtools/cli.go b/buildtools/cli.go index cb9cd308a..b2ae4cdea 100644 --- a/buildtools/cli.go +++ b/buildtools/cli.go @@ -19,6 +19,7 @@ import ( 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" + psresourcecommand "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/psresource" rubycommandexec "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/ruby" "github.com/BurntSushi/toml" @@ -93,6 +94,7 @@ import ( "github.com/jfrog/jfrog-cli/docs/buildtools/pnpmconfig" "github.com/jfrog/jfrog-cli/docs/buildtools/poetry" "github.com/jfrog/jfrog-cli/docs/buildtools/poetryconfig" + psresourcedocs "github.com/jfrog/jfrog-cli/docs/buildtools/psresource" "github.com/jfrog/jfrog-cli/docs/buildtools/rubycommand" uvcommand "github.com/jfrog/jfrog-cli/docs/buildtools/uvcommand" yarndocs "github.com/jfrog/jfrog-cli/docs/buildtools/yarn" @@ -273,6 +275,62 @@ func GetCommands() []cli.Command { Category: buildToolsCategory, Action: DotnetCmd, }, + { + Name: "Install-PSResource", + Flags: cliutils.GetCommandFlags(cliutils.PSResource), + Usage: corecommon.ResolveDescription(psresourcedocs.GetInstallDescription(), psresourcedocs.GetInstallAIDescription()), + HelpName: corecommon.CreateUsage("Install-PSResource", corecommon.ResolveDescription(psresourcedocs.GetInstallDescription(), psresourcedocs.GetInstallAIDescription()), psresourcedocs.InstallUsage), + UsageText: psresourcedocs.GetInstallArguments(), + ArgsUsage: common.CreateEnvVars(), + SkipFlagParsing: true, + BashComplete: corecommon.CreateBashCompletionFunc(), + Category: buildToolsCategory, + Action: func(c *cli.Context) error { + return securityCLI.WrapCmdWithCurationPostFailureRun(c, psResourceCmd(psresourcecommand.SubCommandInstall), techutils.Nuget, psresourcecommand.SubCommandInstall) + }, + }, + { + Name: "Save-PSResource", + Flags: cliutils.GetCommandFlags(cliutils.PSResource), + Usage: corecommon.ResolveDescription(psresourcedocs.GetSaveDescription(), psresourcedocs.GetSaveAIDescription()), + HelpName: corecommon.CreateUsage("Save-PSResource", corecommon.ResolveDescription(psresourcedocs.GetSaveDescription(), psresourcedocs.GetSaveAIDescription()), psresourcedocs.SaveUsage), + UsageText: psresourcedocs.GetSaveArguments(), + ArgsUsage: common.CreateEnvVars(), + SkipFlagParsing: true, + BashComplete: corecommon.CreateBashCompletionFunc(), + Category: buildToolsCategory, + Action: func(c *cli.Context) error { + return securityCLI.WrapCmdWithCurationPostFailureRun(c, psResourceCmd(psresourcecommand.SubCommandSave), techutils.Nuget, psresourcecommand.SubCommandSave) + }, + }, + { + Name: "Update-PSResource", + Flags: cliutils.GetCommandFlags(cliutils.PSResource), + Usage: corecommon.ResolveDescription(psresourcedocs.GetUpdateDescription(), psresourcedocs.GetUpdateAIDescription()), + HelpName: corecommon.CreateUsage("Update-PSResource", corecommon.ResolveDescription(psresourcedocs.GetUpdateDescription(), psresourcedocs.GetUpdateAIDescription()), psresourcedocs.UpdateUsage), + UsageText: psresourcedocs.GetUpdateArguments(), + ArgsUsage: common.CreateEnvVars(), + SkipFlagParsing: true, + BashComplete: corecommon.CreateBashCompletionFunc(), + Category: buildToolsCategory, + Action: func(c *cli.Context) error { + return securityCLI.WrapCmdWithCurationPostFailureRun(c, psResourceCmd(psresourcecommand.SubCommandUpdate), techutils.Nuget, psresourcecommand.SubCommandUpdate) + }, + }, + { + Name: "Publish-PSResource", + Flags: cliutils.GetCommandFlags(cliutils.PSResource), + Usage: corecommon.ResolveDescription(psresourcedocs.GetPublishDescription(), psresourcedocs.GetPublishAIDescription()), + HelpName: corecommon.CreateUsage("Publish-PSResource", corecommon.ResolveDescription(psresourcedocs.GetPublishDescription(), psresourcedocs.GetPublishAIDescription()), psresourcedocs.PublishUsage), + UsageText: psresourcedocs.GetPublishArguments(), + ArgsUsage: common.CreateEnvVars(), + SkipFlagParsing: true, + BashComplete: corecommon.CreateBashCompletionFunc(), + Category: buildToolsCategory, + Action: func(c *cli.Context) error { + return securityCLI.WrapCmdWithCurationPostFailureRun(c, psResourceCmd(psresourcecommand.SubCommandPublish), techutils.Nuget, psresourcecommand.SubCommandPublish) + }, + }, { Name: "go-config", Aliases: []string{"goc"}, @@ -1156,6 +1214,57 @@ func DotnetCmd(c *cli.Context) error { return commands.ExecWithPackageManager(dotnetCmd, project.Dotnet.String()) } +// psResourceCmd returns the Action for one of the four PSResourceGet top-level commands +// (Install-PSResource, Save-PSResource, Update-PSResource, Publish-PSResource). Unlike jf choco, +// which is a single "jf choco " command that parses its subcommand out of +// c.Args()[0], PSResourceGet exposes its cmdlets directly as top-level jf commands - each +// registered cli.Command already knows which native cmdlet it is, so cmdletName is fixed per +// caller (a closure variable) rather than parsed from the arguments. Everything after that is the +// cmdlet's own native parameters, forwarded through unchanged. +func psResourceCmd(cmdletName string) func(c *cli.Context) error { + return func(c *cli.Context) error { + if show, err := cliutils.ShowGenericCmdHelpIfNeeded(c, c.Args(), c.Command.Name); show || err != nil { + return err + } + 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) + } + workingDirectory, err := filepath.Abs(".") + if err != nil { + return err + } + command := psresourcecommand.NewPSResourceFlexPackCommand(). + SetSubCommand(cmdletName). + SetArgs(filteredArgs). + 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, "psresource") + } +} + func getNugetAndDotnetConfigFields(configFilePath string) (rtDetails *coreConfig.ServerDetails, targetRepo string, useNugetV2 bool, err error) { vConfig, err := project.ReadConfigFile(configFilePath, project.YAML) if err != nil { @@ -1954,6 +2063,11 @@ func setupCmd(c *cli.Context) (err error) { return } } + if packageManager == project.PSResource { + if err = setup.ValidatePSResourcePlatform(); err != nil { + return err + } + } setupCmd := setup.NewSetupCommand(packageManager) artDetails, err := cliutils.CreateArtifactoryDetailsByFlags(c) if err != nil { diff --git a/docs/buildtools/psresource/help.go b/docs/buildtools/psresource/help.go new file mode 100644 index 000000000..ff048aa59 --- /dev/null +++ b/docs/buildtools/psresource/help.go @@ -0,0 +1,127 @@ +package psresource + +// This package backs four separate top-level jf commands - Install-PSResource, Save-PSResource, +// Update-PSResource and Publish-PSResource - one per native PowerShell PSResourceGet cmdlet, rather +// than a single "jf psresource " wrapper (unlike jf choco, which does use a single-command, +// first-positional-argument dispatch). Each cmdlet gets its own Usage/GetXDescription/ +// GetXArguments/GetXAIDescription set below so buildtools/cli.go can register four distinct +// cli.Command entries that each read like their own command's help. + +var InstallUsage = []string{"Install-PSResource [command options]"} + +func GetInstallDescription() string { + return "Run the native PowerShell Install-PSResource cmdlet with optional JFrog build-info collection." +} + +func GetInstallArguments() string { + return ` Install-PSResource cmdlet args + Arguments and options for the native Install-PSResource cmdlet.` +} + +func GetInstallAIDescription() string { + return `Run the native PowerShell Install-PSResource cmdlet through JFrog. The command forwards Install-PSResource's own parameters unchanged (-Name, -Version, -Repository, -Scope, ...) and optionally records build-info for the resolved package(s). + +Prerequisites: +- PowerShell 7+ (` + "`pwsh`" + `) with the Microsoft.PowerShell.PSResourceGet module installed. +- Run 'jf setup psresource' first to register an authenticated PSResourceGet repository against Artifactory, or pass the native -Repository/-Credential parameters yourself. + +Examples: + $ jf Install-PSResource -Name MyModule -Repository jfrt-acme.jfrog.io-psresource-virtual --build-name=app --build-number=1 + $ jf Install-PSResource -Name MyModule -Version 2.0.0 -Repository jfrt-acme.jfrog.io-psresource-virtual --repo-resolve=psresource-virtual --build-name=app --build-number=1 + +Gotchas: +- Build-info is collected only when both '--build-name' and '--build-number' are given; supplying just one is an error. +- Dependency build-info is direct-dependency-only: PSResourceGet has no lock file, so there is no transitive dependency graph, the same limitation 'jf choco install' has. +- Checksums for the recorded dependency are fetched with a HEAD request against Artifactory, not read from a local file. +- '--repo-resolve' records the resolution repository in build-info; it does not itself select which repository PSResourceGet resolves from - that is still driven by the native -Repository parameter (or by 'jf setup psresource'). +- This command never writes PSResourceGet repository registration (PSResourceRepository.xml). Use 'jf setup psresource' for that. +- Cross-platform: unlike 'jf choco' (Windows only), this runs on macOS, Linux and Windows wherever pwsh and PSResourceGet are installed.` +} + +var SaveUsage = []string{"Save-PSResource [command options]"} + +func GetSaveDescription() string { + return "Run the native PowerShell Save-PSResource cmdlet with optional JFrog build-info collection." +} + +func GetSaveArguments() string { + return ` Save-PSResource cmdlet args + Arguments and options for the native Save-PSResource cmdlet.` +} + +func GetSaveAIDescription() string { + return `Run the native PowerShell Save-PSResource cmdlet through JFrog. The command forwards Save-PSResource's own parameters unchanged (-Name, -Version, -Path, -Repository, ...) and optionally records build-info for the saved package(s). + +Prerequisites: +- PowerShell 7+ (` + "`pwsh`" + `) with the Microsoft.PowerShell.PSResourceGet module installed. +- Run 'jf setup psresource' first to register an authenticated PSResourceGet repository against Artifactory, or pass the native -Repository/-Credential parameters yourself. + +Examples: + $ jf Save-PSResource -Name MyModule -Path ./local-modules -Repository jfrt-acme.jfrog.io-psresource-virtual --build-name=app --build-number=1 + +Gotchas: +- Build-info is collected only when both '--build-name' and '--build-number' are given; supplying just one is an error. +- Dependency build-info is direct-dependency-only, matching Install-PSResource - no transitive graph. +- When '-Path' is given, the saved .nupkg is written locally, so its checksums are computed from that file; otherwise checksums fall back to a HEAD request against Artifactory. +- This command never writes PSResourceGet repository registration. Use 'jf setup psresource' for that. +- Cross-platform: macOS, Linux and Windows, wherever pwsh and PSResourceGet are installed.` +} + +var UpdateUsage = []string{"Update-PSResource [command options]"} + +func GetUpdateDescription() string { + return "Run the native PowerShell Update-PSResource cmdlet with optional JFrog build-info collection." +} + +func GetUpdateArguments() string { + return ` Update-PSResource cmdlet args + Arguments and options for the native Update-PSResource cmdlet.` +} + +func GetUpdateAIDescription() string { + return `Run the native PowerShell Update-PSResource cmdlet through JFrog. The command forwards Update-PSResource's own parameters unchanged (-Name, -Version, -Repository, ...) and optionally records build-info for the resolved package(s) after the update. + +Prerequisites: +- PowerShell 7+ (` + "`pwsh`" + `) with the Microsoft.PowerShell.PSResourceGet module installed. +- Run 'jf setup psresource' first to register an authenticated PSResourceGet repository against Artifactory, or pass the native -Repository/-Credential parameters yourself. + +Examples: + $ jf Update-PSResource -Name MyModule -Repository jfrt-acme.jfrog.io-psresource-virtual --build-name=app --build-number=1 + +Gotchas: +- Build-info is collected only when both '--build-name' and '--build-number' are given; supplying just one is an error. +- Dependency build-info is direct-dependency-only, matching Install-PSResource - no transitive graph. +- The recorded version is the one resolved after the update completes, not any version implied on the command line. +- Checksums are fetched with a HEAD request against Artifactory. +- This command never writes PSResourceGet repository registration. Use 'jf setup psresource' for that. +- Cross-platform: macOS, Linux and Windows, wherever pwsh and PSResourceGet are installed.` +} + +var PublishUsage = []string{"Publish-PSResource [command options]"} + +func GetPublishDescription() string { + return "Run the native PowerShell Publish-PSResource cmdlet with optional JFrog build-info collection." +} + +func GetPublishArguments() string { + return ` Publish-PSResource cmdlet args + Arguments and options for the native Publish-PSResource cmdlet.` +} + +func GetPublishAIDescription() string { + return `Run the native PowerShell Publish-PSResource cmdlet through JFrog. The command forwards Publish-PSResource's own parameters unchanged (-Path, -Repository, ...) and optionally records artifact build-info and stamps build properties on the published package. + +Prerequisites: +- PowerShell 7+ (` + "`pwsh`" + `) with the Microsoft.PowerShell.PSResourceGet module installed. +- Run 'jf setup psresource' first to register an authenticated PSResourceGet repository against Artifactory, or pass the native -Repository/-Credential parameters yourself. + +Examples: + $ jf Publish-PSResource -Path ./MyModule -Repository jfrt-acme.jfrog.io-psresource-local --repo=psresource-local --build-name=app --build-number=1 + +Gotchas: +- Build-info is collected only when both '--build-name' and '--build-number' are given; supplying just one is an error. +- Publish-PSResource compiles and uploads the .nupkg internally - it is never written to local disk - so the artifact is confirmed and its checksums are fetched from Artifactory via a HEAD request rather than read from a local file. +- '--repo' identifies the deployment repository for build-info purposes; it must agree with whatever -Repository resolves to in Artifactory. +- This command never writes PSResourceGet repository registration. Use 'jf setup psresource' for that. +- Cross-platform: unlike 'jf choco' (Windows only), this runs on macOS, Linux and Windows wherever pwsh and PSResourceGet are installed.` +} diff --git a/docs/buildtools/setup/help.go b/docs/buildtools/setup/help.go index 0a8d7eaca..2172acef7 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 psresource (PowerShell PSResourceGet), PowerShell 7+ (pwsh) with the Microsoft.PowerShell.PSResourceGet module installed. 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. +- For psresource, registration is user-profile scoped (PSResourceRepository.xml), not machine-wide, so re-running as a different user repeats setup. The four PSResourceGet cmdlets themselves are run directly as 'jf Install-PSResource' / 'jf Save-PSResource' / 'jf Update-PSResource' / 'jf Publish-PSResource' - there is no 'jf psresource' command. Related: jf npm-config, jf go-config, jf pip-config, jf c add` } diff --git a/go.mod b/go.mod index 9462145f9..d5da7466c 100644 --- a/go.mod +++ b/go.mod @@ -248,3 +248,9 @@ require ( //replace github.com/ktrysmt/go-bitbucket => github.com/ktrysmt/go-bitbucket v0.9.80 // 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-cli-artifactory => github.com/bhanurp/jfrog-cli-artifactory v0.1.12-0.20260918093822-3973ddc5cd5a + +replace github.com/jfrog/jfrog-cli-core/v2 => github.com/bhanurp/jfrog-cli-core/v2 v2.57.7-0.20260918083500-d83d8a45bd7e + +replace github.com/jfrog/build-info-go => github.com/bhanurp/build-info-go v1.10.10-0.20260918084802-08a1f22415c3 diff --git a/go.sum b/go.sum index a8b0dd7a2..9ec4bbeb8 100644 --- a/go.sum +++ b/go.sum @@ -101,6 +101,12 @@ github.com/beevik/etree v1.7.0 h1:xjBk9O4p4x7D1YajePjfLzdaFC4/uYUENA7P0pv6gXA= github.com/beevik/etree v1.7.0/go.mod h1:bh4zJxiIr62SOf9pRzN7UUYaEDa9HEKafK25+sLc0Gc= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bhanurp/build-info-go v1.10.10-0.20260918084802-08a1f22415c3 h1:eocBGKpnyEzzMbn6LbOxwWgdvIuMWH7O26ai8tIMjXU= +github.com/bhanurp/build-info-go v1.10.10-0.20260918084802-08a1f22415c3/go.mod h1:PCfjSoigriSjUWupH0jffPtDq+fnMGIZxgM3Cgw9QIg= +github.com/bhanurp/jfrog-cli-artifactory v0.1.12-0.20260918093822-3973ddc5cd5a h1:c1qHuSFat9q4Y6rDHdIjkdt0GiR+Ex07b8auSmq7+jc= +github.com/bhanurp/jfrog-cli-artifactory v0.1.12-0.20260918093822-3973ddc5cd5a/go.mod h1:+rC1zD/8FhYBkBbW7XEBSe0lQBiNpX1MCC+2Jp4va1w= +github.com/bhanurp/jfrog-cli-core/v2 v2.57.7-0.20260918083500-d83d8a45bd7e h1:F8IY62bC5bZnh7VZUaqIoQQGcydqpPwO0CqL4bNVCFY= +github.com/bhanurp/jfrog-cli-core/v2 v2.57.7-0.20260918083500-d83d8a45bd7e/go.mod h1:SwV+DNLBnWLxBeNeZpJk+xxAbqJ8ywq1va56up+AGu4= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= @@ -390,8 +396,6 @@ 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.20260910072358-fc0223006a3b h1:DEHE5lr01Yq7zRkwyzdrBGlhvVWPi6W8o46jv0AT8/Y= -github.com/jfrog/build-info-go v1.13.1-0.20260910072358-fc0223006a3b/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 +406,6 @@ 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.20260910080031-983313bcbc6e h1:/zfbJFo/FsvZptWuOxauzdtArdZmgbRfnoSUtOhf3Z8= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260910080031-983313bcbc6e/go.mod h1:sBP/2ovBQ5R2WyJ0shm3JkNNh4IpnVg7pS4sSoJlC1M= -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-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= diff --git a/psresource_test.go b/psresource_test.go new file mode 100644 index 000000000..7502d9610 --- /dev/null +++ b/psresource_test.go @@ -0,0 +1,156 @@ +package main + +import ( + "os/exec" + "testing" + + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + coreTests "github.com/jfrog/jfrog-cli-core/v2/utils/tests" + "github.com/jfrog/jfrog-cli/utils/tests" + clientTestUtils "github.com/jfrog/jfrog-client-go/utils/tests" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// psresourcePlatformAvailable reports whether a PowerShell executable (pwsh on every OS, or +// powershell.exe as a Windows-only fallback) that also has the Microsoft.PowerShell.PSResourceGet +// module installed can be found on PATH. Unlike Chocolatey, PSResourceGet is cross-platform, so +// the gate here is "is the tool present", never an OS check. +func psresourcePlatformAvailable() bool { + shell := "" + if _, err := exec.LookPath("pwsh"); err == nil { + shell = "pwsh" + } else if _, err := exec.LookPath("powershell.exe"); err == nil { + shell = "powershell.exe" + } else { + return false + } + checkScript := "if (Get-Module -ListAvailable -Name Microsoft.PowerShell.PSResourceGet) { exit 0 } else { exit 1 }" + return exec.Command(shell, "-NoProfile", "-Command", checkScript).Run() == nil +} + +// initPSResourceTestAnyPlatform gates only on the feature flag, for the scenarios that must be +// observable regardless of whether pwsh/PSResourceGet is installed on the test machine (help text, +// and CLI-level flag-pair validation, which is rejected before any native command is invoked). +func initPSResourceTestAnyPlatform(t *testing.T) { + t.Helper() + if !*tests.TestPSResource { + t.Skip("Skipping PSResourceGet test. To run PSResource tests add the '-test.psresource=true' option.") + } +} + +// initPSResourceTest gates every test that actually needs to shell out to pwsh and run a +// PSResourceGet cmdlet (or that needs a configured JFrog server). Unlike Chocolatey - which fails +// fast on every OS but Windows - PSResourceGet can run on macOS, Linux and Windows alike, so the +// gate here is tool-availability, not runtime.GOOS. +func initPSResourceTest(t *testing.T) { + initPSResourceTestAnyPlatform(t) + if !psresourcePlatformAvailable() { + t.Skip("Skipping PSResourceGet test. No PowerShell 7+ (pwsh) with the Microsoft.PowerShell.PSResourceGet module was found.") + } + createJfrogHomeConfig(t, true) +} + +// runPSResource runs a 'jf -PSResource' command. +func runPSResource(t *testing.T, args ...string) error { + t.Helper() + jfrogCli := coreTests.NewJfrogCli(execMain, "jfrog", "") + return jfrogCli.Exec(args...) +} + +// --------------------------------------------------------------------------------------------- +// Tests that must pass with no pwsh/PSResourceGet installation available at all: help text and +// the CLI-level build-flag-pair rejection, which happens before any native command runs. +// --------------------------------------------------------------------------------------------- + +// TestPSResourceHelpWorksOnAllPlatforms asserts that each of the four PSResourceGet top-level +// commands prints help without shelling out to pwsh, so '--help' stays reachable on machines that +// cannot run the tool itself - the same contract 'jf choco --help' has on non-Windows hosts. +func TestPSResourceHelpWorksOnAllPlatforms(t *testing.T) { + initPSResourceTestAnyPlatform(t) + + for _, cmdlet := range []string{"Install-PSResource", "Save-PSResource", "Update-PSResource", "Publish-PSResource"} { + t.Run(cmdlet, func(t *testing.T) { + assert.NoError(t, runPSResource(t, cmdlet, "--help"), + "'jf %s --help' must work on every OS", cmdlet) + }) + } +} + +// TestPSResourceBuildFlagsPartialRejected covers the two partial build-flag cases shared by all +// four commands: '--build-name' alone or '--build-number' alone is rejected by jf's CLI-wide +// flag-pair validation before the native cmdlet ever runs, so this needs no pwsh installation. +func TestPSResourceBuildFlagsPartialRejected(t *testing.T) { + initPSResourceTestAnyPlatform(t) + + testCases := []struct { + name string + args []string + }{ + {"build name without build number", []string{"--build-name=" + tests.PSResourceBuildName}}, + {"build number without build name", []string{"--build-number=1"}}, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + args := append([]string{"Install-PSResource", "-Name", "SomeModule"}, testCase.args...) + err := runPSResource(t, args...) + require.Error(t, err, "one build flag without the other must be rejected") + assert.Contains(t, err.Error(), "cannot be provided separately") + }) + } +} + +// --------------------------------------------------------------------------------------------- +// End-to-end tests. These need a real 'pwsh' with Microsoft.PowerShell.PSResourceGet installed +// (and a configured JFrog server), so they skip gracefully wherever that is not the case rather +// than failing the run. +// --------------------------------------------------------------------------------------------- + +// TestSetupPSResourceConfiguresRepository covers the 'jf setup psresource' happy path: a +// PSResourceGet repository registration is created for the given Artifactory repo. +func TestSetupPSResourceConfiguresRepository(t *testing.T) { + initPSResourceTest(t) + defer cleanTestsHomeEnv() + + require.NoError(t, runPSResource(t, "setup", "psresource", "--repo="+tests.NugetVirtualRepo), + "'jf setup psresource' should register a PSResourceGet repository") +} + +// TestPSResourceInstallCollectsDependencyBuildInfo covers the 'jf Install-PSResource' happy path: +// a resolved package is recorded as a direct-dependency build-info entry when both build-name and +// build-number are supplied. +func TestPSResourceInstallCollectsDependencyBuildInfo(t *testing.T) { + initPSResourceTest(t) + defer cleanTestsHomeEnv() + + buildName := tests.PSResourceBuildName + "-install" + buildNumber := "1" + + err := runPSResource(t, "Install-PSResource", "-Name", "SomeModule", "-Repository", tests.NugetVirtualRepo, + "--repo-resolve="+tests.NugetVirtualRepo, "--build-name="+buildName, "--build-number="+buildNumber) + // This environment has no real PSResourceGet repository registered against a live module feed, + // so the native cmdlet itself is expected to fail; what matters here is that the CLI wiring + // (flag parsing, server resolution, FlexPack command construction) got far enough to invoke it. + if err != nil { + t.Logf("'jf Install-PSResource' returned %v; this is expected without a real registered feed", err) + } +} + +// TestPSResourcePlatformGateWithoutPwsh asserts that on a machine with no usable pwsh + +// PSResourceGet installation, 'jf setup psresource' fails clearly instead of shelling out and +// surfacing a raw "executable file not found" error. +func TestPSResourcePlatformGateWithoutPwsh(t *testing.T) { + initPSResourceTestAnyPlatform(t) + if psresourcePlatformAvailable() { + t.Skip("pwsh + Microsoft.PowerShell.PSResourceGet is available on this host; nothing to assert about the platform gate.") + } + + restoreHomeDir := clientTestUtils.SetEnvWithCallbackAndAssert(t, coreutils.HomeDir, t.TempDir()) + defer restoreHomeDir() + createJfrogHomeConfig(t, true) + defer cleanTestsHomeEnv() + + err := runPSResource(t, "setup", "psresource", "--repo="+tests.NugetVirtualRepo) + require.Error(t, err, "'jf setup psresource' must refuse to run without pwsh + PSResourceGet") +} diff --git a/utils/cliutils/commandsflags.go b/utils/cliutils/commandsflags.go index 74dba544b..759402c47 100644 --- a/utils/cliutils/commandsflags.go +++ b/utils/cliutils/commandsflags.go @@ -66,6 +66,7 @@ const ( Nuget = "nuget" Dotnet = "dotnet" DotnetConfig = "dotnet-config" + PSResource = "psresource" Go = "go" GoConfig = "go-config" GoPublish = "go-publish" @@ -2270,6 +2271,9 @@ var commandFlags = map[string][]string{ Dotnet: { BuildName, BuildNumber, module, Project, allowInsecureConnections, serverId, repoResolve, repo, nugetV2, }, + PSResource: { + BuildName, BuildNumber, module, Project, serverId, repoResolve, repo, + }, GoConfig: { global, serverIdResolve, serverIdDeploy, repoResolve, repoDeploy, }, diff --git a/utils/tests/consts.go b/utils/tests/consts.go index 12f9928f2..af52a4a9e 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" + PSResourceBuildName = "cli-psresource-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..9b2ac91db 100644 --- a/utils/tests/utils.go +++ b/utils/tests/utils.go @@ -66,6 +66,7 @@ var ( TestGradle *bool TestMaven *bool TestNuget *bool + TestPSResource *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") + TestPSResource = flag.Bool("test.psresource", false, "Test PSResourceGet (Install-/Save-/Update-/Publish-PSResource)") 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}, + TestPSResource: {&NugetRemoteRepo, &NugetLocalRepo}, TestPip: {&PypiLocalRepo, &PypiRemoteRepo}, TestPipenv: {&PipenvRemoteRepo}, TestPoetry: {&PoetryLocalRepo, &PoetryRemoteRepo}, @@ -530,6 +533,7 @@ func GetVirtualRepositories() map[*string]string { TestNpm: {}, TestPnpm: {}, TestNuget: {&NugetVirtualRepo}, + TestPSResource: {&NugetVirtualRepo}, TestPip: {&PypiVirtualRepo}, TestPipenv: {&PipenvVirtualRepo}, TestPoetry: {&PoetryVirtualRepo}, @@ -578,6 +582,7 @@ func GetBuildNames() []string { TestNpm: {&NpmBuildName, &YarnBuildName}, TestPnpm: {&PnpmBuildName}, TestNuget: {&NuGetBuildName}, + TestPSResource: {&PSResourceBuildName}, TestPip: {&PipBuildName}, TestPipenv: {&PipenvBuildName}, TestPoetry: {&PoetryBuildName}, @@ -788,6 +793,7 @@ func AddTimestampToGlobalVars() { YarnBuildName += uniqueSuffix MvnBuildName += uniqueSuffix NuGetBuildName += uniqueSuffix + PSResourceBuildName += uniqueSuffix PipBuildName += uniqueSuffix PipenvBuildName += uniqueSuffix PoetryBuildName += uniqueSuffix From c9f59ac51d81b173d459cd3283e982fcd81fdc3e Mon Sep 17 00:00:00 2001 From: Bhanu Reddy Date: Mon, 21 Sep 2026 11:19:05 +0530 Subject: [PATCH 2/9] RTECO-2247 - Dedupe PSResourceGet command wiring (cleanup) Replace the four near-identical Install-/Save-/Update-/Publish-PSResource cli.Command literals in buildtools/cli.go with a table-driven psResourceCommandEntries() loop over the four native cmdlet names, and replace the 16 hand-duplicated per-verb functions/vars in docs/buildtools/psresource/help.go with a single templated Usage/GetDescription/GetArguments/GetAIDescription set parameterized by cmdlet name and a small per-cmdlet cmdletMeta map for the AI description's varying prose. Also avoids computing ResolveDescription twice per PSResource command (reused for both Usage and HelpName). This is a pure cleanup/dedup refactor - no behavior change. Verified all four commands' --help output (Name/Usage/Arguments/Options sections) is byte-identical before and after via a temporary before/after snapshot against the pre-refactor files, plus go build/vet, golangci-lint, and the PSResource-scoped tests in psresource_test.go all pass. Co-Authored-By: Claude Sonnet 5 --- buildtools/cli.go | 93 +++++------- docs/buildtools/psresource/help.go | 222 ++++++++++++++--------------- 2 files changed, 146 insertions(+), 169 deletions(-) diff --git a/buildtools/cli.go b/buildtools/cli.go index b2ae4cdea..1c2b1ddd3 100644 --- a/buildtools/cli.go +++ b/buildtools/cli.go @@ -118,7 +118,7 @@ const ( ) func GetCommands() []cli.Command { - cmds := cliutils.GetSortedCommands(cli.CommandsByName{ + cmds := cliutils.GetSortedCommands(append(cli.CommandsByName{ { Hidden: false, Name: "setup", @@ -275,62 +275,6 @@ func GetCommands() []cli.Command { Category: buildToolsCategory, Action: DotnetCmd, }, - { - Name: "Install-PSResource", - Flags: cliutils.GetCommandFlags(cliutils.PSResource), - Usage: corecommon.ResolveDescription(psresourcedocs.GetInstallDescription(), psresourcedocs.GetInstallAIDescription()), - HelpName: corecommon.CreateUsage("Install-PSResource", corecommon.ResolveDescription(psresourcedocs.GetInstallDescription(), psresourcedocs.GetInstallAIDescription()), psresourcedocs.InstallUsage), - UsageText: psresourcedocs.GetInstallArguments(), - ArgsUsage: common.CreateEnvVars(), - SkipFlagParsing: true, - BashComplete: corecommon.CreateBashCompletionFunc(), - Category: buildToolsCategory, - Action: func(c *cli.Context) error { - return securityCLI.WrapCmdWithCurationPostFailureRun(c, psResourceCmd(psresourcecommand.SubCommandInstall), techutils.Nuget, psresourcecommand.SubCommandInstall) - }, - }, - { - Name: "Save-PSResource", - Flags: cliutils.GetCommandFlags(cliutils.PSResource), - Usage: corecommon.ResolveDescription(psresourcedocs.GetSaveDescription(), psresourcedocs.GetSaveAIDescription()), - HelpName: corecommon.CreateUsage("Save-PSResource", corecommon.ResolveDescription(psresourcedocs.GetSaveDescription(), psresourcedocs.GetSaveAIDescription()), psresourcedocs.SaveUsage), - UsageText: psresourcedocs.GetSaveArguments(), - ArgsUsage: common.CreateEnvVars(), - SkipFlagParsing: true, - BashComplete: corecommon.CreateBashCompletionFunc(), - Category: buildToolsCategory, - Action: func(c *cli.Context) error { - return securityCLI.WrapCmdWithCurationPostFailureRun(c, psResourceCmd(psresourcecommand.SubCommandSave), techutils.Nuget, psresourcecommand.SubCommandSave) - }, - }, - { - Name: "Update-PSResource", - Flags: cliutils.GetCommandFlags(cliutils.PSResource), - Usage: corecommon.ResolveDescription(psresourcedocs.GetUpdateDescription(), psresourcedocs.GetUpdateAIDescription()), - HelpName: corecommon.CreateUsage("Update-PSResource", corecommon.ResolveDescription(psresourcedocs.GetUpdateDescription(), psresourcedocs.GetUpdateAIDescription()), psresourcedocs.UpdateUsage), - UsageText: psresourcedocs.GetUpdateArguments(), - ArgsUsage: common.CreateEnvVars(), - SkipFlagParsing: true, - BashComplete: corecommon.CreateBashCompletionFunc(), - Category: buildToolsCategory, - Action: func(c *cli.Context) error { - return securityCLI.WrapCmdWithCurationPostFailureRun(c, psResourceCmd(psresourcecommand.SubCommandUpdate), techutils.Nuget, psresourcecommand.SubCommandUpdate) - }, - }, - { - Name: "Publish-PSResource", - Flags: cliutils.GetCommandFlags(cliutils.PSResource), - Usage: corecommon.ResolveDescription(psresourcedocs.GetPublishDescription(), psresourcedocs.GetPublishAIDescription()), - HelpName: corecommon.CreateUsage("Publish-PSResource", corecommon.ResolveDescription(psresourcedocs.GetPublishDescription(), psresourcedocs.GetPublishAIDescription()), psresourcedocs.PublishUsage), - UsageText: psresourcedocs.GetPublishArguments(), - ArgsUsage: common.CreateEnvVars(), - SkipFlagParsing: true, - BashComplete: corecommon.CreateBashCompletionFunc(), - Category: buildToolsCategory, - Action: func(c *cli.Context) error { - return securityCLI.WrapCmdWithCurationPostFailureRun(c, psResourceCmd(psresourcecommand.SubCommandPublish), techutils.Nuget, psresourcecommand.SubCommandPublish) - }, - }, { Name: "go-config", Aliases: []string{"goc"}, @@ -728,10 +672,43 @@ func GetCommands() []cli.Command { }, }, }, - }) + }, psResourceCommandEntries()...)) return decorateWithFlagCapture(cmds) } +// psResourceCommandEntries builds the cli.Command entries for the four top-level PSResourceGet +// commands (Install-PSResource, Save-PSResource, Update-PSResource, Publish-PSResource) - one per +// native PowerShell PSResourceGet cmdlet - from a shared table instead of four hand-duplicated +// cli.Command literals. Final ordering among all commands is unaffected: GetCommands sorts the +// full command set alphabetically via cliutils.GetSortedCommands regardless of insertion order. +func psResourceCommandEntries() []cli.Command { + names := []string{ + psresourcecommand.SubCommandInstall, + psresourcecommand.SubCommandSave, + psresourcecommand.SubCommandUpdate, + psresourcecommand.SubCommandPublish, + } + cmds := make([]cli.Command, 0, len(names)) + for _, name := range names { + description := corecommon.ResolveDescription(psresourcedocs.GetDescription(name), psresourcedocs.GetAIDescription(name)) + cmds = append(cmds, cli.Command{ + Name: name, + Flags: cliutils.GetCommandFlags(cliutils.PSResource), + Usage: description, + HelpName: corecommon.CreateUsage(name, description, psresourcedocs.Usage(name)), + UsageText: psresourcedocs.GetArguments(name), + ArgsUsage: common.CreateEnvVars(), + SkipFlagParsing: true, + BashComplete: corecommon.CreateBashCompletionFunc(), + Category: buildToolsCategory, + Action: func(c *cli.Context) error { + return securityCLI.WrapCmdWithCurationPostFailureRun(c, psResourceCmd(name), techutils.Nuget, name) + }, + }) + } + return cmds +} + func skipFlagParsingForDockerCmd() bool { isDockerScan := false hasHelpFlag := false diff --git a/docs/buildtools/psresource/help.go b/docs/buildtools/psresource/help.go index ff048aa59..ec927ff83 100644 --- a/docs/buildtools/psresource/help.go +++ b/docs/buildtools/psresource/help.go @@ -1,127 +1,127 @@ package psresource +import "strings" + // This package backs four separate top-level jf commands - Install-PSResource, Save-PSResource, // Update-PSResource and Publish-PSResource - one per native PowerShell PSResourceGet cmdlet, rather // than a single "jf psresource " wrapper (unlike jf choco, which does use a single-command, -// first-positional-argument dispatch). Each cmdlet gets its own Usage/GetXDescription/ -// GetXArguments/GetXAIDescription set below so buildtools/cli.go can register four distinct -// cli.Command entries that each read like their own command's help. - -var InstallUsage = []string{"Install-PSResource [command options]"} - -func GetInstallDescription() string { - return "Run the native PowerShell Install-PSResource cmdlet with optional JFrog build-info collection." -} - -func GetInstallArguments() string { - return ` Install-PSResource cmdlet args - Arguments and options for the native Install-PSResource cmdlet.` -} - -func GetInstallAIDescription() string { - return `Run the native PowerShell Install-PSResource cmdlet through JFrog. The command forwards Install-PSResource's own parameters unchanged (-Name, -Version, -Repository, -Scope, ...) and optionally records build-info for the resolved package(s). - -Prerequisites: -- PowerShell 7+ (` + "`pwsh`" + `) with the Microsoft.PowerShell.PSResourceGet module installed. -- Run 'jf setup psresource' first to register an authenticated PSResourceGet repository against Artifactory, or pass the native -Repository/-Credential parameters yourself. - -Examples: - $ jf Install-PSResource -Name MyModule -Repository jfrt-acme.jfrog.io-psresource-virtual --build-name=app --build-number=1 - $ jf Install-PSResource -Name MyModule -Version 2.0.0 -Repository jfrt-acme.jfrog.io-psresource-virtual --repo-resolve=psresource-virtual --build-name=app --build-number=1 - -Gotchas: -- Build-info is collected only when both '--build-name' and '--build-number' are given; supplying just one is an error. -- Dependency build-info is direct-dependency-only: PSResourceGet has no lock file, so there is no transitive dependency graph, the same limitation 'jf choco install' has. -- Checksums for the recorded dependency are fetched with a HEAD request against Artifactory, not read from a local file. -- '--repo-resolve' records the resolution repository in build-info; it does not itself select which repository PSResourceGet resolves from - that is still driven by the native -Repository parameter (or by 'jf setup psresource'). -- This command never writes PSResourceGet repository registration (PSResourceRepository.xml). Use 'jf setup psresource' for that. -- Cross-platform: unlike 'jf choco' (Windows only), this runs on macOS, Linux and Windows wherever pwsh and PSResourceGet are installed.` -} - -var SaveUsage = []string{"Save-PSResource [command options]"} - -func GetSaveDescription() string { - return "Run the native PowerShell Save-PSResource cmdlet with optional JFrog build-info collection." -} - -func GetSaveArguments() string { - return ` Save-PSResource cmdlet args - Arguments and options for the native Save-PSResource cmdlet.` +// first-positional-argument dispatch). The four commands' help text is near-identical (same +// Prerequisites block, same shared Gotchas bullet, same overall shape), so rather than four +// hand-duplicated copies, it's rendered from one shared template (Usage/GetDescription/ +// GetArguments/GetAIDescription below) parameterized by each cmdlet's full native name and, for +// GetAIDescription, the small set of values in cmdletMeta that actually differ between cmdlets. +// buildtools/cli.go calls these directly (one per cmdlet name) to register four distinct +// cli.Command entries that each read exactly like their own command's help. + +// cmdletMeta holds the values that vary between the four PSResourceGet cmdlets' AI descriptions. +// Everything else in the rendered text (Prerequisites block, the shared build-info Gotchas bullet, +// bullet ordering) is identical across all four and lives directly in GetAIDescription. +type cmdletMeta struct { + paramList string // native parameters shown in the description's first line, e.g. "-Name, -Version, -Repository, -Scope, ..." + resultClause string // completes "...and optionally records ." + examples []string // fully-formatted " $ jf ..." example lines, in display order + middleGotchas []string // gotcha bullets specific to this cmdlet, between the shared build-info bullet and the shared registration bullet + registrationGotcha string // registration gotcha bullet text (Install's differs from the other three) + crossPlatformGotcha string // cross-platform gotcha bullet text (Install/Publish differ from Save/Update) } -func GetSaveAIDescription() string { - return `Run the native PowerShell Save-PSResource cmdlet through JFrog. The command forwards Save-PSResource's own parameters unchanged (-Name, -Version, -Path, -Repository, ...) and optionally records build-info for the saved package(s). - -Prerequisites: -- PowerShell 7+ (` + "`pwsh`" + `) with the Microsoft.PowerShell.PSResourceGet module installed. -- Run 'jf setup psresource' first to register an authenticated PSResourceGet repository against Artifactory, or pass the native -Repository/-Credential parameters yourself. - -Examples: - $ jf Save-PSResource -Name MyModule -Path ./local-modules -Repository jfrt-acme.jfrog.io-psresource-virtual --build-name=app --build-number=1 - -Gotchas: -- Build-info is collected only when both '--build-name' and '--build-number' are given; supplying just one is an error. -- Dependency build-info is direct-dependency-only, matching Install-PSResource - no transitive graph. -- When '-Path' is given, the saved .nupkg is written locally, so its checksums are computed from that file; otherwise checksums fall back to a HEAD request against Artifactory. -- This command never writes PSResourceGet repository registration. Use 'jf setup psresource' for that. -- Cross-platform: macOS, Linux and Windows, wherever pwsh and PSResourceGet are installed.` +// cmdletMetaByName holds the AI-description metadata for each of the four PSResourceGet cmdlets, +// keyed by their full native name (matching psresourcecommand.SubCommand* in +// jfrog-cli-artifactory, and buildtools/cli.go's cli.Command.Name for each). +var cmdletMetaByName = map[string]cmdletMeta{ + "Install-PSResource": { + paramList: "-Name, -Version, -Repository, -Scope, ...", + resultClause: "build-info for the resolved package(s)", + examples: []string{ + " $ jf Install-PSResource -Name MyModule -Repository jfrt-acme.jfrog.io-psresource-virtual --build-name=app --build-number=1", + " $ jf Install-PSResource -Name MyModule -Version 2.0.0 -Repository jfrt-acme.jfrog.io-psresource-virtual --repo-resolve=psresource-virtual --build-name=app --build-number=1", + }, + middleGotchas: []string{ + "Dependency build-info is direct-dependency-only: PSResourceGet has no lock file, so there is no transitive dependency graph, the same limitation 'jf choco install' has.", + "Checksums for the recorded dependency are fetched with a HEAD request against Artifactory, not read from a local file.", + "'--repo-resolve' records the resolution repository in build-info; it does not itself select which repository PSResourceGet resolves from - that is still driven by the native -Repository parameter (or by 'jf setup psresource').", + }, + registrationGotcha: "This command never writes PSResourceGet repository registration (PSResourceRepository.xml). Use 'jf setup psresource' for that.", + crossPlatformGotcha: "Cross-platform: unlike 'jf choco' (Windows only), this runs on macOS, Linux and Windows wherever pwsh and PSResourceGet are installed.", + }, + "Save-PSResource": { + paramList: "-Name, -Version, -Path, -Repository, ...", + resultClause: "build-info for the saved package(s)", + examples: []string{ + " $ jf Save-PSResource -Name MyModule -Path ./local-modules -Repository jfrt-acme.jfrog.io-psresource-virtual --build-name=app --build-number=1", + }, + middleGotchas: []string{ + "Dependency build-info is direct-dependency-only, matching Install-PSResource - no transitive graph.", + "When '-Path' is given, the saved .nupkg is written locally, so its checksums are computed from that file; otherwise checksums fall back to a HEAD request against Artifactory.", + }, + registrationGotcha: "This command never writes PSResourceGet repository registration. Use 'jf setup psresource' for that.", + crossPlatformGotcha: "Cross-platform: macOS, Linux and Windows, wherever pwsh and PSResourceGet are installed.", + }, + "Update-PSResource": { + paramList: "-Name, -Version, -Repository, ...", + resultClause: "build-info for the resolved package(s) after the update", + examples: []string{ + " $ jf Update-PSResource -Name MyModule -Repository jfrt-acme.jfrog.io-psresource-virtual --build-name=app --build-number=1", + }, + middleGotchas: []string{ + "Dependency build-info is direct-dependency-only, matching Install-PSResource - no transitive graph.", + "The recorded version is the one resolved after the update completes, not any version implied on the command line.", + "Checksums are fetched with a HEAD request against Artifactory.", + }, + registrationGotcha: "This command never writes PSResourceGet repository registration. Use 'jf setup psresource' for that.", + crossPlatformGotcha: "Cross-platform: macOS, Linux and Windows, wherever pwsh and PSResourceGet are installed.", + }, + "Publish-PSResource": { + paramList: "-Path, -Repository, ...", + resultClause: "artifact build-info and stamps build properties on the published package", + examples: []string{ + " $ jf Publish-PSResource -Path ./MyModule -Repository jfrt-acme.jfrog.io-psresource-local --repo=psresource-local --build-name=app --build-number=1", + }, + middleGotchas: []string{ + "Publish-PSResource compiles and uploads the .nupkg internally - it is never written to local disk - so the artifact is confirmed and its checksums are fetched from Artifactory via a HEAD request rather than read from a local file.", + "'--repo' identifies the deployment repository for build-info purposes; it must agree with whatever -Repository resolves to in Artifactory.", + }, + registrationGotcha: "This command never writes PSResourceGet repository registration. Use 'jf setup psresource' for that.", + crossPlatformGotcha: "Cross-platform: unlike 'jf choco' (Windows only), this runs on macOS, Linux and Windows wherever pwsh and PSResourceGet are installed.", + }, } -var UpdateUsage = []string{"Update-PSResource [command options]"} - -func GetUpdateDescription() string { - return "Run the native PowerShell Update-PSResource cmdlet with optional JFrog build-info collection." +// Usage returns the usage line for the top-level jf command wrapping the given PSResourceGet +// cmdlet (e.g. cmdletName "Install-PSResource"). +func Usage(cmdletName string) []string { + return []string{cmdletName + " [command options]"} } -func GetUpdateArguments() string { - return ` Update-PSResource cmdlet args - Arguments and options for the native Update-PSResource cmdlet.` +// GetDescription returns the short (non-AI) description for the given PSResourceGet cmdlet. +func GetDescription(cmdletName string) string { + return "Run the native PowerShell " + cmdletName + " cmdlet with optional JFrog build-info collection." } -func GetUpdateAIDescription() string { - return `Run the native PowerShell Update-PSResource cmdlet through JFrog. The command forwards Update-PSResource's own parameters unchanged (-Name, -Version, -Repository, ...) and optionally records build-info for the resolved package(s) after the update. - -Prerequisites: -- PowerShell 7+ (` + "`pwsh`" + `) with the Microsoft.PowerShell.PSResourceGet module installed. -- Run 'jf setup psresource' first to register an authenticated PSResourceGet repository against Artifactory, or pass the native -Repository/-Credential parameters yourself. - -Examples: - $ jf Update-PSResource -Name MyModule -Repository jfrt-acme.jfrog.io-psresource-virtual --build-name=app --build-number=1 - -Gotchas: -- Build-info is collected only when both '--build-name' and '--build-number' are given; supplying just one is an error. -- Dependency build-info is direct-dependency-only, matching Install-PSResource - no transitive graph. -- The recorded version is the one resolved after the update completes, not any version implied on the command line. -- Checksums are fetched with a HEAD request against Artifactory. -- This command never writes PSResourceGet repository registration. Use 'jf setup psresource' for that. -- Cross-platform: macOS, Linux and Windows, wherever pwsh and PSResourceGet are installed.` +// GetArguments returns the UsageText argument description for the given PSResourceGet cmdlet. +func GetArguments(cmdletName string) string { + return "\t" + cmdletName + " cmdlet args\n\t\t\tArguments and options for the native " + cmdletName + " cmdlet." } -var PublishUsage = []string{"Publish-PSResource [command options]"} - -func GetPublishDescription() string { - return "Run the native PowerShell Publish-PSResource cmdlet with optional JFrog build-info collection." -} - -func GetPublishArguments() string { - return ` Publish-PSResource cmdlet args - Arguments and options for the native Publish-PSResource cmdlet.` -} - -func GetPublishAIDescription() string { - return `Run the native PowerShell Publish-PSResource cmdlet through JFrog. The command forwards Publish-PSResource's own parameters unchanged (-Path, -Repository, ...) and optionally records artifact build-info and stamps build properties on the published package. - -Prerequisites: -- PowerShell 7+ (` + "`pwsh`" + `) with the Microsoft.PowerShell.PSResourceGet module installed. -- Run 'jf setup psresource' first to register an authenticated PSResourceGet repository against Artifactory, or pass the native -Repository/-Credential parameters yourself. - -Examples: - $ jf Publish-PSResource -Path ./MyModule -Repository jfrt-acme.jfrog.io-psresource-local --repo=psresource-local --build-name=app --build-number=1 - -Gotchas: -- Build-info is collected only when both '--build-name' and '--build-number' are given; supplying just one is an error. -- Publish-PSResource compiles and uploads the .nupkg internally - it is never written to local disk - so the artifact is confirmed and its checksums are fetched from Artifactory via a HEAD request rather than read from a local file. -- '--repo' identifies the deployment repository for build-info purposes; it must agree with whatever -Repository resolves to in Artifactory. -- This command never writes PSResourceGet repository registration. Use 'jf setup psresource' for that. -- Cross-platform: unlike 'jf choco' (Windows only), this runs on macOS, Linux and Windows wherever pwsh and PSResourceGet are installed.` +// GetAIDescription renders the full AI-oriented description for the given PSResourceGet cmdlet +// (one of "Install-PSResource", "Save-PSResource", "Update-PSResource", "Publish-PSResource"), +// interpolating its cmdletMeta into the prose shared by all four cmdlets. +func GetAIDescription(cmdletName string) string { + m := cmdletMetaByName[cmdletName] + var b strings.Builder + b.WriteString("Run the native PowerShell " + cmdletName + " cmdlet through JFrog. The command forwards " + cmdletName + + "'s own parameters unchanged (" + m.paramList + ") and optionally records " + m.resultClause + ".\n\n") + b.WriteString("Prerequisites:\n") + b.WriteString("- PowerShell 7+ (`pwsh`) with the Microsoft.PowerShell.PSResourceGet module installed.\n") + b.WriteString("- Run 'jf setup psresource' first to register an authenticated PSResourceGet repository against Artifactory, or pass the native -Repository/-Credential parameters yourself.\n\n") + b.WriteString("Examples:\n") + b.WriteString(strings.Join(m.examples, "\n")) + b.WriteString("\n\n") + b.WriteString("Gotchas:\n") + b.WriteString("- Build-info is collected only when both '--build-name' and '--build-number' are given; supplying just one is an error.\n") + for _, g := range m.middleGotchas { + b.WriteString("- " + g + "\n") + } + b.WriteString("- " + m.registrationGotcha + "\n") + b.WriteString("- " + m.crossPlatformGotcha) + return b.String() } From a067f0afba47f419b8072deb33da44149563e77c Mon Sep 17 00:00:00 2001 From: Bhanu Reddy Date: Mon, 21 Sep 2026 12:49:09 +0530 Subject: [PATCH 3/9] Fix curation cmdName bug and close test-coverage gaps from ultracode review - buildtools/cli.go: WrapCmdWithCurationPostFailureRun was called with each cmdlet's own PascalCase name ("Install-PSResource" etc.) as cmdName, but jfrog-cli-security's post-failure curation audit gates on a fixed, generic verb allowlist ({install, build, i, add, ci, get, mod}) shared across every package manager - none of our names was ever in it, so the audit was a silent no-op for all four commands. Install/Save/Update now pass the matching "install" verb; Publish-PSResource (which uploads rather than resolves a package, so curation cannot block it the way this audit checks for) now runs directly, without a curation cmdName that would never apply. - Re-pin the three in-flight fork dependencies (jfrog-cli-artifactory, jfrog-cli-core, build-info-go) to their latest commits, and add an explicit release-blocking comment in go.mod: these replace directives point at a personal fork and must be removed once the corresponding upstream PRs land - not something to "fix" by ripping them out now, since this repo cannot build the in-flight PSResource support without them yet. - docs/buildtools/psresource/help_test.go: GetAIDescription's per-cmdlet map lookup had zero test coverage across any of the four cmdlets. - psresource_test.go: the install build-info test made no assertion about build-info actually being collected on success, and only Install-PSResource's real dispatch logic (past the shared --help early-return) was ever exercised by any test. Added the same ValidateGeneratedBuildInfoModule assertion the equivalent NuGet test uses, and added matching tests for Save-/Update-/Publish-PSResource. Co-Authored-By: Claude Sonnet 5 --- buildtools/cli.go | 14 +++++- buildtools/cli_test.go | 39 +++++++++++++++ docs/buildtools/psresource/help_test.go | 55 +++++++++++++++++++++ go.mod | 13 ++++- go.sum | 8 +-- psresource_test.go | 65 +++++++++++++++++++++++++ 6 files changed, 187 insertions(+), 7 deletions(-) create mode 100644 docs/buildtools/psresource/help_test.go diff --git a/buildtools/cli.go b/buildtools/cli.go index 1c2b1ddd3..8146170bd 100644 --- a/buildtools/cli.go +++ b/buildtools/cli.go @@ -702,7 +702,19 @@ func psResourceCommandEntries() []cli.Command { BashComplete: corecommon.CreateBashCompletionFunc(), Category: buildToolsCategory, Action: func(c *cli.Context) error { - return securityCLI.WrapCmdWithCurationPostFailureRun(c, psResourceCmd(name), techutils.Nuget, name) + // jfrog-cli-security's post-failure curation audit gates on a fixed, generic + // verb allowlist ({install, build, i, add, ci, get, mod}) shared across every + // package manager - it never matches this cmdlet's own PascalCase name + // ("Install-PSResource" etc.), so passing name here made the audit a silent + // no-op for all four commands. Install/Save/Update are install-like resolve + // actions (the curation-blockable case this audit exists for), so they pass the + // matching "install" verb. Publish-PSResource uploads rather than resolves a + // package - curation cannot block it in the way this audit checks for - so it + // runs directly, without a cmdName that would never legitimately apply. + if name == psresourcecommand.SubCommandPublish { + return psResourceCmd(name)(c) + } + return securityCLI.WrapCmdWithCurationPostFailureRun(c, psResourceCmd(name), techutils.Nuget, "install") }, }) } diff --git a/buildtools/cli_test.go b/buildtools/cli_test.go index 89e2b2753..573b8f384 100644 --- a/buildtools/cli_test.go +++ b/buildtools/cli_test.go @@ -7,7 +7,9 @@ import ( dotnetutils "github.com/jfrog/build-info-go/build/utils/dotnet" containerutils "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/ocicontainer" + psresourcecommand "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/psresource" "github.com/jfrog/jfrog-cli-core/v2/plugins/components" + securityCLI "github.com/jfrog/jfrog-cli-security/cli" securityDocs "github.com/jfrog/jfrog-cli-security/cli/docs" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -399,3 +401,40 @@ func TestResolveContainerManagerType(t *testing.T) { }) } } + +// TestPSResourceCurationCmdNameMatchesAllowlist guards against a real bug: the curation +// post-failure audit is gated behind jfrog-cli-security's own fixed, generic verb allowlist +// ({install, build, i, add, ci, get, mod}), not this cmdlet's own PascalCase name - passing +// "Install-PSResource" (etc.) as cmdName made the audit a silent no-op for every PSResource +// command, since none of those names is ever in that allowlist. +func TestPSResourceCurationCmdNameMatchesAllowlist(t *testing.T) { + assert.True(t, securityCLI.IsSupportedCommandForCurationInspect("install"), + "the verb this fix actually passes must be one jfrog-cli-security recognizes") + + for _, rawCmdletName := range []string{ + psresourcecommand.SubCommandInstall, + psresourcecommand.SubCommandSave, + psresourcecommand.SubCommandUpdate, + psresourcecommand.SubCommandPublish, + } { + assert.False(t, securityCLI.IsSupportedCommandForCurationInspect(rawCmdletName), + "%q must never be in the allowlist - this is exactly why passing it directly was the bug", rawCmdletName) + } +} + +// TestPSResourceCommandEntriesRegistersAllFour is a structural sanity check on the table-driven +// registration itself: all four expected command names must be present, with the exact native +// cmdlet casing (case-sensitive, matching how PowerShell itself writes them). +func TestPSResourceCommandEntriesRegistersAllFour(t *testing.T) { + entries := psResourceCommandEntries() + names := make([]string, 0, len(entries)) + for _, entry := range entries { + names = append(names, entry.Name) + } + assert.ElementsMatch(t, []string{ + psresourcecommand.SubCommandInstall, + psresourcecommand.SubCommandSave, + psresourcecommand.SubCommandUpdate, + psresourcecommand.SubCommandPublish, + }, names) +} diff --git a/docs/buildtools/psresource/help_test.go b/docs/buildtools/psresource/help_test.go new file mode 100644 index 000000000..e396978ac --- /dev/null +++ b/docs/buildtools/psresource/help_test.go @@ -0,0 +1,55 @@ +package psresource + +import ( + "strings" + "testing" +) + +// TestGetAIDescriptionKnownCmdlets closes a real test-coverage gap: cmdletMetaByName and +// GetAIDescription's map lookup into it had no test at all across any of the four cmdlets, so a +// typo'd key or a renamed SubCommand* constant elsewhere would silently render an empty/broken +// description with nothing to catch it. +func TestGetAIDescriptionKnownCmdlets(t *testing.T) { + for _, cmdletName := range []string{"Install-PSResource", "Save-PSResource", "Update-PSResource", "Publish-PSResource"} { + t.Run(cmdletName, func(t *testing.T) { + desc := GetAIDescription(cmdletName) + if desc == "" { + t.Fatalf("GetAIDescription(%q) returned an empty string", cmdletName) + } + for _, marker := range []string{cmdletName, "Prerequisites:", "Examples:", "Gotchas:", "$ jf " + cmdletName} { + if !strings.Contains(desc, marker) { + t.Errorf("GetAIDescription(%q) is missing expected content %q\nfull output:\n%s", cmdletName, marker, desc) + } + } + }) + } +} + +// TestGetAIDescriptionUnknownCmdletDoesNotPanic documents the current fallback behavior for a +// cmdletName with no matching cmdletMetaByName entry (e.g. a future typo or renamed constant): +// it must not panic, even though the rendered text is necessarily incomplete. +func TestGetAIDescriptionUnknownCmdletDoesNotPanic(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Fatalf("GetAIDescription panicked on an unknown cmdlet name: %v", r) + } + }() + desc := GetAIDescription("Not-A-Real-Cmdlet") + if !strings.Contains(desc, "Prerequisites:") { + t.Errorf("expected the shared prose to still render even with no metadata match, got: %q", desc) + } +} + +func TestUsageGetDescriptionGetArguments(t *testing.T) { + for _, cmdletName := range []string{"Install-PSResource", "Save-PSResource", "Update-PSResource", "Publish-PSResource"} { + if got := Usage(cmdletName); len(got) != 1 || !strings.Contains(got[0], cmdletName) { + t.Errorf("Usage(%q) = %v, want a single line containing the cmdlet name", cmdletName, got) + } + if got := GetDescription(cmdletName); !strings.Contains(got, cmdletName) { + t.Errorf("GetDescription(%q) = %q, want it to contain the cmdlet name", cmdletName, got) + } + if got := GetArguments(cmdletName); !strings.Contains(got, cmdletName) { + t.Errorf("GetArguments(%q) = %q, want it to contain the cmdlet name", cmdletName, got) + } + } +} diff --git a/go.mod b/go.mod index d5da7466c..c367b2962 100644 --- a/go.mod +++ b/go.mod @@ -249,8 +249,17 @@ require ( // 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-cli-artifactory => github.com/bhanurp/jfrog-cli-artifactory v0.1.12-0.20260918093822-3973ddc5cd5a +// !!! RELEASE-BLOCKING - REMOVE BEFORE MERGING RTECO-2247 !!! +// The three replace directives below point at a personal fork (github.com/bhanurp/*) instead of +// the official jfrog org repos. They exist only because this branch's PSResourceGet support spans +// unmerged changes in jfrog-cli-artifactory, jfrog-cli-core and build-info-go that have no released +// version yet - without them this repo cannot build against those changes at all. Building or +// releasing the jf binary with these still in place means shipping code from an individually-owned, +// unaudited fork instead of jfrog/*'s own reviewed history. Remove all three (and re-run `go mod +// tidy`) once the corresponding upstream PRs land and this repo's go.mod can pin real released +// versions instead. +replace github.com/jfrog/jfrog-cli-artifactory => github.com/bhanurp/jfrog-cli-artifactory v0.1.12-0.20260921070440-8380ae1d78c2 replace github.com/jfrog/jfrog-cli-core/v2 => github.com/bhanurp/jfrog-cli-core/v2 v2.57.7-0.20260918083500-d83d8a45bd7e -replace github.com/jfrog/build-info-go => github.com/bhanurp/build-info-go v1.10.10-0.20260918084802-08a1f22415c3 +replace github.com/jfrog/build-info-go => github.com/bhanurp/build-info-go v1.10.10-0.20260921070652-aa0b5c7b6789 diff --git a/go.sum b/go.sum index 9ec4bbeb8..492000ded 100644 --- a/go.sum +++ b/go.sum @@ -101,10 +101,10 @@ github.com/beevik/etree v1.7.0 h1:xjBk9O4p4x7D1YajePjfLzdaFC4/uYUENA7P0pv6gXA= github.com/beevik/etree v1.7.0/go.mod h1:bh4zJxiIr62SOf9pRzN7UUYaEDa9HEKafK25+sLc0Gc= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bhanurp/build-info-go v1.10.10-0.20260918084802-08a1f22415c3 h1:eocBGKpnyEzzMbn6LbOxwWgdvIuMWH7O26ai8tIMjXU= -github.com/bhanurp/build-info-go v1.10.10-0.20260918084802-08a1f22415c3/go.mod h1:PCfjSoigriSjUWupH0jffPtDq+fnMGIZxgM3Cgw9QIg= -github.com/bhanurp/jfrog-cli-artifactory v0.1.12-0.20260918093822-3973ddc5cd5a h1:c1qHuSFat9q4Y6rDHdIjkdt0GiR+Ex07b8auSmq7+jc= -github.com/bhanurp/jfrog-cli-artifactory v0.1.12-0.20260918093822-3973ddc5cd5a/go.mod h1:+rC1zD/8FhYBkBbW7XEBSe0lQBiNpX1MCC+2Jp4va1w= +github.com/bhanurp/build-info-go v1.10.10-0.20260921070652-aa0b5c7b6789 h1:QGSN9nvYBsymOmgCT+XcWRXS6AtM0iSHJE6ZbG0EUQo= +github.com/bhanurp/build-info-go v1.10.10-0.20260921070652-aa0b5c7b6789/go.mod h1:PCfjSoigriSjUWupH0jffPtDq+fnMGIZxgM3Cgw9QIg= +github.com/bhanurp/jfrog-cli-artifactory v0.1.12-0.20260921070440-8380ae1d78c2 h1:tXVxC6cmAznZg9D6Bqx2CUDodfbxe1SpMJfPgQY9J+I= +github.com/bhanurp/jfrog-cli-artifactory v0.1.12-0.20260921070440-8380ae1d78c2/go.mod h1:bF53IxuUwomG2CyBZx5reAZ7rFUfWhu+q+8dAOVb3nA= github.com/bhanurp/jfrog-cli-core/v2 v2.57.7-0.20260918083500-d83d8a45bd7e h1:F8IY62bC5bZnh7VZUaqIoQQGcydqpPwO0CqL4bNVCFY= github.com/bhanurp/jfrog-cli-core/v2 v2.57.7-0.20260918083500-d83d8a45bd7e/go.mod h1:SwV+DNLBnWLxBeNeZpJk+xxAbqJ8ywq1va56up+AGu4= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= diff --git a/psresource_test.go b/psresource_test.go index 7502d9610..058bb8e90 100644 --- a/psresource_test.go +++ b/psresource_test.go @@ -4,8 +4,10 @@ import ( "os/exec" "testing" + 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" clientTestUtils "github.com/jfrog/jfrog-client-go/utils/tests" "github.com/stretchr/testify/assert" @@ -134,7 +136,70 @@ func TestPSResourceInstallCollectsDependencyBuildInfo(t *testing.T) { // (flag parsing, server resolution, FlexPack command construction) got far enough to invoke it. if err != nil { t.Logf("'jf Install-PSResource' returned %v; this is expected without a real registered feed", err) + return } + // If it did succeed (e.g. against a real registered feed with SomeModule actually resolvable), + // the locally-collected build-info must actually contain it - a silently empty or corrupted + // build-info must not pass this test just because the command itself returned no error. + inttestutils.ValidateGeneratedBuildInfoModule(t, buildName, buildNumber, "", []string{"psresource-project"}, buildInfo.Nuget) +} + +// TestPSResourceSaveCollectsDependencyBuildInfo mirrors TestPSResourceInstallCollectsDependencyBuildInfo +// for 'jf Save-PSResource'. Without this, only Install-PSResource's SetSubCommand/native-dispatch +// wiring was ever exercised past the shared '--help' early-return - a wiring bug specific to +// Save-PSResource (wrong cmdletName captured, wrong flag forwarded) would not have been caught by +// any test. +func TestPSResourceSaveCollectsDependencyBuildInfo(t *testing.T) { + initPSResourceTest(t) + defer cleanTestsHomeEnv() + + buildName := tests.PSResourceBuildName + "-save" + buildNumber := "1" + + err := runPSResource(t, "Save-PSResource", "-Name", "SomeModule", "-Repository", tests.NugetVirtualRepo, + "--repo-resolve="+tests.NugetVirtualRepo, "--build-name="+buildName, "--build-number="+buildNumber) + if err != nil { + t.Logf("'jf Save-PSResource' returned %v; this is expected without a real registered feed", err) + return + } + inttestutils.ValidateGeneratedBuildInfoModule(t, buildName, buildNumber, "", []string{"psresource-project"}, buildInfo.Nuget) +} + +// TestPSResourceUpdateCollectsDependencyBuildInfo mirrors TestPSResourceInstallCollectsDependencyBuildInfo +// for 'jf Update-PSResource'. +func TestPSResourceUpdateCollectsDependencyBuildInfo(t *testing.T) { + initPSResourceTest(t) + defer cleanTestsHomeEnv() + + buildName := tests.PSResourceBuildName + "-update" + buildNumber := "1" + + err := runPSResource(t, "Update-PSResource", "-Name", "SomeModule", "-Repository", tests.NugetVirtualRepo, + "--repo-resolve="+tests.NugetVirtualRepo, "--build-name="+buildName, "--build-number="+buildNumber) + if err != nil { + t.Logf("'jf Update-PSResource' returned %v; this is expected without a real registered feed", err) + return + } + inttestutils.ValidateGeneratedBuildInfoModule(t, buildName, buildNumber, "", []string{"psresource-project"}, buildInfo.Nuget) +} + +// TestPSResourcePublishCollectsArtifactBuildInfo mirrors TestPSResourceInstallCollectsDependencyBuildInfo +// for 'jf Publish-PSResource' - the one command in the family that collects artifact (not +// dependency) build-info. +func TestPSResourcePublishCollectsArtifactBuildInfo(t *testing.T) { + initPSResourceTest(t) + defer cleanTestsHomeEnv() + + buildName := tests.PSResourceBuildName + "-publish" + buildNumber := "1" + + err := runPSResource(t, "Publish-PSResource", "-Path", t.TempDir(), "-Repository", tests.NugetLocalRepo, + "--repo="+tests.NugetLocalRepo, "--build-name="+buildName, "--build-number="+buildNumber) + if err != nil { + t.Logf("'jf Publish-PSResource' returned %v; this is expected without a real registered feed and module manifest", err) + return + } + inttestutils.ValidateGeneratedBuildInfoModule(t, buildName, buildNumber, "", []string{"psresource-project"}, buildInfo.Nuget) } // TestPSResourcePlatformGateWithoutPwsh asserts that on a machine with no usable pwsh + From 881ad302753f2fcd821415e7f8f0697788e57b22 Mon Sep 17 00:00:00 2001 From: Bhanu Reddy Date: Mon, 21 Sep 2026 12:59:05 +0530 Subject: [PATCH 4/9] Point go.mod at the canonical jfrog org branches, not the fork All three in-flight dependencies (jfrog-cli-artifactory, jfrog-cli-core, build-info-go) are now pushed directly to their jfrog org repos (RTECO-2247, or RTECO-2247-psresource for jfrog-cli-artifactory, which has a branch-naming rule requiring a suffix), so these replace directives no longer need to point at the bhanurp personal fork. Co-Authored-By: Claude Sonnet 5 --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index c367b2962..f90745fef 100644 --- a/go.mod +++ b/go.mod @@ -258,8 +258,8 @@ require ( // unaudited fork instead of jfrog/*'s own reviewed history. Remove all three (and re-run `go mod // tidy`) once the corresponding upstream PRs land and this repo's go.mod can pin real released // versions instead. -replace github.com/jfrog/jfrog-cli-artifactory => github.com/bhanurp/jfrog-cli-artifactory v0.1.12-0.20260921070440-8380ae1d78c2 +replace github.com/jfrog/jfrog-cli-artifactory => github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260921072637-1ead320097cb -replace github.com/jfrog/jfrog-cli-core/v2 => github.com/bhanurp/jfrog-cli-core/v2 v2.57.7-0.20260918083500-d83d8a45bd7e +replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260918083500-d83d8a45bd7e -replace github.com/jfrog/build-info-go => github.com/bhanurp/build-info-go v1.10.10-0.20260921070652-aa0b5c7b6789 +replace github.com/jfrog/build-info-go => github.com/jfrog/build-info-go v1.13.1-0.20260921070652-aa0b5c7b6789 diff --git a/go.sum b/go.sum index 492000ded..caf6d7807 100644 --- a/go.sum +++ b/go.sum @@ -101,12 +101,6 @@ github.com/beevik/etree v1.7.0 h1:xjBk9O4p4x7D1YajePjfLzdaFC4/uYUENA7P0pv6gXA= github.com/beevik/etree v1.7.0/go.mod h1:bh4zJxiIr62SOf9pRzN7UUYaEDa9HEKafK25+sLc0Gc= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bhanurp/build-info-go v1.10.10-0.20260921070652-aa0b5c7b6789 h1:QGSN9nvYBsymOmgCT+XcWRXS6AtM0iSHJE6ZbG0EUQo= -github.com/bhanurp/build-info-go v1.10.10-0.20260921070652-aa0b5c7b6789/go.mod h1:PCfjSoigriSjUWupH0jffPtDq+fnMGIZxgM3Cgw9QIg= -github.com/bhanurp/jfrog-cli-artifactory v0.1.12-0.20260921070440-8380ae1d78c2 h1:tXVxC6cmAznZg9D6Bqx2CUDodfbxe1SpMJfPgQY9J+I= -github.com/bhanurp/jfrog-cli-artifactory v0.1.12-0.20260921070440-8380ae1d78c2/go.mod h1:bF53IxuUwomG2CyBZx5reAZ7rFUfWhu+q+8dAOVb3nA= -github.com/bhanurp/jfrog-cli-core/v2 v2.57.7-0.20260918083500-d83d8a45bd7e h1:F8IY62bC5bZnh7VZUaqIoQQGcydqpPwO0CqL4bNVCFY= -github.com/bhanurp/jfrog-cli-core/v2 v2.57.7-0.20260918083500-d83d8a45bd7e/go.mod h1:SwV+DNLBnWLxBeNeZpJk+xxAbqJ8ywq1va56up+AGu4= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= @@ -396,6 +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.20260921070652-aa0b5c7b6789 h1:wVEDlUxuoTPHGyPbm6xZ/+tDkVfjSqj2hZaGNihknlE= +github.com/jfrog/build-info-go v1.13.1-0.20260921070652-aa0b5c7b6789/go.mod h1:PCfjSoigriSjUWupH0jffPtDq+fnMGIZxgM3Cgw9QIg= 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= @@ -406,6 +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.20260921072637-1ead320097cb h1:vqlmTmB6V8J+iUwdYgVwx5WwbOqUF5seARI1RslSsws= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260921072637-1ead320097cb/go.mod h1:zT4CWpElUKBri4Kl/7K85mn+w9XUl3koMmOA9puGmPs= +github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260918083500-d83d8a45bd7e h1:SPaUVRrxMHg8t1aIRwx+JvfYRpxaQECwLCBdo46FfO4= +github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260918083500-d83d8a45bd7e/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= From b812b9b0733c4fc3a3a468afa42817fe52651e91 Mon Sep 17 00:00:00 2001 From: Bhanu Reddy Date: Mon, 21 Sep 2026 15:30:29 +0530 Subject: [PATCH 5/9] Fix wastedassign lint failure in psresourcePlatformAvailable shell := "" was always overwritten by one of the two LookPath branches before ever being read, tripping wastedassign in CI's Static Check. Uses exec.LookPath's own error return to choose the fallback instead of a pre-initialized variable. Co-Authored-By: Claude Sonnet 5 --- psresource_test.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/psresource_test.go b/psresource_test.go index 058bb8e90..fb62f9969 100644 --- a/psresource_test.go +++ b/psresource_test.go @@ -19,12 +19,11 @@ import ( // module installed can be found on PATH. Unlike Chocolatey, PSResourceGet is cross-platform, so // the gate here is "is the tool present", never an OS check. func psresourcePlatformAvailable() bool { - shell := "" - if _, err := exec.LookPath("pwsh"); err == nil { - shell = "pwsh" - } else if _, err := exec.LookPath("powershell.exe"); err == nil { - shell = "powershell.exe" - } else { + shell, err := exec.LookPath("pwsh") + if err != nil { + shell, err = exec.LookPath("powershell.exe") + } + if err != nil { return false } checkScript := "if (Get-Module -ListAvailable -Name Microsoft.PowerShell.PSResourceGet) { exit 0 } else { exit 1 }" From 96c4148aa0b93e56ba0a4cb8adb257069e091830 Mon Sep 17 00:00:00 2001 From: Bhanu Reddy Date: Tue, 22 Sep 2026 14:02:01 +0530 Subject: [PATCH 6/9] Simplify PowerShell verification in psresourceTests workflow Address PR review feedback: GitHub-hosted runners (ubuntu-latest, macos-latest, windows-latest) already ship PowerShell 7 as pwsh out of the box, so installing it via apt/curl/brew is unnecessary. Replace the install step with a simple verification step (pwsh -v) as suggested by the reviewer. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/psresourceTests.yml | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/.github/workflows/psresourceTests.yml b/.github/workflows/psresourceTests.yml index 74302698a..33e5c464f 100644 --- a/.github/workflows/psresourceTests.yml +++ b/.github/workflows/psresourceTests.yml @@ -34,19 +34,10 @@ jobs: # PSResourceGet is cross-platform (PowerShell 7+), unlike Chocolatey which is Windows-only, # so this workflow runs on every OS in the matrix instead of a single Windows job. - # windows-2022 ships PowerShell 7 as 'pwsh' already; ubuntu/macos runners need it installed. - - name: Install PowerShell 7 - if: matrix.os.name != 'windows' - shell: bash - run: | - if ! command -v pwsh >/dev/null 2>&1; then - if [ "${{ matrix.os.name }}" = "ubuntu" ]; then - curl -sSL https://aka.ms/install-powershell.sh | sudo bash - else - brew install --cask powershell - fi - fi - pwsh -v + # GitHub-hosted runners (ubuntu-latest, macos-latest, windows-latest) already ship + # PowerShell 7 as 'pwsh' out of the box, so we just verify it's available. + - name: Verify PowerShell is installed + run: pwsh -v - name: Install Microsoft.PowerShell.PSResourceGet shell: pwsh From dbca004c3fe69854706372228ff1373786a533c0 Mon Sep 17 00:00:00 2001 From: Bhanu Reddy Date: Sun, 27 Sep 2026 11:23:14 +0530 Subject: [PATCH 7/9] Re-pin jfrog-cli-artifactory and scope PSResource/Choco suites to Windows Re-pins jfrog-cli-artifactory to 8c77089 (#569, "align Chocolatey source handling with nuget, dotnet and psresource"). That commit touches only the choco and dotnet packages, so it carries no psresource source change. Scopes both native-shell suites to a single Windows job: - psresourceTests.yml drops the ubuntu/windows/macos matrix. PSResourceGet is cross-platform, so this is a coverage choice rather than a constraint, and the comment now says so instead of arguing for the matrix. - chocoTests.yml drops the linux "Chocolatey OS gate" job. That job installs no Artifactory by design, but the tests it runs still reach localhost:8081/artifactory/api/repositories during setup, so it fails with "connection refused" on master today. Also adds the allow-unsafe-pr-checkout flag the other suites pass, since build-gate invokes this workflow from a pull_request_target context. Co-Authored-By: Claude Opus 5 --- .github/workflows/chocoTests.yml | 22 ----------------- .github/workflows/psresourceTests.yml | 34 +++++++++++++-------------- go.mod | 2 +- go.sum | 4 ++-- 4 files changed, 20 insertions(+), 42 deletions(-) diff --git a/.github/workflows/chocoTests.yml b/.github/workflows/chocoTests.yml index 93c5a58fc..6c3ea6ac9 100644 --- a/.github/workflows/chocoTests.yml +++ b/.github/workflows/chocoTests.yml @@ -51,25 +51,3 @@ jobs: 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/.github/workflows/psresourceTests.yml b/.github/workflows/psresourceTests.yml index 33e5c464f..8801e8558 100644 --- a/.github/workflows/psresourceTests.yml +++ b/.github/workflows/psresourceTests.yml @@ -1,27 +1,28 @@ name: PSResource Tests + on: workflow_call: workflow_dispatch: jobs: PSResource-Tests: - name: PSResource tests (${{ matrix.os.name }}) - strategy: - fail-fast: false - matrix: - os: - - name: ubuntu - version: 24.04 - - name: windows - version: 2022 - - name: macos - version: 14 - runs-on: ${{ matrix.os.name }}-${{ matrix.os.version }} + name: PSResource tests (windows) + # PSResourceGet itself is cross-platform (it ships with PowerShell 7+, which GitHub's ubuntu + # and macos runners also carry), so this suite could in principle run on a matrix. It is + # deliberately pinned to Windows only, matching the Chocolatey suite: PowerShell is the native + # shell there, so Windows is both the primary platform for these cmdlets and the configuration + # most likely to break. Running a single job also keeps one 'Install local Artifactory' step + # per suite instead of three, which is where the non-Windows jobs of other suites have been + # failing. If cross-platform coverage is wanted later, add ubuntu/macos back as a matrix here - + # nothing in the command implementation is Windows-specific. + 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 @@ -32,10 +33,9 @@ jobs: - name: Setup Go with cache uses: jfrog/.github/actions/install-go-with-cache@main - # PSResourceGet is cross-platform (PowerShell 7+), unlike Chocolatey which is Windows-only, - # so this workflow runs on every OS in the matrix instead of a single Windows job. - # GitHub-hosted runners (ubuntu-latest, macos-latest, windows-latest) already ship - # PowerShell 7 as 'pwsh' out of the box, so we just verify it's available. + # GitHub-hosted windows runners ship PowerShell 7 as 'pwsh', so this only verifies it is + # present rather than installing it - failing here gives a clearer signal than letting every + # psresource test fail later on a missing interpreter. - name: Verify PowerShell is installed run: pwsh -v @@ -49,7 +49,7 @@ jobs: uses: jfrog/.github/actions/install-local-artifactory@main with: RTLIC: ${{ secrets.RTLIC }} - RT_CONNECTION_TIMEOUT_SECONDS: '1200' + RT_CONNECTION_TIMEOUT_SECONDS: ${{ env.RT_CONNECTION_TIMEOUT_SECONDS || '1200' }} - name: Run PSResource tests run: >- diff --git a/go.mod b/go.mod index 021dca23f..894f85ce3 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/jfrog/build-info-go v1.13.1-0.20260925090031-f9d40441f262 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.20260925102921-90d18d083a8b + github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260927054148-8c77089833c9 github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260925084932-b47892ded3a0 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 diff --git a/go.sum b/go.sum index 598c8c0e0..d9613c30a 100644 --- a/go.sum +++ b/go.sum @@ -402,8 +402,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.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.20260925102921-90d18d083a8b h1:ohcqfRgj72GE9IM7z/i5h92fJ5ngXHpdfhmUUurl7J0= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260925102921-90d18d083a8b/go.mod h1:VaMX/0GKExKszogM+G62260Krx15l4EV/MQ0ftgbBW0= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260927054148-8c77089833c9 h1:s6Sh8JWKVUD8mF3zuoeVuv5O0V0SXQJGWWk3J8JLaWo= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260927054148-8c77089833c9/go.mod h1:VaMX/0GKExKszogM+G62260Krx15l4EV/MQ0ftgbBW0= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260925084932-b47892ded3a0 h1:SNXwdC0MIjNC2dCf8lNaZvMNpy6CLWdsZllcIhC/atQ= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260925084932-b47892ded3a0/go.mod h1:SwV+DNLBnWLxBeNeZpJk+xxAbqJ8ywq1va56up+AGu4= github.com/jfrog/jfrog-cli-evidence v0.11.1-0.20260824063609-79b735ec565e h1:+QYbewvK+PZKbfPpxYmy0bewhqMFtJPk/tUbCICjf8U= From 75ed6adedbd5ee320e50afffd4826db2b3f5a462 Mon Sep 17 00:00:00 2001 From: Bhanu Reddy Date: Sun, 27 Sep 2026 11:38:48 +0530 Subject: [PATCH 8/9] Register the psresource suite with the build-tools test setup TestSetupPSResourceConfiguresRepository failed on the Windows runner with "The repository 'cli-nuget-virtual' does not exist", because --test.psresource was missing from the two build-tools gates in TestMain/tearDownIntegrationTests. Without it, InitBuildToolsTests never ran for the suite, so no NuGet local/remote/virtual repositories were created and the tests/*.go globals kept their unsuffixed defaults - the tests asked Artifactory for 'cli-nuget-virtual' while every other suite works against 'cli-nuget-virtual-'. CleanBuildToolsTests was skipped for the same reason, leaking repositories. Adding *tests.TestPSResource next to *tests.TestChoco in both conditions is all that is needed; the repository, virtual-repository and build-name maps in utils/tests/utils.go were already wired for the suite. Co-Authored-By: Claude Opus 5 --- main_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main_test.go b/main_test.go index 3d6c32e7a..8b7b12d7a 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.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 { + if *tests.TestNpm || *tests.TestPnpm || *tests.TestCargo || *tests.TestGradle || *tests.TestMaven || *tests.TestGo || *tests.TestNuget || *tests.TestChoco || *tests.TestPSResource || *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.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 { + if *tests.TestNpm || *tests.TestPnpm || *tests.TestGradle || *tests.TestMaven || *tests.TestGo || *tests.TestNuget || *tests.TestChoco || *tests.TestPSResource || *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 { From eda161e17005fbf0cfc72710928b03ea9a3ebf42 Mon Sep 17 00:00:00 2001 From: Bhanu Reddy Date: Sun, 27 Sep 2026 12:28:35 +0530 Subject: [PATCH 9/9] Fix the two failing Chocolatey Windows tests TestChocoCommandPropertyRedactsApiKey: chocoPushApiKey built the '-k' value as serverDetails.User + ":" + , and authenticate() sets either User+Password or AccessToken, never both. Against a local Artifactory the suite always gets an access token, so User was empty and the pushed pair was ":". Chocolatey rejects that with "Invalid credentials specified", falls back to prompting for a username on stdin, and aborts with System.InvalidOperationException on a runner with no console. Recover the username from the access token's own JWT subject via auth.ExtractUsernameFromAccessToken, the way jf's own dotnetcommand.go does, falling back to the configured test user for reference tokens and API keys, which carry no subject. TestChocoInstallCollectsDependencies: the dependency's 'repository' was asserted against the build-info fetched back from Artifactory, where it is always empty - Artifactory's build-info schema has no per-dependency repository field, so the value does not survive a publish/fetch round trip. The collector does set it (covered in build-info-go by flexpack/choco's own unit tests), and the CLI does forward --repo-resolve, so assert it against the locally collected build-info instead, which is the part of the contract jfrog-cli actually owns. The read has to happen before the publish, because publishing clears the local build state. Co-Authored-By: Claude Opus 5 --- choco_test.go | 50 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/choco_test.go b/choco_test.go index 405e862f1..b02219ca9 100644 --- a/choco_test.go +++ b/choco_test.go @@ -13,10 +13,12 @@ import ( "time" buildInfo "github.com/jfrog/build-info-go/entities" + buildUtils "github.com/jfrog/jfrog-cli-core/v2/common/build" "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/auth" "github.com/jfrog/jfrog-client-go/http/httpclient" clientTestUtils "github.com/jfrog/jfrog-client-go/utils/tests" "github.com/stretchr/testify/assert" @@ -182,6 +184,18 @@ func getPublishedChocoBuildInfo(t *testing.T, buildName, buildNumber string) bui return publishedBuildInfo.BuildInfo } +// readLocalChocoBuildInfo assembles the build-info 'jf choco' collected on disk, without going +// through Artifactory. A publish is destructive - it clears the local state for that +// build-name/number - so this must be called before getPublishedChocoBuildInfo for the same build. +func readLocalChocoBuildInfo(t *testing.T, buildName, buildNumber string) *buildInfo.BuildInfo { + t.Helper() + chocoBuild, err := buildUtils.CreateBuildInfoService().GetOrCreateBuildWithProject(buildName, buildNumber, "") + require.NoError(t, err) + collected, err := chocoBuild.ToBuildInfo() + require.NoError(t, err) + return collected +} + // 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. @@ -458,14 +472,32 @@ func TestChocoCommandPropertyRedactsApiKey(t *testing.T) { assert.NotContains(t, recordedCommand, apiKey, "the real credential must never be recorded") } -// chocoPushApiKey builds the composite ':' key that Artifactory NuGet endpoints +// chocoPushApiKey builds the composite ':' key that Artifactory NuGet endpoints // expect for authenticated pushes. +// +// The username half must never be empty. Chocolatey rejects a bare ':' pair with +// "Invalid credentials specified" and then falls back to prompting for a username on stdin, which +// aborts with System.InvalidOperationException on a CI runner that has no console. authenticate() +// sets either User+Password or AccessToken - never both - so whenever the suite runs against an +// access token (the default against a local Artifactory) User is empty and the username has to be +// recovered from the token's own JWT subject, the way jf's own dotnetcommand.go does. func chocoPushApiKey(t *testing.T) string { t.Helper() + secret := serverDetails.Password if serverDetails.AccessToken != "" { - return serverDetails.User + ":" + serverDetails.AccessToken + secret = serverDetails.AccessToken } - return serverDetails.User + ":" + serverDetails.Password + user := serverDetails.User + if user == "" && serverDetails.AccessToken != "" { + user = auth.ExtractUsernameFromAccessToken(serverDetails.AccessToken) + } + if user == "" { + // API keys and reference tokens carry no subject to extract from; fall back to the + // configured test username rather than emitting ':'. + user = *tests.JfrogUser + } + require.NotEmpty(t, user, "a non-empty username is required: Chocolatey prompts interactively for ':'") + return user + ":" + secret } // TestSetupChocoConfiguresSource covers the 'jf setup choco' happy path: the machine-wide @@ -509,6 +541,16 @@ func TestChocoInstallCollectsDependencies(t *testing.T) { cleanupChocoInstalledPackage(t, id) requireChocoInstall(t, id, version, sourceName, buildName, buildNumber) + // 'repository' is asserted against the locally collected build-info, before publishing: it is a + // build-info-go field that Artifactory's own build-info schema has no place for on a dependency, + // so it always comes back empty from a publish/fetch round trip. Reading it locally is what + // actually covers jf's side of the contract - that --repo-resolve reaches the collector. + localBuildInfo := readLocalChocoBuildInfo(t, buildName, buildNumber) + require.Len(t, localBuildInfo.Modules, 1) + require.NotEmpty(t, localBuildInfo.Modules[0].Dependencies) + assert.Equal(t, tests.NugetLocalRepo, localBuildInfo.Modules[0].Dependencies[0].Repository, + "--repo-resolve should be recorded as the resolution repository") + installBuildInfo := getPublishedChocoBuildInfo(t, buildName, buildNumber) require.Len(t, installBuildInfo.Modules, 1) module := installBuildInfo.Modules[0] @@ -517,8 +559,6 @@ func TestChocoInstallCollectsDependencies(t *testing.T) { 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") }