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:
parent
1f242ea139
commit
79b07e625b
@ -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"))
|
||||
|
@ -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{},
|
||||
|
@ -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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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).
|
||||
|
Loading…
x
Reference in New Issue
Block a user