diff --git a/artifactory/commands/npm/publish.go b/artifactory/commands/npm/publish.go index 5313d2d74..764fc1b73 100644 --- a/artifactory/commands/npm/publish.go +++ b/artifactory/commands/npm/publish.go @@ -3,6 +3,7 @@ package npm import ( "archive/tar" "compress/gzip" + "errors" "fmt" "io" "os" @@ -37,7 +38,7 @@ type NpmPublishCommandArgs struct { executablePath string workingDirectory string collectBuildInfo bool - packedFilePath string + packedFilePaths []string packageInfo *biutils.PackageInfo publishPath string tarballProvided bool @@ -172,11 +173,11 @@ func (npc *NpmPublishCommand) Run() (err error) { return err } // We should delete the tarball we created - return deleteCreatedTarballAndError(npc.packedFilePath, err) + return errors.Join(err, deleteCreatedTarball(npc.packedFilePaths)) } if !npc.tarballProvided { - if err := deleteCreatedTarball(npc.packedFilePath); err != nil { + if err := deleteCreatedTarball(npc.packedFilePaths); err != nil { return err } } @@ -217,6 +218,7 @@ func (npc *NpmPublishCommand) CommandName() string { } func (npc *NpmPublishCommand) preparePrerequisites() error { + npc.packedFilePaths = make([]string, 0) currentDir, err := os.Getwd() if err != nil { return errorutils.CheckError(err) @@ -251,7 +253,7 @@ func (npc *NpmPublishCommand) preparePrerequisites() error { func (npc *NpmPublishCommand) pack() error { log.Debug("Creating npm package.") - packageFileName, err := npm.Pack(npc.npmArgs, npc.executablePath) + packedFileNames, err := npm.Pack(npc.npmArgs, npc.executablePath) if err != nil { return err } @@ -261,8 +263,10 @@ func (npc *NpmPublishCommand) pack() error { return err } - npc.packedFilePath = filepath.Join(tarballDir, packageFileName) - log.Debug("Created npm package at", npc.packedFilePath) + for _, packageFileName := range packedFileNames { + npc.packedFilePaths = append(npc.packedFilePaths, filepath.Join(tarballDir, packageFileName)) + } + return nil } @@ -279,34 +283,36 @@ func (npc *NpmPublishCommand) getTarballDir() (string, error) { return dest, nil } -func (npc *NpmPublishCommand) publish() error { - log.Debug("Deploying npm package.") - if err := npc.readPackageInfoFromTarball(); err != nil { - return err - } - target := fmt.Sprintf("%s/%s", npc.repo, npc.packageInfo.GetDeployPath()) - - // If requested, perform a Xray binary scan before deployment. If a FailBuildError is returned, skip the deployment. - if npc.xrayScan { - fileSpec := spec.NewBuilder(). - Pattern(npc.packedFilePath). - Target(npc.repo + "/"). - BuildSpec() - err := commandsutils.ConditionalUploadScanFunc(npc.serverDetails, fileSpec, 1, npc.scanOutputFormat) - if err != nil { - return err +func (npc *NpmPublishCommand) publish() (err error) { + for _, packedFilePath := range npc.packedFilePaths { + log.Debug("Deploying npm package.") + if err = npc.readPackageInfoFromTarball(packedFilePath); err != nil { + return } + target := fmt.Sprintf("%s/%s", npc.repo, npc.packageInfo.GetDeployPath()) + + // If requested, perform a Xray binary scan before deployment. If a FailBuildError is returned, skip the deployment. + if npc.xrayScan { + fileSpec := spec.NewBuilder(). + Pattern(packedFilePath). + Target(npc.repo + "/"). + BuildSpec() + if err = commandsutils.ConditionalUploadScanFunc(npc.serverDetails, fileSpec, 1, npc.scanOutputFormat); err != nil { + return + } + } + err = errors.Join(err, npc.doDeploy(target, npc.serverDetails, packedFilePath)) } - return npc.doDeploy(target, npc.serverDetails) + return } -func (npc *NpmPublishCommand) doDeploy(target string, artDetails *config.ServerDetails) error { +func (npc *NpmPublishCommand) doDeploy(target string, artDetails *config.ServerDetails, packedFilePath string) error { servicesManager, err := utils.CreateServiceManager(artDetails, -1, 0, false) if err != nil { return err } up := services.NewUploadParams() - up.CommonParams = &specutils.CommonParams{Pattern: npc.packedFilePath, Target: target} + up.CommonParams = &specutils.CommonParams{Pattern: packedFilePath, Target: target} var totalFailed int if npc.collectBuildInfo || npc.detailedSummary { if npc.collectBuildInfo { @@ -341,12 +347,11 @@ func (npc *NpmPublishCommand) doDeploy(target string, artDetails *config.ServerD } } if npc.detailedSummary { - npc.result.SetReader(summary.TransferDetailsReader) - npc.result.SetFailCount(totalFailed) - npc.result.SetSuccessCount(summary.TotalSucceeded) + if err = npc.setDetailedSummary(summary); err != nil { + return err + } } else { - err = summary.TransferDetailsReader.Close() - if err != nil { + if err = summary.TransferDetailsReader.Close(); err != nil { return err } } @@ -364,6 +369,29 @@ func (npc *NpmPublishCommand) doDeploy(target string, artDetails *config.ServerD return nil } +func (npc *NpmPublishCommand) setDetailedSummary(summary *specutils.OperationSummary) (err error) { + npc.result.SetFailCount(npc.result.FailCount() + summary.TotalFailed) + npc.result.SetSuccessCount(npc.result.SuccessCount() + summary.TotalSucceeded) + if npc.result.Reader() == nil { + npc.result.SetReader(summary.TransferDetailsReader) + } else { + if err = npc.appendReader(summary); err != nil { + return + } + } + return +} + +func (npc *NpmPublishCommand) appendReader(summary *specutils.OperationSummary) error { + readersSlice := []*content.ContentReader{npc.result.Reader(), summary.TransferDetailsReader} + reader, err := content.MergeReaders(readersSlice, content.DefaultKey) + if err != nil { + return err + } + npc.result.SetReader(reader) + return nil +} + func (npc *NpmPublishCommand) setPublishPath() error { log.Debug("Reading Package Json.") @@ -394,13 +422,12 @@ func (npc *NpmPublishCommand) setPackageInfo() error { } log.Debug("The provided path is not a directory, we assume this is a compressed npm package") npc.tarballProvided = true - npc.packedFilePath = npc.publishPath - return npc.readPackageInfoFromTarball() + return npc.readPackageInfoFromTarball(npc.publishPath) } -func (npc *NpmPublishCommand) readPackageInfoFromTarball() (err error) { - log.Debug("Extracting info from npm package:", npc.packedFilePath) - tarball, err := os.Open(npc.packedFilePath) +func (npc *NpmPublishCommand) readPackageInfoFromTarball(packedFilePath string) (err error) { + log.Debug("Extracting info from npm package:", npc.packedFilePaths) + tarball, err := os.Open(packedFilePath) if err != nil { return errorutils.CheckError(err) } @@ -420,7 +447,7 @@ func (npc *NpmPublishCommand) readPackageInfoFromTarball() (err error) { hdr, err := tarReader.Next() if err != nil { if err == io.EOF { - return errorutils.CheckErrorf("Could not find 'package.json' in the compressed npm package: " + npc.packedFilePath) + return errorutils.CheckErrorf("Could not find 'package.json' in the compressed npm package: " + packedFilePath) } return errorutils.CheckError(err) } @@ -436,18 +463,12 @@ func (npc *NpmPublishCommand) readPackageInfoFromTarball() (err error) { } } -func deleteCreatedTarballAndError(packedFilePath string, currentError error) error { - if err := deleteCreatedTarball(packedFilePath); err != nil { - errorText := fmt.Sprintf("Two errors occurred: \n%s \n%s", currentError, err) - return errorutils.CheckErrorf(errorText) - } - return currentError -} - -func deleteCreatedTarball(packedFilePath string) error { - if err := os.Remove(packedFilePath); err != nil { - return errorutils.CheckError(err) +func deleteCreatedTarball(packedFilesPath []string) error { + for _, packedFilePath := range packedFilesPath { + if err := os.Remove(packedFilePath); err != nil { + return errorutils.CheckError(err) + } + log.Debug("Successfully deleted the created npm package:", packedFilePath) } - log.Debug("Successfully deleted the created npm package:", packedFilePath) return nil } diff --git a/artifactory/commands/npm/publish_test.go b/artifactory/commands/npm/publish_test.go index 91174289e..fa3e412c6 100644 --- a/artifactory/commands/npm/publish_test.go +++ b/artifactory/commands/npm/publish_test.go @@ -9,10 +9,17 @@ import ( func TestReadPackageInfoFromTarball(t *testing.T) { npmPublish := NewNpmPublishCommand() - npmPublish.packedFilePath = filepath.Join("..", "testdata", "npm", "npm-example-0.0.3.tgz") - err := npmPublish.readPackageInfoFromTarball() - assert.NoError(t, err) + npmPublish.packedFilePaths = append(npmPublish.packedFilePaths, filepath.Join("..", "testdata", "npm", "npm-example-0.0.3.tgz")) + npmPublish.packedFilePaths = append(npmPublish.packedFilePaths, filepath.Join("..", "testdata", "npm", "npm-example-0.0.4.tgz")) + err := npmPublish.readPackageInfoFromTarball(npmPublish.packedFilePaths[0]) + assert.NoError(t, err) assert.Equal(t, "npm-example", npmPublish.packageInfo.Name) assert.Equal(t, "0.0.3", npmPublish.packageInfo.Version) + + err = npmPublish.readPackageInfoFromTarball(npmPublish.packedFilePaths[1]) + assert.NoError(t, err) + assert.Equal(t, "npm-example", npmPublish.packageInfo.Name) + assert.Equal(t, "0.0.4", npmPublish.packageInfo.Version) + } diff --git a/artifactory/commands/testdata/npm/npm-example-0.0.4.tgz b/artifactory/commands/testdata/npm/npm-example-0.0.4.tgz new file mode 100644 index 000000000..5d3f4db17 Binary files /dev/null and b/artifactory/commands/testdata/npm/npm-example-0.0.4.tgz differ diff --git a/artifactory/utils/npm/pack.go b/artifactory/utils/npm/pack.go index 0cefe8cd8..9f9686709 100644 --- a/artifactory/utils/npm/pack.go +++ b/artifactory/utils/npm/pack.go @@ -8,13 +8,13 @@ import ( "github.com/jfrog/jfrog-client-go/utils/errorutils" ) -func Pack(npmFlags []string, executablePath string) (string, error) { +func Pack(npmFlags []string, executablePath string) ([]string, error) { configListCmdConfig := createPackCmdConfig(executablePath, npmFlags) output, err := gofrogcmd.RunCmdOutput(configListCmdConfig) if err != nil { - return "", errorutils.CheckError(err) + return []string{}, errorutils.CheckError(err) } - return getPackageFileNameFromOutput(output) + return getPackageFileNameFromOutput(output), nil } func createPackCmdConfig(executablePath string, splitFlags []string) *npmutils.NpmConfig { @@ -27,8 +27,7 @@ func createPackCmdConfig(executablePath string, splitFlags []string) *npmutils.N } } -func getPackageFileNameFromOutput(output string) (string, error) { +func getPackageFileNameFromOutput(output string) []string { output = strings.TrimSpace(output) - lines := strings.Split(output, "\n") - return strings.TrimSpace(lines[len(lines)-1]), nil + return strings.Split(output, "\n") } diff --git a/artifactory/utils/npm/pack_test.go b/artifactory/utils/npm/pack_test.go index 8b8533b4b..68ec6c5c8 100644 --- a/artifactory/utils/npm/pack_test.go +++ b/artifactory/utils/npm/pack_test.go @@ -1,36 +1,66 @@ package npm import ( + biutils "github.com/jfrog/build-info-go/build/utils" + "github.com/jfrog/build-info-go/utils" + "github.com/jfrog/jfrog-cli-core/v2/utils/tests" + "github.com/jfrog/jfrog-client-go/utils/log" + testsUtils "github.com/jfrog/jfrog-client-go/utils/tests" "github.com/stretchr/testify/assert" "os" "path/filepath" "testing" ) -const testdataDir = "../testdata/npm/" +const minimumWorkspacesNpmVersion = "7.24.2" -func TestGetPackageFileNameFromOutput(t *testing.T) { - tests := []struct { - testName string - outputTestDataFile string - expectedPackageFilename string - }{ - {"Get package filename for npm 6", "npmPackOutputV6", "npm-example-0.0.3.tgz"}, - {"Get package filename for npm 7", "npmPackOutputV7", "npm-example-ver0.0.3.tgz"}, - } - for _, test := range tests { - t.Run(test.testName, func(t *testing.T) { - output, err := os.ReadFile(filepath.Join(testdataDir, test.outputTestDataFile)) - if err != nil { - assert.NoError(t, err) - return - } - actualFilename, err := getPackageFileNameFromOutput(string(output)) - if err != nil { - assert.NoError(t, err) - return - } - assert.Equal(t, test.expectedPackageFilename, actualFilename) - }) +func TestNpmPackWorkspaces(t *testing.T) { + + npmVersion, executablePath, err := biutils.GetNpmVersionAndExecPath(nil) + assert.NoError(t, err) + // In npm under v7 skip test + if npmVersion.Compare(minimumWorkspacesNpmVersion) > 0 { + log.Info("Test skipped as this function in not supported in npm version " + npmVersion.GetVersion()) + return } + + tmpDir, createTempDirCallback := tests.CreateTempDirWithCallbackAndAssert(t) + defer createTempDirCallback() + + npmProjectPath := filepath.Join("..", "..", "..", "tests", "testdata", "npm-workspaces") + err = utils.CopyDir(npmProjectPath, tmpDir, true, nil) + assert.NoError(t, err) + + cwd, err := os.Getwd() + assert.NoError(t, err) + chdirCallback := testsUtils.ChangeDirWithCallback(t, cwd, tmpDir) + defer chdirCallback() + + packedFileNames, err := Pack([]string{"--workspaces", "--verbose"}, executablePath) + assert.NoError(t, err) + + expected := []string{"module1-1.0.0.tgz", "module2-1.0.0.tgz"} + assert.Equal(t, expected, packedFileNames) +} + +func TestNpmPack(t *testing.T) { + + _, executablePath, err := biutils.GetNpmVersionAndExecPath(nil) + assert.NoError(t, err) + tmpDir, createTempDirCallback := tests.CreateTempDirWithCallbackAndAssert(t) + defer createTempDirCallback() + npmProjectPath := filepath.Join("..", "..", "..", "tests", "testdata", "npm-workspaces") + err = utils.CopyDir(npmProjectPath, tmpDir, false, nil) + assert.NoError(t, err) + + cwd, err := os.Getwd() + assert.NoError(t, err) + chdirCallback := testsUtils.ChangeDirWithCallback(t, cwd, tmpDir) + defer chdirCallback() + + packedFileNames, err := Pack([]string{"--verbose"}, executablePath) + assert.NoError(t, err) + + expected := []string{"npm-pack-test-1.0.0.tgz"} + assert.Equal(t, expected, packedFileNames) } diff --git a/artifactory/utils/testdata/npm/npmPackOutputV6 b/artifactory/utils/testdata/npm/npmPackOutputV6 deleted file mode 100644 index b0eb2668b..000000000 --- a/artifactory/utils/testdata/npm/npmPackOutputV6 +++ /dev/null @@ -1,28 +0,0 @@ -> npm-example@0.0.3 prepack /Users/robin/proj/project-examples/npm-example -> echo pre-helloworld - -pre-helloworld - -> npm-example@0.0.3 postpack /Users/robin/proj/project-examples/npm-example -> echo post-helloworld - -post-helloworld -npm notice -npm notice 📦 npm-example@0.0.3 -npm notice === Tarball Contents === -npm notice 181B helloworld.js -npm notice 276B package.json -npm notice 2.8kB README.md -npm notice 5.5kB npm-example-ver0.0.3.tgz -npm notice 97B .jfrog/projects/npm.yaml -npm notice === Tarball Details === -npm notice name: npm-example -npm notice version: 0.0.3 -npm notice filename: npm-example-0.0.3.tgz -npm notice package size: 7.5 kB -npm notice unpacked size: 8.8 kB -npm notice shasum: fd0a95ccbb62ff833cd89cf4bb5296486c9a63aa -npm notice integrity: sha512-pMRH9mUXGZzeC[...]eJk8tQc1qSbRA== -npm notice total files: 5 -npm notice -npm-example-0.0.3.tgz \ No newline at end of file diff --git a/artifactory/utils/testdata/npm/npmPackOutputV7 b/artifactory/utils/testdata/npm/npmPackOutputV7 deleted file mode 100644 index 602a97930..000000000 --- a/artifactory/utils/testdata/npm/npmPackOutputV7 +++ /dev/null @@ -1,28 +0,0 @@ -> npm-example@ver0.0.3 prepack -> echo pre-helloworld - -pre-helloworld - -> npm-example@ver0.0.3 postpack -> echo post-helloworld - -post-helloworld -npm notice -npm notice 📦 npm-example@ver0.0.3 -npm notice === Tarball Contents === -npm notice 2.8kB README.md -npm notice 97B .jfrog/projects/npm.yaml -npm notice 181B helloworld.js -npm notice 3.5kB npm-example-ver0.0.3.tgz -npm notice 279B package.json -npm notice === Tarball Details === -npm notice name: npm-example -npm notice version: ver0.0.3 -npm notice filename: npm-example-ver0.0.3.tgz -npm notice package size: 5.5 kB -npm notice unpacked size: 6.8 kB -npm notice shasum: e3af25617b6c58c7f803d919949fc3d8993ce9dc -npm notice integrity: sha512-nTrTk6ph83jLL[...]ipNTJhWUci8Wg== -npm notice total files: 5 -npm notice -npm-example-ver0.0.3.tgz \ No newline at end of file diff --git a/tests/testdata/npm-workspaces/module1/package.json b/tests/testdata/npm-workspaces/module1/package.json new file mode 100644 index 000000000..c98ade642 --- /dev/null +++ b/tests/testdata/npm-workspaces/module1/package.json @@ -0,0 +1,12 @@ +{ + "name": "module1", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/tests/testdata/npm-workspaces/module2/package.json b/tests/testdata/npm-workspaces/module2/package.json new file mode 100644 index 000000000..d584476e5 --- /dev/null +++ b/tests/testdata/npm-workspaces/module2/package.json @@ -0,0 +1,12 @@ +{ + "name": "module2", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/tests/testdata/npm-workspaces/package.json b/tests/testdata/npm-workspaces/package.json new file mode 100644 index 000000000..59c936a79 --- /dev/null +++ b/tests/testdata/npm-workspaces/package.json @@ -0,0 +1,13 @@ +{ + "name": "npm-pack-test", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "workspaces": ["module1","module2"] +}