From 1f528e38c4f0cdccfa0c05a0e98dcbf743a89eea Mon Sep 17 00:00:00 2001 From: Alexander Kolodka Date: Mon, 17 Nov 2025 18:23:17 +0700 Subject: [PATCH 1/6] Add dynamic autocompletion for --job and --repo flags # Conflicts: # cmd/root.go --- cmd/backup.go | 2 ++ cmd/exec.go | 2 ++ cmd/forget.go | 2 ++ cmd/helpers.go | 23 +++++++++++++++++++++++ cmd/restore.go | 3 ++- cmd/root.go | 9 +++++++++ cmd/unlock.go | 2 ++ 7 files changed, 42 insertions(+), 1 deletion(-) diff --git a/cmd/backup.go b/cmd/backup.go index 14611e4..0cda8dc 100644 --- a/cmd/backup.go +++ b/cmd/backup.go @@ -81,6 +81,8 @@ func init() { backupCmd.Flags().BoolP("all", "a", false, "Check all repositories") backupCmd.Flags().StringSliceP("job", "j", nil, "Run only specific jobs by name (comma-separated)") backupCmd.Flags().Bool("dry-run", false, "Dry run") + + _ = backupCmd.RegisterFlagCompletionFunc("job", jobAutocompletion) } func filterJobs(cmd *cobra.Command, backups []entity.Job) []entity.Job { diff --git a/cmd/exec.go b/cmd/exec.go index b64f46d..493d628 100644 --- a/cmd/exec.go +++ b/cmd/exec.go @@ -96,4 +96,6 @@ func init() { execCmd.Flags(). StringSliceP("repo", "r", nil, "Repository/repositories to execute command on (can specify multiple)") execCmd.Flags().BoolP("all", "a", false, "Execute on all repositories") + + _ = execCmd.RegisterFlagCompletionFunc("repo", repoAutocompletion) } diff --git a/cmd/forget.go b/cmd/forget.go index 8b7cc9a..4014430 100644 --- a/cmd/forget.go +++ b/cmd/forget.go @@ -77,4 +77,6 @@ func init() { forgetCmd.Flags().BoolP("all", "a", false, "Run forget for all jobs") forgetCmd.Flags().Bool("dry-run", false, "Show what would be deleted without actually deleting") forgetCmd.Flags().Bool("prune", false, "Actually remove the data (frees up space)") + + _ = forgetCmd.RegisterFlagCompletionFunc("repo", repoAutocompletion) } diff --git a/cmd/helpers.go b/cmd/helpers.go index c2ff493..09452c3 100644 --- a/cmd/helpers.go +++ b/cmd/helpers.go @@ -58,3 +58,26 @@ func toZerologLevel(level string) zerolog.Level { return levels[level] } + +func jobAutocompletion(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + cfgPath, _ := cmd.Flags().GetString("config") + cfg, err := loadConfig(cfgPath) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + jobNames := lo.Map(cfg.Jobs, func(j entity.Job, _ int) string { + return j.GetName() + }) + + return jobNames, cobra.ShellCompDirectiveNoFileComp +} + +func repoAutocompletion(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + cfgPath, _ := cmd.Flags().GetString("config") + cfg, err := loadConfig(cfgPath) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + return lo.Keys(cfg.Repositories), cobra.ShellCompDirectiveNoFileComp +} diff --git a/cmd/restore.go b/cmd/restore.go index aee4e9f..808adcc 100644 --- a/cmd/restore.go +++ b/cmd/restore.go @@ -64,6 +64,7 @@ func init() { restoreCmd.Flags().StringP("repo", "r", "", "Restore specific repository (required)") restoreCmd.Flags().StringP("target", "t", "", "Directory to restore to (required)") restoreCmd.Flags().StringP("snapshot", "s", "", "snapshot") - _ = restoreCmd.MarkFlagRequired("to") _ = restoreCmd.MarkFlagRequired("target") + + _ = restoreCmd.RegisterFlagCompletionFunc("repo", repoAutocompletion) } diff --git a/cmd/root.go b/cmd/root.go index bbc3886..5f945ad 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -68,6 +68,15 @@ func init() { rootCmd.PersistentFlags().Bool("print-commands", false, "Print executed shell commands") rootCmd.SilenceUsage = true + + _ = rootCmd.MarkFlagFilename("config", "yaml", "yml") + + _ = rootCmd.RegisterFlagCompletionFunc( + "log-level", + func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + return []string{"debug", "info", "warn", "error"}, cobra.ShellCompDirectiveDefault + }, + ) } func logFormat(ci, json bool) logger.Format { diff --git a/cmd/unlock.go b/cmd/unlock.go index 0a81c05..ecf24d1 100644 --- a/cmd/unlock.go +++ b/cmd/unlock.go @@ -63,4 +63,6 @@ func init() { rootCmd.AddCommand(unlockCmd) unlockCmd.Flags().StringSliceP("repo", "r", nil, "Unlock specific repository/repositories (can specify multiple)") unlockCmd.Flags().BoolP("all", "a", false, "Unlock all repositories") + + _ = unlockCmd.RegisterFlagCompletionFunc("repo", repoAutocompletion) } From 122afa6eeb4b11f9d193beddf16940c4edda009a Mon Sep 17 00:00:00 2001 From: Alexander Kolodka Date: Mon, 17 Nov 2025 17:57:06 +0700 Subject: [PATCH 2/6] Add CI workflow --- .github/workflows/ci.yaml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/ci.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..40f9b45 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,25 @@ +name: CI +on: + push: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 + with: + go-version: stable + - run: go test -v ./... + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 + with: + go-version: stable + - uses: golangci/golangci-lint-action@v9 + with: + version: v2.6 From 27a845656496f4bb88b6574e0386c25611638f0a Mon Sep 17 00:00:00 2001 From: Alexander Kolodka Date: Mon, 17 Nov 2025 18:24:58 +0700 Subject: [PATCH 3/6] Fix lint issues --- internal/healthchecks/client.go | 3 ++- internal/version/version.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/healthchecks/client.go b/internal/healthchecks/client.go index 9631c92..349d932 100644 --- a/internal/healthchecks/client.go +++ b/internal/healthchecks/client.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -76,7 +77,7 @@ func (c *Client) post(ctx context.Context, baseURL, endpoint, rid string, p Payl func buildURL(base, endpoint, rid string) (string, error) { base = strings.TrimSpace(base) if base == "" { - return "", fmt.Errorf("empty base URL") + return "", errors.New("empty base URL") } u, err := neturl.Parse(base) diff --git a/internal/version/version.go b/internal/version/version.go index 5974249..47b244a 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -4,7 +4,7 @@ import ( "runtime/debug" ) -var Version = "" // can be set via -ldflags +var Version = "" //nolint:gochecknoglobals // can be set via -ldflags func String() string { if Version != "" { From b8376258e46621def98a92f896b0631e4da78e89 Mon Sep 17 00:00:00 2001 From: Alexander Kolodka Date: Mon, 17 Nov 2025 18:43:57 +0700 Subject: [PATCH 4/6] Fix import lint issues --- .golangci.yml | 17 +++++++++++++++++ cmd/backup.go | 5 +++-- cmd/cfg.go | 3 ++- cmd/check.go | 3 ++- cmd/cron.go | 3 ++- cmd/exec.go | 3 ++- cmd/forget.go | 3 ++- cmd/helpers.go | 3 ++- cmd/restore.go | 3 ++- cmd/root.go | 3 ++- cmd/unlock.go | 3 ++- internal/cases/backup/healthcheck.go | 3 ++- internal/cases/backup/mw_test.go | 3 ++- internal/cases/handler/chain_test.go | 3 ++- internal/cases/handler/lock.go | 3 ++- internal/cases/handler/panic_test.go | 5 +++-- internal/cron/cron.go | 5 +++-- internal/dto/mapper.go | 3 ++- internal/entity/options_test.go | 3 ++- internal/logger/context.go | 3 ++- internal/shell/executor.go | 3 ++- 21 files changed, 60 insertions(+), 23 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 1a14c0e..a28ab70 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -43,6 +43,23 @@ formatters: # Default: 100 max-len: 120 + gci: + # Section configuration to compare against. + # Section names are case-insensitive and may contain parameters in (). + # The default order of sections is `standard > default > custom > blank > dot > alias > localmodule`. + # If `custom-order` is `true`, it follows the order of `sections` option. + # Default: ["standard", "default"] + sections: + - standard # Standard section: captures all standard packages. + - default # Default section: contains all imports that could not be matched to another section type. + - localmodule # Local module section: contains all local packages. This section is not present unless explicitly enabled. + # Checks that no inline comments are present. + # Default: false + no-inline-comments: true + # Checks that no prefix comments (comment lines above an import) are present. + # Default: false + no-prefix-comments: true + linters: enable: - asasalint # checks for pass []any as any in variadic func(...any) diff --git a/cmd/backup.go b/cmd/backup.go index 0cda8dc..394eb9a 100644 --- a/cmd/backup.go +++ b/cmd/backup.go @@ -3,14 +3,15 @@ package cmd import ( "errors" + "github.com/samber/lo" + "github.com/spf13/cobra" + "github.com/alexander-kolodka/crestic/internal/cases/backup" "github.com/alexander-kolodka/crestic/internal/cases/handler" "github.com/alexander-kolodka/crestic/internal/entity" "github.com/alexander-kolodka/crestic/internal/healthchecks" "github.com/alexander-kolodka/crestic/internal/restic" "github.com/alexander-kolodka/crestic/internal/shell" - "github.com/samber/lo" - "github.com/spf13/cobra" ) var backupCmd = &cobra.Command{ diff --git a/cmd/cfg.go b/cmd/cfg.go index 9a173c1..a0b2102 100644 --- a/cmd/cfg.go +++ b/cmd/cfg.go @@ -6,9 +6,10 @@ import ( "os" "path/filepath" + "gopkg.in/yaml.v3" + "github.com/alexander-kolodka/crestic/internal/dto" "github.com/alexander-kolodka/crestic/internal/entity" - "gopkg.in/yaml.v3" ) // findConfigFile searches for config file in priority order: diff --git a/cmd/check.go b/cmd/check.go index 803fcdc..a5c6348 100644 --- a/cmd/check.go +++ b/cmd/check.go @@ -1,11 +1,12 @@ package cmd import ( + "github.com/spf13/cobra" + "github.com/alexander-kolodka/crestic/internal/cases/check" "github.com/alexander-kolodka/crestic/internal/cases/handler" "github.com/alexander-kolodka/crestic/internal/restic" "github.com/alexander-kolodka/crestic/internal/shell" - "github.com/spf13/cobra" ) var checkCmd = &cobra.Command{ diff --git a/cmd/cron.go b/cmd/cron.go index 92fae9c..3f6a591 100644 --- a/cmd/cron.go +++ b/cmd/cron.go @@ -5,13 +5,14 @@ import ( "path/filepath" "strings" + "github.com/spf13/cobra" + "github.com/alexander-kolodka/crestic/internal/cases/backup" "github.com/alexander-kolodka/crestic/internal/cases/handler" "github.com/alexander-kolodka/crestic/internal/cron" "github.com/alexander-kolodka/crestic/internal/healthchecks" "github.com/alexander-kolodka/crestic/internal/restic" "github.com/alexander-kolodka/crestic/internal/shell" - "github.com/spf13/cobra" ) var cronCmd = &cobra.Command{ diff --git a/cmd/exec.go b/cmd/exec.go index 493d628..8725f56 100644 --- a/cmd/exec.go +++ b/cmd/exec.go @@ -3,11 +3,12 @@ package cmd import ( "errors" + "github.com/spf13/cobra" + "github.com/alexander-kolodka/crestic/internal/cases/exec" "github.com/alexander-kolodka/crestic/internal/cases/handler" "github.com/alexander-kolodka/crestic/internal/restic" "github.com/alexander-kolodka/crestic/internal/shell" - "github.com/spf13/cobra" ) var execCmd = &cobra.Command{ diff --git a/cmd/forget.go b/cmd/forget.go index 4014430..ecc540f 100644 --- a/cmd/forget.go +++ b/cmd/forget.go @@ -1,11 +1,12 @@ package cmd import ( + "github.com/spf13/cobra" + "github.com/alexander-kolodka/crestic/internal/cases/forget" "github.com/alexander-kolodka/crestic/internal/cases/handler" "github.com/alexander-kolodka/crestic/internal/restic" "github.com/alexander-kolodka/crestic/internal/shell" - "github.com/spf13/cobra" ) var forgetCmd = &cobra.Command{ diff --git a/cmd/helpers.go b/cmd/helpers.go index 09452c3..ba1c33a 100644 --- a/cmd/helpers.go +++ b/cmd/helpers.go @@ -4,10 +4,11 @@ import ( "errors" "fmt" - "github.com/alexander-kolodka/crestic/internal/entity" "github.com/rs/zerolog" "github.com/samber/lo" "github.com/spf13/cobra" + + "github.com/alexander-kolodka/crestic/internal/entity" ) // getRepos returns the list of repositories to operate on based on command flags. diff --git a/cmd/restore.go b/cmd/restore.go index 808adcc..a7241f8 100644 --- a/cmd/restore.go +++ b/cmd/restore.go @@ -1,11 +1,12 @@ package cmd import ( + "github.com/spf13/cobra" + "github.com/alexander-kolodka/crestic/internal/cases/handler" "github.com/alexander-kolodka/crestic/internal/cases/restore" "github.com/alexander-kolodka/crestic/internal/restic" "github.com/alexander-kolodka/crestic/internal/shell" - "github.com/spf13/cobra" ) var restoreCmd = &cobra.Command{ diff --git a/cmd/root.go b/cmd/root.go index 5f945ad..1dab7a4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,10 +4,11 @@ import ( "context" "errors" + "github.com/spf13/cobra" + "github.com/alexander-kolodka/crestic/internal/logger" "github.com/alexander-kolodka/crestic/internal/shell" "github.com/alexander-kolodka/crestic/internal/version" - "github.com/spf13/cobra" ) var rootCmd = &cobra.Command{ diff --git a/cmd/unlock.go b/cmd/unlock.go index ecf24d1..123aaa6 100644 --- a/cmd/unlock.go +++ b/cmd/unlock.go @@ -1,11 +1,12 @@ package cmd import ( + "github.com/spf13/cobra" + "github.com/alexander-kolodka/crestic/internal/cases/handler" "github.com/alexander-kolodka/crestic/internal/cases/unlock" "github.com/alexander-kolodka/crestic/internal/restic" "github.com/alexander-kolodka/crestic/internal/shell" - "github.com/spf13/cobra" ) var unlockCmd = &cobra.Command{ diff --git a/internal/cases/backup/healthcheck.go b/internal/cases/backup/healthcheck.go index d288c3e..4b3e9c2 100644 --- a/internal/cases/backup/healthcheck.go +++ b/internal/cases/backup/healthcheck.go @@ -3,9 +3,10 @@ package backup import ( "context" + "github.com/google/uuid" + "github.com/alexander-kolodka/crestic/internal/entity" "github.com/alexander-kolodka/crestic/internal/healthchecks" - "github.com/google/uuid" ) // newHealthcheckMw wraps a do func with Healthchecks.io monitoring. diff --git a/internal/cases/backup/mw_test.go b/internal/cases/backup/mw_test.go index cf840cf..231f7be 100644 --- a/internal/cases/backup/mw_test.go +++ b/internal/cases/backup/mw_test.go @@ -4,9 +4,10 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + "github.com/alexander-kolodka/crestic/internal/entity" "github.com/alexander-kolodka/crestic/internal/testutils" - "github.com/stretchr/testify/require" ) func TestChain(t *testing.T) { diff --git a/internal/cases/handler/chain_test.go b/internal/cases/handler/chain_test.go index 1c63606..a6e0f24 100644 --- a/internal/cases/handler/chain_test.go +++ b/internal/cases/handler/chain_test.go @@ -4,9 +4,10 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + "github.com/alexander-kolodka/crestic/internal/cases/handler" "github.com/alexander-kolodka/crestic/internal/testutils" - "github.com/stretchr/testify/require" ) func TestChain(t *testing.T) { diff --git a/internal/cases/handler/lock.go b/internal/cases/handler/lock.go index b003c32..48a8078 100644 --- a/internal/cases/handler/lock.go +++ b/internal/cases/handler/lock.go @@ -7,8 +7,9 @@ import ( "path/filepath" "sync" - "github.com/alexander-kolodka/crestic/internal/logger" "github.com/gofrs/flock" + + "github.com/alexander-kolodka/crestic/internal/logger" ) // WithLock wraps a handler with file-based locking to prevent concurrent execution. diff --git a/internal/cases/handler/panic_test.go b/internal/cases/handler/panic_test.go index 755be28..df8dafd 100644 --- a/internal/cases/handler/panic_test.go +++ b/internal/cases/handler/panic_test.go @@ -5,10 +5,11 @@ import ( "errors" "testing" - "github.com/alexander-kolodka/crestic/internal/cases/handler" - "github.com/alexander-kolodka/crestic/internal/panix" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/alexander-kolodka/crestic/internal/cases/handler" + "github.com/alexander-kolodka/crestic/internal/panix" ) func TestWithPanicRecoveryCatchesPanic(t *testing.T) { diff --git a/internal/cron/cron.go b/internal/cron/cron.go index 241d0cc..8505171 100644 --- a/internal/cron/cron.go +++ b/internal/cron/cron.go @@ -4,10 +4,11 @@ import ( "context" "time" - "github.com/alexander-kolodka/crestic/internal/entity" - "github.com/alexander-kolodka/crestic/internal/logger" "github.com/robfig/cron/v3" "github.com/samber/lo" + + "github.com/alexander-kolodka/crestic/internal/entity" + "github.com/alexander-kolodka/crestic/internal/logger" ) // FilterJobsByCron filters jobs that should run based on their cron expressions. diff --git a/internal/dto/mapper.go b/internal/dto/mapper.go index 5e75807..e0da9e9 100644 --- a/internal/dto/mapper.go +++ b/internal/dto/mapper.go @@ -4,8 +4,9 @@ import ( "fmt" "strings" - "github.com/alexander-kolodka/crestic/internal/entity" "github.com/samber/lo" + + "github.com/alexander-kolodka/crestic/internal/entity" ) func ToEntity(cfg Config) (*entity.Config, error) { diff --git a/internal/entity/options_test.go b/internal/entity/options_test.go index 075da68..18381c4 100644 --- a/internal/entity/options_test.go +++ b/internal/entity/options_test.go @@ -5,8 +5,9 @@ import ( "sort" "testing" - "github.com/alexander-kolodka/crestic/internal/entity" "github.com/google/go-cmp/cmp" + + "github.com/alexander-kolodka/crestic/internal/entity" ) func TestOptions_ToArgs(t *testing.T) { diff --git a/internal/logger/context.go b/internal/logger/context.go index 238b61d..f82c474 100644 --- a/internal/logger/context.go +++ b/internal/logger/context.go @@ -3,8 +3,9 @@ package logger import ( "context" - "github.com/alexander-kolodka/crestic/internal/entity" "github.com/rs/zerolog" + + "github.com/alexander-kolodka/crestic/internal/entity" ) // FromContext extracts logger from context or returns global logger. diff --git a/internal/shell/executor.go b/internal/shell/executor.go index 8ad96a5..f74e965 100644 --- a/internal/shell/executor.go +++ b/internal/shell/executor.go @@ -10,8 +10,9 @@ import ( "os/exec" "strings" - "github.com/alexander-kolodka/crestic/internal/logger" "github.com/samber/lo" + + "github.com/alexander-kolodka/crestic/internal/logger" ) // Executor runs shell commands with full stdout/stderr logging. From 5840eee85d76615269533e37b9598fe1afcf3aca Mon Sep 17 00:00:00 2001 From: Alexander Kolodka Date: Mon, 17 Nov 2025 19:02:31 +0700 Subject: [PATCH 5/6] Add CTRF report --- .github/workflows/ci.yaml | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 40f9b45..16256c7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,7 +1,11 @@ name: CI on: push: + branches: + - '**' pull_request: + branches: + - '**' jobs: test: @@ -11,7 +15,20 @@ jobs: - uses: actions/setup-go@v6 with: go-version: stable - - run: go test -v ./... + - run: go install github.com/ctrf-io/go-ctrf-json-reporter/cmd/go-ctrf-json-reporter@latest + - run: go test -json ./... | go-ctrf-json-reporter -output ctrf-report.json + - uses: ctrf-io/github-test-reporter@v1 + with: + report-path: 'ctrf-report.json' + + summary-report: true + annotate: true + failed-report: true + slowest-report: true + + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + if: always() lint: runs-on: ubuntu-latest From 3a18a10d5a81d6e2b2d6bc0cd49a72b74f9ab990 Mon Sep 17 00:00:00 2001 From: Alexander Kolodka Date: Mon, 17 Nov 2025 20:03:59 +0700 Subject: [PATCH 6/6] Add CODEOWNERS --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..ab3e817 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @alexander-kolodka