mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-10 17:57:54 -10:00
* docs(readme): document /new, zero upgrade, and add Discord link Add the /new slash command and zero upgrade command to the README's command references, expand docs/UPDATE.md to cover the --apply install flow, and link the project Discord server. * docs(update): escape pipe in --endpoint table cell CodeRabbit flagged the unescaped | in the --endpoint placeholder breaking the Markdown table layout. * docs(readme-zh): bring /new and zero upgrade in line with README.md README_ZH.md had picked up the Discord link but was still missing the /new slash command and zero upgrade entries added to README.md, letting the translation drift out of sync. * cli: add upgrade to root help output * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <[email protected]> * docs: make required update flag explicit in common commands zero update errors without --check or --apply, so the bare form in the common-commands examples was misleading. Addresses the Copilot finding from the PR #489 review. * docs: use a permanent (non-expiring) Discord invite in the READMEs * docs(cli): match upgrade help summary to download/verify/install wording Copilot flagged the root zero --help summary for upgrade as less specific than the download/verify/install phrasing already used in README.md and docs/UPDATE.md for the same command. Co-Authored-By: Claude Sonnet 5 <[email protected]> * docs: use discord.gg short link for the community invite discord.com/invite/<code> and discord.gg/<code> resolve to the same invite; discord.gg is the canonical short form to publish. Co-Authored-By: Claude Sonnet 5 <[email protected]> * fix(update,readme-zh): keep npm --json stdout clean, add ZH Discussions route `zero upgrade --json` (and `zero update --apply --json`) on an npm-managed install wired npm's own stdout directly to the process stdout, so its progress output landed ahead of the JSON result and made it unparseable. Route npm's stdout into a buffer when JSON output is requested, only surfacing it in the error message if the install fails. README_ZH.md's Community section only pointed to Discord, unlike the English README, which reserves Discord for real-time chat and routes Q&A/ideas/sharing to GitHub Discussions per SUPPORT.md/CONTRIBUTING.md policy. Bring the Chinese section in line with the same split. Addresses the outstanding findings from jatmn's review on PR #489. Co-Authored-By: Claude Sonnet 5 <[email protected]> * docs(update): qualify npm-managed JSON output Document the npm subprocess output caveat instead of promising a single parseable JSON document for apply operations. Drop the JSON-specific updater plumbing and tests so the PR remains focused on documentation and help discovery. Amp-Thread-ID: https://ampcode.com/threads/T-019fa55a-8bcd-71bc-9bde-9b4c28b78b38 Co-authored-by: Pierre Bruno <[email protected]> * docs(update): align upgrade guidance Amp-Thread-ID: https://ampcode.com/threads/T-019fa7a0-1223-701d-9529-48ba5d7cf8c8 Co-authored-by: Pierre Bruno <[email protected]> * fix(update): keep the upgrade recommendation about this machine `zero update --check --target windows-x64` answers a question about a different machine, but the new call to action told the user to run `zero upgrade`, which only ever installs onto this one — replacing the target-specific download guidance the flag used to print. The recommendation is now conditional: a current-platform check keeps it, and a cross-target check gets the manual instruction plus a note naming the machine `zero upgrade` would have touched instead. Also align two flag contracts with what the code does: `zero update --help` described --repo/--endpoint as check-only although apply honors them, and UPDATE.md did not mention that both are ignored on an npm-managed apply, which takes its release from the npm registry rather than GitHub. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> * fix(update): do not send a custom-source check to the default source `zero upgrade` is a fresh invocation: it does not inherit the --repo/--endpoint of the check that suggested it, so recommending it bare after a custom-source check told the user to install from a repository they never asked about. The check now records the flag it was given and the guidance repeats it, naming where a bare upgrade would install from instead. ZERO_UPDATE_RELEASE_URL is deliberately not recorded: every Check reads it from the environment, including the one inside Apply, so a bare upgrade already follows it and telling the user to repeat it would be wrong. Two smaller edges from the same review: a cross-target check on a host with no published release target no longer ends by describing what `zero upgrade` does there, since it does not work there at all; and the flag table and help text now carry the npm caveat the prose bullet already had, so docs and help agree. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> * fix(update): keep source guidance accurate Amp-Thread-ID: https://ampcode.com/threads/T-019fafa2-7d9d-75bc-8801-6d0efc8db1df Co-authored-by: Pierre Bruno <[email protected]> --------- Co-authored-by: Copilot Autofix powered by AI <[email protected]> Co-authored-by: Vasanthdev2004 <[email protected]> Co-authored-by: Claude Sonnet 5 <[email protected]> Co-authored-by: Amp <[email protected]>
530 lines
18 KiB
Go
530 lines
18 KiB
Go
package update
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"regexp"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
DefaultRepository = "Gitlawb/zero"
|
|
DefaultTimeout = 5 * time.Second
|
|
)
|
|
|
|
type Release struct {
|
|
TagName string `json:"tag_name"`
|
|
HTMLURL string `json:"html_url"`
|
|
Assets []Asset `json:"assets"`
|
|
}
|
|
|
|
type Asset struct {
|
|
Name string `json:"name"`
|
|
BrowserDownloadURL string `json:"browser_download_url,omitempty"`
|
|
}
|
|
|
|
type Result struct {
|
|
CurrentVersion string `json:"currentVersion"`
|
|
LatestVersion string `json:"latestVersion"`
|
|
ReleaseURL string `json:"releaseUrl"`
|
|
TagName string `json:"tagName"`
|
|
ReleaseAsset AssetCheck `json:"releaseAsset"`
|
|
UpdateAvailable bool `json:"updateAvailable"`
|
|
// SourceFlag is the `--repo`/`--endpoint` argument this check was given, in
|
|
// the form a caller would repeat on `zero upgrade`. Empty when the check used
|
|
// the default release source. Format needs it because `zero upgrade` is a
|
|
// fresh invocation: it does not inherit the flags of the check that suggested
|
|
// it, so recommending it bare after a custom-source check would send the user
|
|
// to install from somewhere they did not ask about.
|
|
SourceFlag string `json:"sourceFlag,omitempty"`
|
|
// installMethod is local process state used only to keep human guidance
|
|
// accurate. ApplyResult exposes the method after an install; adding it to
|
|
// check JSON would unnecessarily change that API.
|
|
installMethod InstallMethod
|
|
}
|
|
|
|
type AssetCheck struct {
|
|
Platform string `json:"platform"`
|
|
Arch string `json:"arch"`
|
|
ArchiveName string `json:"archiveName"`
|
|
ArchiveURL string `json:"archiveUrl,omitempty"`
|
|
ChecksumName string `json:"checksumName"`
|
|
ChecksumURL string `json:"checksumUrl,omitempty"`
|
|
ArchiveFound bool `json:"archiveFound"`
|
|
ChecksumFound bool `json:"checksumFound"`
|
|
Verified bool `json:"verified"`
|
|
}
|
|
|
|
// Target identifies a supported release archive target.
|
|
type Target struct {
|
|
Name string `json:"name"`
|
|
GOOS string `json:"goos"`
|
|
GOARCH string `json:"goarch"`
|
|
Platform string `json:"platform"`
|
|
Arch string `json:"arch"`
|
|
}
|
|
|
|
// Options configures a release update check.
|
|
type Options struct {
|
|
CurrentVersion string
|
|
// Endpoint accepts a full release API URL, an owner/repo slug, or a data:
|
|
// endpoint for deterministic tests.
|
|
Endpoint string
|
|
Repository string
|
|
Timeout time.Duration
|
|
GOOS string
|
|
GOARCH string
|
|
// Fetch overrides the release fetcher for tests and alternate transports.
|
|
Fetch func(context.Context, string) (Release, error)
|
|
}
|
|
|
|
type semverParts [3]int
|
|
|
|
var (
|
|
versionPattern = regexp.MustCompile(`^v?([0-9]+)\.([0-9]+)\.([0-9]+)(?:[-+].*)?$`)
|
|
repositoryPattern = regexp.MustCompile(`^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$`)
|
|
)
|
|
|
|
// Endpoint returns the GitHub latest-release API endpoint for a repository.
|
|
func Endpoint(repository string) string {
|
|
return fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", repository)
|
|
}
|
|
|
|
// ResolveTarget maps a release target name like windows-x64 to Go build
|
|
// coordinates and release asset naming fields.
|
|
func ResolveTarget(target string) (Target, error) {
|
|
value := strings.TrimSpace(strings.ToLower(target))
|
|
switch value {
|
|
case "linux-x64":
|
|
return Target{Name: value, GOOS: "linux", GOARCH: "amd64", Platform: "linux", Arch: "x64"}, nil
|
|
case "linux-arm64":
|
|
return Target{Name: value, GOOS: "linux", GOARCH: "arm64", Platform: "linux", Arch: "arm64"}, nil
|
|
case "macos-x64":
|
|
return Target{Name: value, GOOS: "darwin", GOARCH: "amd64", Platform: "macos", Arch: "x64"}, nil
|
|
case "macos-arm64":
|
|
return Target{Name: value, GOOS: "darwin", GOARCH: "arm64", Platform: "macos", Arch: "arm64"}, nil
|
|
case "windows-x64":
|
|
return Target{Name: value, GOOS: "windows", GOARCH: "amd64", Platform: "windows", Arch: "x64"}, nil
|
|
case "windows-arm64":
|
|
return Target{Name: value, GOOS: "windows", GOARCH: "arm64", Platform: "windows", Arch: "arm64"}, nil
|
|
default:
|
|
return Target{}, fmt.Errorf("unsupported update target %q: expected one of linux-x64, linux-arm64, macos-x64, macos-arm64, windows-x64, windows-arm64", target)
|
|
}
|
|
}
|
|
|
|
func Check(ctx context.Context, options Options) (Result, error) {
|
|
rawVersion := strings.TrimSpace(firstNonEmpty(options.CurrentVersion, "0.0.0"))
|
|
currentVersion, err := normalizeVersionTag(rawVersion)
|
|
if err != nil {
|
|
// Source/dev builds carry a non-semver version ("dev"); the check is
|
|
// still useful there — compare as 0.0.0 so the latest release is always
|
|
// reported as available instead of failing before the network call.
|
|
currentVersion = "0.0.0"
|
|
}
|
|
repository := strings.TrimSpace(firstNonEmpty(options.Repository, DefaultRepository))
|
|
endpoint, err := resolveEndpoint(firstNonEmpty(options.Endpoint, os.Getenv("ZERO_UPDATE_RELEASE_URL")), repository)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
timeout := options.Timeout
|
|
if timeout < 0 {
|
|
return Result{}, fmt.Errorf("timeout must be non-negative")
|
|
}
|
|
if timeout == 0 {
|
|
timeout = DefaultTimeout
|
|
}
|
|
if timeout > 0 {
|
|
var cancel context.CancelFunc
|
|
ctx, cancel = context.WithTimeout(ctx, timeout)
|
|
defer cancel()
|
|
}
|
|
fetch := options.Fetch
|
|
if fetch == nil {
|
|
fetch = fetchRelease
|
|
}
|
|
release, err := fetch(ctx, endpoint)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
if strings.TrimSpace(release.TagName) == "" {
|
|
return Result{}, fmt.Errorf("github release response did not include a tag_name")
|
|
}
|
|
latestVersion, err := normalizeVersionTag(release.TagName)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
releaseURL := strings.TrimSpace(release.HTMLURL)
|
|
if releaseURL == "" {
|
|
releaseURL = fmt.Sprintf("https://github.com/%s/releases/tag/%s", repository, release.TagName)
|
|
}
|
|
assetCheck, err := verifyReleaseAssets(release, latestVersion, options)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
latestParts, err := parseSemverNormalized(latestVersion)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
currentParts, err := parseSemverNormalized(currentVersion)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
result := Result{
|
|
CurrentVersion: currentVersion,
|
|
LatestVersion: latestVersion,
|
|
ReleaseURL: releaseURL,
|
|
TagName: release.TagName,
|
|
ReleaseAsset: assetCheck,
|
|
UpdateAvailable: compareSemverParts(latestParts, currentParts) > 0,
|
|
SourceFlag: upgradeSourceFlag(options),
|
|
}
|
|
if executablePath, executableErr := os.Executable(); executableErr == nil {
|
|
result.installMethod = DetectInstallMethod(executablePath)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// upgradeSourceFlag returns the flag a caller must repeat on `zero upgrade` to
|
|
// install from the same place this check read, or "" when the default source
|
|
// was used.
|
|
//
|
|
// Only the per-invocation FLAGS need repeating. ZERO_UPDATE_RELEASE_URL is read
|
|
// from the environment by every Check, including the one inside Apply, so a bare
|
|
// `zero upgrade` already follows it — naming it here would tell the user to
|
|
// repeat something that is not theirs to drop.
|
|
func upgradeSourceFlag(options Options) string {
|
|
if endpoint := strings.TrimSpace(options.Endpoint); endpoint != "" {
|
|
return "--endpoint " + shellQuote(endpoint)
|
|
}
|
|
if strings.TrimSpace(os.Getenv("ZERO_UPDATE_RELEASE_URL")) != "" {
|
|
return ""
|
|
}
|
|
if repository := strings.TrimSpace(options.Repository); repository != "" && repository != DefaultRepository {
|
|
return "--repo " + repository
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// shellQuote returns one POSIX-shell argument suitable for the copy/paste
|
|
// commands in human-readable guidance.
|
|
func shellQuote(value string) string {
|
|
return "'" + strings.ReplaceAll(value, "'", `'"'"'`) + "'"
|
|
}
|
|
|
|
func Format(result Result) string {
|
|
if result.UpdateAvailable {
|
|
lines := []string{
|
|
fmt.Sprintf("[zero] Update available: %s -> %s", result.CurrentVersion, result.LatestVersion),
|
|
"Release: " + result.ReleaseURL,
|
|
}
|
|
lines = appendAssetLines(lines, result.ReleaseAsset)
|
|
lines = append(lines, upgradeGuidance(result.ReleaseAsset, result.SourceFlag, result.installMethod))
|
|
return strings.Join(lines, "\n")
|
|
}
|
|
lines := []string{
|
|
fmt.Sprintf("[zero] up to date (%s)", result.CurrentVersion),
|
|
"Latest release: " + result.ReleaseURL,
|
|
}
|
|
lines = appendAssetLines(lines, result.ReleaseAsset)
|
|
return strings.Join(lines, "\n")
|
|
}
|
|
|
|
func appendAssetLines(lines []string, asset AssetCheck) []string {
|
|
if asset.ArchiveName == "" {
|
|
return lines
|
|
}
|
|
if target := releaseAssetTarget(asset); target != "" {
|
|
lines = append(lines, "Release target: "+target)
|
|
}
|
|
lines = append(lines, "Release asset: "+asset.ArchiveName)
|
|
if asset.ChecksumName != "" {
|
|
lines = append(lines, "Checksum asset: "+asset.ChecksumName)
|
|
}
|
|
return lines
|
|
}
|
|
|
|
func releaseAssetTarget(asset AssetCheck) string {
|
|
if asset.Platform == "" || asset.Arch == "" {
|
|
return ""
|
|
}
|
|
return asset.Platform + "-" + asset.Arch
|
|
}
|
|
|
|
// localReleaseTarget is the release target of the machine running this process,
|
|
// or "" when no release archive is published for it (Termux, for example).
|
|
func localReleaseTarget() string {
|
|
return publishedReleaseTarget(runtime.GOOS, runtime.GOARCH)
|
|
}
|
|
|
|
func publishedReleaseTarget(goos, goarch string) string {
|
|
platform, err := releasePlatform(goos)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
arch, err := releaseArch(goarch)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
target := platform + "-" + arch
|
|
if target == "windows-arm64" {
|
|
return ""
|
|
}
|
|
return target
|
|
}
|
|
|
|
// upgradeGuidance returns the next step for an available update.
|
|
//
|
|
// `zero upgrade` is a fresh invocation that installs onto THIS machine from the
|
|
// DEFAULT release source, so it is only the right next step when the check
|
|
// matched both. A cross-target check would otherwise answer a question about one
|
|
// machine with an action that changes another; a custom-source check would send
|
|
// the user to install from a repository they did not ask about, because the
|
|
// flags do not carry over.
|
|
//
|
|
// An asset with no target recorded is the ordinary current-platform check (the
|
|
// target fields are only populated when a target was resolved).
|
|
func upgradeGuidance(asset AssetCheck, sourceFlag string, installMethod InstallMethod) string {
|
|
target := releaseAssetTarget(asset)
|
|
local := localReleaseTarget()
|
|
if target != "" && target != local {
|
|
guidance := "Download the verified " + target + " release asset and replace the zero binary on that machine."
|
|
if sourceFlag != "" {
|
|
guidance += " The download URLs above are from the custom source selected by `" + sourceFlag + "`; a bare `zero upgrade` does not repeat that source."
|
|
if local != "" {
|
|
guidance += " It installs onto this machine (" + local + ") instead."
|
|
}
|
|
return guidance
|
|
}
|
|
if local == "" {
|
|
// No published release target for this host (a source build on an OS
|
|
// with no release archive, e.g. Termux). Saying what `zero upgrade`
|
|
// would do here would be worse than saying nothing: it does not work on
|
|
// this machine at all.
|
|
return guidance
|
|
}
|
|
return guidance + " `zero upgrade` installs onto this machine (" + local + ") instead."
|
|
}
|
|
if sourceFlag != "" {
|
|
if installMethod == InstallMethodNpm {
|
|
return "This npm-managed installation can be updated with `npm install -g " + npmPackageName + "@latest`, which installs the official npm package. The custom `" + sourceFlag + "` source only affects the release check and update gating, not the npm install source."
|
|
}
|
|
return "Run `zero upgrade " + sourceFlag + "` to install from the source this check used; a bare `zero upgrade` does not repeat that explicit source flag."
|
|
}
|
|
return "Run `zero upgrade` to download, verify, and install the latest release."
|
|
}
|
|
|
|
func fetchRelease(ctx context.Context, endpoint string) (release Release, err error) {
|
|
if strings.HasPrefix(endpoint, "data:") {
|
|
return fetchDataRelease(endpoint)
|
|
}
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
|
if err != nil {
|
|
return Release{}, err
|
|
}
|
|
request.Header.Set("Accept", "application/vnd.github+json")
|
|
request.Header.Set("User-Agent", "zero/update")
|
|
response, err := http.DefaultClient.Do(request)
|
|
if err != nil {
|
|
return Release{}, err
|
|
}
|
|
defer func() {
|
|
if closeErr := response.Body.Close(); closeErr != nil && err == nil {
|
|
err = fmt.Errorf("close update response: %w", closeErr)
|
|
}
|
|
}()
|
|
if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices {
|
|
return Release{}, fmt.Errorf("github release check failed (%s)", response.Status)
|
|
}
|
|
if err := json.NewDecoder(response.Body).Decode(&release); err != nil {
|
|
return Release{}, err
|
|
}
|
|
return release, nil
|
|
}
|
|
|
|
func fetchDataRelease(endpoint string) (Release, error) {
|
|
comma := strings.Index(endpoint, ",")
|
|
if comma == -1 {
|
|
return Release{}, fmt.Errorf("invalid data update endpoint")
|
|
}
|
|
payload, err := url.QueryUnescape(endpoint[comma+1:])
|
|
if err != nil {
|
|
return Release{}, err
|
|
}
|
|
var release Release
|
|
if err := json.Unmarshal([]byte(payload), &release); err != nil {
|
|
return Release{}, err
|
|
}
|
|
return release, nil
|
|
}
|
|
|
|
func resolveEndpoint(endpointOrRepository string, repository string) (string, error) {
|
|
value := strings.TrimSpace(endpointOrRepository)
|
|
if value == "" {
|
|
return Endpoint(repository), nil
|
|
}
|
|
if repositoryPattern.MatchString(value) {
|
|
return Endpoint(value), nil
|
|
}
|
|
parsed, err := url.ParseRequestURI(value)
|
|
if err != nil || parsed.Scheme == "" {
|
|
return "", fmt.Errorf("invalid update endpoint %q: use a full URL or an owner/repo slug like %s", value, repository)
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func verifyReleaseAssets(release Release, version string, options Options) (AssetCheck, error) {
|
|
assetCheck, err := expectedAssetCheck(version, options.GOOS, options.GOARCH)
|
|
if err != nil {
|
|
return AssetCheck{}, err
|
|
}
|
|
for _, asset := range release.Assets {
|
|
name := strings.TrimSpace(asset.Name)
|
|
switch name {
|
|
case assetCheck.ArchiveName:
|
|
assetCheck.ArchiveFound = true
|
|
assetCheck.ArchiveURL = strings.TrimSpace(asset.BrowserDownloadURL)
|
|
case assetCheck.ChecksumName:
|
|
assetCheck.ChecksumFound = true
|
|
assetCheck.ChecksumURL = strings.TrimSpace(asset.BrowserDownloadURL)
|
|
}
|
|
}
|
|
assetCheck.Verified = assetCheck.ArchiveFound && assetCheck.ChecksumFound
|
|
if assetCheck.Verified {
|
|
return assetCheck, nil
|
|
}
|
|
missing := []string{}
|
|
if !assetCheck.ArchiveFound {
|
|
missing = append(missing, assetCheck.ArchiveName)
|
|
}
|
|
if !assetCheck.ChecksumFound {
|
|
missing = append(missing, assetCheck.ChecksumName)
|
|
}
|
|
return AssetCheck{}, fmt.Errorf("release metadata missing expected asset(s) for %s-%s: %s", assetCheck.Platform, assetCheck.Arch, strings.Join(missing, ", "))
|
|
}
|
|
|
|
func expectedAssetCheck(version string, goos string, goarch string) (AssetCheck, error) {
|
|
platform, err := releasePlatform(firstNonEmpty(goos, runtime.GOOS))
|
|
if err != nil {
|
|
return AssetCheck{}, err
|
|
}
|
|
arch, err := releaseArch(firstNonEmpty(goarch, runtime.GOARCH))
|
|
if err != nil {
|
|
return AssetCheck{}, err
|
|
}
|
|
extension := "tar.gz"
|
|
if platform == "windows" {
|
|
extension = "zip"
|
|
}
|
|
archiveName := fmt.Sprintf("zero-v%s-%s-%s.%s", version, platform, arch, extension)
|
|
return AssetCheck{
|
|
Platform: platform,
|
|
Arch: arch,
|
|
ArchiveName: archiveName,
|
|
ChecksumName: archiveName + ".sha256",
|
|
}, nil
|
|
}
|
|
|
|
func releasePlatform(goos string) (string, error) {
|
|
switch strings.TrimSpace(goos) {
|
|
case "linux":
|
|
return "linux", nil
|
|
case "darwin":
|
|
return "macos", nil
|
|
case "windows":
|
|
return "windows", nil
|
|
default:
|
|
// No prebuilt release archive is published for this GOOS (e.g.
|
|
// Android/Termux). This does not mean the platform is unsupported --
|
|
// Termux runs zero fine via the npm wrapper -- it just has no
|
|
// self-updating release asset. Point users at `npm update` rather
|
|
// than a source rebuild, since that's the documented Termux
|
|
// install/upgrade path and doesn't require a Go toolchain.
|
|
return "", fmt.Errorf("no published release for %q (release assets: linux, macos, windows). Your build is the current version of record. Upgrade with `npm update -g @gitlawb/zero` to get the latest.", goos) //nolint:staticcheck // Preserve established user-facing error text.
|
|
}
|
|
}
|
|
|
|
func releaseArch(goarch string) (string, error) {
|
|
switch strings.TrimSpace(goarch) {
|
|
case "amd64":
|
|
return "x64", nil
|
|
case "arm64":
|
|
return "arm64", nil
|
|
default:
|
|
return "", fmt.Errorf("unsupported release architecture: %s", goarch)
|
|
}
|
|
}
|
|
|
|
func normalizeVersionTag(version string) (string, error) {
|
|
match := versionPattern.FindStringSubmatch(strings.TrimSpace(version))
|
|
if match == nil {
|
|
return "", fmt.Errorf("invalid semantic version: %s", version)
|
|
}
|
|
major, err := parseVersionComponent(version, match[1])
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
minor, err := parseVersionComponent(version, match[2])
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
patch, err := parseVersionComponent(version, match[3])
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("%d.%d.%d", major, minor, patch), nil
|
|
}
|
|
|
|
func parseSemverNormalized(version string) (semverParts, error) {
|
|
parts := strings.Split(version, ".")
|
|
if len(parts) != 3 {
|
|
return semverParts{}, fmt.Errorf("invalid semantic version: %s", version)
|
|
}
|
|
major, err := parseVersionComponent(version, parts[0])
|
|
if err != nil {
|
|
return semverParts{}, err
|
|
}
|
|
minor, err := parseVersionComponent(version, parts[1])
|
|
if err != nil {
|
|
return semverParts{}, err
|
|
}
|
|
patch, err := parseVersionComponent(version, parts[2])
|
|
if err != nil {
|
|
return semverParts{}, err
|
|
}
|
|
return semverParts{major, minor, patch}, nil
|
|
}
|
|
|
|
func compareSemverParts(left semverParts, right semverParts) int {
|
|
for index := range left {
|
|
if left[index] != right[index] {
|
|
return left[index] - right[index]
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func firstNonEmpty(values ...string) string {
|
|
for _, value := range values {
|
|
value = strings.TrimSpace(value)
|
|
if value != "" {
|
|
return value
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func parseVersionComponent(version string, component string) (int, error) {
|
|
parsed, err := strconv.ParseInt(component, 10, 31)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("invalid semantic version: %s", version)
|
|
}
|
|
return int(parsed), nil
|
|
}
|