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-02-10 21:13:32 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/arkade (Old) and /work/SRC/openSUSE:Factory/.arkade.new.1670 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "arkade" Tue Feb 10 21:13:32 2026 rev:74 rq:1332248 version:0.11.75 Changes: -------- --- /work/SRC/openSUSE:Factory/arkade/arkade.changes 2026-02-09 13:43:33.148395497 +0100 +++ /work/SRC/openSUSE:Factory/.arkade.new.1670/arkade.changes 2026-02-10 21:14:39.001163207 +0100 @@ -1,0 +2,17 @@ +Tue Feb 10 12:45:19 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 0.11.75: + * Fixes for assuming gh has --json output + * Update SLICER_BOX.md + +------------------------------------------------------------------- +Tue Feb 10 05:52:08 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 0.11.74: + * Initial gh release hidden command + * Guard verification output for progress bar/alt screen +- Update to version 0.11.73: + * Use an alt screen to protect scroll buffer + * Print summary in non-interactive mode + +------------------------------------------------------------------- Old: ---- arkade-0.11.72.obscpio New: ---- arkade-0.11.75.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ arkade.spec ++++++ --- /var/tmp/diff_new_pack.g0gryt/_old 2026-02-10 21:14:39.881200082 +0100 +++ /var/tmp/diff_new_pack.g0gryt/_new 2026-02-10 21:14:39.885200249 +0100 @@ -17,7 +17,7 @@ Name: arkade -Version: 0.11.72 +Version: 0.11.75 Release: 0 Summary: Open Source Kubernetes Marketplace License: Apache-2.0 ++++++ _service ++++++ --- /var/tmp/diff_new_pack.g0gryt/_old 2026-02-10 21:14:39.921201758 +0100 +++ /var/tmp/diff_new_pack.g0gryt/_new 2026-02-10 21:14:39.925201926 +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.72</param> + <param name="revision">0.11.75</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="changesgenerate">enable</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.g0gryt/_old 2026-02-10 21:14:39.945202764 +0100 +++ /var/tmp/diff_new_pack.g0gryt/_new 2026-02-10 21:14:39.949202931 +0100 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/alexellis/arkade</param> - <param name="changesrevision">0e580bd296b08c3ba66cd0cc51ce6a614f31321c</param></service></servicedata> + <param name="changesrevision">8e860ea5a87682d22908c2d4355c50f089b55c0a</param></service></servicedata> (No newline at EOF) ++++++ arkade-0.11.72.obscpio -> arkade-0.11.75.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/arkade-0.11.72/Makefile new/arkade-0.11.75/Makefile --- old/arkade-0.11.72/Makefile 2026-02-07 10:29:49.000000000 +0100 +++ new/arkade-0.11.75/Makefile 2026-02-10 11:35:47.000000000 +0100 @@ -24,6 +24,11 @@ e2e: CGO_ENABLED=0 go test github.com/alexellis/arkade/pkg/get -cover --tags e2e -v +.PHONY: dist-local +dist-local: + mkdir -p bin + CGO_ENABLED=0 go build -ldflags $(LDFLAGS) -o bin/arkade + .PHONY: dist dist: mkdir -p bin diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/arkade-0.11.72/SLICER_BOX.md new/arkade-0.11.75/SLICER_BOX.md --- old/arkade-0.11.72/SLICER_BOX.md 2026-02-07 10:29:49.000000000 +0100 +++ new/arkade-0.11.75/SLICER_BOX.md 2026-02-10 11:35:47.000000000 +0100 @@ -2,6 +2,8 @@ Linux sandbox for AI agents. +Instructions from https://box.slicervm.com/ + ## Pre-reqs Must point slicer to the box service and token file: @@ -65,8 +67,14 @@ Only do this in case of a major issue or when having to test system-level changes. -# Ensure SLICER_URL and SLICER_TOKEN_FILE are set +Ensure SLICER_URL and SLICER_TOKEN_FILE are set + +``` slicer vm delete VM_NAME +``` + Then launch a new VM: +``` slicer vm launch +``` diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/arkade-0.11.72/cmd/AGENTS.md new/arkade-0.11.75/cmd/AGENTS.md --- old/arkade-0.11.72/cmd/AGENTS.md 1970-01-01 01:00:00.000000000 +0100 +++ new/arkade-0.11.75/cmd/AGENTS.md 2026-02-10 11:35:47.000000000 +0100 @@ -0,0 +1,146 @@ +# AGENTS.md - Guide for AI Agents working on `arkade release` + +This document covers the hidden `release` command (`cmd/release.go`), which creates GitHub releases from any repo using `gh`. + +## Overview + +The command is registered in `main.go` as a hidden top-level command with aliases `rel` and `r`. It is not shown in `arkade --help` output. + +**Source file:** `cmd/release.go` +**Registration:** `main.go` via `rootCmd.AddCommand(cmd.MakeRelease())` + +## What the command does + +1. Detects repo visibility (public/private) via `gh api repos/{owner}/{repo}` +2. Queries the latest release tag via `gh api repos/{owner}/{repo}/releases` +3. Parses the tag as semver and increments the patch version +4. Gets the latest commit subject line via `git log` for the release title +5. Creates the release via `gh release create` + +No release notes are generated. A separate webhook bot handles release note generation. + +## Public vs Private repo rules + +This is the most important design decision in the command. + +| | Public repo | Private / Internal repo | +|---|---|---| +| `--prerelease` | `true` (default) | `false` (default) | +| `--latest` | `false` (default) | `true` (default) | + +**Why:** Public repos use a bot that promotes pre-releases to latest after verification. Private repos have no such bot, so releases are immediately marked as latest and stable. + +Visibility is auto-detected via `gh api repos/{owner}/{repo}`. The result is compared against `PRIVATE` and `INTERNAL` (both treated as private). Everything else (including `PUBLIC`) uses public-repo defaults. + +Both flags can be overridden explicitly: +```bash +arkade release --prerelease=false --latest=true +``` + +**When modifying this logic, never change the defaults without understanding the bot promotion workflow for public repos.** + +## Semver handling + +- Both `v`-prefixed (`v0.1.0`) and bare (`0.1.0`) tags are supported +- The prefix style from the previous release is preserved in the new tag +- The bump strategy is controlled by `--major`, `--minor`, `--patch` flags (mutually exclusive) +- Default is `--patch` when none are specified +- When a version is given as a positional argument, the bump flags are ignored +- The `github.com/Masterminds/semver` library is used (already vendored, same as `cmd/chart/bump.go`) + +### Bump strategies + +| Flag | Example | +|---|---| +| `--patch` (default) | `v1.2.3` -> `v1.2.4` | +| `--minor` | `v1.2.3` -> `v1.3.0` | +| `--major` | `v1.2.3` -> `v2.0.0` | + +The `bumpVersion()` function handles all three via the `bumpStrategy` type. The `parseBumpStrategy()` function validates mutual exclusion of the flags. + +## Positional arguments + +The command accepts 0, 1, or 2 positional arguments: + +| Arguments | Behaviour | +|---|---| +| None | Version auto-detected, title from latest commit | +| One, looks like semver | Used as version override, title from latest commit | +| One, not semver | Used as title override, version auto-detected | +| Two | First is version, second is title | + +The `looksLikeSemver()` heuristic checks: optional `v`/`V` prefix, starts with a digit, contains a dot. + +## External dependencies + +The command shells out to two tools: + +- **`gh`** (GitHub CLI) - for release listing, repo visibility, and release creation. Must be authenticated (`gh auth login`). Supports private repos. +- **`git`** - for reading the latest commit message. + +Both are called via `github.com/alexellis/go-execute/v2`, not `os/exec` directly. This is consistent with the rest of the arkade codebase. + +## Flags + +| Flag | Type | Default | Description | +|---|---|---|---| +| `--dry-run` | bool | `false` | Print what would be created, don't call `gh release create` | +| `--major` | bool | `false` | Bump the major version | +| `--minor` | bool | `false` | Bump the minor version | +| `--patch` | bool | `false` | Bump the patch version (this is the default when none specified) | +| `--prerelease` | string | `""` (auto) | `"true"` or `"false"`. Auto-detected from repo visibility when empty | +| `--latest` | string | `""` (auto) | `"true"` or `"false"`. Auto-detected from repo visibility when empty | +| `--token` | string | `""` | GitHub token passed via `GH_TOKEN` env var to `gh` | + +Note: `--prerelease` and `--latest` are string flags (not bool) so that the empty string means "not set by user, use auto-detected default". This is intentional. + +Note: `--major`, `--minor`, `--patch` are mutually exclusive. If more than one is set, the command returns an error. + +## Testing + +There are no unit tests for this command. To verify behaviour: + +```bash +# Build +go build -o arkade . + +# Dry run in a public repo (default = patch bump) +./arkade release --dry-run +# Expected: Prerelease: true, Latest: false, Bump: patch + +# Minor bump +./arkade release --dry-run --minor + +# Major bump +./arkade release --dry-run --major + +# Mutual exclusion error +./arkade release --dry-run --major --minor +# Expected: error + +# Dry run with overrides +./arkade release --dry-run --prerelease=false --latest=true + +# Test argument parsing +./arkade rel --dry-run v1.0.0 +./arkade r --dry-run "Some title" +./arkade release --dry-run 1.0.0 "Some title" + +# Verify aliases +./arkade rel --help +./arkade r --help +``` + +## Modifying the command + +### Adding new flags +Add them in `MakeRelease()` before the `command.RunE` assignment. Use the same pattern as existing flags. + +### Changing visibility detection +The `repoIsPrivate()` function is self-contained. It returns `true` for `PRIVATE` and `INTERNAL` visibility values from `gh`. + +### Changing the version bump strategy +The `bumpVersion()` function handles version incrementing using the `bumpStrategy` type. It supports `bumpPatchStrategy`, `bumpMinorStrategy`, and `bumpMajorStrategy`. The user selects the strategy via `--major`, `--minor`, `--patch` flags, validated by `parseBumpStrategy()`. + +### Error handling for repos with no releases +When `gh release list` returns no tags, `latestReleaseTag()` returns an error. The error message tells the user to pass a version explicitly as a positional argument. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/arkade-0.11.72/cmd/get.go new/arkade-0.11.75/cmd/get.go --- old/arkade-0.11.72/cmd/get.go 2026-02-07 10:29:49.000000000 +0100 +++ new/arkade-0.11.75/cmd/get.go 2026-02-10 11:35:47.000000000 +0100 @@ -242,18 +242,26 @@ out = os.Stderr } - // Hide cursor during live rendering. + // Use alternate screen buffer for TTY progress so that + // intermediate frames don't pollute scrollback. The final + // completed state is printed as static text after leaving + // the alternate screen. if tty && renderProgress { - fmt.Fprint(out, "\033[?25l") - defer fmt.Fprint(out, "\033[?25h\n") + fmt.Fprint(out, "\033[?1049h") // enter alternate screen + fmt.Fprint(out, "\033[?25l") // hide cursor } - // Restore cursor on signal. - go func() { - <-signalChan + leaveAltScreen := func() { if tty && renderProgress { - fmt.Fprint(out, "\033[?25h\n") + fmt.Fprint(out, "\033[?25h") // restore cursor + fmt.Fprint(out, "\033[?1049l") // leave alternate screen } + } + + // Restore terminal on signal. + go func() { + <-signalChan + leaveAltScreen() os.Exit(2) }() @@ -315,9 +323,14 @@ if renderProgress { if tty { renderTTY(out, progress, parallel) + leaveAltScreen() + // Print a static final frame into the main scrollback. + renderTTYFinal(out, progress) } else { renderPlain(out, progress) } + } else if tty { + leaveAltScreen() } if firstErr != nil { @@ -340,15 +353,8 @@ if !quiet { fmt.Fprintln(out) - multiTool := len(localToolsStore) > 1 - - // Show summary in non-TTY mode always (CI needs it), and - // in TTY mode only for multi-tool downloads (single-tool - // TTY already has rich per-line output). - if multiTool || !tty { - // ── Group summary ──────────────────────────── - printGroupSummary(out, progress, time.Since(groupStart), tty) - } + // ── Group summary ──────────────────────────── + printGroupSummary(out, progress, time.Since(groupStart), tty) if len(localToolsStore) > 0 { arkadeBinInPath := movePath == "" && get.ArkadeInPath() @@ -565,6 +571,79 @@ fmt.Fprint(out, b.String()) } +// ── Static final frame (printed after leaving alternate screen) ──── +// +// Shows the completed state of each tool as static text in the main +// scrollback. No cursor movement or screen clearing. + +func renderTTYFinal(out io.Writer, progress []toolProgress) { + var b strings.Builder + + b.WriteString("\033[1m Arkade by Alex Ellis - https://github.com/sponsors/alexellis\033[0m\n\n") + + nameW := 10 + for i := range progress { + l := len(progress[i].name) + if progress[i].version != "" { + l += len(progress[i].version) + 3 + } + if l > nameW { + nameW = l + } + } + if nameW > 40 { + nameW = 40 + } + + barW := 20 + + for i := range progress { + p := &progress[i] + read := atomic.LoadInt64(&p.bytesRead) + total := atomic.LoadInt64(&p.totalBytes) + + displayName := p.name + if p.version != "" { + displayName = fmt.Sprintf("%s (%s)", p.name, p.version) + } + + switch p.status { + case stDone: + pct := 100 + if total > 0 { + pct = int(math.Round(float64(read) / float64(total) * 100)) + if pct > 100 { + pct = 100 + } + } + bar := renderBar(int64(pct), barW) + sizeStr := units.HumanSize(float64(read)) + speed := float64(0) + if p.elapsed > 0 { + speed = float64(read) / p.elapsed.Seconds() + } + elapsed := fmtDuration(p.elapsed) + b.WriteString(fmt.Sprintf( + " \033[32m✔\033[0m %-*s %3d%% %s %8s %9s/s %s\n", + nameW, displayName, pct, bar, sizeStr, units.HumanSize(speed), elapsed)) + + case stFailed: + errMsg := "" + if p.err != nil { + errMsg = p.err.Error() + if len(errMsg) > 40 { + errMsg = errMsg[:40] + "…" + } + } + b.WriteString(fmt.Sprintf( + " \033[31m✘\033[0m %-*s \033[31mfailed: %s\033[0m\n", + nameW, p.name, errMsg)) + } + } + + fmt.Fprint(out, b.String()) +} + // ── Non-TTY / plain renderer ─────────────────────────────────────── // // Prints each state change on its own line, no ANSI, no overwrites. @@ -587,7 +666,11 @@ case stResolving: fmt.Fprintf(out, "[resolving] %s\n", p.name) case stDownloading: - fmt.Fprintf(out, "[downloading] %s\n", p.name) + displayName := p.name + if p.version != "" { + displayName = fmt.Sprintf("%s (%s)", p.name, p.version) + } + fmt.Fprintf(out, "[downloading] %s\n", displayName) case stDone: read := atomic.LoadInt64(&p.bytesRead) sizeStr := units.HumanSize(float64(read)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/arkade-0.11.72/cmd/release.go new/arkade-0.11.75/cmd/release.go --- old/arkade-0.11.72/cmd/release.go 1970-01-01 01:00:00.000000000 +0100 +++ new/arkade-0.11.75/cmd/release.go 2026-02-10 11:35:47.000000000 +0100 @@ -0,0 +1,398 @@ +// Copyright (c) arkade author(s) 2024. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +package cmd + +import ( + "context" + "fmt" + "strings" + + "github.com/Masterminds/semver" + execute "github.com/alexellis/go-execute/v2" + "github.com/spf13/cobra" +) + +func MakeRelease() *cobra.Command { + var command = &cobra.Command{ + Use: "release [version] [title]", + Short: "Create a GitHub release", + Long: `Create a GitHub release by querying the latest release tag, +incrementing the version, and using the latest commit message as the +release title. + +The latest release tag is obtained via "gh release list", so private +repositories are supported when "gh" is authenticated. + +Version bump strategy (mutually exclusive, default is --patch): + --patch v0.1.2 -> v0.1.3 + --minor v0.1.2 -> v0.2.0 + --major v0.1.2 -> v1.0.0 + +Semver handling: + - Tags such as "v0.1.0" (prefixed) and "0.1.0" (bare) are both recognised. + - The "v" prefix is preserved if the previous tag used one. + +Repository visibility changes the defaults: + - Public repos: --prerelease=true --latest=false + A separate bot promotes pre-releases to latest after verification. + - Private repos: --prerelease=false --latest=true + Releases are immediately marked as latest and stable. + +Visibility is auto-detected via "gh repo view". Use --prerelease or --latest +to override. + +Both version and title can be overridden with positional arguments. If only +one argument is given it is treated as a version when it looks like semver, +otherwise it is used as the title. Supply both to set both. +When a version is given explicitly, --major/--minor/--patch are ignored.`, + Example: ` # Auto-detect everything (version, title, visibility) + # Bumps patch by default: v0.1.2 -> v0.1.3 + arkade release + + # Bump minor version: v0.1.2 -> v0.2.0 + arkade release --minor + + # Bump major version: v0.1.2 -> v1.0.0 + arkade release --major + + # Public repo: creates a pre-release, not marked latest + arkade release + + # Private repo: creates a full release, marked latest + arkade rel + + # Override the version (--major/--minor/--patch ignored) + arkade rel v0.5.0 + + # Override just the title + arkade r "Fix CI pipeline" + + # Override both version and title + arkade release 0.3.0 "Add ARM64 support" + + # Force a full (non-prerelease) release on a public repo + arkade release --prerelease=false --latest=true + + # Dry run to see what would happen + arkade release --dry-run`, + Hidden: true, + Aliases: []string{"rel", "r"}, + SilenceUsage: true, + } + + command.Flags().Bool("dry-run", false, "Print what would be created without making the release") + command.Flags().Bool("major", false, "Bump the major version (v1.2.3 -> v2.0.0)") + command.Flags().Bool("minor", false, "Bump the minor version (v1.2.3 -> v1.3.0)") + command.Flags().Bool("patch", false, "Bump the patch version (v1.2.3 -> v1.2.4) (default when none specified)") + command.Flags().String("prerelease", "", "Mark as pre-release (default: true for public repos, false for private)") + command.Flags().String("latest", "", "Mark as latest release (default: false for public repos, true for private)") + command.Flags().String("token", "", "GitHub token, if not using gh's default auth") + + command.RunE = func(cmd *cobra.Command, args []string) error { + var versionOverride, titleOverride string + + if len(args) == 1 { + if looksLikeSemver(args[0]) { + versionOverride = args[0] + } else { + titleOverride = args[0] + } + } else if len(args) == 2 { + versionOverride = args[0] + titleOverride = args[1] + } else if len(args) > 2 { + return fmt.Errorf("expected at most 2 arguments: [version] [title], got %d", len(args)) + } + + dryRun, _ := cmd.Flags().GetBool("dry-run") + major, _ := cmd.Flags().GetBool("major") + minor, _ := cmd.Flags().GetBool("minor") + patch, _ := cmd.Flags().GetBool("patch") + prereleaseStr, _ := cmd.Flags().GetString("prerelease") + latestStr, _ := cmd.Flags().GetString("latest") + token, _ := cmd.Flags().GetString("token") + + bump, err := parseBumpStrategy(major, minor, patch) + if err != nil { + return err + } + + ctx := context.Background() + + // Detect repo visibility to set defaults + private, err := repoIsPrivate(ctx) + if err != nil { + fmt.Printf("Warning: could not detect repo visibility: %s\nDefaulting to public repo behaviour (pre-release).\n", err) + private = false + } + + prerelease := !private // public -> true, private -> false + if prereleaseStr != "" { + prerelease = prereleaseStr == "true" + } + + latest := private // public -> false, private -> true + if latestStr != "" { + latest = latestStr == "true" + } + + if private { + fmt.Println("Private repo detected.") + } else { + fmt.Println("Public repo detected.") + } + + // Resolve the new version tag + newTag, err := resolveNewTag(ctx, versionOverride, bump) + if err != nil { + return err + } + + // Resolve the release title + title, err := resolveTitle(ctx, titleOverride) + if err != nil { + return err + } + + if dryRun { + fmt.Printf("Would create release:\n Tag: %s\n Title: %s\n Bump: %s\n Notes: (none)\n Prerelease: %v\n Latest: %v\n", newTag, title, bump, prerelease, latest) + return nil + } + + ghArgs := []string{ + "release", "create", newTag, + "--title", title, + "--notes", "", + } + + if prerelease { + ghArgs = append(ghArgs, "--prerelease") + } + + if latest { + ghArgs = append(ghArgs, "--latest") + } else { + ghArgs = append(ghArgs, "--latest=false") + } + + task := execute.ExecTask{ + Command: "gh", + Args: ghArgs, + StreamStdio: true, + PrintCommand: true, + } + + if token != "" { + task.Env = []string{fmt.Sprintf("GH_TOKEN=%s", token)} + } + + res, err := task.Execute(ctx) + if err != nil { + return fmt.Errorf("failed to run gh: %w", err) + } + if res.ExitCode != 0 { + return fmt.Errorf("gh exited with code %d: %s", res.ExitCode, res.Stderr) + } + + kind := "release" + if prerelease { + kind = "pre-release" + } + fmt.Printf("Created %s %s.\n", kind, newTag) + return nil + } + + return command +} + +// bumpStrategy represents which semver component to increment. +type bumpStrategy int + +const ( + bumpPatchStrategy bumpStrategy = iota + bumpMinorStrategy + bumpMajorStrategy +) + +func (b bumpStrategy) String() string { + switch b { + case bumpMinorStrategy: + return "minor" + case bumpMajorStrategy: + return "major" + default: + return "patch" + } +} + +// parseBumpStrategy validates the --major/--minor/--patch flags are mutually +// exclusive and returns the selected strategy. Defaults to patch. +func parseBumpStrategy(major, minor, patch bool) (bumpStrategy, error) { + set := 0 + if major { + set++ + } + if minor { + set++ + } + if patch { + set++ + } + if set > 1 { + return 0, fmt.Errorf("--major, --minor, and --patch are mutually exclusive") + } + + if major { + return bumpMajorStrategy, nil + } + if minor { + return bumpMinorStrategy, nil + } + return bumpPatchStrategy, nil +} + +// repoIsPrivate uses gh to check whether the current repo is private. +func repoIsPrivate(ctx context.Context) (bool, error) { + task := execute.ExecTask{ + Command: "gh", + Args: []string{"api", "repos/{owner}/{repo}", "--jq", ".visibility"}, + } + + res, err := task.Execute(ctx) + if err != nil { + return false, fmt.Errorf("failed to run gh: %w", err) + } + if res.ExitCode != 0 { + return false, fmt.Errorf("gh exited with code %d: %s", res.ExitCode, res.Stderr) + } + + visibility := strings.TrimSpace(strings.ToUpper(res.Stdout)) + return visibility == "PRIVATE" || visibility == "INTERNAL", nil +} + +// resolveNewTag returns the tag for the new release. If versionOverride is +// non-empty it is normalised and returned directly. Otherwise the latest +// release tag is fetched via gh and bumped according to the strategy. +func resolveNewTag(ctx context.Context, versionOverride string, bump bumpStrategy) (string, error) { + if versionOverride != "" { + return normaliseSemver(versionOverride) + } + + latestTag, err := latestReleaseTag(ctx) + if err != nil { + return "", fmt.Errorf("could not determine latest release: %w\nUse a positional argument to set the version explicitly", err) + } + + return bumpVersion(latestTag, bump) +} + +// resolveTitle returns a release title. If titleOverride is non-empty it is +// used directly. Otherwise the first line of the latest git commit message is +// returned. +func resolveTitle(ctx context.Context, titleOverride string) (string, error) { + if titleOverride != "" { + return titleOverride, nil + } + + task := execute.ExecTask{ + Command: "git", + Args: []string{"log", "-1", "--format=%s"}, + } + + res, err := task.Execute(ctx) + if err != nil { + return "", fmt.Errorf("could not get latest commit message: %w", err) + } + if res.ExitCode != 0 { + return "", fmt.Errorf("git log exited with code %d: %s", res.ExitCode, res.Stderr) + } + + msg := strings.TrimSpace(res.Stdout) + if msg == "" { + return "", fmt.Errorf("latest commit message is empty") + } + + return msg, nil +} + +// latestReleaseTag queries gh for the most recent release tag. +func latestReleaseTag(ctx context.Context) (string, error) { + task := execute.ExecTask{ + Command: "gh", + Args: []string{"api", "repos/{owner}/{repo}/releases", "--jq", ".[0].tag_name"}, + } + + res, err := task.Execute(ctx) + if err != nil { + return "", fmt.Errorf("failed to run gh: %w", err) + } + if res.ExitCode != 0 { + return "", fmt.Errorf("gh exited with code %d: %s", res.ExitCode, res.Stderr) + } + + tag := strings.TrimSpace(res.Stdout) + if tag == "" { + return "", fmt.Errorf("no releases found in this repository") + } + + return tag, nil +} + +// bumpVersion increments the specified semver component of a tag. +// Both "v"-prefixed and bare versions are supported; the prefix style is +// preserved in the output. +func bumpVersion(tag string, bump bumpStrategy) (string, error) { + hasV := strings.HasPrefix(tag, "v") + + ver, err := semver.NewVersion(tag) + if err != nil { + return "", fmt.Errorf("cannot parse %q as semver: %w", tag, err) + } + + var bumped semver.Version + switch bump { + case bumpMajorStrategy: + bumped = ver.IncMajor() + case bumpMinorStrategy: + bumped = ver.IncMinor() + default: + bumped = ver.IncPatch() + } + + if hasV { + return "v" + bumped.String(), nil + } + return bumped.String(), nil +} + +// normaliseSemver validates that s looks like semver and returns it unchanged. +func normaliseSemver(s string) (string, error) { + raw := s + if strings.HasPrefix(s, "v") { + s = s[1:] + } + if _, err := semver.NewVersion(s); err != nil { + return "", fmt.Errorf("%q is not a valid semver version", raw) + } + return raw, nil +} + +// looksLikeSemver returns true when s resembles a semver string +// (with or without a "v" prefix). This is intentionally lenient so that +// users can pass e.g. "1.0" and have it treated as a version. +func looksLikeSemver(s string) bool { + raw := s + if strings.HasPrefix(raw, "v") || strings.HasPrefix(raw, "V") { + raw = raw[1:] + } + + // Quick heuristic: starts with a digit and contains a dot. + if len(raw) == 0 { + return false + } + if raw[0] < '0' || raw[0] > '9' { + return false + } + return strings.Contains(raw, ".") +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/arkade-0.11.72/main.go new/arkade-0.11.75/main.go --- old/arkade-0.11.72/main.go 2026-02-07 10:29:49.000000000 +0100 +++ new/arkade-0.11.75/main.go 2026-02-10 11:35:47.000000000 +0100 @@ -32,6 +32,8 @@ rootCmd.AddCommand(cmd.MakeUninstall()) rootCmd.AddCommand(cmd.MakeShellCompletion()) + rootCmd.AddCommand(cmd.MakeRelease()) + rootCmd.AddCommand(chart.MakeChart()) rootCmd.AddCommand(system.MakeSystem()) rootCmd.AddCommand(oci.MakeOci()) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/arkade-0.11.72/pkg/get/download.go new/arkade-0.11.75/pkg/get/download.go --- old/arkade-0.11.72/pkg/get/download.go 2026-02-07 10:29:49.000000000 +0100 +++ new/arkade-0.11.75/pkg/get/download.go 2026-02-10 11:35:47.000000000 +0100 @@ -159,7 +159,9 @@ } verifyURL := strings.TrimSpace(buf.String()) - log.Printf("Downloading SHA sum from: %s", verifyURL) + if !quiet { + log.Printf("Downloading SHA sum from: %s", verifyURL) + } shaSumManifest, err := fetchText(verifyURL) if err != nil { return "", "", err @@ -193,7 +195,7 @@ if err := verifySHA(platformInfo.Checksum, outFilePath); err != nil { return "", "", err - } else { + } else if !quiet { log.Printf("SHA sum verified in %s.", time.Since(st).Round(time.Millisecond)) } @@ -222,14 +224,16 @@ } verifyURL := strings.TrimSpace(buf.String()) - log.Printf("Downloading SHA sum from: %s", verifyURL) + if !quiet { + log.Printf("Downloading SHA sum from: %s", verifyURL) + } shaSum, err := fetchText(verifyURL) if err != nil { return "", "", err } if err := verifySHA(shaSum, outFilePath); err != nil { return "", "", err - } else { + } else if !quiet { log.Printf("SHA sum verified in %s.", time.Since(st).Round(time.Millisecond)) } } else if tool.VerifyStrategy == AmpShasumStrategy { @@ -257,14 +261,16 @@ } verifyURL := strings.TrimSpace(buf.String()) - log.Printf("Downloading SHA sum from: %s", verifyURL) + if !quiet { + log.Printf("Downloading SHA sum from: %s", verifyURL) + } shaSum, err := fetchText(verifyURL) if err != nil { return "", "", err } if err := verifySHA(shaSum, outFilePath); err != nil { return "", "", err - } else { + } else if !quiet { log.Printf("SHA sum verified in %s.", time.Since(st).Round(time.Millisecond)) } } ++++++ arkade.obsinfo ++++++ --- /var/tmp/diff_new_pack.g0gryt/_old 2026-02-10 21:14:41.257257742 +0100 +++ /var/tmp/diff_new_pack.g0gryt/_new 2026-02-10 21:14:41.257257742 +0100 @@ -1,5 +1,5 @@ name: arkade -version: 0.11.72 -mtime: 1770456589 -commit: 0e580bd296b08c3ba66cd0cc51ce6a614f31321c +version: 0.11.75 +mtime: 1770719747 +commit: 8e860ea5a87682d22908c2d4355c50f089b55c0a ++++++ vendor.tar.gz ++++++
