Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package arkade for openSUSE:Factory checked in at 2026-01-30 18:23:38 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/arkade (Old) and /work/SRC/openSUSE:Factory/.arkade.new.1995 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "arkade" Fri Jan 30 18:23:38 2026 rev:71 rq:1329943 version:0.11.69 Changes: -------- --- /work/SRC/openSUSE:Factory/arkade/arkade.changes 2026-01-28 15:09:44.572472558 +0100 +++ /work/SRC/openSUSE:Factory/.arkade.new.1995/arkade.changes 2026-01-30 18:23:47.805332879 +0100 @@ -1,0 +2,9 @@ +Fri Jan 30 06:35:03 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 0.11.69 (.68 was not released): + * Reformat text + * Update sponsors message + * Retry failed downloads, improve messages + * Add copilot and charm + +------------------------------------------------------------------- Old: ---- arkade-0.11.67.obscpio New: ---- arkade-0.11.69.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ arkade.spec ++++++ --- /var/tmp/diff_new_pack.QV4VdC/_old 2026-01-30 18:23:50.249436151 +0100 +++ /var/tmp/diff_new_pack.QV4VdC/_new 2026-01-30 18:23:50.261436658 +0100 @@ -17,7 +17,7 @@ Name: arkade -Version: 0.11.67 +Version: 0.11.69 Release: 0 Summary: Open Source Kubernetes Marketplace License: Apache-2.0 ++++++ _service ++++++ --- /var/tmp/diff_new_pack.QV4VdC/_old 2026-01-30 18:23:50.557449166 +0100 +++ /var/tmp/diff_new_pack.QV4VdC/_new 2026-01-30 18:23:50.585450348 +0100 @@ -3,7 +3,7 @@ <param name="url">https://github.com/alexellis/arkade</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="revision">0.11.67</param> + <param name="revision">0.11.69</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="changesgenerate">enable</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.QV4VdC/_old 2026-01-30 18:23:50.725456264 +0100 +++ /var/tmp/diff_new_pack.QV4VdC/_new 2026-01-30 18:23:50.757457617 +0100 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/alexellis/arkade</param> - <param name="changesrevision">3db8251b4c65a3903e6e80b9a053bdef59e87d8d</param></service></servicedata> + <param name="changesrevision">d41670cce4347218c930be4d11988a3697e82737</param></service></servicedata> (No newline at EOF) ++++++ arkade-0.11.67.obscpio -> arkade-0.11.69.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/arkade-0.11.67/README.md new/arkade-0.11.69/README.md --- old/arkade-0.11.67/README.md 2026-01-27 11:29:09.000000000 +0100 +++ new/arkade-0.11.69/README.md 2026-01-28 23:40:13.000000000 +0100 @@ -761,7 +761,6 @@ ### Catalog of CLIs <!-- start of tool list --> - | TOOL | DESCRIPTION | |------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [actions-usage](https://github.com/self-actuated/actions-usage) | Get usage insights from GitHub Actions. | @@ -791,11 +790,13 @@ | [conftest](https://github.com/open-policy-agent/conftest) | Write tests against structured configuration data using the Open Policy Agent Rego query language | | [consul](https://github.com/hashicorp/consul) | A solution to connect and configure applications across dynamic, distributed infrastructure | | [copa](https://github.com/project-copacetic/copacetic) | CLI for patching container images | +| [copilot](https://github.com/github/copilot-cli) | GitHub Copilot CLI - AI-powered command line assistant | | [cosign](https://github.com/sigstore/cosign) | Container Signing, Verification and Storage in an OCI registry. | | [cr](https://github.com/helm/chart-releaser) | Hosting Helm Charts via GitHub Pages and Releases | | [crane](https://github.com/google/go-containerregistry) | crane is a tool for interacting with remote images and registries | | [croc](https://github.com/schollz/croc) | Easily and securely send things from one computer to another | | [crossplane](https://github.com/crossplane/crossplane) | Simplify some development and administration aspects of Crossplane. | +| [crush](https://github.com/charmbracelet/crush) | A delightful AI assistant for your terminal | | [dagger](https://github.com/dagger/dagger) | A portable devkit for CI/CD pipelines. | | [devpod](https://github.com/loft-sh/devpod) | Codespaces but open-source, client-only and unopinionated: Works with any IDE and lets you use any cloud, kubernetes or just localhost docker. | | [devspace](https://github.com/devspace-sh/devspace) | Automate your deployment workflow with DevSpace and develop software directly inside Kubernetes. | @@ -953,9 +954,7 @@ | [websocat](https://github.com/vi/websocat) | Command-line client for WebSockets, like netcat/socat but for WebSockets | | [yq](https://github.com/mikefarah/yq) | Portable command-line YAML processor. | | [yt-dlp](https://github.com/yt-dlp/yt-dlp) | Fork of youtube-dl with additional features and fixes | -There are 189 tools, use `arkade get NAME` to download one. - +There are 191 tools, use `arkade get NAME` to download one. -<!-- end of tool list --> > Note to contributors, run `go run . get --format markdown` to generate this > list diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/arkade-0.11.67/pkg/get/download.go new/arkade-0.11.69/pkg/get/download.go --- old/arkade-0.11.67/pkg/get/download.go 2026-01-27 11:29:09.000000000 +0100 +++ new/arkade-0.11.69/pkg/get/download.go 2026-01-28 23:40:13.000000000 +0100 @@ -15,6 +15,8 @@ "text/template" "time" + units "github.com/docker/go-units" + "github.com/alexellis/arkade/pkg" "github.com/alexellis/arkade/pkg/archive" "github.com/alexellis/arkade/pkg/config" @@ -45,16 +47,23 @@ } if !quiet { - fmt.Printf("Downloading: %s\n", downloadURL) + log.Printf("Downloading: %s", downloadURL) } + start := time.Now() outFilePath, err := downloadFile(downloadURL, displayProgress) if err != nil { return "", "", err } if !quiet { - fmt.Printf("%s written.\n", outFilePath) + filename := path.Base(downloadURL) + stat, err := os.Stat(outFilePath) + size := "" + if err == nil { + size = "(" + units.HumanSize(float64(stat.Size())) + ")" + } + log.Printf("Downloaded %s %s in %s.", filename, size, time.Since(start).Round(time.Millisecond)) } if verify { @@ -210,7 +219,7 @@ } outFilePath = outPath - if !quiet { + if v, ok := os.LookupEnv("ARK_DEBUG"); ok && v == "1" { log.Printf("Extracted: %s", outFilePath) } } @@ -233,7 +242,7 @@ localPath = filepath.Join(movePath, finalName) } - if !quiet { + if v, ok := os.LookupEnv("ARK_DEBUG"); ok && v == "1" { log.Printf("Copying %s to %s\n", outFilePath, localPath) } @@ -259,57 +268,58 @@ } func downloadFile(downloadURL string, displayProgress bool) (string, error) { + return retryWithBackoff(func() (string, error) { + req, err := http.NewRequest(http.MethodGet, downloadURL, nil) + if err != nil { + return "", err + } - req, err := http.NewRequest(http.MethodGet, downloadURL, nil) - if err != nil { - return "", err - } - - req.Header.Set("User-Agent", pkg.UserAgent()) + req.Header.Set("User-Agent", pkg.UserAgent()) - res, err := http.DefaultClient.Do(req) - if err != nil { - return "", err - } + res, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } - if res.Body != nil { - defer res.Body.Close() - } + if res.Body != nil { + defer res.Body.Close() + } - if res.StatusCode == http.StatusNotFound { - return "", &ErrNotFound{} - } + if res.StatusCode == http.StatusNotFound { + return "", &ErrNotFound{} + } - if res.StatusCode != http.StatusOK { - return "", fmt.Errorf("server returned status: %d", res.StatusCode) - } + if res.StatusCode != http.StatusOK { + return "", fmt.Errorf("server returned status: %d", res.StatusCode) + } - _, fileName := path.Split(downloadURL) - tmp := os.TempDir() + _, fileName := path.Split(downloadURL) + tmp := os.TempDir() - customTmp, err := os.MkdirTemp(tmp, "arkade-*") - if err != nil { - return "", err - } + customTmp, err := os.MkdirTemp(tmp, "arkade-*") + if err != nil { + return "", err + } - outFilePath := path.Join(customTmp, fileName) - wrappedReader := withProgressBar(res.Body, int(res.ContentLength), displayProgress) + outFilePath := path.Join(customTmp, fileName) + wrappedReader := withProgressBar(res.Body, int(res.ContentLength), displayProgress) - // Owner/Group read/write/execute - // World - execute - out, err := os.OpenFile(outFilePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775) - if err != nil { - return "", err - } + // Owner/Group read/write/execute + // World - execute + out, err := os.OpenFile(outFilePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775) + if err != nil { + return "", err + } - defer out.Close() - defer wrappedReader.Close() + defer out.Close() + defer wrappedReader.Close() - if _, err := io.Copy(out, wrappedReader); err != nil { - return "", err - } + if _, err := io.Copy(out, wrappedReader); err != nil { + return "", err + } - return outFilePath, nil + return outFilePath, nil + }, 10, 100*time.Millisecond) } func CopyFile(src, dst string) (int64, error) { @@ -404,7 +414,7 @@ } if !quiet { - fmt.Printf("Name: %s, size: %d", fInfo.Name(), fInfo.Size()) + log.Printf("Name: %s, size: %d", fInfo.Name(), fInfo.Size()) } if err := archive.Unzip(archiveFile, fInfo.Size(), outFilePathDir, forceQuiet); err != nil { @@ -416,27 +426,29 @@ } func fetchText(url string) (string, error) { - req, err := http.NewRequest(http.MethodGet, url, nil) - if err != nil { - return "", err - } - req.Header.Set("User-Agent", pkg.UserAgent()) - res, err := http.DefaultClient.Do(req) - if err != nil { - return "", err - } + return retryWithBackoff(func() (string, error) { + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return "", err + } + req.Header.Set("User-Agent", pkg.UserAgent()) + res, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } - var body []byte - if res.Body != nil { - defer res.Body.Close() - body, _ = io.ReadAll(res.Body) - } + var body []byte + if res.Body != nil { + defer res.Body.Close() + body, _ = io.ReadAll(res.Body) + } - if res.StatusCode != http.StatusOK { - return "", fmt.Errorf("unexpected status code %d, body: %s", res.StatusCode, string(body)) - } + if res.StatusCode != http.StatusOK { + return "", fmt.Errorf("unexpected status code %d, body: %s", res.StatusCode, string(body)) + } - return string(body), nil + return string(body), nil + }, 10, 100*time.Millisecond) } func verifySHA(shaSum, outFilePath string) error { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/arkade-0.11.67/pkg/get/get.go new/arkade-0.11.69/pkg/get/get.go --- old/arkade-0.11.67/pkg/get/get.go 2026-01-27 11:29:09.000000000 +0100 +++ new/arkade-0.11.69/pkg/get/get.go 2026-01-28 23:40:13.000000000 +0100 @@ -9,6 +9,7 @@ "log" "net" "net/http" + "strings" "text/template" "time" @@ -30,6 +31,45 @@ var supportedOS = [...]string{"linux", "darwin", "ming"} var supportedArchitectures = [...]string{"x86_64", "arm", "amd64", "armv6l", "armv7l", "arm64", "aarch64"} +// retryWithBackoff implements exponential backoff with retry logic +func retryWithBackoff(fn func() (string, error), maxRetries int, initialBackoff time.Duration) (string, error) { + var lastErr error + for attempt := 0; attempt <= maxRetries; attempt++ { + if attempt > 0 { + backoff := initialBackoff * time.Duration(1<<uint(attempt-1)) + if backoff > 10*time.Second { + backoff = 10 * time.Second + } + log.Printf("Attempt %d: Retrying after %v due to: %v", attempt, backoff, lastErr) + time.Sleep(backoff) + } + + result, err := fn() + if err == nil { + return result, nil + } + + lastErr = err + + // Don't retry permanent errors + if isPermanentError(err) { + return "", err + } + } + return "", fmt.Errorf("failed after %d attempts: %w", maxRetries+1, lastErr) +} + +func isPermanentError(err error) bool { + // 404, 429 are permanent errors + if strings.Contains(err.Error(), "404") { + return true + } + if strings.Contains(err.Error(), "429") { + return true + } + return false +} + // Tool describes how to download a CLI tool from a binary // release - whether a single binary, or an archive. type Tool struct { @@ -205,16 +245,32 @@ if _, supported := releaseLocations[releaseType]; supported { + start := time.Now() v, err := FindRelease(releaseType, tool.Owner, tool.Repo) if err != nil { return "", "", err } version = v + if !quiet { + log.Printf("Found %s version: %s (in %s)", tool.Name, version, time.Since(start).Round(time.Millisecond)) + } } - if !quiet { - log.Printf("Found: %s", version) + resolvedVersion = version + + if len(tool.URLTemplate) > 0 { + res, err := getByDownloadTemplate(tool, os, arch, version) + if err != nil { + return "", "", err + } + return res, resolvedVersion, nil + } + + res, err := getURLByGithubTemplate(tool, os, arch, version) + if err != nil { + return "", "", err } + return res, resolvedVersion, nil } resolvedVersion = version @@ -276,46 +332,48 @@ return http.ErrUseLastResponse } - req, err := http.NewRequest(releaseLocations[location].Method, url, nil) - if err != nil { - return "", err - } + return retryWithBackoff(func() (string, error) { + req, err := http.NewRequest(releaseLocations[location].Method, url, nil) + if err != nil { + return "", err + } - req.Header.Set("User-Agent", pkg.UserAgent()) + req.Header.Set("User-Agent", pkg.UserAgent()) - res, err := client.Do(req) - if err != nil { - return "", err - } + res, err := client.Do(req) + if err != nil { + return "", err + } + + defer res.Body.Close() + + if releaseLocations[location].Method == http.MethodHead { - defer res.Body.Close() + if res.StatusCode != http.StatusMovedPermanently && res.StatusCode != http.StatusFound { + return "", fmt.Errorf("server returned status: %d", res.StatusCode) + } - if releaseLocations[location].Method == http.MethodHead { + loc := res.Header.Get("Location") + if len(loc) == 0 { + return "", fmt.Errorf("unable to determine release of tool") + } - if res.StatusCode != http.StatusMovedPermanently && res.StatusCode != http.StatusFound { - return "", fmt.Errorf("server returned status: %d", res.StatusCode) + version := loc[strings.LastIndex(loc, "/")+1:] + return version, nil } - loc := res.Header.Get("Location") - if len(loc) == 0 { + if res.Body == nil { return "", fmt.Errorf("unable to determine release of tool") } - version := loc[strings.LastIndex(loc, "/")+1:] - return version, nil - } - - if res.Body == nil { - return "", fmt.Errorf("unable to determine release of tool") - } - - bodyBytes, err := io.ReadAll(res.Body) - if err != nil { - return "", err - } + bodyBytes, err := io.ReadAll(res.Body) + if err != nil { + return "", err + } - version := strings.TrimSpace(string(bodyBytes)) - return version, nil + version := strings.TrimSpace(string(bodyBytes)) + return version, nil + }, 10, 100*time.Millisecond) } func formatUrl(url, owner, repo string) string { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/arkade-0.11.67/pkg/get/get_test.go new/arkade-0.11.69/pkg/get/get_test.go --- old/arkade-0.11.67/pkg/get/get_test.go 2026-01-27 11:29:09.000000000 +0100 +++ new/arkade-0.11.69/pkg/get/get_test.go 2026-01-28 23:40:13.000000000 +0100 @@ -9613,3 +9613,115 @@ }) } } + +func Test_DownloadCopilot(t *testing.T) { + tools := MakeTools() + name := "copilot" + const version = "v0.0.396" + + tool := getTool(name, tools) + + tests := []test{ + { + os: "ming", + arch: arch64bit, + version: version, + url: `https://github.com/github/copilot-cli/releases/download/v0.0.396/copilot-win32-x64.zip`, + }, + { + os: "ming", + arch: archARM64, + version: version, + url: `https://github.com/github/copilot-cli/releases/download/v0.0.396/copilot-win32-arm64.zip`, + }, + { + os: "linux", + arch: arch64bit, + version: version, + url: `https://github.com/github/copilot-cli/releases/download/v0.0.396/copilot-linux-x64.tar.gz`, + }, + { + os: "linux", + arch: archARM64, + version: version, + url: `https://github.com/github/copilot-cli/releases/download/v0.0.396/copilot-linux-arm64.tar.gz`, + }, + { + os: "darwin", + arch: arch64bit, + version: version, + url: `https://github.com/github/copilot-cli/releases/download/v0.0.396/copilot-darwin-x64.tar.gz`, + }, + { + os: "darwin", + arch: archDarwinARM64, + version: version, + url: `https://github.com/github/copilot-cli/releases/download/v0.0.396/copilot-darwin-arm64.tar.gz`, + }, + } + + for _, tc := range tests { + t.Run(fmt.Sprintf("Download for: %s %s %s", tc.os, tc.arch, tc.version), func(r *testing.T) { + got, _, err := tool.GetURL(tc.os, tc.arch, tc.version, false) + if err != nil { + t.Fatal(err) + } + if got != tc.url { + t.Errorf("\nwant: %s\ngot: %s", tc.url, got) + } + }) + } +} + +func Test_DownloadCrush(t *testing.T) { + tools := MakeTools() + name := "crush" + const version = "v0.36.0" + + tool := getTool(name, tools) + + tests := []test{ + { + os: "ming", + arch: arch64bit, + version: version, + url: `https://github.com/charmbracelet/crush/releases/download/v0.36.0/crush_0.36.0_Windows_x86_64.zip`, + }, + { + os: "linux", + arch: arch64bit, + version: version, + url: `https://github.com/charmbracelet/crush/releases/download/v0.36.0/crush_0.36.0_Linux_x86_64.tar.gz`, + }, + { + os: "linux", + arch: archARM64, + version: version, + url: `https://github.com/charmbracelet/crush/releases/download/v0.36.0/crush_0.36.0_Linux_arm64.tar.gz`, + }, + { + os: "darwin", + arch: arch64bit, + version: version, + url: `https://github.com/charmbracelet/crush/releases/download/v0.36.0/crush_0.36.0_Darwin_x86_64.tar.gz`, + }, + { + os: "darwin", + arch: archDarwinARM64, + version: version, + url: `https://github.com/charmbracelet/crush/releases/download/v0.36.0/crush_0.36.0_Darwin_arm64.tar.gz`, + }, + } + + for _, tc := range tests { + t.Run(fmt.Sprintf("Download for: %s %s %s", tc.os, tc.arch, tc.version), func(r *testing.T) { + got, _, err := tool.GetURL(tc.os, tc.arch, tc.version, false) + if err != nil { + t.Fatal(err) + } + if got != tc.url { + t.Errorf("\nwant: %s\ngot: %s", tc.url, got) + } + }) + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/arkade-0.11.67/pkg/get/tools.go new/arkade-0.11.69/pkg/get/tools.go --- old/arkade-0.11.67/pkg/get/tools.go 2026-01-27 11:29:09.000000000 +0100 +++ new/arkade-0.11.69/pkg/get/tools.go 2026-01-28 23:40:13.000000000 +0100 @@ -5281,5 +5281,57 @@ https://github.com/vi/websocat/releases/download/{{.Version}}/websocat.{{$target}}`, }) + tools = append(tools, + Tool{ + Owner: "github", + Repo: "copilot-cli", + Name: "copilot", + Description: "GitHub Copilot CLI - AI-powered command line assistant", + BinaryTemplate: ` +{{$os := ""}} +{{$arch := ""}} +{{$ext := "tar.gz"}} +{{- if eq .OS "darwin" -}} + {{$os = "darwin"}} +{{- else if eq .OS "linux" -}} + {{$os = "linux"}} +{{- else if HasPrefix .OS "ming" -}} + {{$os = "win32"}} + {{$ext = "zip"}} +{{- end -}} +{{- if or (eq .Arch "x86_64") (eq .Arch "amd64") -}} + {{$arch = "x64"}} +{{- else if or (eq .Arch "aarch64") (eq .Arch "arm64") -}} + {{$arch = "arm64"}} +{{- end -}} +{{.Name}}-{{$os}}-{{$arch}}.{{$ext}}`, + }) + + tools = append(tools, + Tool{ + Owner: "charmbracelet", + Repo: "crush", + Name: "crush", + Description: "A delightful AI assistant for your terminal", + URLTemplate: ` +{{$arch := .Arch}} +{{- if eq .Arch "x86_64" -}} + {{$arch = "x86_64"}} +{{- else if eq .Arch "aarch64" -}} + {{$arch = "arm64"}} +{{- end -}} +{{$osStr := ""}} +{{$extStr := "tar.gz"}} +{{- if eq .OS "darwin" -}} + {{$osStr = "Darwin"}} +{{- else if eq .OS "linux" -}} + {{$osStr = "Linux"}} +{{- else if HasPrefix .OS "ming" -}} + {{$osStr = "Windows"}} + {{$extStr = "zip"}} +{{- end -}} +https://github.com/{{.Owner}}/{{.Repo}}/releases/download/{{.Version}}/{{.Name}}_{{.VersionNumber}}_{{$osStr}}_{{$arch}}.{{$extStr}}`, + }) + return tools } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/arkade-0.11.67/pkg/thanks.go new/arkade-0.11.69/pkg/thanks.go --- old/arkade-0.11.67/pkg/thanks.go 2026-01-27 11:29:09.000000000 +0100 +++ new/arkade-0.11.69/pkg/thanks.go 2026-01-28 23:40:13.000000000 +0100 @@ -4,4 +4,4 @@ package pkg // SupportMessageShort shows how to support arkade -const SupportMessageShort = `👏 Say thanks for arkade and sponsor Alex via GitHub: https://github.com/sponsors/alexellis` +const SupportMessageShort = `🤝 Sponsor Alex's work: https://github.com/sponsors/alexellis` ++++++ arkade.obsinfo ++++++ --- /var/tmp/diff_new_pack.QV4VdC/_old 2026-01-30 18:23:53.085555986 +0100 +++ /var/tmp/diff_new_pack.QV4VdC/_new 2026-01-30 18:23:53.101556663 +0100 @@ -1,5 +1,5 @@ name: arkade -version: 0.11.67 -mtime: 1769509749 -commit: 3db8251b4c65a3903e6e80b9a053bdef59e87d8d +version: 0.11.69 +mtime: 1769640013 +commit: d41670cce4347218c930be4d11988a3697e82737 ++++++ vendor.tar.gz ++++++
