1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2025-02-21 19:48:53 +02:00

Add linting capability to step ``golangBuild`` (#3903)

* add golangci-lint functionality

* fix log typos

* fix golangci-lint install dir

* log golangci-lint output report

* specify golangci-lint version, as recommended

* log spelling consistency

* clean code

* refactor golangci-lint runner

* fail build if linter found issues

* fix bug where exit status can't be derived from nil error

* refactor runGolangciLint

* refactor retrieveGolangciLint

* uncomment golang tests

* Use FileWrite method from utils

* Add tests

* Fix test

* fix typo

* alter runLinter param name, improve docs

* undo commenting RunTests...

* alter runLinter name in generated and tests too

* fix variable name (thanks code climate)

* Add usage of ‘go install’ instead of ‘curl’

* Fix tests

* Add usage of functionality of http pkg

* Update tests

* Update tests

* Add usage of piperhttp pkg && update tests

* Add DownloadFile method

* Update tests

Co-authored-by: Jordi van Liempt <35920075+jliempt@users.noreply.github.com>
This commit is contained in:
Vyacheslav Starostin 2022-07-27 11:22:35 +06:00 committed by GitHub
parent 1f242ea139
commit 79b07e625b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 281 additions and 5 deletions

View File

@ -35,6 +35,8 @@ const (
golangTestsumPackage = "gotest.tools/gotestsum@latest"
golangCycloneDXPackage = "github.com/CycloneDX/cyclonedx-gomod/cmd/cyclonedx-gomod@latest"
sbomFilename = "bom.xml"
golangciLintURL = "https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh"
golangciLintVersion = "v1.46.2"
)
type golangBuildUtils interface {
@ -44,8 +46,9 @@ type golangBuildUtils interface {
piperutils.FileUtils
piperhttp.Uploader
DownloadFile(url, filename string, header http.Header, cookies []*http.Cookie) error
getDockerImageValue(stepName string) (string, error)
GetExitCode() int
DownloadFile(url, filename string, header http.Header, cookies []*http.Cookie) error
// Add more methods here, or embed additional interfaces, or remove/replace as required.
// The golangBuildUtils interface should be descriptive of your runtime dependencies,
@ -57,6 +60,7 @@ type golangBuildUtilsBundle struct {
*command.Command
*piperutils.Files
piperhttp.Uploader
httpClient *piperhttp.Client
goget.Client
@ -67,7 +71,7 @@ type golangBuildUtilsBundle struct {
}
func (g *golangBuildUtilsBundle) DownloadFile(url, filename string, header http.Header, cookies []*http.Cookie) error {
return fmt.Errorf("not implemented")
return g.httpClient.DownloadFile(url, filename, header, cookies)
}
func (g *golangBuildUtilsBundle) getDockerImageValue(stepName string) (string, error) {
@ -92,6 +96,7 @@ func newGolangBuildUtils(config golangBuildOptions) golangBuildUtils {
Client: &goget.ClientImpl{
HTTPClient: &httpClient,
},
httpClient: &httpClient,
}
// Reroute command output to logging framework
utils.Stdout(log.Writer())
@ -164,6 +169,26 @@ func runGolangBuild(config *golangBuildOptions, telemetryData *telemetry.CustomD
return fmt.Errorf("some tests failed")
}
if config.RunLint {
goPath := os.Getenv("GOPATH")
golangciLintDir := filepath.Join(goPath, "bin")
if err := retrieveGolangciLint(utils, golangciLintDir); err != nil {
return err
}
// hardcode those for now
lintSettings := map[string]string{
"reportStyle": "checkstyle", // readable by Sonar
"reportOutputPath": "golangci-lint-report.xml",
"additionalParams": "",
}
if err := runGolangciLint(utils, golangciLintDir, lintSettings); err != nil {
return err
}
}
if config.CreateBOM {
if err := runBOMCreation(utils, sbomFilename); err != nil {
return err
@ -404,6 +429,49 @@ func reportGolangTestCoverage(config *golangBuildOptions, utils golangBuildUtils
return nil
}
func retrieveGolangciLint(utils golangBuildUtils, golangciLintDir string) error {
installationScript := "./install.sh"
err := utils.DownloadFile(golangciLintURL, installationScript, nil, nil)
if err != nil {
return fmt.Errorf("failed to download golangci-lint: %w", err)
}
err = utils.Chmod(installationScript, 0777)
if err != nil {
return err
}
err = utils.RunExecutable(installationScript, "-b", golangciLintDir, golangciLintVersion)
if err != nil {
return fmt.Errorf("failed to install golangci-lint: %w", err)
}
return nil
}
func runGolangciLint(utils golangBuildUtils, golangciLintDir string, lintSettings map[string]string) error {
binaryPath := filepath.Join(golangciLintDir, "golangci-lint")
var outputBuffer bytes.Buffer
utils.Stdout(&outputBuffer)
err := utils.RunExecutable(binaryPath, "run", "--out-format", lintSettings["reportStyle"])
if err != nil && utils.GetExitCode() != 1 {
return fmt.Errorf("running golangci-lint failed: %w", err)
}
log.Entry().Infof("lint report: \n" + outputBuffer.String())
log.Entry().Infof("writing lint report to %s", lintSettings["reportOutputPath"])
err = utils.FileWrite(lintSettings["reportOutputPath"], outputBuffer.Bytes(), 0644)
if err != nil {
return fmt.Errorf("writing golangci-lint report failed: %w", err)
}
if utils.GetExitCode() == 1 {
return fmt.Errorf("golangci-lint found issues, see report above")
}
return nil
}
func prepareLdflags(config *golangBuildOptions, utils golangBuildUtils, envRootPath string) (*bytes.Buffer, error) {
cpe := piperenv.CPEMap{}
err := cpe.LoadFromDisk(path.Join(envRootPath, "commonPipelineEnvironment"))

View File

@ -37,6 +37,7 @@ type golangBuildOptions struct {
TargetRepositoryUser string `json:"targetRepositoryUser,omitempty"`
TargetRepositoryURL string `json:"targetRepositoryURL,omitempty"`
ReportCoverage bool `json:"reportCoverage,omitempty"`
RunLint bool `json:"runLint,omitempty"`
RunTests bool `json:"runTests,omitempty"`
RunIntegrationTests bool `json:"runIntegrationTests,omitempty"`
TargetArchitectures []string `json:"targetArchitectures,omitempty"`
@ -235,6 +236,7 @@ func addGolangBuildFlags(cmd *cobra.Command, stepConfig *golangBuildOptions) {
cmd.Flags().StringVar(&stepConfig.TargetRepositoryUser, "targetRepositoryUser", os.Getenv("PIPER_targetRepositoryUser"), "Username for the target repository where the compiled binaries shall be uploaded - typically provided by the CI/CD environment.")
cmd.Flags().StringVar(&stepConfig.TargetRepositoryURL, "targetRepositoryURL", os.Getenv("PIPER_targetRepositoryURL"), "URL of the target repository where the compiled binaries shall be uploaded - typically provided by the CI/CD environment.")
cmd.Flags().BoolVar(&stepConfig.ReportCoverage, "reportCoverage", true, "Defines if a coverage report should be created.")
cmd.Flags().BoolVar(&stepConfig.RunLint, "runLint", false, "Configures the build to run linters with [golangci-lint](https://golangci-lint.run/).")
cmd.Flags().BoolVar(&stepConfig.RunTests, "runTests", true, "Activates execution of tests using [gotestsum](https://github.com/gotestyourself/gotestsum).")
cmd.Flags().BoolVar(&stepConfig.RunIntegrationTests, "runIntegrationTests", false, "Activates execution of a second test run using tag `integration`.")
cmd.Flags().StringSliceVar(&stepConfig.TargetArchitectures, "targetArchitectures", []string{`linux,amd64`}, "Defines the target architectures for which the build should run using OS and architecture separated by a comma.")
@ -431,6 +433,15 @@ func golangBuildMetadata() config.StepData {
Aliases: []config.Alias{},
Default: true,
},
{
Name: "runLint",
ResourceRef: []config.ResourceReference{},
Scope: []string{"STEPS", "STAGES", "PARAMETERS"},
Type: "bool",
Mandatory: false,
Aliases: []config.Alias{},
Default: false,
},
{
Name: "runTests",
ResourceRef: []config.ResourceReference{},

View File

@ -24,15 +24,20 @@ type golangBuildMockUtils struct {
*mock.ExecMockRunner
*mock.FilesMock
returnFileUploadStatus int // expected to be set upfront
returnFileUploadError error // expected to be set upfront
returnFileUploadStatus int // expected to be set upfront
returnFileUploadError error // expected to be set upfront
returnFileDownloadError error // expected to be set upfront
clientOptions []piperhttp.ClientOptions // set by mock
fileUploads map[string]string // set by mock
}
func (g *golangBuildMockUtils) DownloadFile(url, filename string, header http.Header, cookies []*http.Cookie) error {
return fmt.Errorf("not implemented")
if g.returnFileDownloadError != nil {
return g.returnFileDownloadError
}
g.AddFile(filename, []byte("content"))
return nil
}
func (g *golangBuildMockUtils) GetRepositoryURL(module string) (string, error) {
@ -250,6 +255,31 @@ go 1.17`
assert.Equal(t, []string{"build", "-trimpath"}, utils.ExecMockRunner.Calls[2].Params)
})
t.Run("success - RunLint", func(t *testing.T) {
goPath := os.Getenv("GOPATH")
golangciLintDir := filepath.Join(goPath, "bin")
binaryPath := filepath.Join(golangciLintDir, "golangci-lint")
config := golangBuildOptions{
RunLint: true,
}
utils := newGolangBuildTestsUtils()
utils.AddFile("go.mod", []byte(modTestFile))
telemetry := telemetry.CustomData{}
err := runGolangBuild(&config, &telemetry, utils, &cpe)
assert.NoError(t, err)
b, err := utils.FileRead("install.sh")
assert.NoError(t, err)
assert.Equal(t, []byte("content"), b)
assert.Equal(t, "./install.sh", utils.Calls[0].Exec)
assert.Equal(t, []string{"-b", golangciLintDir, golangciLintVersion}, utils.Calls[0].Params)
assert.Equal(t, binaryPath, utils.Calls[1].Exec)
assert.Equal(t, []string{"run", "--out-format", "checkstyle"}, utils.Calls[1].Params)
})
t.Run("failure - install pre-requisites for testing", func(t *testing.T) {
config := golangBuildOptions{
RunTests: true,
@ -423,6 +453,39 @@ go 1.17`
err := runGolangBuild(&config, &telemetryData, utils, &cpe)
assert.EqualError(t, err, "BOM creation failed: BOM creation failure")
})
t.Run("failure - RunLint: retrieveGolangciLint failed", func(t *testing.T) {
goPath := os.Getenv("GOPATH")
golangciLintDir := filepath.Join(goPath, "bin")
config := golangBuildOptions{
RunLint: true,
}
utils := newGolangBuildTestsUtils()
utils.AddFile("go.mod", []byte(modTestFile))
utils.ShouldFailOnCommand = map[string]error{
fmt.Sprintf("./install.sh -b %s %s", golangciLintDir, golangciLintVersion): fmt.Errorf("installation err"),
}
telemetry := telemetry.CustomData{}
err := runGolangBuild(&config, &telemetry, utils, &cpe)
assert.EqualError(t, err, "failed to install golangci-lint: installation err")
})
t.Run("failure - RunLint: runGolangciLint failed", func(t *testing.T) {
goPath := os.Getenv("GOPATH")
golangciLintDir := filepath.Join(goPath, "bin")
binaryPath := filepath.Join(golangciLintDir, "golangci-lint")
config := golangBuildOptions{
RunLint: true,
}
utils := newGolangBuildTestsUtils()
utils.AddFile("go.mod", []byte(modTestFile))
utils.ShouldFailOnCommand = map[string]error{fmt.Sprintf("%s run --out-format checkstyle", binaryPath): fmt.Errorf("err")}
telemetry := telemetry.CustomData{}
err := runGolangBuild(&config, &telemetry, utils, &cpe)
assert.EqualError(t, err, "running golangci-lint failed: err")
})
}
func TestRunGolangTests(t *testing.T) {
@ -932,3 +995,130 @@ go 1.17`
})
}
}
func TestRunGolangciLint(t *testing.T) {
t.Parallel()
goPath := os.Getenv("GOPATH")
golangciLintDir := filepath.Join(goPath, "bin")
binaryPath := filepath.Join(golangciLintDir, "golangci-lint")
lintSettings := map[string]string{
"reportStyle": "checkstyle",
"reportOutputPath": "golangci-lint-report.xml",
"additionalParams": "",
}
tt := []struct {
name string
shouldFailOnCommand map[string]error
fileWriteError error
exitCode int
expectedCommand []string
expectedErr error
}{
{
name: "success",
shouldFailOnCommand: map[string]error{},
fileWriteError: nil,
exitCode: 0,
expectedCommand: []string{binaryPath, "run", "--out-format", lintSettings["reportStyle"]},
expectedErr: nil,
},
{
name: "failure - failed to run golangci-lint",
shouldFailOnCommand: map[string]error{fmt.Sprintf("%s run --out-format %s", binaryPath, lintSettings["reportStyle"]): fmt.Errorf("err")},
fileWriteError: nil,
exitCode: 0,
expectedCommand: []string{},
expectedErr: fmt.Errorf("running golangci-lint failed: err"),
},
{
name: "failure - failed to write golangci-lint report",
shouldFailOnCommand: map[string]error{},
fileWriteError: fmt.Errorf("failed to write golangci-lint report"),
exitCode: 0,
expectedCommand: []string{},
expectedErr: fmt.Errorf("writing golangci-lint report failed: failed to write golangci-lint report"),
},
{
name: "failure - failed with ExitCode == 1",
shouldFailOnCommand: map[string]error{},
exitCode: 1,
expectedCommand: []string{},
expectedErr: fmt.Errorf("golangci-lint found issues, see report above"),
},
}
for i, test := range tt {
i, test := i, test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
utils := newGolangBuildTestsUtils()
utils.ShouldFailOnCommand = test.shouldFailOnCommand
utils.FileWriteError = test.fileWriteError
utils.ExitCode = test.exitCode
err := runGolangciLint(utils, golangciLintDir, lintSettings)
if test.expectedErr == nil {
assert.Equal(t, test.expectedCommand[0], utils.Calls[i].Exec)
assert.Equal(t, test.expectedCommand[1:], utils.Calls[i].Params)
} else {
assert.EqualError(t, err, test.expectedErr.Error())
}
})
}
}
func TestRetrieveGolangciLint(t *testing.T) {
t.Parallel()
goPath := os.Getenv("GOPATH")
golangciLintDir := filepath.Join(goPath, "bin")
tt := []struct {
name string
shouldFailOnCommand map[string]error
downloadErr error
expectedCommand []string
expectedErr error
}{
{
name: "success",
shouldFailOnCommand: map[string]error{},
downloadErr: nil,
expectedCommand: []string{"./install.sh", "-b", golangciLintDir, golangciLintVersion},
expectedErr: nil,
},
{
name: "failure - failed to download golangci-lint",
shouldFailOnCommand: map[string]error{},
downloadErr: fmt.Errorf("download err"),
expectedCommand: []string{},
expectedErr: fmt.Errorf("failed to download golangci-lint: download err"),
},
{
name: "failure - failed to install golangci-lint with sh error",
shouldFailOnCommand: map[string]error{fmt.Sprintf("./install.sh -b %s %s", golangciLintDir, golangciLintVersion): fmt.Errorf("installation err")},
expectedCommand: []string{},
expectedErr: fmt.Errorf("failed to install golangci-lint: installation err"),
},
}
for i, test := range tt {
i, test := i, test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
utils := newGolangBuildTestsUtils()
utils.ShouldFailOnCommand = test.shouldFailOnCommand
utils.returnFileDownloadError = test.downloadErr
err := retrieveGolangciLint(utils, golangciLintDir)
if test.expectedErr == nil {
assert.Equal(t, test.expectedCommand[0], utils.Calls[i].Exec)
assert.Equal(t, test.expectedCommand[1:], utils.Calls[i].Params)
} else {
assert.EqualError(t, err, test.expectedErr.Error())
}
})
}
}

View File

@ -155,6 +155,13 @@ spec:
- STEPS
- STAGES
- PARAMETERS
- name: runLint
type: bool
description: Configures the build to run linters with [golangci-lint](https://golangci-lint.run/).
scope:
- STEPS
- STAGES
- PARAMETERS
- name: runTests
type: bool
description: Activates execution of tests using [gotestsum](https://github.com/gotestyourself/gotestsum).