Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/build-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,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.
Expand Down Expand Up @@ -217,6 +221,7 @@ jobs:
- alpine
- npm
- nuget
- psresource
- oidc
- plugins
- pnpm
Expand Down
57 changes: 57 additions & 0 deletions .github/workflows/psresourceTests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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.
# 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
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) || '' }}
107 changes: 105 additions & 2 deletions buildtools/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,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"
Expand Down Expand Up @@ -97,6 +98,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"
Expand All @@ -120,7 +122,7 @@ const (
)

func GetCommands() []cli.Command {
cmds := cliutils.GetSortedCommands(cli.CommandsByName{
cmds := cliutils.GetSortedCommands(append(cli.CommandsByName{
{
Hidden: false,
Name: "setup",
Expand Down Expand Up @@ -689,10 +691,55 @@ 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 {
// 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")
},
})
}
return cmds
}

func skipFlagParsingForDockerCmd() bool {
isDockerScan := false
hasHelpFlag := false
Expand Down Expand Up @@ -1252,6 +1299,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 <subcommand>" 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 {
Expand Down Expand Up @@ -2050,6 +2148,11 @@ func setupCmd(c *cli.Context) (err error) {
return
}
}
if packageManager == project.PSResource {
if err = setup.ValidatePSResourcePlatform(); err != nil {
return err
}
}
if packageManager == project.Choco {
if err = setup.ValidateChocoPlatform(); err != nil {
return err
Expand Down
39 changes: 39 additions & 0 deletions buildtools/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
Loading
Loading