github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/cmd/npmExecuteLint.go (about)

     1  package cmd
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"net/http"
     7  	"os"
     8  	"path/filepath"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"github.com/SAP/jenkins-library/pkg/command"
    13  	piperhttp "github.com/SAP/jenkins-library/pkg/http"
    14  	"github.com/SAP/jenkins-library/pkg/log"
    15  	"github.com/SAP/jenkins-library/pkg/npm"
    16  	"github.com/SAP/jenkins-library/pkg/piperutils"
    17  	"github.com/SAP/jenkins-library/pkg/telemetry"
    18  )
    19  
    20  type lintUtils interface {
    21  	Glob(pattern string) (matches []string, err error)
    22  
    23  	getExecRunner() command.ExecRunner
    24  	getGeneralPurposeConfig(configURL string)
    25  }
    26  
    27  type lintUtilsBundle struct {
    28  	*piperutils.Files
    29  	execRunner *command.Command
    30  	client     *piperhttp.Client
    31  }
    32  
    33  func newLintUtilsBundle() *lintUtilsBundle {
    34  	return &lintUtilsBundle{
    35  		Files:  &piperutils.Files{},
    36  		client: &piperhttp.Client{},
    37  	}
    38  }
    39  
    40  func (u *lintUtilsBundle) getExecRunner() command.ExecRunner {
    41  	if u.execRunner == nil {
    42  		u.execRunner = &command.Command{}
    43  		u.execRunner.Stdout(log.Writer())
    44  		u.execRunner.Stderr(log.Writer())
    45  	}
    46  	return u.execRunner
    47  }
    48  
    49  func (u *lintUtilsBundle) getGeneralPurposeConfig(configURL string) {
    50  	response, err := u.client.SendRequest(http.MethodGet, configURL, nil, nil, nil)
    51  	if err != nil {
    52  		log.Entry().Warnf("failed to download general purpose configuration: %v", err)
    53  		return
    54  	}
    55  
    56  	defer response.Body.Close()
    57  
    58  	content, err := io.ReadAll(response.Body)
    59  	if err != nil {
    60  		log.Entry().Warnf("error while reading the general purpose configuration: %v", err)
    61  		return
    62  	}
    63  
    64  	err = u.FileWrite(filepath.Join(".pipeline", ".eslintrc.json"), content, os.ModePerm)
    65  	if err != nil {
    66  		log.Entry().Warnf("failed to write .eslintrc.json file to .pipeline/: %v", err)
    67  	}
    68  }
    69  
    70  func npmExecuteLint(config npmExecuteLintOptions, telemetryData *telemetry.CustomData) {
    71  	utils := newLintUtilsBundle()
    72  	npmExecutorOptions := npm.ExecutorOptions{DefaultNpmRegistry: config.DefaultNpmRegistry, ExecRunner: utils.getExecRunner()}
    73  	npmExecutor := npm.NewExecutor(npmExecutorOptions)
    74  
    75  	err := runNpmExecuteLint(npmExecutor, utils, &config)
    76  	if err != nil {
    77  		log.Entry().WithError(err).Fatal("step execution failed")
    78  	}
    79  }
    80  
    81  func runNpmExecuteLint(npmExecutor npm.Executor, utils lintUtils, config *npmExecuteLintOptions) error {
    82  	if len(config.RunScript) == 0 {
    83  		return fmt.Errorf("runScript is not allowed to be empty!")
    84  	}
    85  
    86  	packageJSONFiles := npmExecutor.FindPackageJSONFiles()
    87  	packagesWithLintScript, _ := npmExecutor.FindPackageJSONFilesWithScript(packageJSONFiles, config.RunScript)
    88  
    89  	if len(packagesWithLintScript) > 0 {
    90  		if config.Install {
    91  			err := npmExecutor.InstallAllDependencies(packagesWithLintScript)
    92  			if err != nil {
    93  				return err
    94  			}
    95  		}
    96  
    97  		err := runLintScript(npmExecutor, config.RunScript, config.FailOnError)
    98  		if err != nil {
    99  			return err
   100  		}
   101  	} else {
   102  		if config.Install {
   103  			err := npmExecutor.InstallAllDependencies(packageJSONFiles)
   104  			if err != nil {
   105  				return err
   106  			}
   107  		}
   108  
   109  		err := runDefaultLint(npmExecutor, utils, config.FailOnError, config.OutputFormat, config.OutputFileName)
   110  
   111  		if err != nil {
   112  			return err
   113  		}
   114  	}
   115  	return nil
   116  }
   117  
   118  func runLintScript(npmExecutor npm.Executor, runScript string, failOnError bool) error {
   119  	runScripts := []string{runScript}
   120  	runOptions := []string{"--silent"}
   121  
   122  	err := npmExecutor.RunScriptsInAllPackages(runScripts, runOptions, nil, false, nil, nil)
   123  	if err != nil {
   124  		if failOnError {
   125  			return fmt.Errorf("%s script execution failed with error: %w. This might be the result of severe linting findings, or some other issue while executing the script. Please examine the linting results in the UI, the cilint.xml file, if available, or the log above. ", runScript, err)
   126  		}
   127  	}
   128  	return nil
   129  }
   130  
   131  func runDefaultLint(npmExecutor npm.Executor, utils lintUtils, failOnError bool, outputFormat string, outputFileName string) error {
   132  	execRunner := utils.getExecRunner()
   133  	eslintConfigs := findEslintConfigs(utils)
   134  
   135  	err := npmExecutor.SetNpmRegistries()
   136  	if err != nil {
   137  		log.Entry().Warnf("failed to set npm registries before running default lint: %v", err)
   138  	}
   139  
   140  	// If the user has ESLint configs in the project we use them to lint existing JS files. In this case we do not lint other types of files,
   141  	// i.e., .jsx, .ts, .tsx, since we can not be sure that the provided config enables parsing of these file types.
   142  	if len(eslintConfigs) > 0 {
   143  		for i, config := range eslintConfigs {
   144  			lintPattern := "."
   145  			dir := filepath.Dir(config)
   146  			if dir != "." {
   147  				lintPattern = dir + "/**/*.js"
   148  			}
   149  
   150  			args := prepareArgs([]string{
   151  				"eslint",
   152  				lintPattern,
   153  				"-f", outputFormat,
   154  				"--ignore-pattern", "node_modules/",
   155  				"--ignore-pattern", ".eslintrc.js",
   156  			}, fmt.Sprintf("./%s_%%s", strconv.Itoa(i)), outputFileName)
   157  
   158  			err = execRunner.RunExecutable("npx", args...)
   159  			if err != nil {
   160  				if failOnError {
   161  					return fmt.Errorf("Lint execution failed. This might be the result of severe linting findings, problems with the provided ESLint configuration (%s), or another issue. Please examine the linting results in the UI or in %s, if available, or the log above. ", config, strconv.Itoa(i)+"_defaultlint.xml")
   162  				}
   163  			}
   164  		}
   165  	} else {
   166  		// install dependencies manually, since npx cannot resolve the dependencies required for general purpose
   167  		// ESLint config, e.g., TypeScript ESLint plugin
   168  		log.Entry().Info("Run ESLint with general purpose config")
   169  		utils.getGeneralPurposeConfig("https://raw.githubusercontent.com/SAP/jenkins-library/master/resources/.eslintrc.json")
   170  
   171  		// Ignore possible errors when invoking ESLint to not fail the pipeline based on linting results
   172  		_ = execRunner.RunExecutable("npm", "install", "eslint@^7.0.0", "typescript@^3.7.4", "@typescript-eslint/parser@^3.0.0", "@typescript-eslint/eslint-plugin@^3.0.0")
   173  
   174  		args := prepareArgs([]string{
   175  			"--no-install",
   176  			"eslint",
   177  			".",
   178  			"--ext", ".js,.jsx,.ts,.tsx",
   179  			"-c", ".pipeline/.eslintrc.json",
   180  			"-f", outputFormat,
   181  			"--ignore-pattern", ".eslintrc.js",
   182  		}, "./%s", outputFileName)
   183  
   184  		_ = execRunner.RunExecutable("npx", args...)
   185  	}
   186  	return nil
   187  }
   188  
   189  func findEslintConfigs(utils lintUtils) []string {
   190  	unfilteredListOfEslintConfigs, err := utils.Glob("**/.eslintrc*")
   191  	if err != nil {
   192  		log.Entry().Warnf("Error during resolving lint config files: %v", err)
   193  	}
   194  	var eslintConfigs []string
   195  
   196  	for _, config := range unfilteredListOfEslintConfigs {
   197  		if strings.Contains(config, "node_modules") {
   198  			continue
   199  		}
   200  
   201  		if strings.HasPrefix(config, ".pipeline"+string(os.PathSeparator)) {
   202  			continue
   203  		}
   204  
   205  		eslintConfigs = append(eslintConfigs, config)
   206  		log.Entry().Info("Discovered ESLint config " + config)
   207  	}
   208  	return eslintConfigs
   209  }
   210  
   211  func prepareArgs(defaultArgs []string, outputFileNamePattern, outputFileName string) []string {
   212  	if outputFileName != "" { // in this case we omit the -o flag and output will go to the log
   213  		defaultArgs = append(defaultArgs, "-o", fmt.Sprintf(outputFileNamePattern, outputFileName))
   214  	}
   215  	return defaultArgs
   216  
   217  }