2020-05-20 13:41:23 +02:00
package npm
import (
"bytes"
2020-06-18 17:30:17 +02:00
"encoding/json"
"fmt"
2020-05-20 13:41:23 +02:00
"io"
2020-06-18 17:30:17 +02:00
"path/filepath"
2020-05-20 13:41:23 +02:00
"strings"
2021-03-04 11:16:59 +02:00
"github.com/SAP/jenkins-library/pkg/command"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/piperutils"
2020-05-20 13:41:23 +02:00
)
2022-08-01 13:38:49 +02:00
const (
2023-07-11 16:18:20 +02:00
npmBomFilename = "bom-npm.xml"
cycloneDxNpmPackageVersion = "@cyclonedx/cyclonedx-npm@1.11.0"
cycloneDxBomPackageVersion = "@cyclonedx/bom@^3.10.6"
cycloneDxNpmInstallationFolder = "./tmp" // This folder is also added to npmignore in publish.go.Any changes to this folder needs a change in publish.go publish()
cycloneDxSchemaVersion = "1.4"
2022-08-01 13:38:49 +02:00
)
2020-06-18 17:30:17 +02:00
// Execute struct holds utils to enable mocking and common parameters
type Execute struct {
Utils Utils
Options ExecutorOptions
}
// Executor interface to enable mocking for testing
type Executor interface {
FindPackageJSONFiles ( ) [ ] string
2020-07-16 17:16:55 +02:00
FindPackageJSONFilesWithExcludes ( excludeList [ ] string ) ( [ ] string , error )
2020-06-18 17:30:17 +02:00
FindPackageJSONFilesWithScript ( packageJSONFiles [ ] string , script string ) ( [ ] string , error )
2020-11-04 17:20:26 +02:00
RunScriptsInAllPackages ( runScripts [ ] string , runOptions [ ] string , scriptOptions [ ] string , virtualFrameBuffer bool , excludeList [ ] string , packagesList [ ] string ) error
2020-06-18 17:30:17 +02:00
InstallAllDependencies ( packageJSONFiles [ ] string ) error
2022-01-25 10:52:22 +02:00
PublishAllPackages ( packageJSONFiles [ ] string , registry , username , password string , packBeforePublish bool ) error
2020-06-18 17:30:17 +02:00
SetNpmRegistries ( ) error
2021-03-04 11:16:59 +02:00
CreateBOM ( packageJSONFiles [ ] string ) error
2020-06-18 17:30:17 +02:00
}
// ExecutorOptions holds common parameters for functions of Executor
type ExecutorOptions struct {
2020-05-20 13:41:23 +02:00
DefaultNpmRegistry string
2020-06-18 17:30:17 +02:00
ExecRunner ExecRunner
}
// NewExecutor instantiates Execute struct and sets executeOptions
func NewExecutor ( executorOptions ExecutorOptions ) Executor {
utils := utilsBundle { Files : & piperutils . Files { } , execRunner : executorOptions . ExecRunner }
return & Execute {
Utils : & utils ,
Options : executorOptions ,
}
2020-05-20 13:41:23 +02:00
}
2020-06-18 17:30:17 +02:00
// ExecRunner interface to enable mocking for testing
type ExecRunner interface {
SetEnv ( e [ ] string )
2020-05-20 13:41:23 +02:00
Stdout ( out io . Writer )
2020-06-18 17:30:17 +02:00
Stderr ( out io . Writer )
2020-05-20 13:41:23 +02:00
RunExecutable ( executable string , params ... string ) error
2020-06-18 17:30:17 +02:00
RunExecutableInBackground ( executable string , params ... string ) ( command . Execution , error )
}
// Utils interface for mocking
type Utils interface {
2022-03-02 15:06:51 +02:00
piperutils . FileUtils
2020-06-18 17:30:17 +02:00
GetExecRunner ( ) ExecRunner
}
type utilsBundle struct {
* piperutils . Files
execRunner ExecRunner
}
// GetExecRunner returns an execRunner if it's not yet initialized
func ( u * utilsBundle ) GetExecRunner ( ) ExecRunner {
if u . execRunner == nil {
2022-08-05 13:08:19 +02:00
u . execRunner = & command . Command {
StepName : "npmExecuteScripts" ,
}
2020-06-18 17:30:17 +02:00
u . execRunner . Stdout ( log . Writer ( ) )
u . execRunner . Stderr ( log . Writer ( ) )
}
return u . execRunner
2020-05-20 13:41:23 +02:00
}
// SetNpmRegistries configures the given npm registries.
// CAUTION: This will change the npm configuration in the user's home directory.
2020-06-18 17:30:17 +02:00
func ( exec * Execute ) SetNpmRegistries ( ) error {
execRunner := exec . Utils . GetExecRunner ( )
2020-05-20 13:41:23 +02:00
const npmRegistry = "registry"
2020-08-11 15:58:39 +02:00
var buffer bytes . Buffer
execRunner . Stdout ( & buffer )
err := execRunner . RunExecutable ( "npm" , "config" , "get" , npmRegistry )
execRunner . Stdout ( log . Writer ( ) )
if err != nil {
return err
}
preConfiguredRegistry := buffer . String ( )
2020-05-20 13:41:23 +02:00
2020-08-11 15:58:39 +02:00
if registryIsNonEmpty ( preConfiguredRegistry ) {
log . Entry ( ) . Info ( "Discovered pre-configured npm registry " + npmRegistry + " with value " + preConfiguredRegistry )
}
2020-05-20 13:41:23 +02:00
2020-08-11 15:58:39 +02:00
if exec . Options . DefaultNpmRegistry != "" && registryRequiresConfiguration ( preConfiguredRegistry , "https://registry.npmjs.org" ) {
log . Entry ( ) . Info ( "npm registry " + npmRegistry + " was not configured, setting it to " + exec . Options . DefaultNpmRegistry )
err = execRunner . RunExecutable ( "npm" , "config" , "set" , npmRegistry , exec . Options . DefaultNpmRegistry )
if err != nil {
return err
2020-05-20 13:41:23 +02:00
}
}
2020-08-11 15:58:39 +02:00
2020-05-20 13:41:23 +02:00
return nil
}
func registryIsNonEmpty ( preConfiguredRegistry string ) bool {
return ! strings . HasPrefix ( preConfiguredRegistry , "undefined" ) && len ( preConfiguredRegistry ) > 0
}
func registryRequiresConfiguration ( preConfiguredRegistry , url string ) bool {
return strings . HasPrefix ( preConfiguredRegistry , "undefined" ) || strings . HasPrefix ( preConfiguredRegistry , url )
}
2020-06-18 17:30:17 +02:00
// RunScriptsInAllPackages runs all scripts defined in ExecuteOptions.RunScripts
2020-11-04 17:20:26 +02:00
func ( exec * Execute ) RunScriptsInAllPackages ( runScripts [ ] string , runOptions [ ] string , scriptOptions [ ] string , virtualFrameBuffer bool , excludeList [ ] string , packagesList [ ] string ) error {
var packageJSONFiles [ ] string
var err error
if len ( packagesList ) > 0 {
packageJSONFiles = packagesList
} else {
packageJSONFiles , err = exec . FindPackageJSONFilesWithExcludes ( excludeList )
if err != nil {
return err
}
2020-07-16 17:16:55 +02:00
}
2020-06-18 17:30:17 +02:00
execRunner := exec . Utils . GetExecRunner ( )
if virtualFrameBuffer {
cmd , err := execRunner . RunExecutableInBackground ( "Xvfb" , "-ac" , ":99" , "-screen" , "0" , "1280x1024x16" )
if err != nil {
return fmt . Errorf ( "failed to start virtual frame buffer%w" , err )
}
defer cmd . Kill ( )
execRunner . SetEnv ( [ ] string { "DISPLAY=:99" } )
}
for _ , script := range runScripts {
packagesWithScript , err := exec . FindPackageJSONFilesWithScript ( packageJSONFiles , script )
if err != nil {
return err
}
if len ( packagesWithScript ) == 0 {
log . Entry ( ) . Warnf ( "could not find any package.json file with script " + script )
continue
}
for _ , packageJSON := range packagesWithScript {
2020-07-09 14:57:41 +02:00
err = exec . executeScript ( packageJSON , script , runOptions , scriptOptions )
2020-06-18 17:30:17 +02:00
if err != nil {
return err
}
}
}
return nil
}
2020-07-09 14:57:41 +02:00
func ( exec * Execute ) executeScript ( packageJSON string , script string , runOptions [ ] string , scriptOptions [ ] string ) error {
2020-06-18 17:30:17 +02:00
execRunner := exec . Utils . GetExecRunner ( )
oldWorkingDirectory , err := exec . Utils . Getwd ( )
if err != nil {
return fmt . Errorf ( "failed to get current working directory before executing npm scripts: %w" , err )
}
dir := filepath . Dir ( packageJSON )
err = exec . Utils . Chdir ( dir )
if err != nil {
return fmt . Errorf ( "failed to change into directory for executing script: %w" , err )
}
// set in each directory to respect existing config in rc fileUtils
err = exec . SetNpmRegistries ( )
if err != nil {
return err
}
log . Entry ( ) . WithField ( "WorkingDirectory" , dir ) . Info ( "run-script " + script )
npmRunArgs := [ ] string { "run" , script }
if len ( runOptions ) > 0 {
npmRunArgs = append ( npmRunArgs , runOptions ... )
}
2020-07-09 14:57:41 +02:00
if len ( scriptOptions ) > 0 {
npmRunArgs = append ( npmRunArgs , "--" )
npmRunArgs = append ( npmRunArgs , scriptOptions ... )
}
2020-06-18 17:30:17 +02:00
err = execRunner . RunExecutable ( "npm" , npmRunArgs ... )
if err != nil {
return fmt . Errorf ( "failed to run npm script %s: %w" , script , err )
}
err = exec . Utils . Chdir ( oldWorkingDirectory )
if err != nil {
return fmt . Errorf ( "failed to change back into original directory: %w" , err )
}
return nil
}
2020-07-16 17:16:55 +02:00
// FindPackageJSONFiles returns a list of all package.json files of the project excluding node_modules and gen/ directories
2020-06-18 17:30:17 +02:00
func ( exec * Execute ) FindPackageJSONFiles ( ) [ ] string {
2020-07-16 17:16:55 +02:00
packageJSONFiles , _ := exec . FindPackageJSONFilesWithExcludes ( [ ] string { } )
return packageJSONFiles
}
// FindPackageJSONFilesWithExcludes returns a list of all package.json files of the project excluding node_modules, gen/ and directories/patterns defined by excludeList
func ( exec * Execute ) FindPackageJSONFilesWithExcludes ( excludeList [ ] string ) ( [ ] string , error ) {
2020-06-18 17:30:17 +02:00
unfilteredListOfPackageJSONFiles , _ := exec . Utils . Glob ( "**/package.json" )
2020-07-16 17:16:55 +02:00
nodeModulesExclude := "**/node_modules/**"
genExclude := "**/gen/**"
excludeList = append ( excludeList , nodeModulesExclude , genExclude )
2020-11-26 12:45:53 +02:00
packageJSONFiles , err := piperutils . ExcludeFiles ( unfilteredListOfPackageJSONFiles , excludeList )
if err != nil {
return nil , err
}
2020-06-18 17:30:17 +02:00
2020-11-26 12:45:53 +02:00
for _ , file := range packageJSONFiles {
2020-06-18 17:30:17 +02:00
log . Entry ( ) . Info ( "Discovered package.json file " + file )
}
2020-07-16 17:16:55 +02:00
return packageJSONFiles , nil
2020-06-18 17:30:17 +02:00
}
// FindPackageJSONFilesWithScript returns a list of package.json fileUtils that contain the script
func ( exec * Execute ) FindPackageJSONFilesWithScript ( packageJSONFiles [ ] string , script string ) ( [ ] string , error ) {
var packagesWithScript [ ] string
for _ , file := range packageJSONFiles {
var packageJSON map [ string ] interface { }
packageRaw , err := exec . Utils . FileRead ( file )
if err != nil {
return nil , fmt . Errorf ( "failed to read %s to check for existence of %s script: %w" , file , script , err )
}
err = json . Unmarshal ( packageRaw , & packageJSON )
if err != nil {
return nil , fmt . Errorf ( "failed to unmarshal %s to check for existence of %s script: %w" , file , script , err )
}
scripts , ok := packageJSON [ "scripts" ] . ( map [ string ] interface { } )
if ok {
_ , ok := scripts [ script ] . ( string )
if ok {
packagesWithScript = append ( packagesWithScript , file )
log . Entry ( ) . Info ( "Discovered " + script + " script in " + file )
}
}
}
return packagesWithScript , nil
}
// InstallAllDependencies executes npm or yarn Install for all package.json fileUtils defined in packageJSONFiles
func ( exec * Execute ) InstallAllDependencies ( packageJSONFiles [ ] string ) error {
for _ , packageJSON := range packageJSONFiles {
2020-11-04 17:20:26 +02:00
fileExists , err := exec . Utils . FileExists ( packageJSON )
if err != nil {
return fmt . Errorf ( "cannot check if '%s' exists: %w" , packageJSON , err )
}
if ! fileExists {
return fmt . Errorf ( "package.json file '%s' not found: %w" , packageJSON , err )
}
err = exec . install ( packageJSON )
2020-06-18 17:30:17 +02:00
if err != nil {
return err
}
}
return nil
}
// install executes npm or yarn Install for package.json
func ( exec * Execute ) install ( packageJSON string ) error {
execRunner := exec . Utils . GetExecRunner ( )
oldWorkingDirectory , err := exec . Utils . Getwd ( )
if err != nil {
return fmt . Errorf ( "failed to get current working directory before executing npm scripts: %w" , err )
}
dir := filepath . Dir ( packageJSON )
err = exec . Utils . Chdir ( dir )
if err != nil {
return fmt . Errorf ( "failed to change into directory for executing script: %w" , err )
}
err = exec . SetNpmRegistries ( )
if err != nil {
return err
}
packageLockExists , yarnLockExists , err := exec . checkIfLockFilesExist ( )
if err != nil {
return err
}
log . Entry ( ) . WithField ( "WorkingDirectory" , dir ) . Info ( "Running Install" )
if packageLockExists {
err = execRunner . RunExecutable ( "npm" , "ci" )
if err != nil {
return err
}
} else if yarnLockExists {
err = execRunner . RunExecutable ( "yarn" , "install" , "--frozen-lockfile" )
if err != nil {
return err
}
} else {
log . Entry ( ) . Warn ( "No package lock file found. " +
"It is recommended to create a `package-lock.json` file by running `npm Install` locally." +
" Add this file to your version control. " +
"By doing so, the builds of your application become more reliable." )
err = execRunner . RunExecutable ( "npm" , "install" )
if err != nil {
return err
}
}
err = exec . Utils . Chdir ( oldWorkingDirectory )
if err != nil {
return fmt . Errorf ( "failed to change back into original directory: %w" , err )
}
return nil
}
// checkIfLockFilesExist checks if yarn/package lock fileUtils exist
func ( exec * Execute ) checkIfLockFilesExist ( ) ( bool , bool , error ) {
packageLockExists , err := exec . Utils . FileExists ( "package-lock.json" )
if err != nil {
return false , false , err
}
yarnLockExists , err := exec . Utils . FileExists ( "yarn.lock" )
if err != nil {
return false , false , err
}
return packageLockExists , yarnLockExists , nil
}
2021-03-04 11:16:59 +02:00
// CreateBOM generates BOM file using CycloneDX from all package.json files
func ( exec * Execute ) CreateBOM ( packageJSONFiles [ ] string ) error {
2023-07-11 16:18:20 +02:00
// Install cyclonedx-npm in a new folder (to avoid extraneous errors) and generate BOM
cycloneDxNpmInstallParams := [ ] string { "install" , "--no-save" , cycloneDxNpmPackageVersion , "--prefix" , cycloneDxNpmInstallationFolder }
cycloneDxNpmRunParams := [ ] string { "--output-format" , "XML" , "--spec-version" , cycloneDxSchemaVersion , "--output-file" }
// Install cyclonedx/bom with --nosave and generate BOM.
cycloneDxBomInstallParams := [ ] string { "install" , cycloneDxBomPackageVersion , "--no-save" }
cycloneDxBomRunParams := [ ] string { "cyclonedx-bom" , "--output" }
// Attempt#1, generate BOM via cyclonedx-npm
err := exec . createBOMWithParams ( cycloneDxNpmInstallParams , cycloneDxNpmRunParams , packageJSONFiles , false )
if err != nil {
log . Entry ( ) . Infof ( "Failed to generate BOM CycloneDX BOM with cyclonedx-npm ,fallback to cyclonedx/bom" )
// Attempt #2, generate BOM via cyclonedx/bom@^3.10.6
err = exec . createBOMWithParams ( cycloneDxBomInstallParams , cycloneDxBomRunParams , packageJSONFiles , true )
if err != nil {
log . Entry ( ) . Infof ( "Failed to generate BOM CycloneDX BOM with fallback package cyclonedx/bom " )
return err
}
}
return nil
}
// Facilitates BOM generation with different packages
func ( exec * Execute ) createBOMWithParams ( packageInstallParams [ ] string , packageRunParams [ ] string , packageJSONFiles [ ] string , fallback bool ) error {
2021-03-04 11:16:59 +02:00
execRunner := exec . Utils . GetExecRunner ( )
2023-07-11 16:18:20 +02:00
// Install package
err := execRunner . RunExecutable ( "npm" , packageInstallParams ... )
2021-03-04 11:16:59 +02:00
if err != nil {
2023-07-11 16:18:20 +02:00
return fmt . Errorf ( "failed to install CycloneDX BOM %w" , err )
2021-03-04 11:16:59 +02:00
}
2022-08-01 13:38:49 +02:00
2023-07-11 16:18:20 +02:00
// Run package for all package JSON files
2021-03-04 11:16:59 +02:00
if len ( packageJSONFiles ) > 0 {
2022-02-07 15:46:03 +02:00
for _ , packageJSONFile := range packageJSONFiles {
2021-03-04 11:16:59 +02:00
path := filepath . Dir ( packageJSONFile )
2023-07-11 16:18:20 +02:00
executable := "npx"
params := append ( packageRunParams , filepath . Join ( path , npmBomFilename ) )
//Below code needed as to adjust according to needs of cyclonedx-npm and fallback cyclonedx/bom@^3.10.6
if ! fallback {
params = append ( params , packageJSONFile )
executable = cycloneDxNpmInstallationFolder + "/node_modules/.bin/cyclonedx-npm"
} else {
params = append ( params , path )
2021-03-04 11:16:59 +02:00
}
2023-07-11 16:18:20 +02:00
err := execRunner . RunExecutable ( executable , params ... )
2021-03-04 11:16:59 +02:00
if err != nil {
2023-07-11 16:18:20 +02:00
return fmt . Errorf ( "failed to generate CycloneDX BOM :%w" , err )
2021-03-04 11:16:59 +02:00
}
}
}
2023-07-11 16:18:20 +02:00
2021-03-04 11:16:59 +02:00
return nil
}