Skip to content

Commit

Permalink
Add an update command to update JKL, the version command notifies…
Browse files Browse the repository at this point in the history
… if an update is available, fix passing the `GH_TOKEN` environment variable to TestScript tests

Failure to pass the `GH_TOKEN` environment variable to TestScript tests
could cause github to quickly rate-limit access to its API.
  • Loading branch information
ivanfetch committed Jan 7, 2023
1 parent f99d71b commit 33c1f9d
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 5 deletions.
34 changes: 34 additions & 0 deletions cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,20 @@ func RunCLI(args []string, output, errOutput io.Writer) error {
return
}
fmt.Fprintf(cmd.OutOrStdout(), "%s version %s, git commit %s\n", callMeProgName, Version, GitCommit)
g, err := NewGithubRepo("ivanfetch/jkl")
if err != nil {
// Since the current version was displayed, do not display an error if unable to contact Github.
debugLog.Printf("unable to determine if there is a newer version of jkl: %v\n", err)
return
}
latestTag, err := g.GetTagForLatestRelease()
if err != nil {
debugLog.Printf("unable to determine if there is a newer version of jkl: %v\n", err)
return
}
if latestTag != "v"+Version {
fmt.Printf("The latest released version of jkl is %s - to update, run: jkl update\n", latestTag)
}
},
}
versionCmd.Flags().BoolVarP(&versionOnly, "version-only", "v", false, "Only output the jkl version.")
Expand Down Expand Up @@ -155,6 +169,26 @@ jkl list rbac-lookup`,
}
rootCmd.AddCommand(listCmd)

var updateSelfCmd = &cobra.Command{
Use: "update",
Short: "Update JKL to the latest release",
Long: fmt.Sprintf("Update the JKL binary to the latest release. This will replace %s, retaining its current file mode.", j.executable),
Aliases: []string{"update-self", "update-jkl"},
RunE: func(cmd *cobra.Command, args []string) error {
newVersion, isNewVersion, err := j.UpdateSelf()
if err != nil {
return err
}
if isNewVersion {
fmt.Printf("%s was updated to version %s\n", j.executable, newVersion)
} else {
fmt.Printf("JKL is already at the most current version (%s).\n", Version)
}
return nil
},
}
rootCmd.AddCommand(updateSelfCmd)

cobra.CheckErr(rootCmd.Execute())
return nil
}
Expand Down
4 changes: 2 additions & 2 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import (
"syscall"
)

// RunCommand execs the specified command, the command will replace the
// ExecCommand exec()s the specified command, the command will replace the
// current (Go program) process. If commandAndArgs[0] is not an absolute path,
// the PATH environment variable will be searched for the executable.
func RunCommand(commandAndArgs []string) error {
func ExecCommand(commandAndArgs []string) error {
cmd, err := exec.LookPath(commandAndArgs[0])
if err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func TestExecHelper(t *testing.T) {
return
}
commandString := os.Getenv("test_exec_helper_command")
err := jkl.RunCommand(strings.Split(commandString, " "))
err := jkl.ExecCommand(strings.Split(commandString, " "))
if err != nil {
t.Fatal(err)
}
Expand Down
21 changes: 20 additions & 1 deletion script_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@ package jkl_test
// Note: The go test -testwork flag preserves the TestScript temporary directory.

import (
"fmt"
"os"
"testing"

"github.com/ivanfetch/jkl"
"github.com/rogpeppe/go-internal/testscript"
)

var testScriptSetup func(*testscript.Env) error = func(e *testscript.Env) error {
e.Vars = append(e.Vars, fmt.Sprintf("GH_TOKEN=%s", os.Getenv("GH_TOKEN")))
return nil
}

func TestMain(m *testing.M) {
// Map binary names called by TestScript scripts, to run jkl.
// This causes TestScript to symlink these binary names, affectively doing the
Expand All @@ -27,6 +33,19 @@ func TestMain(m *testing.M) {
}
func TestScript(t *testing.T) {
testscript.Run(t, testscript.Params{
Dir: "testdata/script",
Dir: "testdata/script",
Setup: testScriptSetup,
})
}

// TestUpdateSelf tests updating the jkl binary to the latest available
// release from Github.
// This test canot run in parallel with other TestScript tests, because it
// messes with the jkl binary while other tests are using the
// TestScript-managed symlink.
func TestScriptUpdateSelf(t *testing.T) {
testscript.Run(t, testscript.Params{
Dir: "testdata/update-self",
Setup: testScriptSetup,
})
}
10 changes: 10 additions & 0 deletions testdata/update-self/update_jkl.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
exec jkl version
stdout 'jkl version'
stdout 'git commit '
# Update the TestScript-facilitated jkl to the latest released version.
mkdir $WORK/home/testuser
env HOME=$WORK/home/testuser
env PATH=$HOME/.jkl/bin:$PATH
exec jkl update
stdout 'jkl was updated to version'
! stderr .
2 changes: 1 addition & 1 deletion toolmanaged.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func (t managedTool) Run(args []string) error {
if !ok {
return fmt.Errorf("version %s of %s is not installed by %[3]s, please see the `%[3]s install` command to install it", desiredVersion, t.name, callMeProgName)
}
err = RunCommand(append([]string{installedCommandPath}, args...))
err = ExecCommand(append([]string{installedCommandPath}, args...))
if err != nil {
return err
}
Expand Down
92 changes: 92 additions & 0 deletions updateself.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package jkl

import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)

// UpdateSelf downloads the latest jkl binary and overwrites the currently
// executing one. The new binary is run, to verify it reports the expected
// newer version.
func (j JKL) UpdateSelf() (newVersion string, isNewerVersion bool, err error) {
debugLog.Printf("updating %s from %s to the latest version", j.executable, Version)
downloadedJKLPath, newVersion, isNewerVersion, err := j.DownloadAndExtractlaterJKLVersion()
if err != nil {
return
}
if !isNewerVersion {
return
}
debugLog.Printf("downloaded jkl %s to %q\n", newVersion, downloadedJKLPath)
versionReportedByNewBinary, err := getVersionOfJKLBinary(downloadedJKLPath)
if err != nil {
return newVersion, isNewerVersion, fmt.Errorf("while executing a newly downloaded JKL binary (%s version -v) to verify its version is %q: %v: %s", downloadedJKLPath, newVersion, err, versionReportedByNewBinary)
}
debugLog.Printf("the downloaded JKL binary reports version %q", versionReportedByNewBinary)
if "v"+versionReportedByNewBinary != newVersion {
return newVersion, isNewerVersion, fmt.Errorf("the newly downloaded JKL binary reports version %q instead of the expected %q", versionReportedByNewBinary, newVersion)
}
destDir := filepath.Dir(j.executable)
debugLog.Printf("copying new JKL binary to %q\n", destDir)
err = CopyFile(downloadedJKLPath, destDir)
if err != nil {
return newVersion, isNewerVersion, fmt.Errorf("while copying new JKL binary to %s: %v", destDir, err)
}
return
}

// DownloadAndExtractlaterJKLVersion downloads and extracts the latest version
// of jkl, if that version is newer than the currently executing one.
// The JKL binary will be set to the file-mode of the current binary.
// It returns the path to the downloaded binary, the latestversion number, and whether a
// newer version exists.
func (j JKL) DownloadAndExtractlaterJKLVersion() (binaryPath, matchedVersion string, newerVerAvailable bool, err error) {
g, err := NewGithubRepo("ivanfetch/jkl")
if err != nil {
return
}
latestTag, err := g.GetTagForLatestRelease()
if err != nil {
return
}
if latestTag == "v"+Version {
debugLog.Printf("while downloading an updated version of JKL, version %q is already the latest release", Version)
return
}
newerVerAvailable = true
downloadPath, _, err := g.DownloadReleaseForTag(latestTag)
if err != nil {
return
}
_, err = ExtractFile(downloadPath)
if err != nil {
return
}
existingJKLStat, err := os.Stat(j.executable)
if err != nil {
return
}
existingJKLFileMode := existingJKLStat.Mode()
newJKLBinaryPath := filepath.Join(filepath.Dir(downloadPath), "jkl")
err = os.Chmod(newJKLBinaryPath, existingJKLFileMode)
if err != nil {
return
}
return newJKLBinaryPath, latestTag, newerVerAvailable, nil
}

// getVersionOfJKLBinary runs the specified jkl binary to determine its
// version.
func getVersionOfJKLBinary(binaryPath string) (version string, err error) {
cmd := exec.Command(binaryPath, "version", "-v") // returns only the version
cmd.Env = append(os.Environ(), `JKL_DEBUG=`) // debug output can obscure the version output
outputBytes, err := cmd.CombinedOutput()
returnedVersion := strings.TrimSuffix(string(outputBytes), "\n")
if err != nil {
return returnedVersion, err
}
return returnedVersion, nil
}

0 comments on commit 33c1f9d

Please sign in to comment.