From 804b06ade7ef46486f35173f433296574e5812c0 Mon Sep 17 00:00:00 2001 From: Kanishk Date: Wed, 11 Mar 2026 18:40:02 +0530 Subject: [PATCH 1/8] RTECO-935 - Support pnpm command for jfrog-cli --- .gitignore | 4 +- buildtools/cli.go | 74 +++++++++++++++++++++++++++-- docs/buildtools/pnpmcommand/help.go | 13 +++++ go.mod | 2 +- go.sum | 4 +- utils/cliutils/commandsflags.go | 4 ++ 6 files changed, 92 insertions(+), 9 deletions(-) create mode 100644 docs/buildtools/pnpmcommand/help.go diff --git a/.gitignore b/.gitignore index 4fe717c4d..de312e299 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,6 @@ node_modules # Class files *.class -**/testdata/**/bin \ No newline at end of file +**/testdata/**/bin + +.cursor \ No newline at end of file diff --git a/buildtools/cli.go b/buildtools/cli.go index 509defd9d..15ea0bbd9 100644 --- a/buildtools/cli.go +++ b/buildtools/cli.go @@ -31,6 +31,7 @@ import ( huggingfaceCommands "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/huggingface" "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/mvn" "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/npm" + pnpmcmd "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/pnpm" containerutils "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/ocicontainer" "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/terraform" "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/yarn" @@ -64,6 +65,7 @@ import ( "github.com/jfrog/jfrog-cli/docs/buildtools/mvnconfig" "github.com/jfrog/jfrog-cli/docs/buildtools/npmcommand" "github.com/jfrog/jfrog-cli/docs/buildtools/npmconfig" + "github.com/jfrog/jfrog-cli/docs/buildtools/pnpmcommand" nugetdocs "github.com/jfrog/jfrog-cli/docs/buildtools/nuget" "github.com/jfrog/jfrog-cli/docs/buildtools/nugetconfig" "github.com/jfrog/jfrog-cli/docs/buildtools/pipconfig" @@ -431,6 +433,17 @@ func GetCommands() []cli.Command { return cliutils.CreateConfigCmd(c, project.Pnpm) }, }, + { + Name: "pnpm", + Flags: cliutils.GetCommandFlags(cliutils.Pnpm), + Usage: pnpmcommand.GetDescription(), + HelpName: corecommon.CreateUsage("pnpm", pnpmcommand.GetDescription(), pnpmcommand.Usage), + UsageText: pnpmcommand.GetArguments(), + SkipFlagParsing: true, + BashComplete: corecommon.CreateBashCompletionFunc("install", "i", "publish", "p"), + Category: buildToolsCategory, + Action: pnpmCmd, + }, { Name: "docker", Flags: cliutils.GetCommandFlags(cliutils.Docker), @@ -771,6 +784,61 @@ func YarnCmd(c *cli.Context) error { return commands.Exec(yarnCmd) } +func pnpmCmd(c *cli.Context) error { + if show, err := cliutils.ShowCmdHelpIfNeeded(c, c.Args()); show || err != nil { + return err + } + if c.NArg() < 1 { + return cliutils.WrongNumberOfArgumentsHandler(c) + } + args := cliutils.ExtractCommand(c) + cmdName, filteredArgs := getCommandName(args) + + switch cmdName { + case "install", "i", "publish", "p": + serverDetails, cleanArgs, buildConfiguration, err := extractPnpmOptionsFromArgs(filteredArgs) + if err != nil { + return err + } + pnpmCommand, err := pnpmcmd.NewCommand(cmdName, cleanArgs, buildConfiguration, serverDetails) + if err != nil { + return err + } + return commands.Exec(pnpmCommand) + default: + return runNativePackageManagerCmd("pnpm", append([]string{cmdName}, filteredArgs...)) + } +} + +// runNativePackageManagerCmd runs a package manager command directly, passing through stdio. +func runNativePackageManagerCmd(binary string, args []string) error { + cmd := exec.Command(binary, args...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +// extractPnpmOptionsFromArgs extracts all JFrog CLI options from pnpm command args. +// Returns server details, cleaned args (with JFrog flags removed), and build configuration. +func extractPnpmOptionsFromArgs(args []string) (serverDetails *coreConfig.ServerDetails, cleanArgs []string, buildConfig *build.BuildConfiguration, err error) { + cleanArgs = append([]string(nil), args...) + var serverID string + cleanArgs, serverID, err = coreutils.ExtractServerIdFromCommand(cleanArgs) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to extract server ID: %w", err) + } + serverDetails, err = coreConfig.GetSpecificConfig(serverID, true, true) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to get server configuration for ID '%s': %w", serverID, err) + } + cleanArgs, buildConfig, err = build.ExtractBuildDetailsFromArgs(cleanArgs) + if err != nil { + return nil, nil, nil, err + } + return serverDetails, cleanArgs, buildConfig, nil +} + func NugetCmd(c *cli.Context) error { if show, err := cliutils.ShowCmdHelpIfNeeded(c, c.Args()); show || err != nil { return err @@ -1790,12 +1858,8 @@ func pythonCmd(c *cli.Context, projectType project.ProjectType) error { log.Info(fmt.Sprintf("Publishing to repository: %s (from --repository flag)", deployerRepo)) } - // Execute native poetry command directly (similar to Maven FlexPack) log.Info(fmt.Sprintf("Running Poetry %s.", cmdName)) - poetryCmd := exec.Command("poetry", append([]string{cmdName}, poetryArgs...)...) - poetryCmd.Stdout = os.Stdout - poetryCmd.Stderr = os.Stderr - if err := poetryCmd.Run(); err != nil { + if err := runNativePackageManagerCmd("poetry", append([]string{cmdName}, poetryArgs...)); err != nil { return fmt.Errorf("poetry %s failed: %w", cmdName, err) } diff --git a/docs/buildtools/pnpmcommand/help.go b/docs/buildtools/pnpmcommand/help.go new file mode 100644 index 000000000..56576139d --- /dev/null +++ b/docs/buildtools/pnpmcommand/help.go @@ -0,0 +1,13 @@ +package pnpmcommand + +var Usage = []string{"pnpm [command options]"} + +func GetDescription() string { + return "Run pnpm command." +} + +func GetArguments() string { + return ` install, i Run pnpm install. + publish, p Packs and deploys the pnpm package to the designated npm repository. + help, h` +} diff --git a/go.mod b/go.mod index 7366970be..b892c7b94 100644 --- a/go.mod +++ b/go.mod @@ -248,7 +248,7 @@ require ( // replace github.com/jfrog/jfrog-cli-artifactory => github.com/reshmifrog/jfrog-cli-artifactory v0.0.0-20260303084642-b208fbba798b -// replace github.com/jfrog/jfrog-cli-artifactory => github.com/fluxxBot/jfrog-cli-artifactory v0.0.0-20260130044429-464a5025d08a +replace github.com/jfrog/jfrog-cli-artifactory => github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260311125605-d3731795ec05 //replace github.com/jfrog/build-info-go => github.com/fluxxBot/build-info-go v1.10.10-0.20260105070825-d3f36f619ba5 diff --git a/go.sum b/go.sum index 643f26e7d..db9f19341 100644 --- a/go.sum +++ b/go.sum @@ -419,8 +419,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.20260216085810-1ade6c26b3df h1:raSyae8/h1y8HtzFLf7vZZj91fP/qD94AX+biwBJiqs= github.com/jfrog/jfrog-cli-application v1.0.2-0.20260216085810-1ade6c26b3df/go.mod h1:xum2HquWO5uExa/A7MQs3TgJJVEeoqTR+6Z4mfBr1Xw= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260313044645-ed6f0a05bb5b h1:XTlhwNidgPYk/91FblSENm5/P9kAUkHSLUc3I7FRBdg= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260313044645-ed6f0a05bb5b/go.mod h1:kgw6gIQvJx9bCcOdtAGSUEiCz7nNQmaFbFvNg6byZ6I= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260311125605-d3731795ec05 h1:TN9XIVdj0nV/VaRqAijezreez6wxtv5qDGQqCc2SSsQ= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260311125605-d3731795ec05/go.mod h1:zjbDerW+Pin6VExtlgwRtpnvtI/ySJTnmqnOwXbsrmc= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260225195817-bc599cec3973 h1:awB01Y4m0cWzmXuR3waf5IQnoQxDlbUmqT+FMWOpjbs= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260225195817-bc599cec3973/go.mod h1:yhi+XpiEx18a3t8CZ6M2VpAf3EGqKpBhTzoPBTFe0dk= github.com/jfrog/jfrog-cli-evidence v0.8.3-0.20260202100913-d9ee9476845a h1:lTOAhUjKcOmM/0Kbj4V+I/VHPlW7YNAhIEVpGnCM5mI= diff --git a/utils/cliutils/commandsflags.go b/utils/cliutils/commandsflags.go index c264c7c54..5ed48ca3c 100644 --- a/utils/cliutils/commandsflags.go +++ b/utils/cliutils/commandsflags.go @@ -56,6 +56,7 @@ const ( Npm = "npm" NpmInstallCi = "npm-install-ci" NpmPublish = "npm-publish" + Pnpm = "pnpm" PnpmConfig = "pnpm-config" YarnConfig = "yarn-config" Yarn = "yarn" @@ -1933,6 +1934,9 @@ var commandFlags = map[string][]string{ PnpmConfig: { global, serverIdResolve, repoResolve, }, + Pnpm: { + BuildName, BuildNumber, module, Project, + }, YarnConfig: { global, serverIdResolve, repoResolve, }, From efaeff712aaa2c9e5af48b0c82c99b431332180b Mon Sep 17 00:00:00 2001 From: Kanishk Date: Fri, 13 Mar 2026 22:48:10 +0530 Subject: [PATCH 2/8] Tests and validations --- .github/workflows/pnpmTests.yml | 160 +++++ .gitignore | 1 + go.mod | 2 +- go.sum | 4 +- main_test.go | 4 +- pnpm_test.go | 642 ++++++++++++++++++ testdata/pnpm/pnpmemptylockfile/package.json | 14 + .../pnpm/pnpmemptylockfile/pnpm-lock.yaml | 0 testdata/pnpm/pnpmproject/package.json | 17 + .../pnpm/pnpmprojectonlyallow/package.json | 18 + testdata/pnpm/pnpmscopedproject/package.json | 17 + testdata/pnpm/pnpmtransitive/package.json | 14 + testdata/pnpm/pnpmworkspace/package.json | 17 + .../packages/nested1/package.json | 15 + .../packages/nested2/package.json | 15 + .../pnpm/pnpmworkspace/pnpm-workspace.yaml | 2 + utils/tests/consts.go | 1 + utils/tests/utils.go | 9 +- 18 files changed, 946 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/pnpmTests.yml create mode 100644 pnpm_test.go create mode 100644 testdata/pnpm/pnpmemptylockfile/package.json create mode 100644 testdata/pnpm/pnpmemptylockfile/pnpm-lock.yaml create mode 100644 testdata/pnpm/pnpmproject/package.json create mode 100644 testdata/pnpm/pnpmprojectonlyallow/package.json create mode 100644 testdata/pnpm/pnpmscopedproject/package.json create mode 100644 testdata/pnpm/pnpmtransitive/package.json create mode 100644 testdata/pnpm/pnpmworkspace/package.json create mode 100644 testdata/pnpm/pnpmworkspace/packages/nested1/package.json create mode 100644 testdata/pnpm/pnpmworkspace/packages/nested2/package.json create mode 100644 testdata/pnpm/pnpmworkspace/pnpm-workspace.yaml diff --git a/.github/workflows/pnpmTests.yml b/.github/workflows/pnpmTests.yml new file mode 100644 index 000000000..032f5826e --- /dev/null +++ b/.github/workflows/pnpmTests.yml @@ -0,0 +1,160 @@ +name: pnpm Tests +on: + workflow_dispatch: + push: + branches: + - "master" + pull_request_target: + types: [labeled] + branches: + - "master" + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-pnpm-matrix: + name: Build pnpm version matrix + runs-on: ubuntu-latest + if: github.event_name == 'workflow_dispatch' || github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'safe to test') + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - name: Fetch supported pnpm versions and build matrix + id: set-matrix + run: | + # Fetch active (non-EOL) pnpm versions with major > 9 from endoflife.date API + PNPM_VERSIONS=$(curl -sf https://endoflife.date/api/pnpm.json) + + ACTIVE_MATRIX=$(echo "$PNPM_VERSIONS" | jq -c ' + [ + .[] + | select(.eol == false) + | .cycle as $cycle + | select(($cycle | tonumber) > 9) + | { + pnpm_version: .latest, + pnpm_major: $cycle, + node_version: (if ($cycle | tonumber) >= 11 then "22" else "20" end), + experimental: false + } + ] + ') + + # Dynamically detect the next pre-release (alpha/beta/rc) pnpm version from npm dist-tags + DIST_TAGS=$(curl -sf https://registry.npmjs.org/pnpm | jq -r '."dist-tags" | to_entries[] | select(.key | test("^next-")) | "\(.key)=\(.value)"') + + # Find the highest next-* tag that is beyond the current active versions + HIGHEST_ACTIVE=$(echo "$ACTIVE_MATRIX" | jq -r '[.[].pnpm_major | tonumber] | max') + NEXT_ENTRY="[]" + + while IFS='=' read -r tag version; do + [ -z "$tag" ] && continue + NEXT_MAJOR=$(echo "$tag" | sed 's/next-//') + if [ "$NEXT_MAJOR" -gt "$HIGHEST_ACTIVE" ] 2>/dev/null; then + NEXT_ENTRY=$(jq -n -c --arg tag "$tag" --arg major "$NEXT_MAJOR" '[{ + pnpm_version: $tag, + pnpm_major: $major, + node_version: "22", + experimental: true + }]') + break + fi + done <<< "$DIST_TAGS" + + # Combine active versions + next alpha/beta + MATRIX=$(jq -n -c --argjson active "$ACTIVE_MATRIX" --argjson next "$NEXT_ENTRY" '$active + $next') + + echo "matrix=${MATRIX}" >> "$GITHUB_OUTPUT" + echo "Generated matrix:" + echo "$MATRIX" | jq . + + pnpm-Tests: + name: "pnpm ${{ matrix.pnpm.pnpm_major }} tests (${{ matrix.os.name }})" + needs: build-pnpm-matrix + if: github.event_name == 'workflow_dispatch' || github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'safe to test') + strategy: + fail-fast: false + matrix: + os: + - name: ubuntu + version: 24.04 + - name: windows + version: 2022 + - name: macos + version: 14 + pnpm: ${{ fromJson(needs.build-pnpm-matrix.outputs.matrix) }} + runs-on: ${{ matrix.os.name }}-${{ matrix.os.version }} + steps: + - name: Skip macOS - JGC-413 + if: matrix.os.name == 'macos' + run: | + echo "::warning::JGC-413 - Skip until artifactory bootstrap in osx is fixed" + exit 0 + + - name: Checkout code + if: matrix.os.name != 'macos' + uses: actions/checkout@v5 + with: + ref: ${{ github.event.pull_request.head.sha || github.ref }} + + - name: Setup FastCI + if: matrix.os.name != 'macos' + uses: jfrog-fastci/fastci@v0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + fastci_otel_token: ${{ secrets.FASTCI_TOKEN }} + + - name: Install Node.js + if: matrix.os.name != 'macos' + uses: actions/setup-node@v5 + with: + node-version: ${{ matrix.pnpm.node_version }} + + - name: Install pnpm + if: matrix.os.name != 'macos' + run: npm install -g pnpm@${{ matrix.pnpm.pnpm_version }} + + - name: Validate pnpm and Node.js versions + if: matrix.os.name != 'macos' + run: | + echo "=== Version Validation ===" + PNPM_VER=$(pnpm --version) + NODE_VER=$(node --version | sed 's/^v//') + echo "pnpm version: ${PNPM_VER}" + echo "Node.js version: ${NODE_VER}" + + # Validate pnpm major version >= 10 + PNPM_MAJOR=$(echo "$PNPM_VER" | cut -d. -f1) + if [ "$PNPM_MAJOR" -lt 10 ]; then + echo "::error::pnpm version ${PNPM_VER} is below minimum required (10.x). Only pnpm >= 10 is supported." + exit 1 + fi + echo "✓ pnpm version ${PNPM_VER} meets minimum requirement (>= 10)" + + # Validate Node.js version >= 18.12 (required by pnpm 10+) + NODE_MAJOR=$(echo "$NODE_VER" | cut -d. -f1) + NODE_MINOR=$(echo "$NODE_VER" | cut -d. -f2) + if [ "$NODE_MAJOR" -lt 18 ] || { [ "$NODE_MAJOR" -eq 18 ] && [ "$NODE_MINOR" -lt 12 ]; }; then + echo "::error::Node.js version ${NODE_VER} is below minimum required (18.12) for pnpm ${PNPM_MAJOR}.x" + exit 1 + fi + echo "✓ Node.js version ${NODE_VER} meets minimum requirement (>= 18.12)" + + - name: Setup Go with cache + if: matrix.os.name != 'macos' + uses: jfrog/.github/actions/install-go-with-cache@main + + - name: Install local Artifactory + if: matrix.os.name != 'macos' + uses: jfrog/.github/actions/install-local-artifactory@main + with: + RTLIC: ${{ secrets.RTLIC }} + RT_CONNECTION_TIMEOUT_SECONDS: ${{ env.RT_CONNECTION_TIMEOUT_SECONDS || '1200' }} + + - name: Run pnpm tests + if: matrix.os.name != 'macos' + env: + YARN_IGNORE_NODE: 1 + run: go test -v github.com/jfrog/jfrog-cli --timeout 0 --test.pnpm diff --git a/.gitignore b/.gitignore index de312e299..99f76c980 100644 --- a/.gitignore +++ b/.gitignore @@ -36,5 +36,6 @@ node_modules # Class files *.class **/testdata/**/bin +testdata/**/.gradle .cursor \ No newline at end of file diff --git a/go.mod b/go.mod index b892c7b94..64c2337cd 100644 --- a/go.mod +++ b/go.mod @@ -248,7 +248,7 @@ require ( // replace github.com/jfrog/jfrog-cli-artifactory => github.com/reshmifrog/jfrog-cli-artifactory v0.0.0-20260303084642-b208fbba798b -replace github.com/jfrog/jfrog-cli-artifactory => github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260311125605-d3731795ec05 +replace github.com/jfrog/jfrog-cli-artifactory => github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260313172139-c4ff104c0ae4 //replace github.com/jfrog/build-info-go => github.com/fluxxBot/build-info-go v1.10.10-0.20260105070825-d3f36f619ba5 diff --git a/go.sum b/go.sum index db9f19341..31c77f5fa 100644 --- a/go.sum +++ b/go.sum @@ -419,8 +419,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.20260216085810-1ade6c26b3df h1:raSyae8/h1y8HtzFLf7vZZj91fP/qD94AX+biwBJiqs= github.com/jfrog/jfrog-cli-application v1.0.2-0.20260216085810-1ade6c26b3df/go.mod h1:xum2HquWO5uExa/A7MQs3TgJJVEeoqTR+6Z4mfBr1Xw= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260311125605-d3731795ec05 h1:TN9XIVdj0nV/VaRqAijezreez6wxtv5qDGQqCc2SSsQ= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260311125605-d3731795ec05/go.mod h1:zjbDerW+Pin6VExtlgwRtpnvtI/ySJTnmqnOwXbsrmc= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260313172139-c4ff104c0ae4 h1:Snicq70G82I+NjnH1gdxxPOTG50IAPbR7w2Rm+UrQEE= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260313172139-c4ff104c0ae4/go.mod h1:kgw6gIQvJx9bCcOdtAGSUEiCz7nNQmaFbFvNg6byZ6I= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260225195817-bc599cec3973 h1:awB01Y4m0cWzmXuR3waf5IQnoQxDlbUmqT+FMWOpjbs= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260225195817-bc599cec3973/go.mod h1:yhi+XpiEx18a3t8CZ6M2VpAf3EGqKpBhTzoPBTFe0dk= github.com/jfrog/jfrog-cli-evidence v0.8.3-0.20260202100913-d9ee9476845a h1:lTOAhUjKcOmM/0Kbj4V+I/VHPlW7YNAhIEVpGnCM5mI= diff --git a/main_test.go b/main_test.go index 779e63dc5..b7b10e151 100644 --- a/main_test.go +++ b/main_test.go @@ -67,7 +67,7 @@ func setupIntegrationTests() { InitArtifactoryTests() } - if *tests.TestNpm || *tests.TestGradle || *tests.TestMaven || *tests.TestGo || *tests.TestNuget || *tests.TestPip || *tests.TestPipenv || *tests.TestPoetry || *tests.TestConan || *tests.TestHelm || (*tests.TestArtifactory && !*tests.TestArtifactoryProxy) || *tests.TestArtifactoryProject { + if *tests.TestNpm || *tests.TestPnpm || *tests.TestGradle || *tests.TestMaven || *tests.TestGo || *tests.TestNuget || *tests.TestPip || *tests.TestPipenv || *tests.TestPoetry || *tests.TestConan || *tests.TestHelm || (*tests.TestArtifactory && !*tests.TestArtifactoryProxy) || *tests.TestArtifactoryProject { InitBuildToolsTests() } if *tests.TestDocker || *tests.TestPodman || *tests.TestDockerScan { @@ -103,7 +103,7 @@ func tearDownIntegrationTests() { if (*tests.TestArtifactory && !*tests.TestArtifactoryProxy) || *tests.TestArtifactoryProject { CleanArtifactoryTests() } - if *tests.TestNpm || *tests.TestGradle || *tests.TestMaven || *tests.TestGo || *tests.TestNuget || *tests.TestPip || *tests.TestPipenv || *tests.TestPoetry || *tests.TestConan || *tests.TestHelm || *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.TestPip || *tests.TestPipenv || *tests.TestPoetry || *tests.TestConan || *tests.TestHelm || *tests.TestDocker || *tests.TestPodman || *tests.TestDockerScan || (*tests.TestArtifactory && !*tests.TestArtifactoryProxy) || *tests.TestArtifactoryProject { CleanBuildToolsTests() } if *tests.TestDistribution { diff --git a/pnpm_test.go b/pnpm_test.go new file mode 100644 index 000000000..2233c79fa --- /dev/null +++ b/pnpm_test.go @@ -0,0 +1,642 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "testing" + + buildutils "github.com/jfrog/build-info-go/build/utils" + buildinfo "github.com/jfrog/build-info-go/entities" + biutils "github.com/jfrog/build-info-go/utils" + npmCmdUtils "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/utils" + "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" + "github.com/jfrog/jfrog-cli-core/v2/common/build" + "github.com/jfrog/jfrog-cli-core/v2/common/project" + "github.com/jfrog/jfrog-cli-core/v2/common/spec" + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "github.com/jfrog/jfrog-cli-core/v2/utils/ioutils" + 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/lifecycle/services" + clientTestUtils "github.com/jfrog/jfrog-client-go/utils/tests" + "github.com/stretchr/testify/assert" +) + +type pnpmTestParams struct { + testName string + command string + repo string + pnpmArgs string + wd string + buildNumber string + moduleName string + validationFunc func(*testing.T, pnpmTestParams) +} + +func cleanPnpmTest(t *testing.T) { + clientTestUtils.UnSetEnvAndAssert(t, coreutils.HomeDir) + deleteSpec := spec.NewBuilder().Pattern(tests.NpmRepo).BuildSpec() + _, _, err := tests.DeleteFiles(deleteSpec, serverDetails) + assert.NoError(t, err) + tests.CleanFileSystem() +} + +func TestPnpmInstall(t *testing.T) { + initPnpmTest(t) + defer cleanPnpmTest(t) + wd, err := os.Getwd() + assert.NoError(t, err, "Failed to get current dir") + defer clientTestUtils.ChangeDirAndAssert(t, wd) + + tempCacheDirPath, createTempDirCallback := coretests.CreateTempDirWithCallbackAndAssert(t) + defer createTempDirCallback() + + pnpmProjectPath, pnpmScopedProjectPath := initPnpmFilesTest(t) + var pnpmTests = []pnpmTestParams{ + {testName: "pnpm i", command: "install", repo: tests.NpmRemoteRepo, wd: pnpmProjectPath, validationFunc: validatePnpmInstall}, + {testName: "pnpm i with module", command: "install", repo: tests.NpmRemoteRepo, wd: pnpmProjectPath, moduleName: ModuleNameJFrogTest, validationFunc: validatePnpmInstall}, + {testName: "pnpm i with scoped project", command: "install", repo: tests.NpmRemoteRepo, wd: pnpmScopedProjectPath, validationFunc: validatePnpmInstall}, + } + + for i, pt := range pnpmTests { + t.Run(pt.testName, func(t *testing.T) { + buildNumber := strconv.Itoa(i + 200) + clientTestUtils.ChangeDirAndAssert(t, filepath.Dir(pt.wd)) + + args := []string{"pnpm", pt.command, "--store-dir=" + tempCacheDirPath, + "--build-name=" + tests.PnpmBuildName, "--build-number=" + buildNumber} + if pt.pnpmArgs != "" { + args = append(args, strings.Split(pt.pnpmArgs, " ")...) + } + if pt.moduleName != "" { + args = append(args, "--module="+pt.moduleName) + } else { + pt.moduleName = readPnpmModuleId(t, pt.wd) + } + + runJfrogCli(t, args...) + validatePnpmLocalBuildInfo(t, tests.PnpmBuildName, buildNumber, pt.moduleName) + assert.NoError(t, artifactoryCli.Exec("bp", tests.PnpmBuildName, buildNumber)) + pt.buildNumber = buildNumber + pt.validationFunc(t, pt) + }) + } + + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, tests.PnpmBuildName, artHttpDetails) +} + +func TestPnpmPublish(t *testing.T) { + initPnpmTest(t) + defer cleanPnpmTest(t) + wd, err := os.Getwd() + assert.NoError(t, err, "Failed to get current dir") + defer clientTestUtils.ChangeDirAndAssert(t, wd) + + pnpmProjectPath, pnpmScopedProjectPath := initPnpmFilesTest(t) + var pnpmTests = []pnpmTestParams{ + {testName: "pnpm publish", command: "publish", repo: tests.NpmRepo, wd: pnpmProjectPath, validationFunc: validatePnpmPublish}, + {testName: "pnpm p with module", command: "p", repo: tests.NpmScopedRepo, wd: pnpmScopedProjectPath, moduleName: ModuleNameJFrogTest, validationFunc: validatePnpmScopedPublish}, + } + + for i, pt := range pnpmTests { + t.Run(pt.testName, func(t *testing.T) { + buildNumber := strconv.Itoa(i + 300) + projectDir := filepath.Dir(pt.wd) + clientTestUtils.ChangeDirAndAssert(t, projectDir) + + cleanupAuth := setupPnpmPublishAuth(t, pt.repo) + defer cleanupAuth() + + args := []string{"pnpm", pt.command, "--no-git-checks", + "--build-name=" + tests.PnpmBuildName, "--build-number=" + buildNumber} + if pt.moduleName != "" { + args = append(args, "--module="+pt.moduleName) + } else { + pt.moduleName = readPnpmModuleId(t, pt.wd) + } + + runJfrogCli(t, args...) + validatePnpmLocalBuildInfo(t, tests.PnpmBuildName, buildNumber, pt.moduleName) + assert.NoError(t, artifactoryCli.Exec("bp", tests.PnpmBuildName, buildNumber)) + pt.buildNumber = buildNumber + pt.validationFunc(t, pt) + }) + } + + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, tests.PnpmBuildName, artHttpDetails) +} + +func TestPnpmPublishWorkspace(t *testing.T) { + initPnpmTest(t) + defer cleanPnpmTest(t) + wd, err := os.Getwd() + assert.NoError(t, err, "Failed to get current dir") + defer clientTestUtils.ChangeDirAndAssert(t, wd) + + pnpmWorkspacePath := initPnpmWorkspaceTest(t) + buildNumber := "400" + clientTestUtils.ChangeDirAndAssert(t, filepath.Dir(pnpmWorkspacePath)) + + cleanupAuth := setupPnpmPublishAuth(t, tests.NpmRepo) + defer cleanupAuth() + + runJfrogCli(t, "pnpm", "publish", "-r", "--no-git-checks", + "--build-name="+tests.PnpmBuildName, "--build-number="+buildNumber) + + buildInfoService := build.CreateBuildInfoService() + pnpmBuild, err := buildInfoService.GetOrCreateBuildWithProject(tests.PnpmBuildName, buildNumber, "") + assert.NoError(t, err) + bi, err := pnpmBuild.ToBuildInfo() + assert.NoError(t, err) + assert.NotEmpty(t, bi.Started) + expectedWorkspaceCount := 2 // nested1 and nested2 (root is private) + assert.Len(t, bi.Modules, expectedWorkspaceCount, + "module count should equal number of published workspaces (nested1, nested2), got %d", len(bi.Modules)) + + assert.NoError(t, artifactoryCli.Exec("bp", tests.PnpmBuildName, buildNumber)) + + workspaceArtifacts := []string{ + tests.NpmRepo + "/nested1/-/nested1-1.0.0.tgz", + tests.NpmRepo + "/nested2/-/nested2-1.0.0.tgz", + } + verifyExistInArtifactoryByProps(workspaceArtifacts, + tests.NpmRepo+"/*", + fmt.Sprintf("build.name=%v;build.number=%v;build.timestamp=*", tests.PnpmBuildName, buildNumber), t) + + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, tests.PnpmBuildName, artHttpDetails) +} + +func TestPnpmInstallAndPublishNormalProject(t *testing.T) { + initPnpmTest(t) + defer cleanPnpmTest(t) + wd, err := os.Getwd() + assert.NoError(t, err, "Failed to get current dir") + defer clientTestUtils.ChangeDirAndAssert(t, wd) + + tempCacheDirPath, createTempDirCallback := coretests.CreateTempDirWithCallbackAndAssert(t) + defer createTempDirCallback() + + buildNumber := "500" + pnpmProjectPath := createPnpmProject(t, "pnpmproject") + projectDir := filepath.Dir(pnpmProjectPath) + err = createConfigFileForTest([]string{projectDir}, tests.NpmRemoteRepo, tests.NpmRepo, t, project.Pnpm, false) + assert.NoError(t, err) + prepareArtifactoryForPnpmBuild(t, projectDir) + + clientTestUtils.ChangeDirAndAssert(t, projectDir) + runJfrogCli(t, "pnpm", "install", "--store-dir="+tempCacheDirPath, + "--build-name="+tests.PnpmBuildName, "--build-number="+buildNumber) + + cleanupAuth := setupPnpmPublishAuth(t, tests.NpmRepo) + defer cleanupAuth() + runJfrogCli(t, "pnpm", "publish", "--no-git-checks", + "--build-name="+tests.PnpmBuildName, "--build-number="+buildNumber) + assert.NoError(t, artifactoryCli.Exec("bp", tests.PnpmBuildName, buildNumber)) + + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, tests.PnpmBuildName, buildNumber) + assert.NoError(t, err) + assert.True(t, found) + bi := publishedBuildInfo.BuildInfo + assert.Len(t, bi.Modules, 1) + assert.NotEmpty(t, bi.Modules[0].Dependencies, "module should have dependencies") + assert.NotEmpty(t, bi.Modules[0].Artifacts, "module should have artifacts") + + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, tests.PnpmBuildName, artHttpDetails) +} + +func TestPnpmInstallAndPublishWorkspace(t *testing.T) { + initPnpmTest(t) + defer cleanPnpmTest(t) + wd, err := os.Getwd() + assert.NoError(t, err, "Failed to get current dir") + defer clientTestUtils.ChangeDirAndAssert(t, wd) + + tempCacheDirPath, createTempDirCallback := coretests.CreateTempDirWithCallbackAndAssert(t) + defer createTempDirCallback() + + buildNumber := "501" + pnpmWorkspacePath := initPnpmWorkspaceTest(t) + clientTestUtils.ChangeDirAndAssert(t, pnpmWorkspacePath) + + runJfrogCli(t, "pnpm", "install", "--store-dir="+tempCacheDirPath, + "--build-name="+tests.PnpmBuildName, "--build-number="+buildNumber) + + cleanupAuth := setupPnpmPublishAuth(t, tests.NpmRepo) + defer cleanupAuth() + runJfrogCli(t, "pnpm", "publish", "-r", "--no-git-checks", + "--build-name="+tests.PnpmBuildName, "--build-number="+buildNumber) + assert.NoError(t, artifactoryCli.Exec("bp", tests.PnpmBuildName, buildNumber)) + + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, tests.PnpmBuildName, buildNumber) + assert.NoError(t, err) + assert.True(t, found) + bi := publishedBuildInfo.BuildInfo + packagesPublished := 2 // nested1 and nested2 + modulesWithDepsAndArtifacts := 0 + for _, mod := range bi.Modules { + hasDeps := len(mod.Dependencies) > 0 + hasArtifacts := len(mod.Artifacts) > 0 + assert.True(t, hasDeps, "module %s should have dependencies", mod.Id) + if hasArtifacts { + modulesWithDepsAndArtifacts++ + assert.True(t, hasDeps, "module %s has artifacts so must have dependencies", mod.Id) + } + } + assert.Equal(t, packagesPublished, modulesWithDepsAndArtifacts, + "number of modules with both dependencies and artifacts should equal packages published (nested1, nested2), got %d", modulesWithDepsAndArtifacts) + + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, tests.PnpmBuildName, artHttpDetails) +} + +// setupPnpmPublishAuth writes Artifactory registry and auth to ~/.npmrc +// so that pnpm publish (which delegates to npm from a temp dir) can authenticate. +// The registry URL must end with "/" for npm's nerfDart URL matching to work. +// Returns a cleanup function that restores the original ~/.npmrc. +func setupPnpmPublishAuth(t *testing.T, repo string) func() { + homeDir, err := os.UserHomeDir() + assert.NoError(t, err) + npmrcPath := filepath.Join(homeDir, ".npmrc") + + origContent, origErr := os.ReadFile(npmrcPath) + + registry := npmCmdUtils.GetNpmRepositoryUrl(repo, serverDetails.GetArtifactoryUrl()) + registryWithSlash := strings.TrimSuffix(registry, "/") + "/" + authKey, authValue := npmCmdUtils.GetNpmAuthKeyValue(serverDetails, registryWithSlash) + assert.NotEmpty(t, authKey, "npm auth key must not be empty") + + npmrcContent := fmt.Sprintf("registry=%s\n%s=%s\n", registryWithSlash, authKey, authValue) + err = os.WriteFile(npmrcPath, []byte(npmrcContent), 0644) + assert.NoError(t, err) + + return func() { + if origErr == nil { + _ = os.WriteFile(npmrcPath, origContent, 0644) + } else { + _ = os.Remove(npmrcPath) + } + } +} + +func readPnpmModuleId(t *testing.T, wd string) string { + packageInfo, err := buildutils.ReadPackageInfoFromPackageJsonIfExists(filepath.Dir(wd), nil) + assert.NoError(t, err) + packageInfo.Version = strings.TrimPrefix(packageInfo.Version, "v") + packageInfo.Version = strings.TrimPrefix(packageInfo.Version, "=") + return packageInfo.BuildInfoModuleId() +} + +func validatePnpmLocalBuildInfo(t *testing.T, buildName, buildNumber, moduleName string) { + buildInfoService := build.CreateBuildInfoService() + pnpmBuild, err := buildInfoService.GetOrCreateBuildWithProject(buildName, buildNumber, "") + assert.NoError(t, err) + bi, err := pnpmBuild.ToBuildInfo() + assert.NoError(t, err) + assert.NotEmpty(t, bi.Started) + if assert.Len(t, bi.Modules, 1) { + assert.Equal(t, moduleName, bi.Modules[0].Id) + assert.Equal(t, buildinfo.Npm, bi.Modules[0].Type) + } +} + +func validatePnpmInstall(t *testing.T, pt pnpmTestParams) { + expectedDependencies := []expectedDependency{{id: "xml:1.0.1", scopes: []string{"prod"}}} + if !strings.Contains(pt.pnpmArgs, "-prod") && !strings.Contains(pt.pnpmArgs, "--prod") { + expectedDependencies = append(expectedDependencies, expectedDependency{id: "json:9.0.6", scopes: []string{"dev"}}) + } + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, tests.PnpmBuildName, pt.buildNumber) + if err != nil { + assert.NoError(t, err) + return + } + if !found { + assert.True(t, found, "build info was expected to be found") + return + } + buildInfo := publishedBuildInfo.BuildInfo + if buildInfo.Modules == nil { + assert.NotNil(t, buildInfo.Modules) + return + } + assert.NotEmpty(t, buildInfo.Modules) + equalDependenciesSlices(t, expectedDependencies, buildInfo.Modules[0].Dependencies) +} + +func validatePnpmPublish(t *testing.T, pt pnpmTestParams) { + // pnpm publish normalizes versions (strips "v" prefix), so use isNpm7=false for Artifactory path expectations + verifyExistInArtifactoryByProps(tests.GetNpmDeployedArtifacts(false), + tests.NpmRepo+"/*", + fmt.Sprintf("build.name=%v;build.number=%v;build.timestamp=*", tests.PnpmBuildName, pt.buildNumber), t) + // pnpm pack preserves the "v" prefix in the local tarball filename used for build info + validatePnpmCommonPublish(t, pt, tests.GetNpmArtifactName(true, false)) +} + +func validatePnpmScopedPublish(t *testing.T, pt pnpmTestParams) { + // pnpm publish normalizes versions (strips "=" prefix), so use isNpm7=false for Artifactory path expectations + verifyExistInArtifactoryByProps(tests.GetNpmDeployedScopedArtifacts(pt.repo, false), + pt.repo+"/*", + fmt.Sprintf("build.name=%v;build.number=%v;build.timestamp=*", tests.PnpmBuildName, pt.buildNumber), t) + // pnpm pack includes the scope in the tarball filename (e.g., jscope-jfrog-cli-tests-=1.0.0.tgz) + validatePnpmCommonPublish(t, pt, "jscope-jfrog-cli-tests-=1.0.0.tgz") +} + +func validatePnpmCommonPublish(t *testing.T, pt pnpmTestParams, expectedArtifactName string) { + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, tests.PnpmBuildName, pt.buildNumber) + if err != nil { + assert.NoError(t, err) + return + } + if !found { + assert.True(t, found, "build info was expected to be found") + return + } + buildInfo := publishedBuildInfo.BuildInfo + if len(buildInfo.Modules) == 0 { + assert.Fail(t, "pnpm publish test failed", "params: \n%v \nexpected to have module with artifact: \n%v \nbut has no modules", pt, expectedArtifactName) + return + } + assert.Len(t, buildInfo.Modules[0].Artifacts, 1, "pnpm publish test with params: \n%v \nexpected artifact: \n%v \nbut has: \n%v", pt, expectedArtifactName, buildInfo.Modules[0].Artifacts) + assert.Equal(t, pt.moduleName, buildInfo.Modules[0].Id) + assert.Equal(t, expectedArtifactName, buildInfo.Modules[0].Artifacts[0].Name) +} + +func initPnpmFilesTest(t *testing.T) (pnpmProjectPath, pnpmScopedProjectPath string) { + pnpmProjectPath = createPnpmProject(t, "pnpmproject") + pnpmScopedProjectPath = createPnpmProject(t, "pnpmscopedproject") + err := createConfigFileForTest([]string{filepath.Dir(pnpmProjectPath), filepath.Dir(pnpmScopedProjectPath)}, + tests.NpmRemoteRepo, tests.NpmRepo, t, project.Pnpm, false) + assert.NoError(t, err) + prepareArtifactoryForPnpmBuild(t, filepath.Dir(pnpmProjectPath)) + return +} + +func initPnpmWorkspaceTest(t *testing.T) (pnpmWorkspacePath string) { + pnpmWorkspacePath = filepath.Dir(createPnpmProject(t, "pnpmworkspace")) + err := createConfigFileForTest([]string{pnpmWorkspacePath}, tests.NpmRemoteRepo, tests.NpmRepo, t, project.Pnpm, false) + assert.NoError(t, err) + testFolder := filepath.Join(filepath.FromSlash(tests.GetTestResourcesPath()), "pnpm", "pnpmworkspace") + err = biutils.CopyDir(testFolder, pnpmWorkspacePath, true, []string{}) + assert.NoError(t, err) + prepareArtifactoryForPnpmBuild(t, pnpmWorkspacePath) + return +} + +func prepareArtifactoryForPnpmBuild(t *testing.T, workingDirectory string) { + clientTestUtils.ChangeDirAndAssert(t, workingDirectory) + caches := ioutils.DoubleWinPathSeparator(filepath.Join(workingDirectory, "caches")) + jfrogCli := coretests.NewJfrogCli(execMain, "jfrog", "") + assert.NoError(t, jfrogCli.Exec("pnpm", "install", "-store-dir="+caches)) + clientTestUtils.RemoveAllAndAssert(t, filepath.Join(workingDirectory, "node_modules")) + clientTestUtils.RemoveAllAndAssert(t, caches) +} + +func createPnpmProject(t *testing.T, dir string) string { + srcPackageJson := filepath.Join(filepath.FromSlash(tests.GetTestResourcesPath()), "pnpm", dir, "package.json") + targetPackageJson := filepath.Join(tests.Out, dir) + packageJson, err := tests.ReplaceTemplateVariables(srcPackageJson, targetPackageJson) + assert.NoError(t, err) + packageJson, err = filepath.Abs(packageJson) + assert.NoError(t, err) + return packageJson +} + +// TestPnpmInstallWithoutBuildInfo verifies install succeeds when build-name/number are missing (RTECO-907). +func TestPnpmInstallWithoutBuildInfo(t *testing.T) { + initPnpmTest(t) + defer cleanPnpmTest(t) + wd, err := os.Getwd() + assert.NoError(t, err) + defer clientTestUtils.ChangeDirAndAssert(t, wd) + + tempCacheDirPath, createTempDirCallback := coretests.CreateTempDirWithCallbackAndAssert(t) + defer createTempDirCallback() + + pnpmProjectPath := createPnpmProject(t, "pnpmproject") + projectDir := filepath.Dir(pnpmProjectPath) + err = createConfigFileForTest([]string{projectDir}, tests.NpmRemoteRepo, tests.NpmRepo, t, project.Pnpm, false) + assert.NoError(t, err) + prepareArtifactoryForPnpmBuild(t, projectDir) + + clientTestUtils.ChangeDirAndAssert(t, projectDir) + // No --build-name or --build-number: install should succeed, build info not collected + err = runJfrogCliWithoutAssertion("pnpm", "install", "--store-dir="+tempCacheDirPath) + assert.NoError(t, err) +} + +// TestPnpmInstallOnlyAllow verifies install succeeds when the project contains an 'only-allow pnpm' preinstall script (RTECO-920). +func TestPnpmInstallOnlyAllow(t *testing.T) { + initPnpmTest(t) + defer cleanPnpmTest(t) + wd, err := os.Getwd() + assert.NoError(t, err) + defer clientTestUtils.ChangeDirAndAssert(t, wd) + + tempCacheDirPath, createTempDirCallback := coretests.CreateTempDirWithCallbackAndAssert(t) + defer createTempDirCallback() + + pnpmProjectPath := createPnpmProject(t, "pnpmprojectonlyallow") + projectDir := filepath.Dir(pnpmProjectPath) + err = createConfigFileForTest([]string{projectDir}, tests.NpmRemoteRepo, tests.NpmRepo, t, project.Pnpm, false) + assert.NoError(t, err) + prepareArtifactoryForPnpmBuild(t, projectDir) + + buildNumber := "600" + clientTestUtils.ChangeDirAndAssert(t, projectDir) + runJfrogCli(t, "pnpm", "install", "--store-dir="+tempCacheDirPath, + "--build-name="+tests.PnpmBuildName, "--build-number="+buildNumber) + + moduleName := readPnpmModuleId(t, pnpmProjectPath) + validatePnpmLocalBuildInfo(t, tests.PnpmBuildName, buildNumber, moduleName) + assert.NoError(t, artifactoryCli.Exec("bp", tests.PnpmBuildName, buildNumber)) + + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, tests.PnpmBuildName, buildNumber) + assert.NoError(t, err) + assert.True(t, found) + assert.NotEmpty(t, publishedBuildInfo.BuildInfo.Modules) + + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, tests.PnpmBuildName, artHttpDetails) +} + +// TestPnpmAddCommand verifies that 'pnpm add' does not return an error with or without build info flags (RTECO-918). +func TestPnpmAddCommand(t *testing.T) { + initPnpmTest(t) + defer cleanPnpmTest(t) + wd, err := os.Getwd() + assert.NoError(t, err) + defer clientTestUtils.ChangeDirAndAssert(t, wd) + + tempCacheDirPath, createTempDirCallback := coretests.CreateTempDirWithCallbackAndAssert(t) + defer createTempDirCallback() + + pnpmProjectPath := createPnpmProject(t, "pnpmproject") + projectDir := filepath.Dir(pnpmProjectPath) + err = createConfigFileForTest([]string{projectDir}, tests.NpmRemoteRepo, tests.NpmRepo, t, project.Pnpm, false) + assert.NoError(t, err) + prepareArtifactoryForPnpmBuild(t, projectDir) + + t.Run("without build flags", func(t *testing.T) { + clientTestUtils.ChangeDirAndAssert(t, projectDir) + err := runJfrogCliWithoutAssertion("pnpm", "add", "xml@1.0.1", "--store-dir="+tempCacheDirPath) + assert.NoError(t, err, "pnpm add without build flags should not return an error") + }) + + t.Run("with build flags", func(t *testing.T) { + clientTestUtils.ChangeDirAndAssert(t, projectDir) + err := runJfrogCliWithoutAssertion("pnpm", "add", "xml@1.0.1", "--store-dir="+tempCacheDirPath, + "--build-name="+tests.PnpmBuildName, "--build-number=603") + assert.NoError(t, err, "pnpm add with build flags should not return an error") + }) +} + +// TestPnpmReleaseBundleCreation verifies successful creation of a Release Bundle using pnpm build info (RTECO-910). +func TestPnpmReleaseBundleCreation(t *testing.T) { + initPnpmTest(t) + defer cleanPnpmTest(t) + wd, err := os.Getwd() + assert.NoError(t, err) + defer clientTestUtils.ChangeDirAndAssert(t, wd) + + tempCacheDirPath, createTempDirCallback := coretests.CreateTempDirWithCallbackAndAssert(t) + defer createTempDirCallback() + + pnpmProjectPath := createPnpmProject(t, "pnpmproject") + projectDir := filepath.Dir(pnpmProjectPath) + err = createConfigFileForTest([]string{projectDir}, tests.NpmRemoteRepo, tests.NpmRepo, t, project.Pnpm, false) + assert.NoError(t, err) + prepareArtifactoryForPnpmBuild(t, projectDir) + + buildNumber := "615" + clientTestUtils.ChangeDirAndAssert(t, projectDir) + runJfrogCli(t, "pnpm", "install", "--store-dir="+tempCacheDirPath, + "--build-name="+tests.PnpmBuildName, "--build-number="+buildNumber) + assert.NoError(t, artifactoryCli.Exec("bp", tests.PnpmBuildName, buildNumber)) + + // Verify the published build info has dependencies before creating a release bundle + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, tests.PnpmBuildName, buildNumber) + assert.NoError(t, err) + if !assert.True(t, found, "build info should be found after publish") { + return + } + assert.NotEmpty(t, publishedBuildInfo.BuildInfo.Modules, "build info should have modules") + assert.NotEmpty(t, publishedBuildInfo.BuildInfo.Modules[0].Dependencies, "build info module should have dependencies") + + // Create a release bundle from the pnpm build info + rbName := "pnpm-rb-creation-test" + rbVersion := "1.0.0" + err = runJfrogCliWithoutAssertion("rbc", rbName, rbVersion, "--build-name="+tests.PnpmBuildName, "--build-number="+buildNumber) + if err != nil { + // Release bundle operations require Distribution or lifecycle service - skip if unavailable + t.Skipf("Skipping release bundle test: %v", err) + } + + // Verify the release bundle was created by checking its status + lcManager, err := utils.CreateLifecycleServiceManager(serverDetails, false) + if assert.NoError(t, err) { + rbDetails := services.ReleaseBundleDetails{ + ReleaseBundleName: rbName, + ReleaseBundleVersion: rbVersion, + } + resp, err := lcManager.GetReleaseBundleCreationStatus(rbDetails, "", true) + if assert.NoError(t, err) { + assert.Equal(t, services.Completed, resp.Status, "release bundle creation status should be COMPLETED") + } + // Clean up the release bundle + _ = lcManager.DeleteReleaseBundleVersion(rbDetails, services.CommonOptionalQueryParams{Async: false}) + } + + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, tests.PnpmBuildName, artHttpDetails) +} + +// TestPnpmInstallTransitiveDependencies verifies that transitive dependencies are correctly resolved and included in the build info (RTECO-904). +func TestPnpmInstallTransitiveDependencies(t *testing.T) { + initPnpmTest(t) + defer cleanPnpmTest(t) + wd, err := os.Getwd() + assert.NoError(t, err) + defer clientTestUtils.ChangeDirAndAssert(t, wd) + + tempCacheDirPath, createTempDirCallback := coretests.CreateTempDirWithCallbackAndAssert(t) + defer createTempDirCallback() + + pnpmProjectPath := createPnpmProject(t, "pnpmtransitive") + projectDir := filepath.Dir(pnpmProjectPath) + err = createConfigFileForTest([]string{projectDir}, tests.NpmRemoteRepo, tests.NpmRepo, t, project.Pnpm, false) + assert.NoError(t, err) + prepareArtifactoryForPnpmBuild(t, projectDir) + + buildNumber := "617" + clientTestUtils.ChangeDirAndAssert(t, projectDir) + runJfrogCli(t, "pnpm", "install", "--store-dir="+tempCacheDirPath, + "--build-name="+tests.PnpmBuildName, "--build-number="+buildNumber) + + assert.NoError(t, artifactoryCli.Exec("bp", tests.PnpmBuildName, buildNumber)) + + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, tests.PnpmBuildName, buildNumber) + assert.NoError(t, err) + assert.True(t, found) + if assert.NotEmpty(t, publishedBuildInfo.BuildInfo.Modules) { + deps := publishedBuildInfo.BuildInfo.Modules[0].Dependencies + // chalk@2.4.2 has transitive deps: ansi-styles, escape-string-regexp, supports-color, color-convert, has-flag, color-name + assert.Greater(t, len(deps), 1, "should have transitive dependencies beyond the direct dependency (chalk)") + + // Verify that at least one known transitive dependency is present + hasTransitiveDep := false + for _, dep := range deps { + if strings.HasPrefix(dep.Id, "ansi-styles:") || strings.HasPrefix(dep.Id, "supports-color:") || strings.HasPrefix(dep.Id, "color-convert:") { + hasTransitiveDep = true + break + } + } + assert.True(t, hasTransitiveDep, "build info should include transitive dependencies of chalk") + } + + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, tests.PnpmBuildName, artHttpDetails) +} + +// TestPnpmInstallEmptyLockfile verifies that the CLI correctly handles an empty pnpm-lock.yaml (RTECO-903). +func TestPnpmInstallEmptyLockfile(t *testing.T) { + initPnpmTest(t) + defer cleanPnpmTest(t) + wd, err := os.Getwd() + assert.NoError(t, err) + defer clientTestUtils.ChangeDirAndAssert(t, wd) + + tempCacheDirPath, createTempDirCallback := coretests.CreateTempDirWithCallbackAndAssert(t) + defer createTempDirCallback() + + pnpmProjectPath := createPnpmProject(t, "pnpmemptylockfile") + projectDir := filepath.Dir(pnpmProjectPath) + + // Also copy the empty lockfile to the target directory + srcLockfile := filepath.Join(filepath.FromSlash(tests.GetTestResourcesPath()), "pnpm", "pnpmemptylockfile", "pnpm-lock.yaml") + targetLockfile := filepath.Join(projectDir, "pnpm-lock.yaml") + lockfileContent, err := os.ReadFile(srcLockfile) + assert.NoError(t, err) + err = os.WriteFile(targetLockfile, lockfileContent, 0644) + assert.NoError(t, err) + + err = createConfigFileForTest([]string{projectDir}, tests.NpmRemoteRepo, tests.NpmRepo, t, project.Pnpm, false) + assert.NoError(t, err) + + clientTestUtils.ChangeDirAndAssert(t, projectDir) + // Install should succeed even with an empty lockfile (pnpm regenerates it) + buildNumber := "618" + runJfrogCli(t, "pnpm", "install", "--store-dir="+tempCacheDirPath, + "--build-name="+tests.PnpmBuildName, "--build-number="+buildNumber) + + moduleName := readPnpmModuleId(t, pnpmProjectPath) + validatePnpmLocalBuildInfo(t, tests.PnpmBuildName, buildNumber, moduleName) + + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, tests.PnpmBuildName, artHttpDetails) +} + +func initPnpmTest(t *testing.T) { + if !*tests.TestPnpm { + t.Skip("Skipping Pnpm test. To run Pnpm test add the '-test.pnpm=true' option.") + } + _ = os.Unsetenv("JFROG_RUN_NATIVE") + createJfrogHomeConfig(t, true) +} diff --git a/testdata/pnpm/pnpmemptylockfile/package.json b/testdata/pnpm/pnpmemptylockfile/package.json new file mode 100644 index 000000000..28bad25bb --- /dev/null +++ b/testdata/pnpm/pnpmemptylockfile/package.json @@ -0,0 +1,14 @@ +{ + "name": "jfrog-cli-tests-empty-lockfile", + "version": "1.0.0", + "description": "test package with empty lockfile", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "xml": "1.0.1" + } +} diff --git a/testdata/pnpm/pnpmemptylockfile/pnpm-lock.yaml b/testdata/pnpm/pnpmemptylockfile/pnpm-lock.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/testdata/pnpm/pnpmproject/package.json b/testdata/pnpm/pnpmproject/package.json new file mode 100644 index 000000000..a2aebf491 --- /dev/null +++ b/testdata/pnpm/pnpmproject/package.json @@ -0,0 +1,17 @@ +{ + "name": "jfrog-cli-tests", + "version": "v1.0.0", + "description": "test package", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "xml": "1.0.1" + }, + "devDependencies": { + "json": "9.0.6" + } +} diff --git a/testdata/pnpm/pnpmprojectonlyallow/package.json b/testdata/pnpm/pnpmprojectonlyallow/package.json new file mode 100644 index 000000000..11336efc0 --- /dev/null +++ b/testdata/pnpm/pnpmprojectonlyallow/package.json @@ -0,0 +1,18 @@ +{ + "name": "jfrog-cli-tests-only-allow", + "version": "v1.0.0", + "description": "test package with only-allow preinstall script", + "main": "index.js", + "scripts": { + "preinstall": "npx only-allow pnpm", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "xml": "1.0.1" + }, + "devDependencies": { + "json": "9.0.6" + } +} diff --git a/testdata/pnpm/pnpmscopedproject/package.json b/testdata/pnpm/pnpmscopedproject/package.json new file mode 100644 index 000000000..f5844d7c2 --- /dev/null +++ b/testdata/pnpm/pnpmscopedproject/package.json @@ -0,0 +1,17 @@ +{ + "name": "@jscope/jfrog-cli-tests", + "version": "=1.0.0", + "description": "test package with scope", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "xml": "1.0.1" + }, + "devDependencies": { + "json": "9.0.6" + } +} diff --git a/testdata/pnpm/pnpmtransitive/package.json b/testdata/pnpm/pnpmtransitive/package.json new file mode 100644 index 000000000..3f1c963dc --- /dev/null +++ b/testdata/pnpm/pnpmtransitive/package.json @@ -0,0 +1,14 @@ +{ + "name": "jfrog-cli-tests-transitive", + "version": "1.0.0", + "description": "test package with transitive dependencies", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "chalk": "2.4.2" + } +} diff --git a/testdata/pnpm/pnpmworkspace/package.json b/testdata/pnpm/pnpmworkspace/package.json new file mode 100644 index 000000000..c4c47a79d --- /dev/null +++ b/testdata/pnpm/pnpmworkspace/package.json @@ -0,0 +1,17 @@ +{ + "name": "pnpm-workspace-root", + "version": "1.0.0", + "private": true, + "description": "pnpm workspace for CLI tests", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "xml": "1.0.1" + }, + "devDependencies": { + "json": "9.0.6" + } +} diff --git a/testdata/pnpm/pnpmworkspace/packages/nested1/package.json b/testdata/pnpm/pnpmworkspace/packages/nested1/package.json new file mode 100644 index 000000000..629f660ce --- /dev/null +++ b/testdata/pnpm/pnpmworkspace/packages/nested1/package.json @@ -0,0 +1,15 @@ +{ + "name": "nested1", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "loadash": "1.0.0" + } +} diff --git a/testdata/pnpm/pnpmworkspace/packages/nested2/package.json b/testdata/pnpm/pnpmworkspace/packages/nested2/package.json new file mode 100644 index 000000000..c38fdeaf9 --- /dev/null +++ b/testdata/pnpm/pnpmworkspace/packages/nested2/package.json @@ -0,0 +1,15 @@ +{ + "name": "nested2", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "loadash": "1.0.0" + } +} diff --git a/testdata/pnpm/pnpmworkspace/pnpm-workspace.yaml b/testdata/pnpm/pnpmworkspace/pnpm-workspace.yaml new file mode 100644 index 000000000..18ec407ef --- /dev/null +++ b/testdata/pnpm/pnpmworkspace/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - 'packages/*' diff --git a/utils/tests/consts.go b/utils/tests/consts.go index c7319852f..fd48adea8 100644 --- a/utils/tests/consts.go +++ b/utils/tests/consts.go @@ -243,6 +243,7 @@ var ( GradleBuildName = "cli-gradle-build" MvnBuildName = "cli-maven-build" NpmBuildName = "cli-npm-build" + PnpmBuildName = "cli-pnpm-build" YarnBuildName = "cli-yarn-build" NuGetBuildName = "cli-nuget-build" PipBuildName = "cli-pip-build" diff --git a/utils/tests/utils.go b/utils/tests/utils.go index 64a18622a..a2dd5d739 100644 --- a/utils/tests/utils.go +++ b/utils/tests/utils.go @@ -62,6 +62,7 @@ var ( ContainerRegistry *string TestGo *bool TestNpm *bool + TestPnpm *bool TestGradle *bool TestMaven *bool TestNuget *bool @@ -102,6 +103,7 @@ func init() { TestPodman = flag.Bool("test.podman", false, "Test Podman build") TestGo = flag.Bool("test.go", false, "Test Go") TestNpm = flag.Bool("test.npm", false, "Test Npm") + TestPnpm = flag.Bool("test.pnpm", false, "Test Pnpm") TestGradle = flag.Bool("test.gradle", false, "Test Gradle") TestMaven = flag.Bool("test.maven", false, "Test Maven") TestNuget = flag.Bool("test.nuget", false, "Test Nuget") @@ -350,6 +352,7 @@ func GetNonVirtualRepositories() map[*string]string { TestGradle: {&GradleRepo, &GradleRemoteRepo}, TestMaven: {&MvnRepo1, &MvnRepo2, &MvnRemoteRepo}, TestNpm: {&NpmRepo, &NpmScopedRepo, &NpmRemoteRepo}, + TestPnpm: {&NpmRepo, &NpmScopedRepo, &NpmRemoteRepo}, TestNuget: {&NugetRemoteRepo}, TestPip: {&PypiLocalRepo, &PypiRemoteRepo}, TestPipenv: {&PipenvRemoteRepo}, @@ -379,6 +382,7 @@ func GetVirtualRepositories() map[*string]string { TestGradle: {}, TestMaven: {}, TestNpm: {}, + TestPnpm: {}, TestNuget: {}, TestPip: {&PypiVirtualRepo}, TestPipenv: {&PipenvVirtualRepo}, @@ -419,6 +423,7 @@ func GetBuildNames() []string { TestGradle: {&GradleBuildName}, TestMaven: {&MvnBuildName}, TestNpm: {&NpmBuildName, &YarnBuildName}, + TestPnpm: {&PnpmBuildName}, TestNuget: {&NuGetBuildName}, TestPip: {&PipBuildName}, TestPipenv: {&PipenvBuildName}, @@ -462,6 +467,7 @@ func getSubstitutionMap() map[string]string { "${NPM_REPO}": NpmRepo, "${NPM_SCOPED_REPO}": NpmScopedRepo, "${NPM_REMOTE_REPO}": NpmRemoteRepo, + "${PNPM_BUILD_NAME}": PnpmBuildName, "${NUGET_REMOTE_REPO}": NugetRemoteRepo, "${YARN_REMOTE_REPO}": YarnRemoteRepo, "${GO_REPO}": GoRepo, @@ -575,7 +581,8 @@ func AddTimestampToGlobalVars() { DotnetBuildName += uniqueSuffix GoBuildName += uniqueSuffix GradleBuildName += uniqueSuffix - NpmBuildName += uniqueSuffix + NpmBuildName += uniqueSuffix + PnpmBuildName += uniqueSuffix YarnBuildName += uniqueSuffix MvnBuildName += uniqueSuffix NuGetBuildName += uniqueSuffix From a904facde619d66bae31fd25602eaed6fdd3d996 Mon Sep 17 00:00:00 2001 From: Kanishk Date: Fri, 13 Mar 2026 23:08:56 +0530 Subject: [PATCH 3/8] temp workflow test --- .github/workflows/npmTests.yml | 114 +++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/.github/workflows/npmTests.yml b/.github/workflows/npmTests.yml index 170dfd759..4a21030c8 100644 --- a/.github/workflows/npmTests.yml +++ b/.github/workflows/npmTests.yml @@ -85,3 +85,117 @@ jobs: env: YARN_IGNORE_NODE: 1 run: go test -v github.com/jfrog/jfrog-cli --timeout 0 --test.npm + + # TODO: Remove this job before merging — temporary pnpm test for RTECO-935 + build-pnpm-matrix: + name: Build pnpm version matrix + runs-on: ubuntu-latest + if: github.event_name == 'workflow_dispatch' + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - name: Fetch supported pnpm versions and build matrix + id: set-matrix + run: | + PNPM_VERSIONS=$(curl -sf https://endoflife.date/api/pnpm.json) + ACTIVE_MATRIX=$(echo "$PNPM_VERSIONS" | jq -c ' + [ + .[] + | select(.eol == false) + | .cycle as $cycle + | select(($cycle | tonumber) > 9) + | { + pnpm_version: .latest, + pnpm_major: $cycle, + node_version: (if ($cycle | tonumber) >= 11 then "22" else "20" end), + experimental: false + } + ] + ') + DIST_TAGS=$(curl -sf https://registry.npmjs.org/pnpm | jq -r '."dist-tags" | to_entries[] | select(.key | test("^next-")) | "\(.key)=\(.value)"') + HIGHEST_ACTIVE=$(echo "$ACTIVE_MATRIX" | jq -r '[.[].pnpm_major | tonumber] | max') + NEXT_ENTRY="[]" + while IFS='=' read -r tag version; do + [ -z "$tag" ] && continue + NEXT_MAJOR=$(echo "$tag" | sed 's/next-//') + if [ "$NEXT_MAJOR" -gt "$HIGHEST_ACTIVE" ] 2>/dev/null; then + NEXT_ENTRY=$(jq -n -c --arg tag "$tag" --arg major "$NEXT_MAJOR" '[{ + pnpm_version: $tag, + pnpm_major: $major, + node_version: "22", + experimental: true + }]') + break + fi + done <<< "$DIST_TAGS" + MATRIX=$(jq -n -c --argjson active "$ACTIVE_MATRIX" --argjson next "$NEXT_ENTRY" '$active + $next') + echo "matrix=${MATRIX}" >> "$GITHUB_OUTPUT" + echo "Generated matrix:" + echo "$MATRIX" | jq . + + pnpm-Tests: + name: "pnpm ${{ matrix.pnpm.pnpm_major }} tests (${{ matrix.os.name }})" + needs: build-pnpm-matrix + if: github.event_name == 'workflow_dispatch' + strategy: + fail-fast: false + matrix: + os: + - name: ubuntu + version: 24.04 + pnpm: ${{ fromJson(needs.build-pnpm-matrix.outputs.matrix) }} + runs-on: ${{ matrix.os.name }}-${{ matrix.os.version }} + steps: + - name: Checkout code + uses: actions/checkout@v5 + with: + ref: ${{ github.ref }} + + - name: Setup FastCI + uses: jfrog-fastci/fastci@v0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + fastci_otel_token: ${{ secrets.FASTCI_TOKEN }} + + - name: Install Node.js + uses: actions/setup-node@v5 + with: + node-version: ${{ matrix.pnpm.node_version }} + + - name: Install pnpm + run: npm install -g pnpm@${{ matrix.pnpm.pnpm_version }} + + - name: Validate pnpm and Node.js versions + run: | + echo "=== Version Validation ===" + PNPM_VER=$(pnpm --version) + NODE_VER=$(node --version | sed 's/^v//') + echo "pnpm version: ${PNPM_VER}" + echo "Node.js version: ${NODE_VER}" + PNPM_MAJOR=$(echo "$PNPM_VER" | cut -d. -f1) + if [ "$PNPM_MAJOR" -lt 10 ]; then + echo "::error::pnpm version ${PNPM_VER} is below minimum required (10.x)" + exit 1 + fi + echo "pnpm ${PNPM_VER} OK" + NODE_MAJOR=$(echo "$NODE_VER" | cut -d. -f1) + NODE_MINOR=$(echo "$NODE_VER" | cut -d. -f2) + if [ "$NODE_MAJOR" -lt 18 ] || { [ "$NODE_MAJOR" -eq 18 ] && [ "$NODE_MINOR" -lt 12 ]; }; then + echo "::error::Node.js ${NODE_VER} is below minimum (18.12) for pnpm ${PNPM_MAJOR}.x" + exit 1 + fi + echo "Node.js ${NODE_VER} OK" + + - name: Setup Go with cache + uses: jfrog/.github/actions/install-go-with-cache@main + + - name: Install local Artifactory + uses: jfrog/.github/actions/install-local-artifactory@main + with: + RTLIC: ${{ secrets.RTLIC }} + RT_CONNECTION_TIMEOUT_SECONDS: ${{ env.RT_CONNECTION_TIMEOUT_SECONDS || '1200' }} + + - name: Run pnpm tests + env: + YARN_IGNORE_NODE: 1 + run: go test -v github.com/jfrog/jfrog-cli --timeout 0 --test.pnpm From 5c6fd7199e7364c160e02f11be07074337dd23dc Mon Sep 17 00:00:00 2001 From: Kanishk Date: Sat, 14 Mar 2026 01:06:20 +0530 Subject: [PATCH 4/8] Added new test --- .github/workflows/npmTests.yml | 2 + .github/workflows/pnpmTests.yml | 2 +- buildtools/cli.go | 13 +- go.mod | 2 +- go.sum | 4 +- pnpm_test.go | 271 +++++++++++++++++++++++++++++--- 6 files changed, 263 insertions(+), 31 deletions(-) diff --git a/.github/workflows/npmTests.yml b/.github/workflows/npmTests.yml index 4a21030c8..404048f31 100644 --- a/.github/workflows/npmTests.yml +++ b/.github/workflows/npmTests.yml @@ -143,6 +143,8 @@ jobs: os: - name: ubuntu version: 24.04 + - name: windows + version: 2022 pnpm: ${{ fromJson(needs.build-pnpm-matrix.outputs.matrix) }} runs-on: ${{ matrix.os.name }}-${{ matrix.os.version }} steps: diff --git a/.github/workflows/pnpmTests.yml b/.github/workflows/pnpmTests.yml index 032f5826e..8184eda0f 100644 --- a/.github/workflows/pnpmTests.yml +++ b/.github/workflows/pnpmTests.yml @@ -157,4 +157,4 @@ jobs: if: matrix.os.name != 'macos' env: YARN_IGNORE_NODE: 1 - run: go test -v github.com/jfrog/jfrog-cli --timeout 0 --test.pnpm + run: go test -v github.com/jfrog/jfrog-cli --timeout 0 --test.pnpm --jfrog.url=http://127.0.0.1:8082 --jfrog.adminToken=${{ env.JFROG_TESTS_LOCAL_ACCESS_TOKEN }} diff --git a/buildtools/cli.go b/buildtools/cli.go index 15ea0bbd9..ca12f4771 100644 --- a/buildtools/cli.go +++ b/buildtools/cli.go @@ -794,19 +794,22 @@ func pnpmCmd(c *cli.Context) error { args := cliutils.ExtractCommand(c) cmdName, filteredArgs := getCommandName(args) + // Extract JFrog-specific flags (--build-name, --build-number, --project, --module, --server-id) + // once, so both supported commands and native pass-through use cleaned args. + serverDetails, cleanArgs, buildConfiguration, err := extractPnpmOptionsFromArgs(filteredArgs) + if err != nil { + return err + } + switch cmdName { case "install", "i", "publish", "p": - serverDetails, cleanArgs, buildConfiguration, err := extractPnpmOptionsFromArgs(filteredArgs) - if err != nil { - return err - } pnpmCommand, err := pnpmcmd.NewCommand(cmdName, cleanArgs, buildConfiguration, serverDetails) if err != nil { return err } return commands.Exec(pnpmCommand) default: - return runNativePackageManagerCmd("pnpm", append([]string{cmdName}, filteredArgs...)) + return runNativePackageManagerCmd("pnpm", append([]string{cmdName}, cleanArgs...)) } } diff --git a/go.mod b/go.mod index 64c2337cd..21824f6ec 100644 --- a/go.mod +++ b/go.mod @@ -248,7 +248,7 @@ require ( // replace github.com/jfrog/jfrog-cli-artifactory => github.com/reshmifrog/jfrog-cli-artifactory v0.0.0-20260303084642-b208fbba798b -replace github.com/jfrog/jfrog-cli-artifactory => github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260313172139-c4ff104c0ae4 +replace github.com/jfrog/jfrog-cli-artifactory => github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260313193427-0c6cf28a9fc8 //replace github.com/jfrog/build-info-go => github.com/fluxxBot/build-info-go v1.10.10-0.20260105070825-d3f36f619ba5 diff --git a/go.sum b/go.sum index 31c77f5fa..9a76c7eea 100644 --- a/go.sum +++ b/go.sum @@ -419,8 +419,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.20260216085810-1ade6c26b3df h1:raSyae8/h1y8HtzFLf7vZZj91fP/qD94AX+biwBJiqs= github.com/jfrog/jfrog-cli-application v1.0.2-0.20260216085810-1ade6c26b3df/go.mod h1:xum2HquWO5uExa/A7MQs3TgJJVEeoqTR+6Z4mfBr1Xw= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260313172139-c4ff104c0ae4 h1:Snicq70G82I+NjnH1gdxxPOTG50IAPbR7w2Rm+UrQEE= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260313172139-c4ff104c0ae4/go.mod h1:kgw6gIQvJx9bCcOdtAGSUEiCz7nNQmaFbFvNg6byZ6I= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260313193427-0c6cf28a9fc8 h1:lL2C2y6tGh2g9MOnX07bJdloc8lsM0ylKMuuStNT7wI= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260313193427-0c6cf28a9fc8/go.mod h1:kgw6gIQvJx9bCcOdtAGSUEiCz7nNQmaFbFvNg6byZ6I= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260225195817-bc599cec3973 h1:awB01Y4m0cWzmXuR3waf5IQnoQxDlbUmqT+FMWOpjbs= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260225195817-bc599cec3973/go.mod h1:yhi+XpiEx18a3t8CZ6M2VpAf3EGqKpBhTzoPBTFe0dk= github.com/jfrog/jfrog-cli-evidence v0.8.3-0.20260202100913-d9ee9476845a h1:lTOAhUjKcOmM/0Kbj4V+I/VHPlW7YNAhIEVpGnCM5mI= diff --git a/pnpm_test.go b/pnpm_test.go index 2233c79fa..081ae6168 100644 --- a/pnpm_test.go +++ b/pnpm_test.go @@ -14,13 +14,14 @@ import ( npmCmdUtils "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/utils" "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" "github.com/jfrog/jfrog-cli-core/v2/common/build" - "github.com/jfrog/jfrog-cli-core/v2/common/project" "github.com/jfrog/jfrog-cli-core/v2/common/spec" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli-core/v2/utils/ioutils" coretests "github.com/jfrog/jfrog-cli-core/v2/utils/tests" "github.com/jfrog/jfrog-cli/inttestutils" "github.com/jfrog/jfrog-cli/utils/tests" + accessServices "github.com/jfrog/jfrog-client-go/access/services" + artServices "github.com/jfrog/jfrog-client-go/artifactory/services" "github.com/jfrog/jfrog-client-go/lifecycle/services" clientTestUtils "github.com/jfrog/jfrog-client-go/utils/tests" "github.com/stretchr/testify/assert" @@ -183,8 +184,6 @@ func TestPnpmInstallAndPublishNormalProject(t *testing.T) { buildNumber := "500" pnpmProjectPath := createPnpmProject(t, "pnpmproject") projectDir := filepath.Dir(pnpmProjectPath) - err = createConfigFileForTest([]string{projectDir}, tests.NpmRemoteRepo, tests.NpmRepo, t, project.Pnpm, false) - assert.NoError(t, err) prepareArtifactoryForPnpmBuild(t, projectDir) clientTestUtils.ChangeDirAndAssert(t, projectDir) @@ -366,19 +365,14 @@ func validatePnpmCommonPublish(t *testing.T, pt pnpmTestParams, expectedArtifact func initPnpmFilesTest(t *testing.T) (pnpmProjectPath, pnpmScopedProjectPath string) { pnpmProjectPath = createPnpmProject(t, "pnpmproject") pnpmScopedProjectPath = createPnpmProject(t, "pnpmscopedproject") - err := createConfigFileForTest([]string{filepath.Dir(pnpmProjectPath), filepath.Dir(pnpmScopedProjectPath)}, - tests.NpmRemoteRepo, tests.NpmRepo, t, project.Pnpm, false) - assert.NoError(t, err) prepareArtifactoryForPnpmBuild(t, filepath.Dir(pnpmProjectPath)) return } func initPnpmWorkspaceTest(t *testing.T) (pnpmWorkspacePath string) { pnpmWorkspacePath = filepath.Dir(createPnpmProject(t, "pnpmworkspace")) - err := createConfigFileForTest([]string{pnpmWorkspacePath}, tests.NpmRemoteRepo, tests.NpmRepo, t, project.Pnpm, false) - assert.NoError(t, err) testFolder := filepath.Join(filepath.FromSlash(tests.GetTestResourcesPath()), "pnpm", "pnpmworkspace") - err = biutils.CopyDir(testFolder, pnpmWorkspacePath, true, []string{}) + err := biutils.CopyDir(testFolder, pnpmWorkspacePath, true, []string{}) assert.NoError(t, err) prepareArtifactoryForPnpmBuild(t, pnpmWorkspacePath) return @@ -416,8 +410,6 @@ func TestPnpmInstallWithoutBuildInfo(t *testing.T) { pnpmProjectPath := createPnpmProject(t, "pnpmproject") projectDir := filepath.Dir(pnpmProjectPath) - err = createConfigFileForTest([]string{projectDir}, tests.NpmRemoteRepo, tests.NpmRepo, t, project.Pnpm, false) - assert.NoError(t, err) prepareArtifactoryForPnpmBuild(t, projectDir) clientTestUtils.ChangeDirAndAssert(t, projectDir) @@ -439,8 +431,6 @@ func TestPnpmInstallOnlyAllow(t *testing.T) { pnpmProjectPath := createPnpmProject(t, "pnpmprojectonlyallow") projectDir := filepath.Dir(pnpmProjectPath) - err = createConfigFileForTest([]string{projectDir}, tests.NpmRemoteRepo, tests.NpmRepo, t, project.Pnpm, false) - assert.NoError(t, err) prepareArtifactoryForPnpmBuild(t, projectDir) buildNumber := "600" @@ -473,8 +463,6 @@ func TestPnpmAddCommand(t *testing.T) { pnpmProjectPath := createPnpmProject(t, "pnpmproject") projectDir := filepath.Dir(pnpmProjectPath) - err = createConfigFileForTest([]string{projectDir}, tests.NpmRemoteRepo, tests.NpmRepo, t, project.Pnpm, false) - assert.NoError(t, err) prepareArtifactoryForPnpmBuild(t, projectDir) t.Run("without build flags", func(t *testing.T) { @@ -491,6 +479,72 @@ func TestPnpmAddCommand(t *testing.T) { }) } +// TestPnpmInstallWithServerID verifies that --server-id selects the correct server configuration +// for build info collection. It creates a dummy default config (invalid creds) and a valid +// secondary config, then runs pnpm install with --server-id pointing to the valid config. +// If --server-id were ignored, build info collection would fail due to the broken default. +func TestPnpmInstallWithServerID(t *testing.T) { + initPnpmTest(t) + defer cleanPnpmTest(t) + wd, err := os.Getwd() + assert.NoError(t, err) + defer clientTestUtils.ChangeDirAndAssert(t, wd) + + tempCacheDirPath, createTempDirCallback := coretests.CreateTempDirWithCallbackAndAssert(t) + defer createTempDirCallback() + + // Set up pnpm project and warm the Artifactory cache (uses valid "default" config). + pnpmProjectPath := createPnpmProject(t, "pnpmproject") + projectDir := filepath.Dir(pnpmProjectPath) + prepareArtifactoryForPnpmBuild(t, projectDir) + + // Create a second server config ("pnpm-valid-server") with valid credentials. + const validServerID = "pnpm-valid-server" + configCli := coretests.NewJfrogCli(execMain, "jfrog config", "") + var validCreds string + if *tests.JfrogAccessToken != "" { + validCreds = "--access-token=" + *tests.JfrogAccessToken + } else { + validCreds = "--user=" + *tests.JfrogUser + " --password=" + *tests.JfrogPassword + } + err = coretests.NewJfrogCli(execMain, "jfrog config", validCreds).Exec( + "add", validServerID, "--interactive=false", "--url="+*tests.JfrogUrl, "--enc-password=false") + assert.NoError(t, err) + defer func() { + assert.NoError(t, configCli.Exec("rm", validServerID, "--quiet")) + }() + + // Remove the "default" config, then re-add with dummy/invalid credentials so it cannot reach Artifactory. + err = coretests.NewJfrogCli(execMain, "jfrog config", "").Exec("rm", "default", "--quiet") + assert.NoError(t, err) + err = coretests.NewJfrogCli(execMain, "jfrog config", "--access-token=invalid-token").Exec( + "add", "default", "--interactive=false", "--url="+*tests.JfrogUrl, "--enc-password=false") + assert.NoError(t, err) + + // Run pnpm install with --server-id pointing to the valid config. + buildNumber := "700" + clientTestUtils.ChangeDirAndAssert(t, projectDir) + runJfrogCli(t, "pnpm", "install", "--store-dir="+tempCacheDirPath, + "--server-id="+validServerID, + "--build-name="+tests.PnpmBuildName, "--build-number="+buildNumber) + + // Validate local build info was collected. + moduleName := readPnpmModuleId(t, pnpmProjectPath) + validatePnpmLocalBuildInfo(t, tests.PnpmBuildName, buildNumber, moduleName) + + // Publish build info using the valid server and verify it was published with dependencies. + assert.NoError(t, coretests.NewJfrogCli(execMain, "jfrog rt", validCreds+" --url="+serverDetails.ArtifactoryUrl). + Exec("bp", tests.PnpmBuildName, buildNumber)) + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, tests.PnpmBuildName, buildNumber) + assert.NoError(t, err) + assert.True(t, found, "build info should be found after publish") + if assert.NotEmpty(t, publishedBuildInfo.BuildInfo.Modules, "build info should contain modules") { + assert.NotEmpty(t, publishedBuildInfo.BuildInfo.Modules[0].Dependencies, "module should contain dependencies") + } + + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, tests.PnpmBuildName, artHttpDetails) +} + // TestPnpmReleaseBundleCreation verifies successful creation of a Release Bundle using pnpm build info (RTECO-910). func TestPnpmReleaseBundleCreation(t *testing.T) { initPnpmTest(t) @@ -504,8 +558,6 @@ func TestPnpmReleaseBundleCreation(t *testing.T) { pnpmProjectPath := createPnpmProject(t, "pnpmproject") projectDir := filepath.Dir(pnpmProjectPath) - err = createConfigFileForTest([]string{projectDir}, tests.NpmRemoteRepo, tests.NpmRepo, t, project.Pnpm, false) - assert.NoError(t, err) prepareArtifactoryForPnpmBuild(t, projectDir) buildNumber := "615" @@ -563,8 +615,6 @@ func TestPnpmInstallTransitiveDependencies(t *testing.T) { pnpmProjectPath := createPnpmProject(t, "pnpmtransitive") projectDir := filepath.Dir(pnpmProjectPath) - err = createConfigFileForTest([]string{projectDir}, tests.NpmRemoteRepo, tests.NpmRepo, t, project.Pnpm, false) - assert.NoError(t, err) prepareArtifactoryForPnpmBuild(t, projectDir) buildNumber := "617" @@ -618,9 +668,6 @@ func TestPnpmInstallEmptyLockfile(t *testing.T) { err = os.WriteFile(targetLockfile, lockfileContent, 0644) assert.NoError(t, err) - err = createConfigFileForTest([]string{projectDir}, tests.NpmRemoteRepo, tests.NpmRepo, t, project.Pnpm, false) - assert.NoError(t, err) - clientTestUtils.ChangeDirAndAssert(t, projectDir) // Install should succeed even with an empty lockfile (pnpm regenerates it) buildNumber := "618" @@ -633,6 +680,186 @@ func TestPnpmInstallEmptyLockfile(t *testing.T) { inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, tests.PnpmBuildName, artHttpDetails) } +// TestPnpmBuildPublishWithCIVcsProps verifies that CI VCS properties (vcs.provider, vcs.org, vcs.repo) +// are set on artifacts published via pnpm after build-publish (RTECO-923). +func TestPnpmBuildPublishWithCIVcsProps(t *testing.T) { + initPnpmTest(t) + defer cleanPnpmTest(t) + + buildName := "pnpm-civcs-test" + buildNumber := "1" + + // Setup GitHub Actions environment (uses real env vars on CI, mock values locally) + cleanupEnv, actualOrg, actualRepo := tests.SetupGitHubActionsEnv(t) + defer cleanupEnv() + + // Clean old build + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + + wd, err := os.Getwd() + assert.NoError(t, err) + defer clientTestUtils.ChangeDirAndAssert(t, wd) + + // Setup pnpm project + pnpmProjectPath := createPnpmProject(t, "pnpmproject") + projectDir := filepath.Dir(pnpmProjectPath) + prepareArtifactoryForPnpmBuild(t, projectDir) + + clientTestUtils.ChangeDirAndAssert(t, projectDir) + + // Run pnpm publish with build info collection + cleanupAuth := setupPnpmPublishAuth(t, tests.NpmRepo) + defer cleanupAuth() + runJfrogCli(t, "pnpm", "publish", "--no-git-checks", + "--build-name="+buildName, "--build-number="+buildNumber) + + // Publish build info - should set CI VCS props on artifacts + assert.NoError(t, artifactoryCli.Exec("bp", buildName, buildNumber)) + + // Restore working directory before getting build info + clientTestUtils.ChangeDirAndAssert(t, wd) + + // Get the published build info to find artifact paths + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, buildName, buildNumber) + assert.NoError(t, err) + assert.True(t, found, "Build info was not found") + + // Create service manager for getting artifact properties + serviceManager, err := utils.CreateServiceManager(serverDetails, 3, 1000, false) + assert.NoError(t, err) + + // Verify VCS properties on each artifact from build info + artifactCount := 0 + for _, module := range publishedBuildInfo.BuildInfo.Modules { + for _, artifact := range module.Artifacts { + fullPath := artifact.OriginalDeploymentRepo + "/" + artifact.Path + if artifact.OriginalDeploymentRepo == "" || artifact.Path == "" { + t.Logf("Artifact %s missing OriginalDeploymentRepo or Path, skipping", artifact.Name) + continue + } + + props, err := serviceManager.GetItemProps(fullPath) + assert.NoError(t, err, "Failed to get properties for artifact: %s", fullPath) + if props == nil { + assert.Fail(t, "Properties are nil for artifact: %s", fullPath) + continue + } + + // Validate VCS properties + assert.Contains(t, props.Properties, "vcs.provider", "Missing vcs.provider on %s", artifact.Name) + assert.Contains(t, props.Properties["vcs.provider"], "github", "Wrong vcs.provider on %s", artifact.Name) + + assert.Contains(t, props.Properties, "vcs.org", "Missing vcs.org on %s", artifact.Name) + assert.Contains(t, props.Properties["vcs.org"], actualOrg, "Wrong vcs.org on %s", artifact.Name) + + assert.Contains(t, props.Properties, "vcs.repo", "Missing vcs.repo on %s", artifact.Name) + assert.Contains(t, props.Properties["vcs.repo"], actualRepo, "Wrong vcs.repo on %s", artifact.Name) + + artifactCount++ + } + } + assert.Greater(t, artifactCount, 0, "No artifacts in build info") +} + +// TestPnpmInstallAndPublishWithProject verifies that pnpm install and publish work correctly +// when targeting a non-default Artifactory project (RTECO-924). +// The test uses --project flag with install, publish, and build-publish to verify that +// build info is correctly stored and published under the specified project key. +func TestPnpmInstallAndPublishWithProject(t *testing.T) { + initPnpmTest(t) + + // Create Access service manager and project before deferring cleanPnpmTest, + // so that t.Skipf doesn't trigger cleanup asserts that override the skip status. + accessManager, err := utils.CreateAccessServiceManager(serverDetails, false) + if err != nil { + t.Skipf("Skipping project test - cannot create access manager: %v", err) + } + + // Try creating project first to verify access works before deferring any cleanup + projectParams := accessServices.ProjectParams{ + ProjectDetails: accessServices.Project{ + DisplayName: "pnpm-project-test " + tests.ProjectKey, + ProjectKey: tests.ProjectKey, + }, + } + // First delete if exists, ignoring errors since access might not support it + _ = accessManager.DeleteProject(tests.ProjectKey) + if err = accessManager.CreateProject(projectParams); err != nil { + t.Skipf("Skipping project test - cannot create project: %v", err) + } + + defer cleanPnpmTest(t) + defer func() { + _ = accessManager.UnassignRepoFromProject(tests.NpmRepo) + _ = accessManager.UnassignRepoFromProject(tests.NpmRemoteRepo) + _ = accessManager.DeleteProject(tests.ProjectKey) + }() + + // Assign npm repos to the project + err = accessManager.AssignRepoToProject(tests.NpmRepo, tests.ProjectKey, true) + assert.NoError(t, err) + err = accessManager.AssignRepoToProject(tests.NpmRemoteRepo, tests.ProjectKey, true) + assert.NoError(t, err) + + wd, err := os.Getwd() + assert.NoError(t, err) + defer clientTestUtils.ChangeDirAndAssert(t, wd) + + tempCacheDirPath, createTempDirCallback := coretests.CreateTempDirWithCallbackAndAssert(t) + defer createTempDirCallback() + + buildName := tests.PnpmBuildName + "-project" + buildNumber := "800" + + // Clean old build + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + + // Setup pnpm project + pnpmProjectPath := createPnpmProject(t, "pnpmproject") + projectDir := filepath.Dir(pnpmProjectPath) + prepareArtifactoryForPnpmBuild(t, projectDir) + + clientTestUtils.ChangeDirAndAssert(t, projectDir) + + // Run pnpm install with --project flag + runJfrogCli(t, "pnpm", "install", "--store-dir="+tempCacheDirPath, + "--build-name="+buildName, "--build-number="+buildNumber, + "--project="+tests.ProjectKey) + + // Run pnpm publish with --project flag + cleanupAuth := setupPnpmPublishAuth(t, tests.NpmRepo) + defer cleanupAuth() + runJfrogCli(t, "pnpm", "publish", "--no-git-checks", + "--build-name="+buildName, "--build-number="+buildNumber, + "--project="+tests.ProjectKey) + + // Publish build info with --project flag + assert.NoError(t, artifactoryCli.Exec("bp", buildName, buildNumber, "--project="+tests.ProjectKey)) + + // Restore working directory + clientTestUtils.ChangeDirAndAssert(t, wd) + + // Get the published build info with project key + servicesManager, err := utils.CreateServiceManager(serverDetails, -1, 0, false) + assert.NoError(t, err) + params := artServices.NewBuildInfoParams() + params.BuildName = buildName + params.BuildNumber = buildNumber + params.ProjectKey = tests.ProjectKey + publishedBuildInfo, found, err := servicesManager.GetBuildInfo(params) + assert.NoError(t, err) + assert.True(t, found, "Build info was not found for project %s", tests.ProjectKey) + + bi := publishedBuildInfo.BuildInfo + // pnpm install + publish on the same build should produce 1 module with both deps and artifacts + if assert.NotEmpty(t, bi.Modules, "Build info should contain modules") { + assert.NotEmpty(t, bi.Modules[0].Dependencies, "Module should have dependencies from pnpm install") + assert.NotEmpty(t, bi.Modules[0].Artifacts, "Module should have artifacts from pnpm publish") + } +} + func initPnpmTest(t *testing.T) { if !*tests.TestPnpm { t.Skip("Skipping Pnpm test. To run Pnpm test add the '-test.pnpm=true' option.") From 4b11e2e54b83a6fdd299bb72a2ec2e4470b23e47 Mon Sep 17 00:00:00 2001 From: Kanishk Date: Sat, 14 Mar 2026 01:24:08 +0530 Subject: [PATCH 5/8] version support --- .github/workflows/npmTests.yml | 75 ++++---------------------- .github/workflows/pnpmTests.yml | 93 ++++----------------------------- go.mod | 2 +- go.sum | 4 +- 4 files changed, 23 insertions(+), 151 deletions(-) diff --git a/.github/workflows/npmTests.yml b/.github/workflows/npmTests.yml index 404048f31..320bb6bc3 100644 --- a/.github/workflows/npmTests.yml +++ b/.github/workflows/npmTests.yml @@ -87,55 +87,8 @@ jobs: run: go test -v github.com/jfrog/jfrog-cli --timeout 0 --test.npm # TODO: Remove this job before merging — temporary pnpm test for RTECO-935 - build-pnpm-matrix: - name: Build pnpm version matrix - runs-on: ubuntu-latest - if: github.event_name == 'workflow_dispatch' - outputs: - matrix: ${{ steps.set-matrix.outputs.matrix }} - steps: - - name: Fetch supported pnpm versions and build matrix - id: set-matrix - run: | - PNPM_VERSIONS=$(curl -sf https://endoflife.date/api/pnpm.json) - ACTIVE_MATRIX=$(echo "$PNPM_VERSIONS" | jq -c ' - [ - .[] - | select(.eol == false) - | .cycle as $cycle - | select(($cycle | tonumber) > 9) - | { - pnpm_version: .latest, - pnpm_major: $cycle, - node_version: (if ($cycle | tonumber) >= 11 then "22" else "20" end), - experimental: false - } - ] - ') - DIST_TAGS=$(curl -sf https://registry.npmjs.org/pnpm | jq -r '."dist-tags" | to_entries[] | select(.key | test("^next-")) | "\(.key)=\(.value)"') - HIGHEST_ACTIVE=$(echo "$ACTIVE_MATRIX" | jq -r '[.[].pnpm_major | tonumber] | max') - NEXT_ENTRY="[]" - while IFS='=' read -r tag version; do - [ -z "$tag" ] && continue - NEXT_MAJOR=$(echo "$tag" | sed 's/next-//') - if [ "$NEXT_MAJOR" -gt "$HIGHEST_ACTIVE" ] 2>/dev/null; then - NEXT_ENTRY=$(jq -n -c --arg tag "$tag" --arg major "$NEXT_MAJOR" '[{ - pnpm_version: $tag, - pnpm_major: $major, - node_version: "22", - experimental: true - }]') - break - fi - done <<< "$DIST_TAGS" - MATRIX=$(jq -n -c --argjson active "$ACTIVE_MATRIX" --argjson next "$NEXT_ENTRY" '$active + $next') - echo "matrix=${MATRIX}" >> "$GITHUB_OUTPUT" - echo "Generated matrix:" - echo "$MATRIX" | jq . - pnpm-Tests: - name: "pnpm ${{ matrix.pnpm.pnpm_major }} tests (${{ matrix.os.name }})" - needs: build-pnpm-matrix + name: "pnpm 10 tests (${{ matrix.os.name }})" if: github.event_name == 'workflow_dispatch' strategy: fail-fast: false @@ -145,7 +98,6 @@ jobs: version: 24.04 - name: windows version: 2022 - pnpm: ${{ fromJson(needs.build-pnpm-matrix.outputs.matrix) }} runs-on: ${{ matrix.os.name }}-${{ matrix.os.version }} steps: - name: Checkout code @@ -162,31 +114,22 @@ jobs: - name: Install Node.js uses: actions/setup-node@v5 with: - node-version: ${{ matrix.pnpm.node_version }} + node-version: "20" - - name: Install pnpm - run: npm install -g pnpm@${{ matrix.pnpm.pnpm_version }} + - name: Install pnpm 10 + run: npm install -g pnpm@10 - - name: Validate pnpm and Node.js versions + - name: Validate pnpm version is 10 + shell: bash run: | - echo "=== Version Validation ===" PNPM_VER=$(pnpm --version) - NODE_VER=$(node --version | sed 's/^v//') - echo "pnpm version: ${PNPM_VER}" - echo "Node.js version: ${NODE_VER}" PNPM_MAJOR=$(echo "$PNPM_VER" | cut -d. -f1) - if [ "$PNPM_MAJOR" -lt 10 ]; then - echo "::error::pnpm version ${PNPM_VER} is below minimum required (10.x)" + echo "pnpm version: ${PNPM_VER}" + if [ "$PNPM_MAJOR" -ne 10 ]; then + echo "::error::Expected pnpm 10.x but got ${PNPM_VER}" exit 1 fi echo "pnpm ${PNPM_VER} OK" - NODE_MAJOR=$(echo "$NODE_VER" | cut -d. -f1) - NODE_MINOR=$(echo "$NODE_VER" | cut -d. -f2) - if [ "$NODE_MAJOR" -lt 18 ] || { [ "$NODE_MAJOR" -eq 18 ] && [ "$NODE_MINOR" -lt 12 ]; }; then - echo "::error::Node.js ${NODE_VER} is below minimum (18.12) for pnpm ${PNPM_MAJOR}.x" - exit 1 - fi - echo "Node.js ${NODE_VER} OK" - name: Setup Go with cache uses: jfrog/.github/actions/install-go-with-cache@main diff --git a/.github/workflows/pnpmTests.yml b/.github/workflows/pnpmTests.yml index 8184eda0f..fd6ba5a5a 100644 --- a/.github/workflows/pnpmTests.yml +++ b/.github/workflows/pnpmTests.yml @@ -14,65 +14,8 @@ concurrency: cancel-in-progress: true jobs: - build-pnpm-matrix: - name: Build pnpm version matrix - runs-on: ubuntu-latest - if: github.event_name == 'workflow_dispatch' || github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'safe to test') - outputs: - matrix: ${{ steps.set-matrix.outputs.matrix }} - steps: - - name: Fetch supported pnpm versions and build matrix - id: set-matrix - run: | - # Fetch active (non-EOL) pnpm versions with major > 9 from endoflife.date API - PNPM_VERSIONS=$(curl -sf https://endoflife.date/api/pnpm.json) - - ACTIVE_MATRIX=$(echo "$PNPM_VERSIONS" | jq -c ' - [ - .[] - | select(.eol == false) - | .cycle as $cycle - | select(($cycle | tonumber) > 9) - | { - pnpm_version: .latest, - pnpm_major: $cycle, - node_version: (if ($cycle | tonumber) >= 11 then "22" else "20" end), - experimental: false - } - ] - ') - - # Dynamically detect the next pre-release (alpha/beta/rc) pnpm version from npm dist-tags - DIST_TAGS=$(curl -sf https://registry.npmjs.org/pnpm | jq -r '."dist-tags" | to_entries[] | select(.key | test("^next-")) | "\(.key)=\(.value)"') - - # Find the highest next-* tag that is beyond the current active versions - HIGHEST_ACTIVE=$(echo "$ACTIVE_MATRIX" | jq -r '[.[].pnpm_major | tonumber] | max') - NEXT_ENTRY="[]" - - while IFS='=' read -r tag version; do - [ -z "$tag" ] && continue - NEXT_MAJOR=$(echo "$tag" | sed 's/next-//') - if [ "$NEXT_MAJOR" -gt "$HIGHEST_ACTIVE" ] 2>/dev/null; then - NEXT_ENTRY=$(jq -n -c --arg tag "$tag" --arg major "$NEXT_MAJOR" '[{ - pnpm_version: $tag, - pnpm_major: $major, - node_version: "22", - experimental: true - }]') - break - fi - done <<< "$DIST_TAGS" - - # Combine active versions + next alpha/beta - MATRIX=$(jq -n -c --argjson active "$ACTIVE_MATRIX" --argjson next "$NEXT_ENTRY" '$active + $next') - - echo "matrix=${MATRIX}" >> "$GITHUB_OUTPUT" - echo "Generated matrix:" - echo "$MATRIX" | jq . - pnpm-Tests: - name: "pnpm ${{ matrix.pnpm.pnpm_major }} tests (${{ matrix.os.name }})" - needs: build-pnpm-matrix + name: "pnpm 10 tests (${{ matrix.os.name }})" if: github.event_name == 'workflow_dispatch' || github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'safe to test') strategy: fail-fast: false @@ -84,7 +27,6 @@ jobs: version: 2022 - name: macos version: 14 - pnpm: ${{ fromJson(needs.build-pnpm-matrix.outputs.matrix) }} runs-on: ${{ matrix.os.name }}-${{ matrix.os.version }} steps: - name: Skip macOS - JGC-413 @@ -110,37 +52,24 @@ jobs: if: matrix.os.name != 'macos' uses: actions/setup-node@v5 with: - node-version: ${{ matrix.pnpm.node_version }} + node-version: "20" - - name: Install pnpm + - name: Install pnpm 10 if: matrix.os.name != 'macos' - run: npm install -g pnpm@${{ matrix.pnpm.pnpm_version }} + run: npm install -g pnpm@10 - - name: Validate pnpm and Node.js versions + - name: Validate pnpm version is 10 if: matrix.os.name != 'macos' + shell: bash run: | - echo "=== Version Validation ===" PNPM_VER=$(pnpm --version) - NODE_VER=$(node --version | sed 's/^v//') - echo "pnpm version: ${PNPM_VER}" - echo "Node.js version: ${NODE_VER}" - - # Validate pnpm major version >= 10 PNPM_MAJOR=$(echo "$PNPM_VER" | cut -d. -f1) - if [ "$PNPM_MAJOR" -lt 10 ]; then - echo "::error::pnpm version ${PNPM_VER} is below minimum required (10.x). Only pnpm >= 10 is supported." - exit 1 - fi - echo "✓ pnpm version ${PNPM_VER} meets minimum requirement (>= 10)" - - # Validate Node.js version >= 18.12 (required by pnpm 10+) - NODE_MAJOR=$(echo "$NODE_VER" | cut -d. -f1) - NODE_MINOR=$(echo "$NODE_VER" | cut -d. -f2) - if [ "$NODE_MAJOR" -lt 18 ] || { [ "$NODE_MAJOR" -eq 18 ] && [ "$NODE_MINOR" -lt 12 ]; }; then - echo "::error::Node.js version ${NODE_VER} is below minimum required (18.12) for pnpm ${PNPM_MAJOR}.x" + echo "pnpm version: ${PNPM_VER}" + if [ "$PNPM_MAJOR" -ne 10 ]; then + echo "::error::Expected pnpm 10.x but got ${PNPM_VER}" exit 1 fi - echo "✓ Node.js version ${NODE_VER} meets minimum requirement (>= 18.12)" + echo "pnpm ${PNPM_VER} OK" - name: Setup Go with cache if: matrix.os.name != 'macos' @@ -157,4 +86,4 @@ jobs: if: matrix.os.name != 'macos' env: YARN_IGNORE_NODE: 1 - run: go test -v github.com/jfrog/jfrog-cli --timeout 0 --test.pnpm --jfrog.url=http://127.0.0.1:8082 --jfrog.adminToken=${{ env.JFROG_TESTS_LOCAL_ACCESS_TOKEN }} + run: go test -v github.com/jfrog/jfrog-cli --timeout 0 --test.pnpm diff --git a/go.mod b/go.mod index 21824f6ec..357286dab 100644 --- a/go.mod +++ b/go.mod @@ -248,7 +248,7 @@ require ( // replace github.com/jfrog/jfrog-cli-artifactory => github.com/reshmifrog/jfrog-cli-artifactory v0.0.0-20260303084642-b208fbba798b -replace github.com/jfrog/jfrog-cli-artifactory => github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260313193427-0c6cf28a9fc8 +replace github.com/jfrog/jfrog-cli-artifactory => github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260313195239-ba934539f79d //replace github.com/jfrog/build-info-go => github.com/fluxxBot/build-info-go v1.10.10-0.20260105070825-d3f36f619ba5 diff --git a/go.sum b/go.sum index 9a76c7eea..8430e84a1 100644 --- a/go.sum +++ b/go.sum @@ -419,8 +419,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.20260216085810-1ade6c26b3df h1:raSyae8/h1y8HtzFLf7vZZj91fP/qD94AX+biwBJiqs= github.com/jfrog/jfrog-cli-application v1.0.2-0.20260216085810-1ade6c26b3df/go.mod h1:xum2HquWO5uExa/A7MQs3TgJJVEeoqTR+6Z4mfBr1Xw= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260313193427-0c6cf28a9fc8 h1:lL2C2y6tGh2g9MOnX07bJdloc8lsM0ylKMuuStNT7wI= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260313193427-0c6cf28a9fc8/go.mod h1:kgw6gIQvJx9bCcOdtAGSUEiCz7nNQmaFbFvNg6byZ6I= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260313195239-ba934539f79d h1:caKH0IP9mz5v4SV9Gohrx6TZxX7ehb6VyudCb7oAtGE= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260313195239-ba934539f79d/go.mod h1:kgw6gIQvJx9bCcOdtAGSUEiCz7nNQmaFbFvNg6byZ6I= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260225195817-bc599cec3973 h1:awB01Y4m0cWzmXuR3waf5IQnoQxDlbUmqT+FMWOpjbs= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260225195817-bc599cec3973/go.mod h1:yhi+XpiEx18a3t8CZ6M2VpAf3EGqKpBhTzoPBTFe0dk= github.com/jfrog/jfrog-cli-evidence v0.8.3-0.20260202100913-d9ee9476845a h1:lTOAhUjKcOmM/0Kbj4V+I/VHPlW7YNAhIEVpGnCM5mI= From 651fef51a41f4144d72026e4060f4b35aff938fd Mon Sep 17 00:00:00 2001 From: Kanishk Date: Tue, 17 Mar 2026 12:17:27 +0530 Subject: [PATCH 6/8] pnpm test log working --- pnpm_test.go | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/pnpm_test.go b/pnpm_test.go index 081ae6168..7defe6e82 100644 --- a/pnpm_test.go +++ b/pnpm_test.go @@ -4,9 +4,11 @@ import ( "fmt" "os" "path/filepath" + "regexp" "strconv" "strings" "testing" + "time" buildutils "github.com/jfrog/build-info-go/build/utils" buildinfo "github.com/jfrog/build-info-go/entities" @@ -23,6 +25,7 @@ import ( accessServices "github.com/jfrog/jfrog-client-go/access/services" artServices "github.com/jfrog/jfrog-client-go/artifactory/services" "github.com/jfrog/jfrog-client-go/lifecycle/services" + "github.com/jfrog/jfrog-client-go/utils/log" clientTestUtils "github.com/jfrog/jfrog-client-go/utils/tests" "github.com/stretchr/testify/assert" ) @@ -251,6 +254,68 @@ func TestPnpmInstallAndPublishWorkspace(t *testing.T) { inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, tests.PnpmBuildName, artHttpDetails) } +// TestPnpmInstallWithPreviousBuildCache verifies that a second install with the same build name +// can use the previous published build's dependencies for checksum cache (GetDependenciesFromLatestBuild). +func TestPnpmInstallWithPreviousBuildCache(t *testing.T) { + initPnpmTest(t) + defer cleanPnpmTest(t) + wd, err := os.Getwd() + assert.NoError(t, err) + defer clientTestUtils.ChangeDirAndAssert(t, wd) + + tempCacheDirPath, createTempDirCallback := coretests.CreateTempDirWithCallbackAndAssert(t) + defer createTempDirCallback() + + buildName := tests.PnpmBuildName + buildNum1, buildNum2 := "602", "603" + pnpmProjectPath := createPnpmProject(t, "pnpmproject") + projectDir := filepath.Dir(pnpmProjectPath) + + // Write .npmrc so pnpm resolves through Artifactory (required for AQL checksum resolution). + registry := npmCmdUtils.GetNpmRepositoryUrl(tests.NpmRemoteRepo, serverDetails.GetArtifactoryUrl()) + registryWithSlash := strings.TrimSuffix(registry, "/") + "/" + authKey, authValue := npmCmdUtils.GetNpmAuthKeyValue(serverDetails, registryWithSlash) + npmrcContent := fmt.Sprintf("registry=%s\n%s=%s\n", registryWithSlash, authKey, authValue) + err = os.WriteFile(filepath.Join(projectDir, ".npmrc"), []byte(npmrcContent), 0644) + assert.NoError(t, err) + + // Clear pnpm metadata cache for the Artifactory host to avoid stale tarball URLs + // from repos created by previous test runs (repo names include a unique timestamp suffix). + artHost := strings.TrimPrefix(strings.TrimPrefix(serverDetails.GetArtifactoryUrl(), "https://"), "http://") + artHost = strings.SplitN(artHost, "/", 2)[0] + if homeDir, hErr := os.UserHomeDir(); hErr == nil { + _ = os.RemoveAll(filepath.Join(homeDir, "Library", "Caches", "pnpm", "metadata-v1.3", artHost)) + } + + prepareArtifactoryForPnpmBuild(t, projectDir) + + clientTestUtils.ChangeDirAndAssert(t, projectDir) + runJfrogCli(t, "pnpm", "install", "--store-dir="+tempCacheDirPath, + "--build-name="+buildName, "--build-number="+buildNum1) + assert.NoError(t, artifactoryCli.Exec("bp", buildName, buildNum1)) + time.Sleep(3 * time.Second) + + // Second install: redirect log to capture "Checksum resolution: N cached, ..." from fetchChecksums + _, logBuffer, previousLog := coretests.RedirectLogOutputToBuffer() + defer log.SetLogger(previousLog) + runJfrogCli(t, "pnpm", "install", "--store-dir="+tempCacheDirPath, + "--build-name="+buildName, "--build-number="+buildNum2) + logOut := logBuffer.String() + assert.Regexp(t, regexp.MustCompile(`Checksum resolution: ([1-9]\d*) cached`), logOut, + "second install must use previous build cache for at least one dependency; log output: %s", logOut) + assert.NoError(t, artifactoryCli.Exec("bp", buildName, buildNum2)) + + for _, buildNumber := range []string{buildNum1, buildNum2} { + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, buildName, buildNumber) + assert.NoError(t, err) + assert.True(t, found, "build %s/%s should be found", buildName, buildNumber) + assert.NotEmpty(t, publishedBuildInfo.BuildInfo.Modules, "build should have modules") + assert.NotEmpty(t, publishedBuildInfo.BuildInfo.Modules[0].Dependencies, "module should have dependencies") + } + + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) +} + // setupPnpmPublishAuth writes Artifactory registry and auth to ~/.npmrc // so that pnpm publish (which delegates to npm from a temp dir) can authenticate. // The registry URL must end with "/" for npm's nerfDart URL matching to work. @@ -340,6 +405,15 @@ func validatePnpmScopedPublish(t *testing.T, pt pnpmTestParams) { fmt.Sprintf("build.name=%v;build.number=%v;build.timestamp=*", tests.PnpmBuildName, pt.buildNumber), t) // pnpm pack includes the scope in the tarball filename (e.g., jscope-jfrog-cli-tests-=1.0.0.tgz) validatePnpmCommonPublish(t, pt, "jscope-jfrog-cli-tests-=1.0.0.tgz") + // E2E: assert artifact Path in build info matches Artifactory layout for scoped packages (@scope/name/-/@scope/name-version.tgz) + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, tests.PnpmBuildName, pt.buildNumber) + assert.NoError(t, err) + assert.True(t, found) + if found && len(publishedBuildInfo.BuildInfo.Modules) > 0 && len(publishedBuildInfo.BuildInfo.Modules[0].Artifacts) > 0 { + path := publishedBuildInfo.BuildInfo.Modules[0].Artifacts[0].Path + assert.Equal(t, "@jscope/jfrog-cli-tests/-/@jscope/jfrog-cli-tests-1.0.0.tgz", path, + "scoped artifact path in build info should match Artifactory layout") + } } func validatePnpmCommonPublish(t *testing.T, pt pnpmTestParams, expectedArtifactName string) { From f84b15e0557674c7863161c415edbe39e11450d6 Mon Sep 17 00:00:00 2001 From: Kanishk Date: Tue, 17 Mar 2026 13:39:52 +0530 Subject: [PATCH 7/8] added test to check caching --- go.mod | 2 +- go.sum | 4 +-- pnpm_test.go | 79 ++++++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 70 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index 357286dab..2fd3d48b1 100644 --- a/go.mod +++ b/go.mod @@ -248,7 +248,7 @@ require ( // replace github.com/jfrog/jfrog-cli-artifactory => github.com/reshmifrog/jfrog-cli-artifactory v0.0.0-20260303084642-b208fbba798b -replace github.com/jfrog/jfrog-cli-artifactory => github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260313195239-ba934539f79d +replace github.com/jfrog/jfrog-cli-artifactory => github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260316111459-0097e69f161e //replace github.com/jfrog/build-info-go => github.com/fluxxBot/build-info-go v1.10.10-0.20260105070825-d3f36f619ba5 diff --git a/go.sum b/go.sum index 8430e84a1..6758f0eae 100644 --- a/go.sum +++ b/go.sum @@ -419,8 +419,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.20260216085810-1ade6c26b3df h1:raSyae8/h1y8HtzFLf7vZZj91fP/qD94AX+biwBJiqs= github.com/jfrog/jfrog-cli-application v1.0.2-0.20260216085810-1ade6c26b3df/go.mod h1:xum2HquWO5uExa/A7MQs3TgJJVEeoqTR+6Z4mfBr1Xw= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260313195239-ba934539f79d h1:caKH0IP9mz5v4SV9Gohrx6TZxX7ehb6VyudCb7oAtGE= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260313195239-ba934539f79d/go.mod h1:kgw6gIQvJx9bCcOdtAGSUEiCz7nNQmaFbFvNg6byZ6I= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260316111459-0097e69f161e h1:DfaRUC28WHcYo4JX8q7wec0l/Lej+lPms8dwKTg/Pm4= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260316111459-0097e69f161e/go.mod h1:kgw6gIQvJx9bCcOdtAGSUEiCz7nNQmaFbFvNg6byZ6I= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260225195817-bc599cec3973 h1:awB01Y4m0cWzmXuR3waf5IQnoQxDlbUmqT+FMWOpjbs= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260225195817-bc599cec3973/go.mod h1:yhi+XpiEx18a3t8CZ6M2VpAf3EGqKpBhTzoPBTFe0dk= github.com/jfrog/jfrog-cli-evidence v0.8.3-0.20260202100913-d9ee9476845a h1:lTOAhUjKcOmM/0Kbj4V+I/VHPlW7YNAhIEVpGnCM5mI= diff --git a/pnpm_test.go b/pnpm_test.go index 7defe6e82..0f0943da7 100644 --- a/pnpm_test.go +++ b/pnpm_test.go @@ -4,7 +4,6 @@ import ( "fmt" "os" "path/filepath" - "regexp" "strconv" "strings" "testing" @@ -25,7 +24,6 @@ import ( accessServices "github.com/jfrog/jfrog-client-go/access/services" artServices "github.com/jfrog/jfrog-client-go/artifactory/services" "github.com/jfrog/jfrog-client-go/lifecycle/services" - "github.com/jfrog/jfrog-client-go/utils/log" clientTestUtils "github.com/jfrog/jfrog-client-go/utils/tests" "github.com/stretchr/testify/assert" ) @@ -255,7 +253,14 @@ func TestPnpmInstallAndPublishWorkspace(t *testing.T) { } // TestPnpmInstallWithPreviousBuildCache verifies that a second install with the same build name -// can use the previous published build's dependencies for checksum cache (GetDependenciesFromLatestBuild). +// uses the previous published build's dependencies as a checksum cache (GetDependenciesFromLatestBuild). +// +// Strategy: after the first pnpm install writes local partial build info (but before publishing), +// we inject a marker ("MARKER_") into one dependency's SHA256 checksum using ReadPartialBuildInfoFiles +// and SavePartialBuildInfo. This "poisoned" checksum is then published with build 1. When the second +// install runs, if it correctly loads checksums from the previous build's cache, the marker will +// propagate into build 2's published build info. Asserting the marker's presence in build 2 proves +// the cache path was taken — without relying on log output or any production code changes. func TestPnpmInstallWithPreviousBuildCache(t *testing.T) { initPnpmTest(t) defer cleanPnpmTest(t) @@ -290,21 +295,56 @@ func TestPnpmInstallWithPreviousBuildCache(t *testing.T) { prepareArtifactoryForPnpmBuild(t, projectDir) clientTestUtils.ChangeDirAndAssert(t, projectDir) + + // First install: collect build info locally (partial files). runJfrogCli(t, "pnpm", "install", "--store-dir="+tempCacheDirPath, "--build-name="+buildName, "--build-number="+buildNum1) + + // Inject a marker into one dependency's checksum in the local partial build info. + marker := "MARKER_" + partials, err := build.ReadPartialBuildInfoFiles(buildName, buildNum1, "") + assert.NoError(t, err) + assert.NotEmpty(t, partials) + + var markedDepID string + for _, p := range partials { + if len(p.Dependencies) > 0 { + markedDepID = p.Dependencies[0].Id + p.Dependencies[0].Sha256 = marker + p.Dependencies[0].Sha256 + break + } + } + assert.NotEmpty(t, markedDepID, "should have at least one dependency to mark") + + // Clear existing partial files and re-save with the modified checksums. + buildDir, err := build.GetBuildDir(buildName, buildNum1, "") + assert.NoError(t, err) + partialsDir := filepath.Join(buildDir, "partials") + entries, err := os.ReadDir(partialsDir) + assert.NoError(t, err) + for _, e := range entries { + if !e.IsDir() && !strings.HasSuffix(e.Name(), "details") { + assert.NoError(t, os.Remove(filepath.Join(partialsDir, e.Name()))) + } + } + for _, p := range partials { + captured := p + err := build.SavePartialBuildInfo(buildName, buildNum1, "", func(partial *buildinfo.Partial) { + *partial = *captured + }) + assert.NoError(t, err) + } + + // Publish build 1 (now contains the marked checksum). assert.NoError(t, artifactoryCli.Exec("bp", buildName, buildNum1)) time.Sleep(3 * time.Second) - // Second install: redirect log to capture "Checksum resolution: N cached, ..." from fetchChecksums - _, logBuffer, previousLog := coretests.RedirectLogOutputToBuffer() - defer log.SetLogger(previousLog) + // Second install + publish build 2. runJfrogCli(t, "pnpm", "install", "--store-dir="+tempCacheDirPath, "--build-name="+buildName, "--build-number="+buildNum2) - logOut := logBuffer.String() - assert.Regexp(t, regexp.MustCompile(`Checksum resolution: ([1-9]\d*) cached`), logOut, - "second install must use previous build cache for at least one dependency; log output: %s", logOut) assert.NoError(t, artifactoryCli.Exec("bp", buildName, buildNum2)) + // Verify both builds were published with modules and dependencies. for _, buildNumber := range []string{buildNum1, buildNum2} { publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, buildName, buildNumber) assert.NoError(t, err) @@ -313,6 +353,21 @@ func TestPnpmInstallWithPreviousBuildCache(t *testing.T) { assert.NotEmpty(t, publishedBuildInfo.BuildInfo.Modules[0].Dependencies, "module should have dependencies") } + // Verify the marker propagated from build 1's cache into build 2. + publishedBI2, found, err := tests.GetBuildInfo(serverDetails, buildName, buildNum2) + assert.NoError(t, err) + assert.True(t, found) + var markedDep *buildinfo.Dependency + for _, dep := range publishedBI2.BuildInfo.Modules[0].Dependencies { + if dep.Id == markedDepID { + markedDep = &dep + break + } + } + assert.NotNil(t, markedDep, "dependency %s should exist in build 2", markedDepID) + assert.True(t, strings.HasPrefix(markedDep.Sha256, marker), + "build 2 should carry the marked checksum from build 1 cache, got: %s", markedDep.Sha256) + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) } @@ -323,7 +378,7 @@ func TestPnpmInstallWithPreviousBuildCache(t *testing.T) { func setupPnpmPublishAuth(t *testing.T, repo string) func() { homeDir, err := os.UserHomeDir() assert.NoError(t, err) - npmrcPath := filepath.Join(homeDir, ".npmrc") + npmrcPath := filepath.Clean(filepath.Join(homeDir, ".npmrc")) origContent, origErr := os.ReadFile(npmrcPath) @@ -338,7 +393,7 @@ func setupPnpmPublishAuth(t *testing.T, repo string) func() { return func() { if origErr == nil { - _ = os.WriteFile(npmrcPath, origContent, 0644) + _ = os.WriteFile(npmrcPath, origContent, 0644) // #nosec G703 -- restoring original file } else { _ = os.Remove(npmrcPath) } @@ -736,7 +791,7 @@ func TestPnpmInstallEmptyLockfile(t *testing.T) { // Also copy the empty lockfile to the target directory srcLockfile := filepath.Join(filepath.FromSlash(tests.GetTestResourcesPath()), "pnpm", "pnpmemptylockfile", "pnpm-lock.yaml") - targetLockfile := filepath.Join(projectDir, "pnpm-lock.yaml") + targetLockfile := filepath.Clean(filepath.Join(projectDir, "pnpm-lock.yaml")) lockfileContent, err := os.ReadFile(srcLockfile) assert.NoError(t, err) err = os.WriteFile(targetLockfile, lockfileContent, 0644) From 2026738475f0d2f8d740abde72ebe0af7113be17 Mon Sep 17 00:00:00 2001 From: Kanishk Date: Tue, 17 Mar 2026 13:52:03 +0530 Subject: [PATCH 8/8] fixed path --- go.mod | 2 +- go.sum | 4 ++-- pnpm_test.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 2fd3d48b1..fd34eb111 100644 --- a/go.mod +++ b/go.mod @@ -248,7 +248,7 @@ require ( // replace github.com/jfrog/jfrog-cli-artifactory => github.com/reshmifrog/jfrog-cli-artifactory v0.0.0-20260303084642-b208fbba798b -replace github.com/jfrog/jfrog-cli-artifactory => github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260316111459-0097e69f161e +replace github.com/jfrog/jfrog-cli-artifactory => github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260317081831-91e84f2c7c5f //replace github.com/jfrog/build-info-go => github.com/fluxxBot/build-info-go v1.10.10-0.20260105070825-d3f36f619ba5 diff --git a/go.sum b/go.sum index 6758f0eae..fc4a6d07a 100644 --- a/go.sum +++ b/go.sum @@ -419,8 +419,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.20260216085810-1ade6c26b3df h1:raSyae8/h1y8HtzFLf7vZZj91fP/qD94AX+biwBJiqs= github.com/jfrog/jfrog-cli-application v1.0.2-0.20260216085810-1ade6c26b3df/go.mod h1:xum2HquWO5uExa/A7MQs3TgJJVEeoqTR+6Z4mfBr1Xw= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260316111459-0097e69f161e h1:DfaRUC28WHcYo4JX8q7wec0l/Lej+lPms8dwKTg/Pm4= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260316111459-0097e69f161e/go.mod h1:kgw6gIQvJx9bCcOdtAGSUEiCz7nNQmaFbFvNg6byZ6I= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260317081831-91e84f2c7c5f h1:NuCL7j0cEVqSI+L5XHfDrkwDmZ3nF9i4/Ow284p4GR4= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260317081831-91e84f2c7c5f/go.mod h1:kgw6gIQvJx9bCcOdtAGSUEiCz7nNQmaFbFvNg6byZ6I= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260225195817-bc599cec3973 h1:awB01Y4m0cWzmXuR3waf5IQnoQxDlbUmqT+FMWOpjbs= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260225195817-bc599cec3973/go.mod h1:yhi+XpiEx18a3t8CZ6M2VpAf3EGqKpBhTzoPBTFe0dk= github.com/jfrog/jfrog-cli-evidence v0.8.3-0.20260202100913-d9ee9476845a h1:lTOAhUjKcOmM/0Kbj4V+I/VHPlW7YNAhIEVpGnCM5mI= diff --git a/pnpm_test.go b/pnpm_test.go index 0f0943da7..48de73e6e 100644 --- a/pnpm_test.go +++ b/pnpm_test.go @@ -794,7 +794,7 @@ func TestPnpmInstallEmptyLockfile(t *testing.T) { targetLockfile := filepath.Clean(filepath.Join(projectDir, "pnpm-lock.yaml")) lockfileContent, err := os.ReadFile(srcLockfile) assert.NoError(t, err) - err = os.WriteFile(targetLockfile, lockfileContent, 0644) + err = os.WriteFile(targetLockfile, lockfileContent, 0644) //#nosec G703 -- test-only path from controlled test infrastructure assert.NoError(t, err) clientTestUtils.ChangeDirAndAssert(t, projectDir)