score-compose icon indicating copy to clipboard operation
score-compose copied to clipboard

ci: add go-static-check

Open 7h3-3mp7y-m4n opened this issue 7 months ago • 9 comments
trafficstars

Add Go Static Check to CI Workflow

Description

This PR adds Staticcheck to the CI pipeline to perform static code analysis on the Go codebase. It helps catch bugs and enforce best practices automatically.

What does this PR do?

It integrates Staticcheck into the CI pipeline, improving code quality and catching potential issues early.

Types of changes

  • [ ] Bug fix (non-breaking change which fixes an issue)
  • [ ] New feature (non-breaking change which adds functionality)
  • [ ] Breaking change (fix or feature that would cause existing functionality to change)
  • [x] New chore (expected functionality to be implemented)

Checklist:

  • [] My change requires a change to the documentation.
    • [ ] I have updated the documentation accordingly.
  • [x] I've signed off with an email address that matches the commit author.

7h3-3mp7y-m4n avatar Apr 10 '25 19:04 7h3-3mp7y-m4n

@astromechza , what do you think of it? Do you think we should add this to our workflow?

7h3-3mp7y-m4n avatar Apr 10 '25 20:04 7h3-3mp7y-m4n

@7h3-3mp7y-m4n personally I'd prefer we go further an add golangci-lint with a config like

version: "2"
linters:
  default: standard
  enable:
    - bodyclose
    - contextcheck
    - goconst
    - gosec
    - mnd
    - testifylint

I use this in a number of other projects and found it very useful!

astromechza avatar Apr 17 '25 14:04 astromechza

@7h3-3mp7y-m4n personally I'd prefer we go further an add golangci-lint with a config like

version: "2"
linters:
  default: standard
  enable:
    - bodyclose
    - contextcheck
    - goconst
    - gosec
    - mnd
    - testifylint

I use this in a number of other projects and found it very useful!

Yeah, makes sense golangci-lint seems like the better call, especially with all the extra checks it brings in. Having everything in one place with a shared config will be super handy. Let’s do it! I’ll just fix the remaining staticCheck issues first so we can include it all in the same PR

7h3-3mp7y-m4n avatar Apr 17 '25 16:04 7h3-3mp7y-m4n

I’m thinking we should remove the go vet step from our ci.yaml, because we can include go vet as a linter in golangci-lint. Since we're centralizing our linting configuration in .golangci.yml , why not add it there as well? What do you think, @astromechza and @mathieu-benoit?

7h3-3mp7y-m4n avatar Apr 17 '25 17:04 7h3-3mp7y-m4n

I think this makes sense if it's redundant.

@astromechza, does it work for you?

mathieu-benoit avatar Apr 17 '25 18:04 mathieu-benoit

@7h3-3mp7y-m4n @mathieu-benoit yes, we can remove vet if we're using the new linter.

astromechza avatar May 20 '25 09:05 astromechza

@7h3-3mp7y-m4n @mathieu-benoit yes, we can remove vet if we're using the new linter.

I'll make the changes :)

7h3-3mp7y-m4n avatar May 21 '25 20:05 7h3-3mp7y-m4n

here is a little breakdown of linters.

 make lint
Running golangci-lint...
golangci-lint run ./...
internal/command/init.go:229:19: Error return value of `f.Close` is not checked (errcheck)
                                        defer f.Close()
                                                     ^
internal/command/run.go:101:17: Error return value of `src.Close` is not checked (errcheck)
        defer src.Close()
                       ^
internal/command/run.go:115:19: Error return value of `ovr.Close` is not checked (errcheck)
                        defer ovr.Close()
                                       ^
internal/command/run.go:273:23: Error return value of `destFile.Close` is not checked (errcheck)
                defer destFile.Close()
                                    ^
internal/command/run.go:292:19: Error return value of `dest.Close` is not checked (errcheck)
                defer dest.Close()
                                ^
internal/command/provisioners.go:79:7: string `json` has 3 occurrences, make it a constant (goconst)
        case "json":
             ^
