Repository: ariga/atlas
Pull request #341 (2021-12-23 20:35:30)

cmd/action: update version


Patch:
diff --git a/cmd/action/internal/update/update.go b/cmd/action/internal/update/update.go
new file mode 100644
index 00000000..ab98ba5c
--- /dev/null
+++ b/cmd/action/internal/update/update.go
@@ -0,0 +1,149 @@
+// Copyright 2021-present The Atlas Authors. All rights reserved.
+// This source code is licensed under the Apache 2.0 license found
+// in the LICENSE file in the root directory of this source tree.
+
+package update
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"os"
+	"path/filepath"
+	"time"
+
+	"github.com/mitchellh/go-homedir"
+	"golang.org/x/mod/semver"
+)
+
+type (
+	// Store is where the latest release data is stored on CLI host.
+	Store struct {
+		Version   string    `json:"version"`
+		URL       string    `json:"url"`
+		CheckedAt time.Time `json:"checkedat"`
+	}
+
+	// LatestRelease holds the required fields from https://api.github.com/repos/ariga/atlas/releases/latest.
+	LatestRelease struct {
+		Version string `json:"tag_name"`
+		URL     string `json:"html_url"`
+	}
+)
+
+// CheckForUpdate implements a notification to the user when a later release is available
+// 1. Check release file ~/.atlas/release.json for latest known release and poll time
+// 2. If last poll was more than 24h, poll GitHub public API https://docs.github.com/en/rest/reference/releases#get-the-latest-release
+// 3. Store the latest release metadata
+// 4. If current build Version, that is not development, is lower than the latest release, notify user
+func CheckForUpdate(version string, logF func(i ...interface{})) {
+	if !enabled(version) {
+		return
+	}
+	p, err := homedir.Expand("~/.atlas")
+	if err != nil {
+		return
+	}
+	ok, message, err := shouldUpdate(version, p, latestReleaseFromGithub)
+	if err != nil || !ok {
+		return
+	}
+	logF(message)
+}
+
+func enabled(version string) bool {
+	if _, ok := os.LookupEnv("ATLAS_NO_UPDATE_NOTIFIER"); ok {
+		return false
+	}
+	if _, ok := os.LookupEnv("GITHUB_ACTIONS"); ok {
+		return false
+	}
+	if ok := semver.IsValid(version); !ok {
+		return false
+	}
+	return true
+}
+
+func shouldUpdate(version string, path string, latestReleaseF func() (LatestRelease, error)) (bool, string, error) {
+	r := localStore(path)
+	if shouldSkip(r) {
+		return false, "", nil
+	}
+	l, err := latestReleaseF()
+	if err != nil {
+		return false, "", err
+	}
+	if err := saveStore(path, l, time.Now()); err != nil {
+		return false, "", err
+	}
+	if semver.Compare(version, l.Version) != -1 {
+		return false, "", nil
+	}
+	return true, fmt.Sprintf("new atlas version %s, %s", l.Version, l.URL), nil
+}
+
+func latestReleaseFromGithub() (LatestRelease, error) {
+	req, err := http.NewRequest(http.MethodGet, "https://api.github.com/repos/ariga/atlas/releases/latest", nil)
+	if err != nil {
+		return LatestRelease{}, err
+	}
+	req.Header.Set("Content-Type", "application/json; charset=utf-8")
+	req.Header.Set("Accept", "application/vnd.github.v3+json")
+	// https://docs.github.com/en/rest/overview/resources-in-the-rest-api#user-agent-required
+	req.Header.Set("User-Agent", "Ariga-Atlas-CLI")
+	client := &http.Client{Timeout: time.Second * 10}
+	resp, err := client.Do(req)
+	if err != nil {
+		return LatestRelease{}, err
+	}
+	defer resp.Body.Close()
+	data, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return LatestRelease{}, err
+	}
+	if resp.StatusCode != http.StatusOK {
+		return LatestRelease{}, fmt.Errorf("update: failed to fetch latest release version")
+	}
+	var b LatestRelease
+	if err := json.Unmarshal(data, &b); err != nil {
+		return LatestRelease{}, err
+	}
+	return b, nil
+}
+
+func shouldSkip(r *Store) bool {
+	if r == nil {
+		return false
+	}
+	return time.Since(r.CheckedAt).Hours() < 24
+}
+
+func localStore(path string) *Store {
+	b, err := ioutil.ReadFile(fileLocation(path))
+	if err != nil {
+		return nil
+	}
+	var s Store
+	if err := json.Unmarshal(b, &s); err != nil {
+		return nil
+	}
+	return &s
+}
+
+func saveStore(path string, l LatestRelease, t time.Time) error {
+	s := Store{Version: l.Version, URL: l.URL, CheckedAt: t}
+	err := os.MkdirAll(path, 0755)
+	if err != nil {
+		return err
+	}
+	b, err := json.Marshal(&s)
+	if err != nil {
+		return err
+	}
+	return ioutil.WriteFile(fileLocation(path), b, 0644)
+}
+
+func fileLocation(p string) string {
+	return filepath.Join(p, "release.json")
+}
diff --git a/cmd/action/root.go b/cmd/action/root.go
index 8d1111a0..807a9f17 100644
--- a/cmd/action/root.go
+++ b/cmd/action/root.go
@@ -1,9 +1,17 @@
 package action
 
