RTECO-2247: Add jf Install-/Save-/Update-/Publish-PSResource commands - #3717
Conversation
…p-level jf commands Wire up the four native PowerShell PSResourceGet cmdlets as separate top-level jf commands (deliberately not a single `jf psresource <verb>` 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
# Conflicts: # go.mod # go.sum
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Resolves the choco/psresource overlap (upstream #3708) in buildtools/cli.go, docs/buildtools/setup/help.go, utils/tests/consts.go and utils/tests/utils.go by keeping both package managers, and re-pins build-info-go, jfrog-cli-core and jfrog-cli-artifactory to their merged PSResource commits so the three RELEASE-BLOCKING replace directives could be removed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…dows 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 <noreply@anthropic.com>
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-<timestamp>'. 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 <noreply@anthropic.com>
TestChocoCommandPropertyRedactsApiKey: chocoPushApiKey built the '-k' value as serverDetails.User + ":" + <secret>, 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 ":<secret>". 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 <noreply@anthropic.com>
View full scan results in JFrog Platform📗 Scan Summary
|
at 🎯 Static Application Security Testing (SAST) VulnerabilityFull descriptionVulnerability Details
OverviewHardcoded credentials are usernames, passwords, API keys, or other secrets Vulnerable exampleIn this example, the database username and password for the frog pond are package main
import (
"database/sql"
"fmt"
"log"
_ "[github.com/go-sql-driver/mysql](https://github.com/go-sql-driver/mysql)"
)
func main() {
// VULNERABLE: Hardcoded database credentials for the frog pond.
frogUser := "pond_admin"
frogPassword := "LeapFlog123!"
pondName := "lilypad_db"
connStr := fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/%s",
frogUser, frogPassword, pondName)
lilypadDB, err := sql.Open("mysql", connStr)
if err != nil {
log.Fatalf("Error opening database: %v", err)
}
defer lilypadDB.Close()
err = lilypadDB.Ping()
if err != nil {
log.Fatalf("Error pinging database: %v", err)
}
fmt.Println("Successfully connected to the frog pond.")
}RemediationThe remediated code retrieves the database credentials from environment package main
import (
"database/sql"
"fmt"
"log"
"os"
_ "[github.com/go-sql-driver/mysql](https://github.com/go-sql-driver/mysql)"
)
func main() {
// SECURE: Retrieve credentials from environment variables.
frogUser := os.Getenv("FROG_DB_USER")
frogPassword := os.Getenv("FROG_DB_PASS")
pondName := os.Getenv("FROG_DB_NAME")
if frogUser == "" || frogPassword == "" || pondName == "" {
log.Fatal("DB credentials are not set in environment variables.")
}
connStr := fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/%s",
frogUser, frogPassword, pondName)
lilypadDB, err := sql.Open("mysql", connStr)
if err != nil {
log.Fatalf("Error opening database: %v", err)
}
defer lilypadDB.Close()
err = lilypadDB.Ping()
if err != nil {
log.Fatalf("Error pinging database: %v", err)
}
fmt.Println("Successfully connected to the frog pond.")
} |
at 🎯 Static Application Security Testing (SAST) VulnerabilityFull descriptionVulnerability Details
OverviewHardcoded credentials are usernames, passwords, API keys, or other secrets Vulnerable exampleIn this example, the database username and password for the frog pond are package main
import (
"database/sql"
"fmt"
"log"
_ "[github.com/go-sql-driver/mysql](https://github.com/go-sql-driver/mysql)"
)
func main() {
// VULNERABLE: Hardcoded database credentials for the frog pond.
frogUser := "pond_admin"
frogPassword := "LeapFlog123!"
pondName := "lilypad_db"
connStr := fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/%s",
frogUser, frogPassword, pondName)
lilypadDB, err := sql.Open("mysql", connStr)
if err != nil {
log.Fatalf("Error opening database: %v", err)
}
defer lilypadDB.Close()
err = lilypadDB.Ping()
if err != nil {
log.Fatalf("Error pinging database: %v", err)
}
fmt.Println("Successfully connected to the frog pond.")
}RemediationThe remediated code retrieves the database credentials from environment package main
import (
"database/sql"
"fmt"
"log"
"os"
_ "[github.com/go-sql-driver/mysql](https://github.com/go-sql-driver/mysql)"
)
func main() {
// SECURE: Retrieve credentials from environment variables.
frogUser := os.Getenv("FROG_DB_USER")
frogPassword := os.Getenv("FROG_DB_PASS")
pondName := os.Getenv("FROG_DB_NAME")
if frogUser == "" || frogPassword == "" || pondName == "" {
log.Fatal("DB credentials are not set in environment variables.")
}
connStr := fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/%s",
frogUser, frogPassword, pondName)
lilypadDB, err := sql.Open("mysql", connStr)
if err != nil {
log.Fatalf("Error opening database: %v", err)
}
defer lilypadDB.Close()
err = lilypadDB.Ping()
if err != nil {
log.Fatalf("Error pinging database: %v", err)
}
fmt.Println("Successfully connected to the frog pond.")
} |
at 🎯 Static Application Security Testing (SAST) VulnerabilityFull descriptionVulnerability Details
OverviewHardcoded credentials are usernames, passwords, API keys, or other secrets Vulnerable exampleIn this example, the database username and password for the frog pond are package main
import (
"database/sql"
"fmt"
"log"
_ "[github.com/go-sql-driver/mysql](https://github.com/go-sql-driver/mysql)"
)
func main() {
// VULNERABLE: Hardcoded database credentials for the frog pond.
frogUser := "pond_admin"
frogPassword := "LeapFlog123!"
pondName := "lilypad_db"
connStr := fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/%s",
frogUser, frogPassword, pondName)
lilypadDB, err := sql.Open("mysql", connStr)
if err != nil {
log.Fatalf("Error opening database: %v", err)
}
defer lilypadDB.Close()
err = lilypadDB.Ping()
if err != nil {
log.Fatalf("Error pinging database: %v", err)
}
fmt.Println("Successfully connected to the frog pond.")
}RemediationThe remediated code retrieves the database credentials from environment package main
import (
"database/sql"
"fmt"
"log"
"os"
_ "[github.com/go-sql-driver/mysql](https://github.com/go-sql-driver/mysql)"
)
func main() {
// SECURE: Retrieve credentials from environment variables.
frogUser := os.Getenv("FROG_DB_USER")
frogPassword := os.Getenv("FROG_DB_PASS")
pondName := os.Getenv("FROG_DB_NAME")
if frogUser == "" || frogPassword == "" || pondName == "" {
log.Fatal("DB credentials are not set in environment variables.")
}
connStr := fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/%s",
frogUser, frogPassword, pondName)
lilypadDB, err := sql.Open("mysql", connStr)
if err != nil {
log.Fatalf("Error opening database: %v", err)
}
defer lilypadDB.Close()
err = lilypadDB.Ping()
if err != nil {
log.Fatalf("Error pinging database: %v", err)
}
fmt.Println("Successfully connected to the frog pond.")
} |


