diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 384fa3a..0990259 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.2.3 +current_version = 1.3.0 commit = True tag = True diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..b435419 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,15 @@ +name: Lint Dockerfile + +on: push + +jobs: + linter: + runs-on: ubuntu-latest + steps: + + - uses: actions/checkout@v2 + + - name: Lint Dockerfile + uses: hadolint/hadolint-action@master + with: + dockerfile: "Dockerfile" diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 1913321..a3f9cbc 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -3,11 +3,8 @@ on: push: branches: - main - - development + - ST-* pull_request: - branches: - - main - - development jobs: build: diff --git a/.github/workflows/golang-lint.yml b/.github/workflows/golang-lint.yml new file mode 100644 index 0000000..891702b --- /dev/null +++ b/.github/workflows/golang-lint.yml @@ -0,0 +1,18 @@ +name: golangci-lint +on: + push: + branches: + - main + - ST-* + pull_request: + +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: golangci-lint + uses: golangci/golangci-lint-action@v2 + with: + version: latest diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..2a6de72 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,82 @@ +run: + concurrency: 4 + timeout: 1m + +linters-settings: + revive: + ignore-generated-header: true + severity: warning + rules: + - name: exported + severity: warning + - name: error-return + severity: warning + - name: error-naming + severity: warning + - name: if-return + severity: warning + - name: var-naming + severity: warning + - name: var-declaration + severity: warning + - name: receiver-naming + severity: warning + - name: errorf + severity: warning + - name: empty-block + severity: warning + - name: unused-parameter + severity: warning + - name: unreachable-code + severity: warning + - name: redefines-builtin-id + severity: warning + - name: superfluous-else + severity: warning + - name: unexported-return + severity: warning + - name: indent-error-flow + severity: warning + - name: blank-imports + severity: warning + - name: range + severity: warning + - name: time-naming + severity: warning + - name: context-as-argument + severity: warning + - name: context-keys-type + severity: warning + - name: indent-error-flow + severity: warning + +linters: + disable-all: true + enable: + - asciicheck + - durationcheck + - errcheck + - errorlint + - exhaustive + - gosec + - govet + - makezero + - nilerr + - rowserrcheck + - exportloopref + - sqlclosecheck + - staticcheck + - typecheck + - bodyclose + - noctx + - prealloc + - gosimple + presets: + - comment + - error + - format + - metalinter + - unused + +issues: + exclude-use-default: false \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 342ae3c..68db697 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,13 @@ FROM golang:1.16-alpine AS builder WORKDIR /go/src/github.com/vigo/statoo COPY . . -RUN apk add --no-cache git -RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o statoo . +RUN apk add --no-cache git=2.34.1-r0 \ + ca-certificates=20211220-r0 \ + && CGO_ENABLED=0 \ + GOOS=linux \ + go build -a -installsuffix cgo -o statoo . -FROM alpine:latest -RUN apk --no-cache add ca-certificates +FROM alpine:3.15 +RUN apk --no-cache add COPY --from=builder /go/src/github.com/vigo/statoo/statoo /bin/statoo ENTRYPOINT ["/bin/statoo"] \ No newline at end of file diff --git a/README.md b/README.md index f8dcfad..596add4 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ -![Version](https://img.shields.io/badge/version-1.2.3-orange.svg) +![Version](https://img.shields.io/badge/version-1.3.0-orange.svg) ![Go](https://img.shields.io/github/go-mod/go-version/vigo/statoo) [![Documentation](https://godoc.org/github.com/vigo/statoo?status.svg)](https://pkg.go.dev/github.com/vigo/statoo) [![Go Report Card](https://goreportcard.com/badge/github.com/vigo/statoo)](https://goreportcard.com/report/github.com/vigo/statoo) [![Build Status](https://travis-ci.org/vigo/statoo.svg?branch=main)](https://travis-ci.org/vigo/statoo) ![Go Build Status](https://github.com/vigo/statoo/actions/workflows/go.yml/badge.svg) -![Test Coverage](https://img.shields.io/badge/coverage-80.2%25-orange.svg) +![GolangCI-Lint Status](https://github.com/vigo/statoo/actions/workflows/golang-lint.yml/badge.svg) +![Docker Status](https://github.com/vigo/statoo/actions/workflows/docker.yml/badge.svg) +![Test Coverage](https://img.shields.io/badge/coverage-82.6%25-orange.svg) ![Docker Pulls](https://img.shields.io/docker/pulls/vigo/statoo) ![Docker Size](https://img.shields.io/docker/image-size/vigo/statoo) @@ -47,11 +49,12 @@ usage: ./statoo [-flags] URL -version display version information (X.X.X) -verbose verbose output (default: false) -header request header, multiple allowed - -t, -timeout default timeout in seconds (default: 10) + -t, -timeout default timeout in seconds (default: 10, min: 1, max: 100) -h, -help display help -j, -json provides json output -f, -find find text in response body if -json is set -a, -auth basic auth "username:password" + -s, -skip skip certificate check and hostname in that certificate (default: false) examples: @@ -59,10 +62,9 @@ usage: ./statoo [-flags] URL $ ./statoo -timeout 30 "https://ugur.ozyilmazel.com" $ ./statoo -verbose "https://ugur.ozyilmazel.com" $ ./statoo -json https://vigo.io - $ ./statoo -json -find "python" https://vigo.io + $ ./statoo -json -find "Golang" https://vigo.io $ ./statoo -header "Authorization: Bearer TOKEN" https://vigo.io $ ./statoo -header "Authorization: Bearer TOKEN" -header "X-Api-Key: APIKEY" https://vigo.io - $ ./statoo -json -find "Meetup organization" https://vigo.io $ ./statoo -auth "user:secret" https://vigo.io ``` @@ -92,45 +94,47 @@ response; "status": 200, "checked_at": "2021-05-13T18:09:26.342012Z", "elapsed": 210.587871, - "length": 1453 + "skipcc": false } ``` -- `elapsed` represents response is in milliseconds. -- `length` represents response size in bytes (*gzipped*) +`elapsed` represents response is in milliseconds. -Let’s find text inside of the response body. This feature is only available -if the `-json` flag is set! +Let’s find text inside of the response body. This feature is only available if +the `-json` flag is set! `length` represents response size in bytes +(*gzipped*) when you search something in body! ```bash -statoo -json -find "Meetup organization" https://vigo.io +statoo -json -find "Golang" https://vigo.io ``` ```json { "url": "https://vigo.io", "status": 200, - "checked_at": "2021-05-13T18:10:38.196705Z", - "elapsed": 183.128016, - "length": 1453, - "find": "Meetup organization", - "found": true + "checked_at": "2022-01-26T20:08:33.735768Z", + "elapsed": 242.93925, + "length": 7827, + "find": "Golang", + "found": true, + "skipcc": false } ``` ```bash -statoo -json -find "meetup organization" https://vigo.io # case sensitive +statoo -json -find "golang" https://vigo.io # case sensitive ``` ```json { "url": "https://vigo.io", "status": 200, - "checked_at": "2021-05-13T18:10:58.100932Z", - "elapsed": 189.403753, - "length": 1453, - "find": "meetup organization", - "found": false + "checked_at": "2022-01-26T20:14:03.487002Z", + "elapsed": 253.665083, + "length": 7827, + "find": "golang", + "found": false, + "skipcc": false } ``` @@ -168,6 +172,7 @@ rake -T rake default # show avaliable tasks (default task) rake docker:build # Build (locally) rake docker:build_and_push # Build and push to docker hub (latest) +rake docker:lint # Lint rake docker:rmi # Delete image (locally) rake docker:run # Run (locally) rake release[revision] # Release new version major,minor,patch, default: patch diff --git a/Rakefile b/Rakefile index fd21d2f..1bb116d 100644 --- a/Rakefile +++ b/Rakefile @@ -111,6 +111,11 @@ end # docker # ----------------------------------------------------------------------------- namespace :docker do + desc "Lint" + task :lint do + system "hadolint Dockerfile" + end + desc "Build (locally)" task :build do system "docker build -t statoo:latest ." diff --git a/app/app.go b/app/app.go index 43f1a08..07f709a 100644 --- a/app/app.go +++ b/app/app.go @@ -13,6 +13,7 @@ package app import ( "compress/gzip" + "context" "crypto/tls" "encoding/json" "errors" @@ -28,81 +29,67 @@ import ( "github.com/vigo/statoo/app/version" ) -const defTimeout = 10 +const ( + defTimeout = 10 + defTimeoutMin = 1 + defTimeoutMax = 100 +) -var _ flag.Value = (*HeadersFlag)(nil) -var _ flag.Value = (*BasicAuthFlag)(nil) +var ( + errEmptyHeader = errors.New("header should not be empty") + errInvalidHeader = errors.New("invalid header value") + errInvalidTimeout = errors.New("invalid timeout") +) -// HeadersFlag ... +// HeadersFlag holds header information for http request. type HeadersFlag []string func (f *HeadersFlag) String() string { return fmt.Sprintf("%s", *f) } -// Set ... +// Set appends valid header values to HeadersFlag. func (f *HeadersFlag) Set(value string) error { value = strings.TrimSpace(value) if value == "" { - return errors.New("header should not be empty") + return errEmptyHeader } if strings.Count(value, ":") != 1 { - return fmt.Errorf("invalind header data: %s", value) + return fmt.Errorf("%w: %s", errInvalidHeader, value) } if len(strings.FieldsFunc(value, func(c rune) bool { return c == ':' })) != 2 { - return fmt.Errorf("invalind header data: %s", value) + return fmt.Errorf("%w: %s", errInvalidHeader, value) } *f = append(*f, value) return nil } -// BasicAuthFlag ... -type BasicAuthFlag string - -// Set ... -func (f *BasicAuthFlag) Set(value string) error { - value = strings.TrimSpace(value) - if value == "" { - return errors.New("auth flag should not be empty") - } - if strings.Count(value, ":") != 1 { - return fmt.Errorf("invalind auth data: %s", value) - } - if len(strings.FieldsFunc(value, func(c rune) bool { return c == ':' })) != 2 { - return fmt.Errorf("invalind auth data: %s", value) - } - - *f = BasicAuthFlag(value) - return nil -} - -func (f *BasicAuthFlag) String() string { - return string(*f) -} - var ( - // ArgURL holds URL input from command-line + // ArgURL holds URL input from command-line. ArgURL string - // OptVersionInformation holds boolean for displaying version information + // OptVersionInformation holds boolean for displaying version information. OptVersionInformation *bool - // OptTimeout holds default timeout for network transport operations + // OptTimeout holds default timeout for network transport operations. OptTimeout *int - // OptVerboseOutput holds boolean for displaying detailed output + // OptVerboseOutput holds boolean for displaying detailed output. OptVerboseOutput *bool - // OptJSONOutput holds boolean for json response instead of text + // OptJSONOutput holds boolean for json response instead of text. OptJSONOutput *bool - // OptHeaders holds custom request header key:value + // OptHeaders holds custom request header key:value. OptHeaders HeadersFlag - // OptFind holds lookup string in the body of the response + // OptFind holds lookup string in the body of the response. OptFind *string - // OptBasicAuth holds basic auth key:value credentials - OptBasicAuth BasicAuthFlag + // OptBasicAuth holds basic auth key:value credentials. + OptBasicAuth *string + + // OptInsecureSkipVerify holds certificate check option. + OptInsecureSkipVerify *bool usage = ` usage: %[1]s [-flags] URL @@ -110,13 +97,14 @@ usage: %[1]s [-flags] URL flags: -version display version information (%s) - -verbose verbose output (default: false) + -verbose verbose output (default: false) -header request header, multiple allowed, "Key: Value" - -t, -timeout default timeout in seconds (default: %d) + -t, -timeout default timeout in seconds (default: %d, min: %d, max: %d) -h, -help display help -j, -json provides json output -f, -find find text in response body if -json is set -a, -auth basic auth "username:password" + -s, -skip skip certificate check and hostname in that certificate (default: false) examples: @@ -127,7 +115,7 @@ usage: %[1]s [-flags] URL $ %[1]s -json -find "python" https://vigo.io $ %[1]s -header "Authorization: Bearer TOKEN" https://vigo.io $ %[1]s -header "Authorization: Bearer TOKEN" -header "X-Api-Key: APIKEY" https://vigo.io - $ %[1]s -json -find "Meetup organization" https://vigo.io + $ %[1]s -json -find "Golang" https://vigo.io $ %[1]s -auth "user:secret" https://vigo.io ` @@ -135,26 +123,27 @@ usage: %[1]s [-flags] URL { local cur next cur="${COMP_WORDS[COMP_CWORD]}" - opts="-a -auth -f -find -header -h -help -j -json -t -timeout -verbose -version" + opts="-a -auth -f -find -header -h -help -j -json -t -timeout -s -skip -verbose -version" COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) } complete -F __statoo_comp statoo` ) -// CLIApplication represents app structure +// CLIApplication represents app structure. type CLIApplication struct { Out io.Writer } -// JSONResponse represents data structure of json response +// JSONResponse represents data structure of json response. type JSONResponse struct { - URL string `json:"url"` - Status int `json:"status"` - CheckedAt time.Time `json:"checked_at"` - Elapsed float64 `json:"elapsed,omitempty"` - Length int `json:"length,omitempty"` - Find *string `json:"find,omitempty"` - Found *bool `json:"found,omitempty"` + URL string `json:"url"` + Status int `json:"status"` + CheckedAt time.Time `json:"checked_at"` + Elapsed float64 `json:"elapsed,omitempty"` + Length int `json:"length,omitempty"` + Find *string `json:"find,omitempty"` + Found *bool `json:"found,omitempty"` + SkipCertificateCheck *bool `json:"skipcc,omitempty"` } func trimSpaces(s []string) { @@ -163,14 +152,26 @@ func trimSpaces(s []string) { } } -// NewCLIApplication creates new CLIApplication instance -func NewCLIApplication() *CLIApplication { - flag.Usage = func() { - // w/o os.Stdout, you need to pipe out via - // cmd &> /path/to/file - fmt.Fprintf(os.Stdout, usage, os.Args[0], version.Version, defTimeout) - os.Exit(0) +func flagUsage(code int) func() { + return func() { + fmt.Fprintf( + os.Stdout, + usage, + os.Args[0], + version.Version, + defTimeout, + defTimeoutMin, + defTimeoutMax, + ) + if code > 0 { + os.Exit(code) + } } +} + +// NewCLIApplication creates new CLIApplication instance. +func NewCLIApplication() *CLIApplication { + flag.Usage = flagUsage(0) OptVersionInformation = flag.Bool("version", false, fmt.Sprintf("display version information (%s)", version.Version)) OptVerboseOutput = flag.Bool("verbose", false, "verbose output") @@ -190,8 +191,12 @@ func NewCLIApplication() *CLIApplication { flag.Var(&OptHeaders, "header", "") helpBasicAuth := "basic auth \"username:password\"" - flag.Var(&OptBasicAuth, "auth", helpBasicAuth) - flag.Var(&OptBasicAuth, "a", helpBasicAuth+" (short)") + OptBasicAuth = flag.String("auth", "", helpBasicAuth) + flag.StringVar(OptBasicAuth, "a", "", helpBasicAuth+" (short)") + + helpOptInsecureSkipVerify := "skip certificate check and hostname in that certificate" + OptInsecureSkipVerify = flag.Bool("skip", false, helpOptInsecureSkipVerify) + flag.BoolVar(OptInsecureSkipVerify, "s", false, helpOptInsecureSkipVerify+" (short)") flag.Parse() @@ -202,7 +207,7 @@ func NewCLIApplication() *CLIApplication { } } -// Run executes main application +// Run executes main application. func (c *CLIApplication) Run() error { if *OptVersionInformation { fmt.Fprintln(c.Out, version.Version) @@ -216,26 +221,31 @@ func (c *CLIApplication) Run() error { return c.Validate() } -// Validate runs validations for flags +// Validate runs validations for flags. func (c *CLIApplication) Validate() error { + if len(ArgURL) == 0 { + flagUsage(-1)() + return nil + } + _, err := url.ParseRequestURI(ArgURL) if err != nil { - return fmt.Errorf(err.Error()) + return fmt.Errorf("url parse error: %w", err) } if *OptTimeout > 100 || *OptTimeout < 1 { - return fmt.Errorf("invalid timeout value: %d", *OptTimeout) + return fmt.Errorf("%w: %d", errInvalidTimeout, *OptTimeout) } return c.GetResult() } -// GetResult fetches the status information of given URL +// GetResult fetches the status information of given URL. func (c *CLIApplication) GetResult() error { tr := http.DefaultTransport.(*http.Transport).Clone() tr.MaxIdleConns = 10 tr.IdleConnTimeout = 30 * time.Second tr.DisableCompression = true - tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: *OptInsecureSkipVerify} //nolint timeout := time.Duration(*OptTimeout) * time.Second client := &http.Client{ @@ -243,9 +253,10 @@ func (c *CLIApplication) GetResult() error { Timeout: timeout, } - req, err := http.NewRequest("GET", ArgURL, nil) + ctx := context.Background() + req, err := http.NewRequestWithContext(ctx, "GET", ArgURL, nil) if err != nil { - return fmt.Errorf("error: %v", err) + return fmt.Errorf("request error: %w", err) } req.Header.Set("Accept-Encoding", "gzip") @@ -258,35 +269,37 @@ func (c *CLIApplication) GetResult() error { } } - if OptBasicAuth.String() != "" { - words := strings.Split(OptBasicAuth.String(), ":") - trimSpaces(words) // remove spaces, foo : bar => foo:bar + if *OptBasicAuth != "" { + words := strings.Split(*OptBasicAuth, ":") + trimSpaces(words) + fmt.Println("words", words) req.SetBasicAuth(words[0], words[1]) } start := time.Now() resp, err := client.Do(req) if err != nil { - return fmt.Errorf("error: %v", err) + return fmt.Errorf("response error: %w", err) } elapsed := time.Since(start) if resp.Body != nil { defer func() { - if err := resp.Body.Close(); err != nil { - fmt.Fprintln(os.Stderr, err.Error()) + if errClose := resp.Body.Close(); err != nil { + fmt.Fprintln(os.Stderr, errClose.Error()) } }() } if *OptJSONOutput { js := &JSONResponse{ - URL: ArgURL, - Status: resp.StatusCode, - CheckedAt: time.Now().UTC(), - Elapsed: float64(elapsed) / float64(time.Millisecond), - Find: nil, - Found: nil, + URL: ArgURL, + Status: resp.StatusCode, + CheckedAt: time.Now().UTC(), + Elapsed: float64(elapsed) / float64(time.Millisecond), + Find: nil, + Found: nil, + SkipCertificateCheck: OptInsecureSkipVerify, } if *OptFind != "" { @@ -296,7 +309,7 @@ func (c *CLIApplication) GetResult() error { case "gzip": bodyReader, err = gzip.NewReader(resp.Body) if err != nil { - return fmt.Errorf("body read (gzip) error: %v", err) + return fmt.Errorf("body read (gzip) error: %w", err) } defer func() { if err := bodyReader.Close(); err != nil { @@ -318,12 +331,12 @@ func (c *CLIApplication) GetResult() error { j, err := json.Marshal(js) if err != nil { - return fmt.Errorf("error: %v", err) + return fmt.Errorf("json marshal error: %w", err) } _, err = c.Out.Write(j) if err != nil { - return fmt.Errorf("error: %v", err) + return fmt.Errorf("write error: %w", err) } return nil } diff --git a/app/app_test.go b/app/app_test.go index 1f3fdc8..604f0b3 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -24,6 +24,10 @@ func TestCustomHeadersFlag(t *testing.T) { flags.Init("test", flag.ContinueOnError) flags.Var(&h, "header", "usage") + + if err := flags.Parse([]string{"-header="}); err == nil { + t.Error(err) + } if err := flags.Parse([]string{"-header=foobar"}); err == nil { t.Error(err) } @@ -33,25 +37,13 @@ func TestCustomHeadersFlag(t *testing.T) { if err := flags.Parse([]string{"-header=foo.bar", "-header=foobar"}); err == nil { t.Error(err) } - if err := flags.Parse([]string{"-header=foo:bar"}); err != nil { - t.Error(err) - } -} - -func TestCustomAuthFlag(t *testing.T) { - var flags flag.FlagSet - var a app.BasicAuthFlag - - flags.Init("test", flag.ContinueOnError) - flags.Var(&a, "auth", "usage") - flags.Var(&a, "a", "usage") - if err := flags.Parse([]string{"-a=foobar"}); err == nil { + if err := flags.Parse([]string{"-header=foo;bar"}); err == nil { t.Error(err) } - if err := flags.Parse([]string{"-auth=foo-bar"}); err == nil { + if err := flags.Parse([]string{"-header=foo:bar:baz"}); err == nil { t.Error(err) } - if err := flags.Parse([]string{"-auth=foo:bar"}); err != nil { + if err := flags.Parse([]string{"-header=foo:bar"}); err != nil { t.Error(err) } } @@ -62,7 +54,8 @@ type gzipResponseWriter struct { } func (w gzipResponseWriter) Write(b []byte) (int, error) { - return w.Writer.Write(b) + n, err := w.Writer.Write(b) + return n, fmt.Errorf("gzip error: %w", err) } func gzipWrapper(handler http.Handler) http.Handler { @@ -88,6 +81,14 @@ func TestResponse(t *testing.T) { cmd := app.NewCLIApplication() + t.Run("test empty URL arg", func(t *testing.T) { + app.ArgURL = "" + cmd.Out = new(bytes.Buffer) + if err := cmd.Run(); err != nil { + t.Errorf("want: nil, got: %v", err) + } + }) + t.Run("test fake 200 response", func(t *testing.T) { cmd.Out = new(bytes.Buffer) ts := httptest.NewServer(handler) @@ -175,28 +176,35 @@ func TestResponse(t *testing.T) { app.OptFind = nil }) - t.Run("test empty URL arg", func(t *testing.T) { - app.ArgURL = "" + t.Run("test URL w/o prefix", func(t *testing.T) { + app.ArgURL = "vigo.io" cmd.Out = new(bytes.Buffer) - if got := cmd.Run(); got.Error() != "parse \"\": empty url" { - t.Errorf("got: %v", got) + + want := "url parse error: parse \"vigo.io\": invalid URI for request" + if got := cmd.Run(); got.Error() != want { + t.Errorf("want: %v, got: %v", want, got) } }) - t.Run("test URL w/o prefix", func(t *testing.T) { - app.ArgURL = "vigo.io" + t.Run("set errorious timeout max", func(t *testing.T) { + *app.OptTimeout = 200 + app.ArgURL = "https://vigo.io" cmd.Out = new(bytes.Buffer) - if got := cmd.Run(); got.Error() != "parse \"vigo.io\": invalid URI for request" { - t.Errorf("got: %v", got) + + want := "invalid timeout: 200" + if got := cmd.Run(); got.Error() != want { + t.Errorf("want: %v, got: %v", want, got) } }) - t.Run("set errorious timeout", func(t *testing.T) { - *app.OptTimeout = 200 + t.Run("set errorious timeout min", func(t *testing.T) { + *app.OptTimeout = 0 app.ArgURL = "https://vigo.io" cmd.Out = new(bytes.Buffer) - if got := cmd.Run(); got.Error() != "invalid timeout value: 200" { - t.Errorf("want nil, got: %v", got) + + want := "invalid timeout: 0" + if got := cmd.Run(); got.Error() != want { + t.Errorf("want: %v, got: %v", want, got) } }) diff --git a/app/version/version.go b/app/version/version.go index a220aef..9dfbe2a 100644 --- a/app/version/version.go +++ b/app/version/version.go @@ -1,4 +1,4 @@ package version -// Version is the current version of statoo -const Version string = "1.2.3" +// Version is the current version of statoo. +const Version string = "1.3.0"