Skip to content

Commit

Permalink
Feature/version info command (#24)
Browse files Browse the repository at this point in the history
* Initial implementation of the version command
* Updates the VersionString constant (and uses it in the init_command.go module)
* Changes the Go version used in the pipeline
* The version command can now confirm the latest version
  • Loading branch information
matzefriedrich authored Sep 9, 2024
1 parent 21ab333 commit 9711b0b
Show file tree
Hide file tree
Showing 10 changed files with 292 additions and 4 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- name: Setup Go environment
uses: actions/setup-go@v4
with:
go-version: '1.22'
go-version: '1.23'

- name: Test
run: go test -v ./...
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

* Added a `version` command to display the current version of the Parsley CLI application. The command can also check for new versions by querying the latest release information from GitHub, notifying users if an update is available and providing instructions on how to update.

### Fixed

* Fixes matching of expected arguments in `mock_verify.go`
Expand Down
3 changes: 2 additions & 1 deletion cmd/parsley-cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ func main() {
app := charmer.NewCommandLineApplication("parsley-cli", description)

app.AddCommand(
commands.NewInitCommand())
commands.NewInitCommand(),
commands.NewVersionCommand())

app.AddGroupCommand(
commands.NewGenerateGroupCommand(),
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/matzefriedrich/parsley
go 1.23

require (
github.com/hashicorp/go-version v1.7.0
github.com/matzefriedrich/cobra-extensions v0.2.6
github.com/pkg/errors v0.9.1
github.com/spf13/cobra v1.8.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/matzefriedrich/cobra-extensions v0.2.6 h1:tn4w3lpEGu7eMmk4mrHF+jNdTUQKT/8P6Y7q14ZLfFI=
Expand Down
5 changes: 3 additions & 2 deletions internal/commands/init_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package commands

import (
"fmt"
"github.com/matzefriedrich/parsley/internal/utils"
"os"

"github.com/matzefriedrich/cobra-extensions/pkg"
Expand All @@ -23,9 +24,9 @@ func (g *initCommand) Execute() {
return
}

const minVersion = "v0.9.1"
minVersion, _ := utils.ApplicationVersion()
const packageName = "github.com/matzefriedrich/parsley"
dependencyErr := p.AddDependency(packageName, minVersion)
dependencyErr := p.AddDependency(packageName, minVersion.String())
if dependencyErr != nil {
fmt.Println(err)
return
Expand Down
60 changes: 60 additions & 0 deletions internal/commands/version_command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package commands

import (
"context"
"fmt"
"github.com/matzefriedrich/cobra-extensions/pkg"
"github.com/matzefriedrich/cobra-extensions/pkg/abstractions"
"github.com/matzefriedrich/parsley/internal/utils"
"github.com/spf13/cobra"
)

type versionCommand struct {
use abstractions.CommandName `flag:"version" short:"Show the current Parsley CLI version"`
CheckForUpdate bool `flag:"check-update" usage:"Checks for available updates and prints the update command"`
}

func (v *versionCommand) Execute() {

appVersion, appVersionErr := utils.ApplicationVersion()
if appVersionErr == nil {
fmt.Printf("Parsley CLI v%s\n", appVersion.String())
}

if v.CheckForUpdate == false {
return
}

githubClient := utils.NewGitHubApiClient()
release, err := githubClient.QueryLatestReleaseTag(context.Background())
if err != nil {
return
}

releaseVersion, releaseVersionErr := release.TryParseVersionFromTag()
if appVersionErr == nil && releaseVersionErr == nil {
if appVersion.LessThan(*releaseVersion) {

fmt.Printf("\n"+
"Your version of Parsley CLI is out of date!\n\n"+
"The latest version is: v%s.\n"+
"To update run the following command: "+
"go install github.com/matzefriedrich/parsley/cmd/parsley-cli@v%s\n\n", releaseVersion.String(), releaseVersion.String())

fmt.Printf("More information about the release %s is available at:\n%s\n", release.Name, release.HtmlUrl)

} else if appVersion.Equal(*releaseVersion) {

fmt.Printf("\n" +
"You are using the latest version of Parsley CLI.\n\n")

}
}
}

var _ pkg.TypedCommand = (*versionCommand)(nil)

func NewVersionCommand() *cobra.Command {
command := &versionCommand{}
return pkg.CreateTypedCommand(command)
}
55 changes: 55 additions & 0 deletions internal/tests/core/version_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package core

import (
"github.com/matzefriedrich/parsley/internal/utils"
"github.com/stretchr/testify/assert"
"testing"
)

func Test_Version_parse_version_from_github_release(t *testing.T) {

// Arrange
const version = "1.2.3"
release := utils.GithubRelease{TagName: version}

const expectedVersionString = "1.2.3"

// Act
actual, err := release.TryParseVersionFromTag()

// Assert
assert.NoError(t, err)
assert.Equal(t, expectedVersionString, actual.String())
}

func Test_Version_parse_prefixed_version_from_github_release(t *testing.T) {

// Arrange
const version = "v1.2.3"
release := utils.GithubRelease{TagName: version}

const expectedVersionString = "1.2.3"

// Act
actual, err := release.TryParseVersionFromTag()

// Assert
assert.NoError(t, err)
assert.Equal(t, expectedVersionString, actual.String())
}

func Test_Version_parse_prefixed_prerelease_version_from_github_release(t *testing.T) {

// Arrange
const version = "v1.2.3-alpha.1"
release := utils.GithubRelease{TagName: version}

const expectedVersionString = "1.2.3"

// Act
actual, err := release.TryParseVersionFromTag()

// Assert
assert.NoError(t, err)
assert.Equal(t, expectedVersionString, actual.String())
}
85 changes: 85 additions & 0 deletions internal/utils/github_api_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package utils

import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
)

type GithubRelease struct {
Id uint64 `json:"id"`
TagName string `json:"tag_name"`
Name string `json:"name"`
HtmlUrl string `json:"html_url"`
PublishedAt time.Time `json:"published_at"`
}

func (r GithubRelease) TryParseVersionFromTag() (*VersionInfo, error) {
version := r.TagName
return tryParseVersionInfo(version)
}

type githubApiClient struct {
options HttpClientOptions
}

type HttpClientOptions struct {
RequestTimeout time.Duration
}

type HttpClientOptionsFunc func(*HttpClientOptions)

func NewGitHubApiClient(config ...HttpClientOptionsFunc) *githubApiClient {
options := HttpClientOptions{
RequestTimeout: 5 * time.Second,
}
for _, optionsFunc := range config {
optionsFunc(&options)
}
return &githubApiClient{
options: options,
}
}

// QueryLatestReleaseTag Queries the latest version from the GitHub releases endpoint and compares it against the current application version.
func (c *githubApiClient) QueryLatestReleaseTag(ctx context.Context) (*GithubRelease, error) {

const owner = "matzefriedrich"
const repo = "parsley"

requestCtx, cancel := context.WithTimeout(ctx, c.options.RequestTimeout)
defer cancel()

url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases", owner, repo)
request, err := http.NewRequestWithContext(requestCtx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}

client := &http.Client{}
response, requestErr := client.Do(request)
if requestErr != nil {
return nil, requestErr
}

defer response.Body.Close()

if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to fetch latest release: %s", response.Status)
}

var releases []GithubRelease
if unmarshalErr := json.NewDecoder(response.Body).Decode(&releases); unmarshalErr != nil {
return nil, err
}

if len(releases) > 0 {
latestRelease := releases[0]
return &latestRelease, nil
}

return nil, errors.New("failed to retrieve release information")
}
79 changes: 79 additions & 0 deletions internal/utils/version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package utils

import (
"errors"
"fmt"
"regexp"
"strconv"

"github.com/hashicorp/go-version"
)

const (
VersionString string = "0.9.2"
)

type VersionInfo struct {
Major int
Minor int
Patch int
}

func (v VersionInfo) String() string {
return fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch)
}

func (v VersionInfo) LessThan(other VersionInfo) bool {
a, _ := version.NewVersion(v.String())
b, _ := version.NewVersion(other.String())
return a.LessThan(b)
}

func (v VersionInfo) Equal(other VersionInfo) bool {
a, _ := version.NewVersion(v.String())
b, _ := version.NewVersion(other.String())
return a.Equal(b)
}

func ApplicationVersion() (*VersionInfo, error) {
version, err := tryParseVersionInfo(VersionString)
if err != nil {
return nil, errors.New("application version not set")
}
return version, nil
}

func tryParseVersionInfo(version string) (*VersionInfo, error) {

re := regexp.MustCompile("(?:[vV])?(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)")
match := re.FindStringSubmatch(version)
if match == nil {
return nil, errors.New("invalid version")
}

extracted := map[string]string{}
names := re.SubexpNames()
for _, name := range names {
index := re.SubexpIndex(name)
if index != -1 && len(name) > 0 {
extracted[name] = match[index]
}
}

readInt := func(name string) int {
value, found := extracted[name]
if found {
n, err := strconv.Atoi(value)
if err == nil {
return n
}
}
return 0
}

major := readInt("major")
minor := readInt("minor")
patch := readInt("patch")

return &VersionInfo{Major: major, Minor: minor, Patch: patch}, nil
}

0 comments on commit 9711b0b

Please sign in to comment.