internal/command/generate.go:377:20: G306: Expect WriteFile permissions to be 0600 or less (gosec)
                } else if err := os.WriteFile(v+".temp", raw, 0644); err != nil {
                                 ^
internal/command/init.go:225:16: G302: Expect file permissions to be 0600 or less (gosec)
                                        f, err := os.OpenFile(defaultProvisioners, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
                                                  ^
internal/command/run.go:235:22: G115: integer overflow conversion int -> uint32 (gosec)
                                Target:    uint32(util.DerefOr(pSpec.TargetPort, pSpec.Port)),
                                                 ^
internal/compose/convert.go:253:19: G304: Potential file inclusion via variable (gosec)
                        content, err = os.ReadFile(sourcePath)
                                       ^
internal/compose/convert.go:291:26: G115: integer overflow conversion int64 -> uint32 (gosec)
                        fileMode = os.FileMode(newMode)
                                              ^
internal/project/project.go:66:12: G301: Expect directory permissions to be 0750 or less (gosec)
        if err := os.Mkdir(sd.Path, 0755); err != nil && !errors.Is(err, os.ErrExist) {
                  ^
internal/project/project.go:69:12: G301: Expect directory permissions to be 0750 or less (gosec)
        if err := os.Mkdir(sd.State.Extras.MountsDirectory, 0755); err != nil && !errors.Is(err, os.ErrExist) {
                  ^
internal/project/project.go:80:12: G306: Expect WriteFile permissions to be 0600 or less (gosec)
        if err := os.WriteFile(filepath.Join(sd.Path, StateFileName+".temp"), out.Bytes(), 0755); err != nil {
                  ^
internal/project/project.go:91:18: G304: Potential file inclusion via variable (gosec)
        content, err := os.ReadFile(filepath.Join(d, StateFileName))
                        ^
internal/provisioners/cmdprov/commandprov.go:134:9: G204: Subprocess launched with variable (gosec)
        cmd := exec.CommandContext(ctx, bin, p.Args...)
               ^
internal/provisioners/core.go:212:14: G301: Expect directory permissions to be 0750 or less (gosec)
                        if err := os.MkdirAll(dst, 0755); err != nil && !errors.Is(err, os.ErrExist) {
                                  ^
internal/provisioners/core.go:234:14: G306: Expect WriteFile permissions to be 0600 or less (gosec)
                        if err := os.WriteFile(dst, []byte(*b), 0644); err != nil {
                                  ^
internal/provisioners/loader/load.go:87:16: G304: Potential file inclusion via variable (gosec)
                        raw, err := os.ReadFile(filepath.Join(path, item.Name()))
                                    ^
internal/provisioners/loader/load.go:114:99: G115: integer overflow conversion int64 -> uint64 (gosec)
        timePrefix := base64.RawURLEncoding.EncodeToString(binary.BigEndian.AppendUint64([]byte{}, uint64(math.MaxInt64-time.Now().UnixNano())))
                                                                                                         ^
internal/command/generate.go:172:46: Magic number: 2, in <argument> detected (mnd)
                                        parts := strings.SplitN(buildFlag, "=", 2)
                                                                                ^
internal/command/generate.go:173:23: Magic number: 2, in <condition> detected (mnd)
                                        if len(parts) != 2 {
                                                         ^
internal/command/generate.go:296:22: Magic number: 2, in <condition> detected (mnd)
                                if len(parts) <= 2 {
                                                 ^
internal/compose/convert.go:217:54: Magic number: 5, in <argument> detected (mnd)
                        Interval: util.Ref(compose.Duration(time.Second * 5)),
                                                                          ^
internal/compose/convert.go:218:54: Magic number: 5, in <argument> detected (mnd)
                        Timeout:  util.Ref(compose.Duration(time.Second * 5)),
                                                                          ^
internal/compose/convert.go:278:27: Magic number: 0644, in <argument> detected (mnd)
                fileMode := os.FileMode(0644)
                                        ^
internal/compose/convert.go:283:24: Magic number: 0777, in <condition> detected (mnd)
                        } else if newMode > 0777 {
                                            ^
internal/compose/convert.go:285:30: Magic number: 0400, in <condition> detected (mnd)
                        } else if newMode&0400 != 0400 {
                                                  ^
internal/compose/convert.go:287:30: Magic number: 0600, in <condition> detected (mnd)
                        } else if newMode&0600 != 0600 {
                                                  ^
internal/compose/convert.go:288:25: Magic number: 0600, in <operation> detected (mnd)
                                newMode = newMode | 0600
                                                    ^
internal/compose/write.go:27:16: Magic number: 2, in <argument> detected (mnd)
        enc.SetIndent(2)
                      ^
internal/project/project.go:74:16: Magic number: 2, in <argument> detected (mnd)
        enc.SetIndent(2)
                      ^
internal/provisioners/loader/load.go:118:40: Magic number: 0600, in <argument> detected (mnd)
        if err := os.WriteFile(tmpPath, data, 0600); err != nil {
                                              ^
internal/version/version.go:51:16: Magic number: 2, in <condition> detected (mnd)
        if len(cpm) > 2 {
                      ^
internal/version/version.go:53:17: Magic number: 3, in <condition> detected (mnd)
                if len(cpm) > 3 {
                              ^
internal/logging/logging.go:15:1: package-comments: should have a package comment (revive)
package logging
^
internal/logging/logging.go:25:6: exported: exported type SimpleHandler should have comment or be unexported (revive)
type SimpleHandler struct {
     ^
internal/logging/logging.go:32:33: unused-parameter: parameter 'ctx' seems to be unused, consider removing or renaming it as _ (revive)
func (h *SimpleHandler) Enabled(ctx context.Context, level slog.Level) bool {
                                ^
internal/logging/logging.go:36:32: unused-parameter: parameter 'ctx' seems to be unused, consider removing or renaming it as _ (revive)
func (h *SimpleHandler) Handle(ctx context.Context, record slog.Record) error {
                               ^
internal/logging/logging.go:43:35: unused-parameter: parameter 'attrs' seems to be unused, consider removing or renaming it as _ (revive)
func (h *SimpleHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
                                  ^
internal/logging/logging.go:48:35: unused-parameter: parameter 'name' seems to be unused, consider removing or renaming it as _ (revive)
func (h *SimpleHandler) WithGroup(name string) slog.Handler {
                                  ^
internal/patching/patching.go:36:6: exported: exported type PatchOperation should have comment or be unexported (revive)
type PatchOperation struct {
     ^
internal/project/project.go:15:1: package-comments: should have a package comment (revive)
package project
^
internal/project/project.go:30:2: exported: exported const DefaultRelativeStateDirectory should have comment (or a comment on this block) or be unexported (revive)
        DefaultRelativeStateDirectory = ".score-compose"
        ^
internal/project/project.go:39:6: exported: exported type StateExtras should have comment or be unexported (revive)
type StateExtras struct {
     ^
internal/project/project.go:45:6: exported: exported type WorkloadExtras should have comment or be unexported (revive)
type WorkloadExtras struct {
     ^
internal/provisioners/cmdprov/commandprov.go:36:6: exported: exported type Provisioner should have comment or be unexported (revive)
type Provisioner struct {
     ^
internal/provisioners/cmdprov/commandprov.go:37:2: var-naming: struct field ProvisionerUri should be ProvisionerURI (revive)
        ProvisionerUri string   `yaml:"uri"`
        ^
internal/provisioners/cmdprov/commandprov.go:40:2: var-naming: struct field ResId should be ResID (revive)
        ResId          *string  `yaml:"id,omitempty"`
        ^
internal/provisioners/cmdprov/commandprov.go:47:1: exported: exported method Provisioner.Description should have comment or be unexported (revive)
func (p *Provisioner) Description() string {
^
internal/provisioners/cmdprov/commandprov.go:55:1: exported: exported method Provisioner.Class should have comment or be unexported (revive)
func (p *Provisioner) Class() string {
^
internal/provisioners/cmdprov/commandprov.go:62:1: exported: exported method Provisioner.Type should have comment or be unexported (revive)
func (p *Provisioner) Type() string {
^
internal/provisioners/cmdprov/commandprov.go:66:1: exported: exported method Provisioner.Params should have comment or be unexported (revive)
func (p *Provisioner) Params() []string {
^
internal/provisioners/cmdprov/commandprov.go:70:1: exported: exported method Provisioner.Outputs should have comment or be unexported (revive)
func (p *Provisioner) Outputs() []string {
^
internal/provisioners/cmdprov/commandprov.go:122:1: exported: exported method Provisioner.Provision should have comment or be unexported (revive)
func (p *Provisioner) Provision(ctx context.Context, input *provisioners.Input) (*provisioners.ProvisionOutput, error) {
^
internal/provisioners/cmdprov/commandprov.go:154:1: exported: exported function Parse should have comment or be unexported (revive)
func Parse(raw map[string]interface{}) (*Provisioner, error) {
^
internal/provisioners/core.go:41:2: var-naming: struct field ResourceUid should be ResourceUID (revive)
        ResourceUid      string                 `json:"resource_uid"`
        ^
internal/provisioners/core.go:44:2: var-naming: struct field ResourceId should be ResourceID (revive)
        ResourceId       string                 `json:"resource_id"`
        ^
internal/provisioners/core.go:67:6: exported: exported type ServicePort should have comment or be unexported (revive)
type ServicePort struct {
     ^
internal/provisioners/core.go:86:2: var-naming: struct field ProvisionerUri should be ProvisionerURI (revive)
        ProvisionerUri       string                           `json:"-"`
        ^
internal/provisioners/core.go:100:6: exported: exported type Provisioner should have comment or be unexported (revive)
type Provisioner interface {
     ^
internal/provisioners/core.go:102:8: var-naming: interface method parameter resUid should be resUID (revive)
        Match(resUid framework.ResourceUid) bool
              ^
internal/provisioners/core.go:113:2: var-naming: struct field matchUid should be matchUID (revive)
        matchUid    framework.ResourceUid
        ^
internal/provisioners/core.go:126:32: var-naming: method Uri should be URI (revive)
func (e *ephemeralProvisioner) Uri() string {
                               ^
internal/provisioners/core.go:130:38: var-naming: method parameter resUid should be resUID (revive)
func (e *ephemeralProvisioner) Match(resUid framework.ResourceUid) bool {
                                     ^
internal/provisioners/core.go:154:42: var-naming: func parameter matchUid should be matchUID (revive)
func NewEphemeralProvisioner(uri string, matchUid framework.ResourceUid, inner func(ctx context.Context, input *Input) (*ProvisionOutput, error)) Provisioner {
                                         ^
internal/provisioners/core.go:160:73: var-naming: method parameter resUid should be resUID (revive)
func (po *ProvisionOutput) ApplyToStateAndProject(state *project.State, resUid framework.ResourceUid, project *compose.Project) (*project.State, error) {
                                                                        ^
internal/provisioners/core.go:300:1: exported: exported function ProvisionResources should have comment or be unexported (revive)
func ProvisionResources(ctx context.Context, state *project.State, provisioners []Provisioner, composeProject *compose.Project) (*project.State, error) {
^
internal/provisioners/core.go:311:9: var-naming: range var resUid should be resUID (revive)
        for _, resUid := range orderedResources {
               ^
internal/provisioners/envprov/envprov.go:41:23: var-naming: method Uri should be URI (revive)
func (e *Provisioner) Uri() string {
                      ^
internal/provisioners/envprov/envprov.go:45:29: var-naming: method parameter resUid should be resUID (revive)
func (e *Provisioner) Match(resUid framework.ResourceUid) bool {
                            ^
internal/provisioners/envprov/envprov.go:49:33: unused-parameter: parameter 'ctx' seems to be unused, consider removing or renaming it as _ (revive)
func (e *Provisioner) Provision(ctx context.Context, input *provisioners.Input) (*provisioners.ProvisionOutput, error) {
                                ^
internal/provisioners/envprov/envprov.go:56:1: exported: exported method Provisioner.Accessed should have comment or be unexported (revive)
func (e *Provisioner) Accessed() map[string]string {
^
internal/provisioners/envprov/envprov.go:76:1: exported: exported method Provisioner.LookupOutput should have comment or be unexported (revive)
func (e *Provisioner) LookupOutput(keys ...string) (interface{}, error) {
^
internal/provisioners/envprov/envprov.go:118:33: var-naming: method Uri should be URI (revive)
func (e *envVarResourceTracker) Uri() string {
                                ^
internal/provisioners/envprov/envprov.go:156:1: receiver-naming: receiver name p should be consistent with previous receiver name e for Provisioner (revive)
func (p *Provisioner) Class() string {
        return p.Class()
}
internal/provisioners/envprov/envprov.go:160:1: receiver-naming: receiver name p should be consistent with previous receiver name e for Provisioner (revive)
func (p *Provisioner) Type() string {
        return p.Type()
}
internal/provisioners/envprov/envprov.go:164:1: receiver-naming: receiver name p should be consistent with previous receiver name e for Provisioner (revive)
func (p *Provisioner) Outputs() []string {
        return nil
}
internal/util/maps.go:15:1: package-comments: should have a package comment (revive)
package util
^
internal/util/ref.go:17:1: exported: exported function Ref should have comment or be unexported (revive)
func Ref[k any](input k) *k {
^
internal/util/ref.go:21:1: exported: exported function DerefOr should have comment or be unexported (revive)
func DerefOr[k any](input *k, def k) k {
^
internal/version/version.go:1:1: package-comments: package comment should be of the form "Package version ..." (revive)
/*
Apache Score
Copyright 2022 The Apache Software Foundation

This product includes software developed at
The Apache Software Foundation (http://www.apache.org/).
*/
internal/version/version.go:18:22: var-declaration: should omit type string from declaration of var Version; it will be inferred from the right-hand side (revive)
        Version             string = "0.0.0"
                            ^
internal/version/version.go:60:1: exported: exported function AssertVersion should have comment or be unexported (revive)
func AssertVersion(constraint string, current string) error {
^
internal/version/version.go:65:9: indent-error-flow: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary) (revive)
        } else {
                op := m[1]
                compareI, err := semverToI(m[0][len(op):])
                if err != nil {
                        return fmt.Errorf("failed to parse constraint: %w", err)
                }
                match := false
                switch op {
                case ">":
                        match = currentI > compareI
                case ">=":
                        match = currentI >= compareI
                case "=":
                        match = currentI == compareI
                }
                if !match {
                        return fmt.Errorf("current version %s does not match requested constraint %s", current, constraint)
                }
                return nil
        }
internal/logging/logging.go:39:12: QF1012: Use fmt.Fprintf(...) instead of Write([]byte(fmt.Sprintf(...))) (staticcheck)
        _, err := h.Writer.Write([]byte(fmt.Sprintf("%s: %s\n", record.Level.String(), record.Message)))
                  ^
internal/provisioners/envprov/envprov.go:157:9: SA5007: infinite recursive call (staticcheck)
        return p.Class()
               ^
internal/provisioners/envprov/envprov.go:161:9: SA5007: infinite recursive call (staticcheck)
        return p.Type()
               ^
internal/provisioners/envprov/envprov.go:181:9: SA5007: infinite recursive call (staticcheck)
        return p.Description()
               ^
89 issues:
* errcheck: 5
* goconst: 1
* gosec: 14
* mnd: 15
* revive: 50
* staticcheck: 4
make: *** [lint] Error 1

7h3-3mp7y-m4n avatar Jun 10 '25 19:06 7h3-3mp7y-m4n

revive is a good linter, but it has strict rules and might discourage new contributors. It can also create noise, so let's not use it for now.

7h3-3mp7y-m4n avatar Jun 10 '25 19:06 7h3-3mp7y-m4n