Summary
Part of RTECO-2247 — registers four top-level commands wrapping PowerShell's PSResourceGet module:
jf Install-PSResource,jf Save-PSResource,jf Update-PSResource,jf Publish-PSResource. Deliberately four separate commands (matching native PowerShell cmdlet names) rather than onejf psresource <verb>wrapper — unlikejf choco, which uses a single-command, first-positional-argument dispatch.Dependencies — all three merged, and
go.modnow pins each one at its merge commit:v1.13.1-0.20260925090031-f9d40441f262v2.60.1-0.20260925084932-b47892ded3a0v0.8.1-0.20260925102921-90d18d083a8bNote on
go.mod: the three release-blockingreplacedirectives are gone — each dependency is pinned at the merge commit of its sibling PR, so the requiredNo-Replacecheck passes. Each of those commits is the current head of its default branch and already contains the Chocolatey work, so nothing is held back. The transitivegolang.org/x/*bumps (includingcryptov0.57.0) come with those pins.What's here
buildtools/cli.go— the fourcli.Commandentries, built from a shared table (psResourceCommandEntries) rather than four hand-duplicated literals, all backed by onepsResourceCmd(cmdletName)helper.docs/buildtools/psresource/help.go— per-cmdlet help text, rendered from one shared template parameterized by a smallcmdletMetastruct (the four cmdlets' help is ~90% identical prose).utils/cliutils/commandsflags.go— the shared flag set (--build-name,--build-number,--module,--project,--repo,--repo-resolve,--server-id).psresource_test.go) — help text on every platform, CLI-level build-flag-pair validation, and (skipping gracefully without a localpwsh+ PSResourceGet install) end-to-end dependency/artifact build-info collection for all four commands.psresourceTests.yml) — cross-platform (Linux/macOS/Windows), unlikejf choco's Windows-only requirement, since PSResourceGet viapwshis cross-platform.Notable bug fix included (found via an adversarial multi-agent code review after the initial implementation)
WrapCmdWithCurationPostFailureRunwas called with each cmdlet's own PascalCase name ("Install-PSResource", etc.) ascmdName, 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 (which uploads rather than resolves a package, so curation can't block it the way this audit checks for) runs directly without a curationcmdNamethat would never apply.Test plan
go test . -run PSResource -v -args -test.psresource=trueandgo test ./buildtools/... ./docs/buildtools/psresource/...— all pass (end-to-end tests skip gracefully on this machine, which has nopwshinstalled).New regression tests for the curation-cmdName fix,
GetAIDescription's per-cmdlet map lookup, and dispatch-reaching coverage for Save/Update/Publish (previously only Install-PSResource's real dispatch logic was ever exercised past the shared--helpearly-return).golangci-lintclean,gofmtclean.All tests passed. New tests added for the bug found during review.
All static analysis checks passed.
This pull request is on the master branch.
I used gofmt for formatting the code before submitting the pull request.
Full flow was locally tested (help text on every platform; end-to-end paths verified with
pwshunavailable, which is the graceful-skip path).🤖 Generated with Claude Code
Ready for review: the sibling PRs have merged and the
replacedirectives have been removed, so the requiredNo-Replacecheck no longer blocks this branch.upstream/masterhas also been merged in, resolving the overlap with the Chocolatey PR (#3708) inbuildtools/cli.go,docs/buildtools/setup/help.go,utils/tests/consts.goandutils/tests/utils.goby keeping both package managers.