a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
241 lines
8.0 KiB
Go
241 lines
8.0 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
"golang.org/x/mod/semver"
|
|
)
|
|
|
|
// InstallMethod is how this gortex binary was installed, inferred from where it
|
|
// lives on disk. Each method maps to the right self-update command, so
|
|
// `gortex upgrade` upgrades the way the user installed rather than clobbering a
|
|
// package-manager-owned binary with a raw download.
|
|
type InstallMethod string
|
|
|
|
const (
|
|
InstallBrew InstallMethod = "brew" // Homebrew Cellar
|
|
InstallScoop InstallMethod = "scoop" // Scoop apps dir (Windows)
|
|
InstallGoInstall InstallMethod = "go-install" // $GOPATH/bin or ~/go/bin
|
|
InstallScript InstallMethod = "script" // get.gortex.dev installer → ~/.local/bin
|
|
InstallUnknown InstallMethod = "unknown" // manual download / packaged elsewhere
|
|
)
|
|
|
|
const upgradeRepoURL = "https://github.com/zzet/gortex"
|
|
|
|
// detectInstallMethod infers the install method from the binary's path. brew
|
|
// and scoop are recognised by their well-known directory anchors; a binary
|
|
// under the Go bin dir is a `go install`; one under ~/.local/bin is the
|
|
// installer script's target. Everything else is unknown (the user gets the
|
|
// release-page fallback). Paths are slash-normalised so the same logic works on
|
|
// Windows.
|
|
func detectInstallMethod(execPath, goBinDir, homeDir string) InstallMethod {
|
|
// Normalise backslashes explicitly rather than via filepath.ToSlash, which
|
|
// only converts on Windows — a Windows path can be classified on any OS.
|
|
p := strings.ReplaceAll(execPath, "\\", "/")
|
|
switch {
|
|
case strings.Contains(p, "/Cellar/") || strings.Contains(p, "/homebrew/"):
|
|
return InstallBrew
|
|
case strings.Contains(p, "/scoop/apps/"):
|
|
return InstallScoop
|
|
case goBinDir != "" && underDir(p, goBinDir):
|
|
return InstallGoInstall
|
|
case homeDir != "" && underDir(p, filepath.Join(homeDir, ".local", "bin")):
|
|
return InstallScript
|
|
default:
|
|
return InstallUnknown
|
|
}
|
|
}
|
|
|
|
// underDir reports whether slash-path p sits directly under dir.
|
|
func underDir(p, dir string) bool {
|
|
d := strings.ReplaceAll(dir, "\\", "/")
|
|
return p == d || strings.HasPrefix(p, strings.TrimSuffix(d, "/")+"/")
|
|
}
|
|
|
|
// upgradeInstructions returns the command that updates gortex for the detected
|
|
// install method, honouring a version pin where the method supports it, and
|
|
// whether the upgrade is a manual step (no scripted command). cosign + SHA256
|
|
// verification is preserved: brew/scoop/the installer script all verify, and
|
|
// `go install` builds from the verified module proxy.
|
|
func upgradeInstructions(m InstallMethod, pinVersion string) (command string, manual bool) {
|
|
switch m {
|
|
case InstallBrew:
|
|
return "brew upgrade gortex", false
|
|
case InstallScoop:
|
|
return "scoop update gortex", false
|
|
case InstallGoInstall:
|
|
v := pinVersion
|
|
if v == "" {
|
|
v = "latest"
|
|
}
|
|
return "go install " + upgradeModulePath + "@" + v, false
|
|
case InstallScript:
|
|
return "curl -fsSL https://get.gortex.dev | sh", false
|
|
default:
|
|
return "", true
|
|
}
|
|
}
|
|
|
|
const upgradeModulePath = "github.com/zzet/gortex/cmd/gortex"
|
|
|
|
// tagFromReleaseLocation extracts the version tag from a GitHub
|
|
// /releases/latest redirect Location (…/releases/tag/v0.49.0 → v0.49.0).
|
|
// Returns "" when the location is not a tag URL.
|
|
func tagFromReleaseLocation(location string) string {
|
|
const marker = "/releases/tag/"
|
|
i := strings.Index(location, marker)
|
|
if i < 0 {
|
|
return ""
|
|
}
|
|
tag := location[i+len(marker):]
|
|
if j := strings.IndexAny(tag, "?#"); j >= 0 {
|
|
tag = tag[:j]
|
|
}
|
|
return strings.TrimSpace(tag)
|
|
}
|
|
|
|
// latestReleaseVersion resolves the newest release tag via the
|
|
// /releases/latest redirect — the redirect target carries the tag, so we read
|
|
// it without the 60-request/hour unauthenticated API limit.
|
|
func latestReleaseVersion() (string, error) {
|
|
client := &http.Client{
|
|
Timeout: 8 * time.Second,
|
|
CheckRedirect: func(*http.Request, []*http.Request) error {
|
|
return http.ErrUseLastResponse // capture the redirect, don't follow it
|
|
},
|
|
}
|
|
resp, err := client.Get(upgradeRepoURL + "/releases/latest")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
tag := tagFromReleaseLocation(resp.Header.Get("Location"))
|
|
if tag == "" {
|
|
return "", fmt.Errorf("could not read latest release tag from %s", upgradeRepoURL)
|
|
}
|
|
return tag, nil
|
|
}
|
|
|
|
// normalizeSemver ensures a leading "v" so golang.org/x/mod/semver accepts it.
|
|
func normalizeSemver(v string) string {
|
|
v = strings.TrimSpace(v)
|
|
if v != "" && !strings.HasPrefix(v, "v") {
|
|
return "v" + v
|
|
}
|
|
return v
|
|
}
|
|
|
|
var upgradeRun bool
|
|
|
|
var upgradeCmd = &cobra.Command{
|
|
Use: "upgrade [version]",
|
|
Short: "Update gortex to the latest release using the method it was installed with",
|
|
Long: "Detects how this gortex binary was installed (Homebrew, Scoop, go install, or the " +
|
|
"installer script) and runs the matching update command. Pass a version (or set " +
|
|
"GORTEX_VERSION) to pin a specific release. By default the command is printed; pass --run to " +
|
|
"execute it. cosign + SHA256 verification is preserved by every supported method.",
|
|
Args: cobra.MaximumNArgs(1),
|
|
RunE: runUpgrade,
|
|
}
|
|
|
|
func init() {
|
|
upgradeCmd.Flags().BoolVar(&upgradeRun, "run", false, "execute the detected upgrade command instead of printing it")
|
|
rootCmd.AddCommand(upgradeCmd)
|
|
}
|
|
|
|
func runUpgrade(cmd *cobra.Command, args []string) error {
|
|
out := cmd.OutOrStdout()
|
|
|
|
pin := os.Getenv("GORTEX_VERSION")
|
|
if len(args) == 1 {
|
|
pin = args[0]
|
|
}
|
|
|
|
execPath, err := os.Executable()
|
|
if err != nil {
|
|
execPath = ""
|
|
}
|
|
method := detectInstallMethod(execPath, goBinDir(), homeDirOrEmpty())
|
|
|
|
current := normalizeSemver(version)
|
|
target := normalizeSemver(pin)
|
|
if target == "" {
|
|
if latest, lerr := latestReleaseVersion(); lerr == nil {
|
|
target = normalizeSemver(latest)
|
|
} else {
|
|
fmt.Fprintf(out, "could not check the latest version (%v); proceeding with the upgrade command\n", lerr)
|
|
}
|
|
}
|
|
|
|
// Already current? Only short-circuit when we actually resolved a target
|
|
// and the user did not pin a (possibly older/specific) version.
|
|
if pin == "" && semver.IsValid(current) && semver.IsValid(target) && semver.Compare(current, target) >= 0 {
|
|
fmt.Fprintf(out, "gortex %s is already the latest release.\n", version)
|
|
return nil
|
|
}
|
|
|
|
command, manual := upgradeInstructions(method, pin)
|
|
if manual {
|
|
fmt.Fprintf(out, "Installed via an unrecognised method — download the latest release from:\n %s/releases/latest\n", upgradeRepoURL)
|
|
return nil
|
|
}
|
|
|
|
if target != "" {
|
|
fmt.Fprintf(out, "Upgrading gortex %s → %s (install method: %s)\n", version, target, method)
|
|
} else {
|
|
fmt.Fprintf(out, "Upgrading gortex (install method: %s)\n", method)
|
|
}
|
|
|
|
if !upgradeRun {
|
|
fmt.Fprintf(out, "Run:\n %s\n", command)
|
|
} else {
|
|
parts := strings.Fields(command)
|
|
fmt.Fprintf(out, "$ %s\n", command)
|
|
ex := exec.CommandContext(cmd.Context(), parts[0], parts[1:]...) //nolint:gosec // command is one of the fixed install-method templates
|
|
ex.Stdout, ex.Stderr, ex.Stdin = out, cmd.ErrOrStderr(), os.Stdin
|
|
if rerr := ex.Run(); rerr != nil {
|
|
return fmt.Errorf("upgrade command failed: %w", rerr)
|
|
}
|
|
}
|
|
|
|
// Post-upgrade advisory: a new binary may carry newer per-language
|
|
// extractors, so the indexed graph for an affected language is stale until
|
|
// reindexed. F2 enriches this with the exact stale languages per repo.
|
|
fmt.Fprintln(out, "\nAfter upgrading, reindex so the graph picks up any extractor changes:\n gortex index .")
|
|
return nil
|
|
}
|
|
|
|
// goBinDir returns the directory `go install` drops binaries into — $GOBIN, or
|
|
// $GOPATH/bin, or ~/go/bin — for install-method detection.
|
|
func goBinDir() string {
|
|
if b := strings.TrimSpace(os.Getenv("GOBIN")); b != "" {
|
|
return b
|
|
}
|
|
if gp := strings.TrimSpace(os.Getenv("GOPATH")); gp != "" {
|
|
// GOPATH may be a list; the first entry owns `go install` output.
|
|
if i := strings.IndexByte(gp, os.PathListSeparator); i >= 0 {
|
|
gp = gp[:i]
|
|
}
|
|
return filepath.Join(gp, "bin")
|
|
}
|
|
if home := homeDirOrEmpty(); home != "" {
|
|
return filepath.Join(home, "go", "bin")
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func homeDirOrEmpty() string {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return home
|
|
}
|