diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..89069cf --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,25 @@ +name: golangci-lint +on: + push: + branches: + - master + pull_request: + branches: + - master + +permissions: + contents: read + pull-requests: read +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v4 + with: + go-version: 1.x + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: v1.54 diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..c7b320f --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,130 @@ +# Options for analysis running. +run: + concurrency: 4 + timeout: 5m + tests: true + # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ + skip-dirs-use-default: true + modules-download-mode: readonly + allow-parallel-runners: false + go: '1.21' + +# output configuration options +output: + # Format: colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions + # + # Multiple can be specified by separating them by comma, output can be provided + # for each of them by separating format name and path by colon symbol. + # Output path can be either `stdout`, `stderr` or path to the file to write to. + # Example: "checkstyle:report.json,colored-line-number" + # + # Default: colored-line-number + format: colored-line-number + print-issued-lines: true + print-linter-name: true + uniq-by-line: true + sort-results: true + +linters: + disable-all: true + enable: + - bidichk + - bodyclose + - containedctx + - contextcheck + - cyclop + - durationcheck + - errcheck + - errname + - errorlint + - exhaustive + - exportloopref + - forbidigo + - forcetypeassert + - gocognit + - goconst + - gocritic + - goerr113 + - gofmt + - gomnd + - gomoddirectives + - gosec + - gosimple + - govet + - grouper + - ineffassign + - makezero + - misspell + - nakedret + - nestif + - nilerr + - nilnil + - noctx + - nolintlint + - nosprintfhostport + - paralleltest + - prealloc + - predeclared + - revive + - staticcheck + - tenv + - testpackage + - thelper + - tparallel + - typecheck + - unconvert + - unparam + - unused + - usestdlibvars + - varnamelen + - wrapcheck + - wsl + +issues: + new: false + fix: false + exclude-rules: + - path: _test\.go + linters: + - containedctx + - forcetypeassert + - goconst + - goerr113 + - varnamelen + - wrapcheck + # for tests, we prioritize readability over memory optimization + - path: _test\.go + text: "fieldalignment: struct with" + linters: + - govet + include: + - EXC0005 + +linters-settings: + gomnd: + ignored-functions: + - context.WithTimeout + govet: + settings: + printf: + funcs: + - (flamingo.me/flamingo/v3/framework/flamingo.Logger).Debugf + enable: + - fieldalignment + nolintlint: + require-specific: true + require-explanation: true + revive: + rules: + - name: var-naming + disabled: true + varnamelen: + max-distance: 10 + ignore-type-assert-ok: true + ignore-map-index-ok: true + ignore-chan-recv-ok: true + ignore-names: + - err + - id + ignore-decls: + - i int diff --git a/db/module.go b/db/module.go index 52513ee..05fa217 100644 --- a/db/module.go +++ b/db/module.go @@ -28,13 +28,13 @@ type ( } dbConfig struct { + ConnectionOptions config.Map `inject:"config:mysql.db.connectionOptions,optional"` Host string `inject:"config:mysql.db.host,optional"` Port string `inject:"config:mysql.db.port,optional"` DatabaseName string `inject:"config:mysql.db.databaseName,optional"` Username string `inject:"config:mysql.db.user,optional"` Password string `inject:"config:mysql.db.password,optional"` MaxConnectionLifetime float64 `inject:"config:mysql.db.maxConnectionLifetime,optional"` - ConnectionOptions config.Map `inject:"config:mysql.db.connectionOptions,optional"` } ) @@ -84,9 +84,13 @@ func dbProvider(cfg *dbConfig, logger flamingo.Logger) DB { if len(cfg.ConnectionOptions) > 0 { options := url.Values{} + for key, value := range cfg.ConnectionOptions { - options.Set(key, value.(string)) + if v, ok := value.(string); ok { + options.Set(key, v) + } } + dataSourceURL += "?" + options.Encode() } diff --git a/db/module_test.go b/db/module_test.go index 34b819e..84edd13 100644 --- a/db/module_test.go +++ b/db/module_test.go @@ -9,6 +9,8 @@ import ( ) func TestModule_Configure(t *testing.T) { + t.Parallel() + if err := config.TryModules(nil, new(db.Module)); err != nil { t.Error(err) } diff --git a/db/shutdown_test.go b/db/shutdown_test.go index f2270eb..a4e43e5 100644 --- a/db/shutdown_test.go +++ b/db/shutdown_test.go @@ -13,9 +13,12 @@ import ( ) func TestShutdownSubscriber_Notify(t *testing.T) { + t.Parallel() + type args struct { event flamingo.Event } + tests := []struct { name string args args @@ -36,8 +39,11 @@ func TestShutdownSubscriber_Notify(t *testing.T) { wantClose: false, }, } + for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { + t.Parallel() mockDB, mock, err := sqlmock.New() if err != nil { diff --git a/migration/application/migrator.go b/migration/application/migrator.go index b94a8c1..25f01d5 100644 --- a/migration/application/migrator.go +++ b/migration/application/migrator.go @@ -1,6 +1,8 @@ package application import ( + "errors" + "fmt" "os" "flamingo.me/flamingo/v3/framework/flamingo" @@ -15,18 +17,20 @@ type ( Migrator struct { db db.DB logger flamingo.Logger + migration *migrate.Migrate databaseName string migrationDirectory string - migration *migrate.Migrate } ) func migrationFactory(m *Migrator) *migrate.Migrate { conn := m.db.Connection() + driver, err := mysql.WithInstance(conn.DB, &mysql.Config{}) if err != nil { panic(err) } + migration, err := migrate.NewWithDatabaseInstance( "file://"+m.migrationDirectory, m.databaseName, @@ -51,9 +55,11 @@ func (m *Migrator) Inject( m.db = db m.logger = logger m.databaseName = conf.DatabaseName + if dbname, ok := os.LookupEnv("DBNAME"); ok { m.databaseName = dbname } + m.migrationDirectory = conf.MigrationDirectory } @@ -68,10 +74,12 @@ func (m *Migrator) Up(steps *int) error { // and will migrate step versions down or applying all down migrations if step is not given func (m *Migrator) Down(steps *int) error { m.migration = migrationFactory(m) + if steps != nil { tmpSteps := -*steps steps = &tmpSteps } + return m.runMigration(m.migration.Down, steps) } @@ -88,7 +96,7 @@ func (m *Migrator) runMigration(migratorFunc func() error, steps *int) error { err = m.migration.Steps(*steps) } - if err == migrate.ErrNoChange { + if errors.Is(err, migrate.ErrNoChange) { logger.Info("migrations: No change") return nil } else if err != nil { @@ -97,5 +105,5 @@ func (m *Migrator) runMigration(migratorFunc func() error, steps *int) error { logger.Info("Migrations complete") - return err + return fmt.Errorf("db migration failed: %w", err) } diff --git a/migration/application/seeder.go b/migration/application/seeder.go index 28c2642..33085a8 100644 --- a/migration/application/seeder.go +++ b/migration/application/seeder.go @@ -1,7 +1,7 @@ package application import ( - "io/ioutil" + "fmt" "os" "path/filepath" @@ -36,10 +36,10 @@ func (s *Seeder) Inject( func (s *Seeder) Seed() error { logger := s.logger.WithField(flamingo.LogKeyCategory, "seeds") - return filepath.Walk(s.seedsDirectory, func(path string, info os.FileInfo, err error) error { + err := filepath.Walk(s.seedsDirectory, func(path string, info os.FileInfo, err error) error { if !info.IsDir() && filepath.Ext(info.Name()) == ".sql" { logger.Info("Seed file %s ...", info.Name()) - data, err := ioutil.ReadFile(path) + data, err := os.ReadFile(path) if err != nil { logger.Fatal("Problem while reading file content of %s:", info.Name()) panic(err) @@ -52,4 +52,9 @@ func (s *Seeder) Seed() error { return nil }) + if err != nil { + return fmt.Errorf("seeding failed: %w", err) + } + + return nil } diff --git a/migration/application/startup.go b/migration/application/startup.go index e06abce..eba1317 100644 --- a/migration/application/startup.go +++ b/migration/application/startup.go @@ -27,6 +27,7 @@ func (s *StartUpMigrations) Inject( func (s *StartUpMigrations) Notify(_ context.Context, event flamingo.Event) { if _, ok := event.(*flamingo.StartupEvent); ok { s.logger.Info("Run auto migrations...") + err := s.migrator.Up(nil) if err != nil { panic(err) diff --git a/migration/module.go b/migration/module.go index dd673c7..5ad3cfc 100644 --- a/migration/module.go +++ b/migration/module.go @@ -2,6 +2,7 @@ package migration import ( "errors" + "fmt" "flamingo.me/dingo" "flamingo.me/flamingo/v3/framework/cmd" @@ -21,6 +22,10 @@ type ( } ) +var ( + ErrArgumentMissing = errors.New("argument up or down missing") +) + // Inject dependencies func (m *Module) Inject( cfg *struct { @@ -34,6 +39,7 @@ func (m *Module) Inject( func (m *Module) Configure(injector *dingo.Injector) { injector.BindMulti(new(cobra.Command)).ToProvider(migrateProvider) injector.BindMulti(new(cobra.Command)).ToProvider(seedProvider) + if m.autoMigrate { flamingo.BindEventSubscriber(injector).To(&application.StartUpMigrations{}) } @@ -49,6 +55,7 @@ func (m *Module) FlamingoLegacyConfigAlias() map[string]string { // CueConfig for the module func (m *Module) CueConfig() string { + // language=cue return ` mysql: { db: connectionOptions: multiStatements: "true" //required for migration and seed scripts @@ -73,11 +80,12 @@ func (m *Module) Depends() []dingo.Module { func exactValidArgs(cmd *cobra.Command, args []string) error { err := cobra.ExactArgs(1)(cmd, args) if err != nil { - return err + return fmt.Errorf("need exact 1 argument: %w", err) } + err = cobra.OnlyValidArgs(cmd, args) if err != nil { - return err + return fmt.Errorf("call contains invalid arguments: %w", err) } return nil @@ -97,16 +105,27 @@ func migrateProvider(migrator *application.Migrator) *cobra.Command { switch mode := args[0]; mode { case "up": - return migrator.Up(steps) + err := migrator.Up(steps) + if err != nil { + return fmt.Errorf("up migration failed: %w", err) + } + + return nil case "down": - return migrator.Down(steps) + err := migrator.Down(steps) + if err != nil { + return fmt.Errorf("down migration failed: %w", err) + } + + return nil default: - return errors.New("argument up or down missing") + return ErrArgumentMissing } }, Args: exactValidArgs, ValidArgs: []string{"up", "down"}, } + migrateCMD.Flags().IntP("steps", "s", 0, "Steps to migrate") return migrateCMD @@ -117,9 +136,15 @@ func seedProvider(seeder *application.Seeder) *cobra.Command { Use: "seed", Short: "Run all sql files from sql/seeds on the database", RunE: func(cmd *cobra.Command, args []string) error { - return seeder.Seed() + err := seeder.Seed() + if err != nil { + return fmt.Errorf("seeding failed: %w", err) + } + + return nil }, Args: cobra.NoArgs, } + return seedCMD } diff --git a/migration/module_test.go b/migration/module_test.go index d38304d..307128d 100644 --- a/migration/module_test.go +++ b/migration/module_test.go @@ -9,6 +9,8 @@ import ( ) func TestModule_Configure(t *testing.T) { + t.Parallel() + if err := config.TryModules(nil, new(migration.Module)); err != nil { t.Error(err) }