-import "github.com/spf13/cobra"
+import (
+	"ariga.io/atlas/cmd/action/internal/update"
+	"github.com/spf13/cobra"
+)
 
 // RootCmd represents the base command when called without any subcommands
 var RootCmd = &cobra.Command{
 	Use:   "atlas",
 	Short: "A database toolkit.",
 }
+
+// CheckForUpdate exposes internal update logic to CLI
+func CheckForUpdate() {
+	update.CheckForUpdate(version, RootCmd.PrintErrln)
+}
diff --git a/cmd/action/version.go b/cmd/action/version.go
index 89dee2ea..28aac516 100644
--- a/cmd/action/version.go
+++ b/cmd/action/version.go
@@ -7,7 +7,7 @@ import (
 	"golang.org/x/mod/semver"
 )
 
-// Version is the atlas CLI build version
+// version is the atlas CLI build version
 // Should be set by build script "-X 'ariga.io/atlas/cmd/action.version=${version}'"
 var version string
 
diff --git a/cmd/atlas/main.go b/cmd/atlas/main.go
index 8cdb2051..617d804d 100644
--- a/cmd/atlas/main.go
+++ b/cmd/atlas/main.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"fmt"
 	"os"
 
 	"ariga.io/atlas/cmd/action"
@@ -11,7 +12,18 @@ import (
 
 func main() {
 	cobra.OnInitialize(initConfig)
-	cobra.CheckErr(action.RootCmd.Execute())
+	err := action.RootCmd.Execute()
+	// Print error from command
+	if err != nil {
+		fmt.Fprintln(os.Stderr, "Error:", err)
+	}
+	// Check for update
+	action.CheckForUpdate()
+	// Exit code according to command success
+	if err != nil {
+		os.Exit(1)
+	}
+
 }
 
 // initConfig reads in config file and ENV variables if set.
diff --git a/go.mod b/go.mod
index 1b4a9eb4..bed4d890 100644
--- a/go.mod
+++ b/go.mod
@@ -11,7 +11,7 @@ require (
 	github.com/lib/pq v1.10.3
 	github.com/manifoldco/promptui v0.9.0
 	github.com/spf13/cobra v1.2.1
-	github.com/stretchr/testify v1.7.0
+	github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942
 	github.com/zclconf/go-cty v1.8.0
 	golang.org/x/mod v0.4.3-0.20210512182355-6088ed88cecd
 	golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
@@ -19,7 +19,11 @@ require (
 	gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
 )
 
-require github.com/go-openapi/inflect v0.19.0
+require (
+	github.com/go-openapi/inflect v0.19.0
+	github.com/mitchellh/go-homedir v1.0.0
+	github.com/pelletier/go-toml/v2 v2.0.0-beta.4
+)
 
 require (
 	github.com/agext/levenshtein v1.2.1 // indirect
diff --git a/go.sum b/go.sum
index 922c2ab1..89014137 100644
--- a/go.sum
+++ b/go.sum
@@ -67,7 +67,6 @@ github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnht
 github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
 github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
-github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
 github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -210,6 +209,7 @@ github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaO
 github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
 github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
+github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0=
 github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
 github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
@@ -223,7 +223,10 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ
 github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ=
 github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
+github.com/pelletier/go-toml/v2 v2.0.0-beta.4 h1:GCs8ebsDtEH3RiO78+BvhHqj65d/I6tjESitJZc07Rc=
+github.com/pelletier/go-toml/v2 v2.0.0-beta.4/go.mod h1:ke6xncR3W76Ba8xnVxkrZG0js6Rd2BsQEAYrfgJ6eQA=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -232,13 +235,11 @@ github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndr
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
-github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
 github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
 github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
-github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
@@ -257,8 +258,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942 h1:t0lM6y/M5IiUZyvbBTcngso8SZEZICH7is9B6g/obVU=
+github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
 github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
 github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
@@ -615,7 +617,6 @@ gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
diff --git a/internal/integration/go.mod b/internal/integration/go.mod
index c1e176e7..a04ad032 100644
--- a/internal/integration/go.mod
+++ b/internal/integration/go.mod
@@ -11,14 +11,13 @@ require (
 	github.com/hashicorp/hcl/v2 v2.10.1 // indirect
 	github.com/lib/pq v1.10.4
 	github.com/mattn/go-sqlite3 v1.14.9
-	github.com/stretchr/testify v1.7.0
+	github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942
 	github.com/zclconf/go-cty v1.10.0 // indirect
 )
 
 require (
 	github.com/agext/levenshtein v1.2.3 // indirect
 	github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
-	github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/go-openapi/inflect v0.19.0 // indirect
 	github.com/google/go-cmp v0.5.6 // indirect
diff --git a/internal/integration/go.sum b/internal/integration/go.sum
index 7cf0b599..0f4effb5 100644
--- a/internal/integration/go.sum
+++ b/internal/integration/go.sum
@@ -252,6 +252,7 @@ github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
 github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
+github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0=
 github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
@@ -275,6 +276,8 @@ github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
+github.com/pelletier/go-toml/v2 v2.0.0-beta.4 h1:GCs8ebsDtEH3RiO78+BvhHqj65d/I6tjESitJZc07Rc=
+github.com/pelletier/go-toml/v2 v2.0.0-beta.4/go.mod h1:ke6xncR3W76Ba8xnVxkrZG0js6Rd2BsQEAYrfgJ6eQA=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
@@ -328,8 +331,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942 h1:t0lM6y/M5IiUZyvbBTcngso8SZEZICH7is9B6g/obVU=
+github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=


Test patch:
diff --git a/cmd/action/internal/update/update_test.go b/cmd/action/internal/update/update_test.go
new file mode 100644
index 00000000..86a4268b
--- /dev/null
+++ b/cmd/action/internal/update/update_test.go
@@ -0,0 +1,83 @@
+package update
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestCheckForUpdate(t *testing.T) {
+	url := "https://github.com/ariga/atlas/releases/tag/test"
+	tests := []struct {
+		name           string
+		currentVersion string
+		latestVersion  string
+		store          *Store
+		latestRelease  *LatestRelease
+		shouldNotify   bool
+		message        string
+	}{
+		{
+			name:           "stale store fetch and update newer version",
+			currentVersion: "v1.2.3",
+			latestVersion:  "v2.2.3",
+			store:          &Store{Version: "v1.2.3", URL: url, CheckedAt: time.Now().Add(-25 * time.Hour)},
+			latestRelease:  &LatestRelease{Version: "v2.2.3", URL: url},
+			shouldNotify:   true,
+			message:        fmt.Sprintf("new atlas version v2.2.3, %s", url),
+		},
+		{
+			name:           "fresh store do nothing",
+			currentVersion: "v1.2.3",
+			latestVersion:  "v2.2.3",
+			store:          &Store{Version: "v1.2.3", URL: url, CheckedAt: time.Now().Add(-23 * time.Hour)},
+			shouldNotify:   false,
+		},
+		{
+			name:           "no store fetch and update newer version",
+			currentVersion: "v1.2.3",
+			latestVersion:  "v2.2.3",
+			latestRelease:  &LatestRelease{Version: "v2.2.3", URL: url},
+			shouldNotify:   true,
+			message:        fmt.Sprintf("new atlas version v2.2.3, %s", url),
+		},
+		{
+			name:           "no store fetch and update newer minor version",
+			currentVersion: "v1.2.3",
+			latestVersion:  "v1.3.3",
+			latestRelease:  &LatestRelease{Version: "v2.2.3", URL: url},
+			shouldNotify:   true,
+			message:        fmt.Sprintf("new atlas version v2.2.3, %s", url),
+		},
+		{
+			name:           "no store fetch and update newer patch version",
+			currentVersion: "v1.2.3",
+			latestVersion:  "v1.2.4",
+			latestRelease:  &LatestRelease{Version: "v2.2.3", URL: url},
+			shouldNotify:   true,
+			message:        fmt.Sprintf("new atlas version v2.2.3, %s", url),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			p := t.TempDir()
+			if tt.store != nil {
+				b, err := json.Marshal(tt.store)
+				require.NoError(t, err)
+				require.NoError(t, ioutil.WriteFile(fileLocation(p), b, 0600))
+			}
+			var f func() (LatestRelease, error)
+			if tt.latestRelease != nil {
+				f = func() (LatestRelease, error) { return *tt.latestRelease, nil }
+			}
+			ok, m, err := shouldUpdate(tt.currentVersion, p, f)
+			require.NoError(t, err)
+			require.Equal(t, tt.shouldNotify, ok)
+			require.Equal(t, tt.message, m)
+		})
+	}
+}
diff --git a/internal/integration/integration_test.go b/internal/integration/integration_test.go
index c4f5cb81..d42ccfc1 100644
--- a/internal/integration/integration_test.go
+++ b/internal/integration/integration_test.go
@@ -197,6 +197,7 @@ func TestCLI_Version(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
+			t.Setenv("ATLAS_NO_UPDATE_NOTIFIER", "true")
 			stdout := bytes.NewBuffer(nil)
 			tt.cmd.Stdout = stdout
 			require.NoError(t, tt.cmd.Run())