github.com/jfrog/jfrog-cli-core/v2@v2.51.0/artifactory/commands/npm/publish.go (about)

     1  package npm
     2  
     3  import (
     4  	"archive/tar"
     5  	"compress/gzip"
     6  	"errors"
     7  	"fmt"
     8  	ioutils "github.com/jfrog/gofrog/io"
     9  	"io"
    10  	"os"
    11  	"path/filepath"
    12  	"strings"
    13  
    14  	"github.com/jfrog/build-info-go/build"
    15  	biutils "github.com/jfrog/build-info-go/build/utils"
    16  	"github.com/jfrog/gofrog/version"
    17  	commandsutils "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/utils"
    18  	"github.com/jfrog/jfrog-cli-core/v2/artifactory/utils"
    19  	"github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/npm"
    20  	buildUtils "github.com/jfrog/jfrog-cli-core/v2/common/build"
    21  	"github.com/jfrog/jfrog-cli-core/v2/common/format"
    22  	"github.com/jfrog/jfrog-cli-core/v2/common/project"
    23  	"github.com/jfrog/jfrog-cli-core/v2/common/spec"
    24  	"github.com/jfrog/jfrog-cli-core/v2/utils/config"
    25  	"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
    26  	"github.com/jfrog/jfrog-client-go/artifactory/services"
    27  	specutils "github.com/jfrog/jfrog-client-go/artifactory/services/utils"
    28  	clientutils "github.com/jfrog/jfrog-client-go/utils"
    29  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
    30  	"github.com/jfrog/jfrog-client-go/utils/io/content"
    31  	"github.com/jfrog/jfrog-client-go/utils/log"
    32  )
    33  
    34  // The --pack-destination argument of npm pack was introduced in npm version 7.18.0.
    35  const packDestinationNpmMinVersion = "7.18.0"
    36  
    37  type NpmPublishCommandArgs struct {
    38  	CommonArgs
    39  	executablePath         string
    40  	workingDirectory       string
    41  	collectBuildInfo       bool
    42  	packedFilePaths        []string
    43  	packageInfo            *biutils.PackageInfo
    44  	publishPath            string
    45  	tarballProvided        bool
    46  	artifactsDetailsReader *content.ContentReader
    47  	xrayScan               bool
    48  	scanOutputFormat       format.OutputFormat
    49  }
    50  
    51  type NpmPublishCommand struct {
    52  	configFilePath  string
    53  	commandName     string
    54  	result          *commandsutils.Result
    55  	detailedSummary bool
    56  	npmVersion      *version.Version
    57  	*NpmPublishCommandArgs
    58  }
    59  
    60  func NewNpmPublishCommand() *NpmPublishCommand {
    61  	return &NpmPublishCommand{NpmPublishCommandArgs: NewNpmPublishCommandArgs(), commandName: "rt_npm_publish", result: new(commandsutils.Result)}
    62  }
    63  
    64  func NewNpmPublishCommandArgs() *NpmPublishCommandArgs {
    65  	return &NpmPublishCommandArgs{}
    66  }
    67  
    68  func (npc *NpmPublishCommand) ServerDetails() (*config.ServerDetails, error) {
    69  	return npc.serverDetails, nil
    70  }
    71  
    72  func (npc *NpmPublishCommand) SetConfigFilePath(configFilePath string) *NpmPublishCommand {
    73  	npc.configFilePath = configFilePath
    74  	return npc
    75  }
    76  
    77  func (npc *NpmPublishCommand) SetArgs(args []string) *NpmPublishCommand {
    78  	npc.NpmPublishCommandArgs.npmArgs = args
    79  	return npc
    80  }
    81  
    82  func (npc *NpmPublishCommand) SetDetailedSummary(detailedSummary bool) *NpmPublishCommand {
    83  	npc.detailedSummary = detailedSummary
    84  	return npc
    85  }
    86  
    87  func (npc *NpmPublishCommand) SetXrayScan(xrayScan bool) *NpmPublishCommand {
    88  	npc.xrayScan = xrayScan
    89  	return npc
    90  }
    91  
    92  func (npc *NpmPublishCommand) GetXrayScan() bool {
    93  	return npc.xrayScan
    94  }
    95  
    96  func (npc *NpmPublishCommand) SetScanOutputFormat(format format.OutputFormat) *NpmPublishCommand {
    97  	npc.scanOutputFormat = format
    98  	return npc
    99  }
   100  
   101  func (npc *NpmPublishCommand) Result() *commandsutils.Result {
   102  	return npc.result
   103  }
   104  
   105  func (npc *NpmPublishCommand) IsDetailedSummary() bool {
   106  	return npc.detailedSummary
   107  }
   108  
   109  func (npc *NpmPublishCommand) Init() error {
   110  	var err error
   111  	npc.npmVersion, npc.executablePath, err = biutils.GetNpmVersionAndExecPath(log.Logger)
   112  	if err != nil {
   113  		return err
   114  	}
   115  	detailedSummary, xrayScan, scanOutputFormat, filteredNpmArgs, buildConfiguration, err := commandsutils.ExtractNpmOptionsFromArgs(npc.NpmPublishCommandArgs.npmArgs)
   116  	if err != nil {
   117  		return err
   118  	}
   119  	if npc.configFilePath != "" {
   120  		// Read config file.
   121  		log.Debug("Preparing to read the config file", npc.configFilePath)
   122  		vConfig, err := project.ReadConfigFile(npc.configFilePath, project.YAML)
   123  		if err != nil {
   124  			return err
   125  		}
   126  		deployerParams, err := project.GetRepoConfigByPrefix(npc.configFilePath, project.ProjectConfigDeployerPrefix, vConfig)
   127  		if err != nil {
   128  			return err
   129  		}
   130  		rtDetails, err := deployerParams.ServerDetails()
   131  		if err != nil {
   132  			return errorutils.CheckError(err)
   133  		}
   134  		npc.SetBuildConfiguration(buildConfiguration).SetRepo(deployerParams.TargetRepo()).SetNpmArgs(filteredNpmArgs).SetServerDetails(rtDetails)
   135  	}
   136  	npc.SetDetailedSummary(detailedSummary).SetXrayScan(xrayScan).SetScanOutputFormat(scanOutputFormat)
   137  	return nil
   138  }
   139  
   140  func (npc *NpmPublishCommand) Run() (err error) {
   141  	log.Info("Running npm Publish")
   142  	err = npc.preparePrerequisites()
   143  	if err != nil {
   144  		return err
   145  	}
   146  
   147  	var npmBuild *build.Build
   148  	var buildName, buildNumber, project string
   149  	if npc.collectBuildInfo {
   150  		buildName, err = npc.buildConfiguration.GetBuildName()
   151  		if err != nil {
   152  			return err
   153  		}
   154  		buildNumber, err = npc.buildConfiguration.GetBuildNumber()
   155  		if err != nil {
   156  			return err
   157  		}
   158  		project = npc.buildConfiguration.GetProject()
   159  		buildInfoService := buildUtils.CreateBuildInfoService()
   160  		npmBuild, err = buildInfoService.GetOrCreateBuildWithProject(buildName, buildNumber, project)
   161  		if err != nil {
   162  			return errorutils.CheckError(err)
   163  		}
   164  	}
   165  
   166  	if !npc.tarballProvided {
   167  		if err := npc.pack(); err != nil {
   168  			return err
   169  		}
   170  	}
   171  
   172  	if err := npc.publish(); err != nil {
   173  		if npc.tarballProvided {
   174  			return err
   175  		}
   176  		// We should delete the tarball we created
   177  		return errors.Join(err, deleteCreatedTarball(npc.packedFilePaths))
   178  	}
   179  
   180  	if !npc.tarballProvided {
   181  		if err := deleteCreatedTarball(npc.packedFilePaths); err != nil {
   182  			return err
   183  		}
   184  	}
   185  
   186  	if !npc.collectBuildInfo {
   187  		log.Info("npm publish finished successfully.")
   188  		return nil
   189  	}
   190  
   191  	npmModule, err := npmBuild.AddNpmModule("")
   192  	if err != nil {
   193  		return errorutils.CheckError(err)
   194  	}
   195  	if npc.buildConfiguration.GetModule() != "" {
   196  		npmModule.SetName(npc.buildConfiguration.GetModule())
   197  	}
   198  	buildArtifacts, err := specutils.ConvertArtifactsDetailsToBuildInfoArtifacts(npc.artifactsDetailsReader)
   199  	if err != nil {
   200  		return err
   201  	}
   202  	defer ioutils.Close(npc.artifactsDetailsReader, &err)
   203  	err = npmModule.AddArtifacts(buildArtifacts...)
   204  	if err != nil {
   205  		return errorutils.CheckError(err)
   206  	}
   207  
   208  	log.Info("npm publish finished successfully.")
   209  	return nil
   210  }
   211  
   212  func (npc *NpmPublishCommand) CommandName() string {
   213  	return npc.commandName
   214  }
   215  
   216  func (npc *NpmPublishCommand) preparePrerequisites() error {
   217  	npc.packedFilePaths = make([]string, 0)
   218  	currentDir, err := os.Getwd()
   219  	if err != nil {
   220  		return errorutils.CheckError(err)
   221  	}
   222  
   223  	currentDir, err = filepath.Abs(currentDir)
   224  	if err != nil {
   225  		return errorutils.CheckError(err)
   226  	}
   227  
   228  	npc.workingDirectory = currentDir
   229  	log.Debug("Working directory set to:", npc.workingDirectory)
   230  	npc.collectBuildInfo, err = npc.buildConfiguration.IsCollectBuildInfo()
   231  	if err != nil {
   232  		return err
   233  	}
   234  	if err = npc.setPublishPath(); err != nil {
   235  		return err
   236  	}
   237  
   238  	artDetails, err := npc.serverDetails.CreateArtAuthConfig()
   239  	if err != nil {
   240  		return err
   241  	}
   242  
   243  	if err = utils.ValidateRepoExists(npc.repo, artDetails); err != nil {
   244  		return err
   245  	}
   246  
   247  	return npc.setPackageInfo()
   248  }
   249  
   250  func (npc *NpmPublishCommand) pack() error {
   251  	log.Debug("Creating npm package.")
   252  	packedFileNames, err := npm.Pack(npc.npmArgs, npc.executablePath)
   253  	if err != nil {
   254  		return err
   255  	}
   256  
   257  	tarballDir, err := npc.getTarballDir()
   258  	if err != nil {
   259  		return err
   260  	}
   261  
   262  	for _, packageFileName := range packedFileNames {
   263  		npc.packedFilePaths = append(npc.packedFilePaths, filepath.Join(tarballDir, packageFileName))
   264  	}
   265  
   266  	return nil
   267  }
   268  
   269  func (npc *NpmPublishCommand) getTarballDir() (string, error) {
   270  	if npc.npmVersion == nil || npc.npmVersion.Compare(packDestinationNpmMinVersion) > 0 {
   271  		return npc.workingDirectory, nil
   272  	}
   273  
   274  	// Extract pack destination argument from the args.
   275  	flagIndex, _, dest, err := coreutils.FindFlag("--pack-destination", npc.NpmPublishCommandArgs.npmArgs)
   276  	if err != nil || flagIndex == -1 {
   277  		return npc.workingDirectory, err
   278  	}
   279  	return dest, nil
   280  }
   281  
   282  func (npc *NpmPublishCommand) publish() (err error) {
   283  	for _, packedFilePath := range npc.packedFilePaths {
   284  		log.Debug("Deploying npm package.")
   285  		if err = npc.readPackageInfoFromTarball(packedFilePath); err != nil {
   286  			return
   287  		}
   288  		target := fmt.Sprintf("%s/%s", npc.repo, npc.packageInfo.GetDeployPath())
   289  
   290  		// If requested, perform a Xray binary scan before deployment. If a FailBuildError is returned, skip the deployment.
   291  		if npc.xrayScan {
   292  			fileSpec := spec.NewBuilder().
   293  				Pattern(packedFilePath).
   294  				Target(npc.repo + "/").
   295  				BuildSpec()
   296  			if err = commandsutils.ConditionalUploadScanFunc(npc.serverDetails, fileSpec, 1, npc.scanOutputFormat); err != nil {
   297  				return
   298  			}
   299  		}
   300  		err = errors.Join(err, npc.doDeploy(target, npc.serverDetails, packedFilePath))
   301  	}
   302  	return
   303  }
   304  
   305  func (npc *NpmPublishCommand) doDeploy(target string, artDetails *config.ServerDetails, packedFilePath string) error {
   306  	servicesManager, err := utils.CreateServiceManager(artDetails, -1, 0, false)
   307  	if err != nil {
   308  		return err
   309  	}
   310  	up := services.NewUploadParams()
   311  	up.CommonParams = &specutils.CommonParams{Pattern: packedFilePath, Target: target}
   312  	var totalFailed int
   313  	if npc.collectBuildInfo || npc.detailedSummary {
   314  		if npc.collectBuildInfo {
   315  			buildName, err := npc.buildConfiguration.GetBuildName()
   316  			if err != nil {
   317  				return err
   318  			}
   319  			buildNumber, err := npc.buildConfiguration.GetBuildNumber()
   320  			if err != nil {
   321  				return err
   322  			}
   323  			err = buildUtils.SaveBuildGeneralDetails(buildName, buildNumber, npc.buildConfiguration.GetProject())
   324  			if err != nil {
   325  				return err
   326  			}
   327  			up.BuildProps, err = buildUtils.CreateBuildProperties(buildName, buildNumber, npc.buildConfiguration.GetProject())
   328  			if err != nil {
   329  				return err
   330  			}
   331  		}
   332  		summary, err := servicesManager.UploadFilesWithSummary(up)
   333  		if err != nil {
   334  			return err
   335  		}
   336  		totalFailed = summary.TotalFailed
   337  		if npc.collectBuildInfo {
   338  			npc.artifactsDetailsReader = summary.ArtifactsDetailsReader
   339  		} else {
   340  			err = summary.ArtifactsDetailsReader.Close()
   341  			if err != nil {
   342  				return err
   343  			}
   344  		}
   345  		if npc.detailedSummary {
   346  			if err = npc.setDetailedSummary(summary); err != nil {
   347  				return err
   348  			}
   349  		} else {
   350  			if err = summary.TransferDetailsReader.Close(); err != nil {
   351  				return err
   352  			}
   353  		}
   354  	} else {
   355  		_, totalFailed, err = servicesManager.UploadFiles(up)
   356  		if err != nil {
   357  			return err
   358  		}
   359  	}
   360  
   361  	// We are deploying only one Artifact which have to be deployed, in case of failure we should fail
   362  	if totalFailed > 0 {
   363  		return errorutils.CheckErrorf("Failed to upload the npm package to Artifactory. See Artifactory logs for more details.")
   364  	}
   365  	return nil
   366  }
   367  
   368  func (npc *NpmPublishCommand) setDetailedSummary(summary *specutils.OperationSummary) (err error) {
   369  	npc.result.SetFailCount(npc.result.FailCount() + summary.TotalFailed)
   370  	npc.result.SetSuccessCount(npc.result.SuccessCount() + summary.TotalSucceeded)
   371  	if npc.result.Reader() == nil {
   372  		npc.result.SetReader(summary.TransferDetailsReader)
   373  	} else {
   374  		if err = npc.appendReader(summary); err != nil {
   375  			return
   376  		}
   377  	}
   378  	return
   379  }
   380  
   381  func (npc *NpmPublishCommand) appendReader(summary *specutils.OperationSummary) error {
   382  	readersSlice := []*content.ContentReader{npc.result.Reader(), summary.TransferDetailsReader}
   383  	reader, err := content.MergeReaders(readersSlice, content.DefaultKey)
   384  	if err != nil {
   385  		return err
   386  	}
   387  	npc.result.SetReader(reader)
   388  	return nil
   389  }
   390  
   391  func (npc *NpmPublishCommand) setPublishPath() error {
   392  	log.Debug("Reading Package Json.")
   393  
   394  	npc.publishPath = npc.workingDirectory
   395  	if len(npc.npmArgs) > 0 && !strings.HasPrefix(strings.TrimSpace(npc.npmArgs[0]), "-") {
   396  		path := strings.TrimSpace(npc.npmArgs[0])
   397  		path = clientutils.ReplaceTildeWithUserHome(path)
   398  
   399  		if filepath.IsAbs(path) {
   400  			npc.publishPath = path
   401  		} else {
   402  			npc.publishPath = filepath.Join(npc.workingDirectory, path)
   403  		}
   404  	}
   405  	return nil
   406  }
   407  
   408  func (npc *NpmPublishCommand) setPackageInfo() error {
   409  	log.Debug("Setting Package Info.")
   410  	fileInfo, err := os.Stat(npc.publishPath)
   411  	if err != nil {
   412  		return errorutils.CheckError(err)
   413  	}
   414  
   415  	if fileInfo.IsDir() {
   416  		npc.packageInfo, err = biutils.ReadPackageInfoFromPackageJsonIfExists(npc.publishPath, npc.npmVersion)
   417  		return err
   418  	}
   419  	log.Debug("The provided path is not a directory, we assume this is a compressed npm package")
   420  	npc.tarballProvided = true
   421  	// Sets the location of the provided tarball
   422  	npc.packedFilePaths = []string{npc.publishPath}
   423  	return npc.readPackageInfoFromTarball(npc.publishPath)
   424  }
   425  
   426  func (npc *NpmPublishCommand) readPackageInfoFromTarball(packedFilePath string) (err error) {
   427  	log.Debug("Extracting info from npm package:", packedFilePath)
   428  	tarball, err := os.Open(packedFilePath)
   429  	if err != nil {
   430  		return errorutils.CheckError(err)
   431  	}
   432  	defer func() {
   433  		err = errors.Join(err, errorutils.CheckError(tarball.Close()))
   434  	}()
   435  	gZipReader, err := gzip.NewReader(tarball)
   436  	if err != nil {
   437  		return errorutils.CheckError(err)
   438  	}
   439  
   440  	tarReader := tar.NewReader(gZipReader)
   441  	for {
   442  		hdr, err := tarReader.Next()
   443  		if err != nil {
   444  			if err == io.EOF {
   445  				return errorutils.CheckErrorf("Could not find 'package.json' in the compressed npm package: " + packedFilePath)
   446  			}
   447  			return errorutils.CheckError(err)
   448  		}
   449  		if hdr.Name == "package/package.json" {
   450  			packageJson, err := io.ReadAll(tarReader)
   451  			if err != nil {
   452  				return errorutils.CheckError(err)
   453  			}
   454  			npc.packageInfo, err = biutils.ReadPackageInfo(packageJson, npc.npmVersion)
   455  			return err
   456  		}
   457  	}
   458  }
   459  
   460  func deleteCreatedTarball(packedFilesPath []string) error {
   461  	for _, packedFilePath := range packedFilesPath {
   462  		if err := os.Remove(packedFilePath); err != nil {
   463  			return errorutils.CheckError(err)
   464  		}
   465  		log.Debug("Successfully deleted the created npm package:", packedFilePath)
   466  	}
   467  	return nil
   468  }