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 }