How Aqua Manages Tool Versions: A Deep Dive into the Modular VersionGetter Pipeline

Aqua determines tool versions through a modular VersionGetter pipeline that aggregates multiple source-specific getters (GitHub tags, releases, Cargo, Go modules) and parses semantic versions using regex-based extraction.

Understanding how aquaproj/aqua resolves tool versions requires examining its pluggable architecture. The system delegates version discovery to a chain of specialized getters that handle different package registries, normalizing raw tags into clean semantic versions for installation and updates.

The VersionGetter Architecture

At the core of aqua’s version management lies the VersionGetter interface defined in [pkg/versiongetter/version_getter.go](https://github.com/aquaproj/aqua/blob/main/pkg/versiongetter/version_getter.go). This abstraction allows the CLI to treat GitHub tags, Cargo crates, and Go modules as interchangeable version sources.

The Interface Contract

The VersionGetter interface declares two essential methods:

type VersionGetter interface {
    Get(ctx context.Context, pkg *registry.PackageInfo, currentVersion string) (string, error)
    List(ctx context.Context, pkg *registry.PackageInfo) ([]string, error)
}

Concrete implementations handle the specifics of querying their respective upstream APIs, while consumers remain agnostic to the underlying registry type.

The Fuzzy Finder Wrapper

Before reaching the concrete getters, version requests pass through FuzzyGetter located in [pkg/versiongetter/fuzzy_getter.go](https://github.com/aquaproj/aqua/blob/main/pkg/versiongetter/fuzzy_getter.go). This wrapper conditionally launches a fuzzy-finder UI when useFinder is enabled, or directly delegates to the embedded VersionGetter during automated operations:

func (f *FuzzyGetter) Get(... ) string {
    if !useFinder { return f.getter.Get(...) }
    // … fuzzy UI logic …
}

This separation ensures that interactive and programmatic workflows share the same resolution logic.

The Version Resolution Flow

When you run aqua update or aqua install, the system triggers a coordinated lookup process across multiple files.

Controller Entry Point

The journey begins in [pkg/controller/update/update.go](https://github.com/aquaproj/aqua/blob/main/pkg/controller/update/update.go) at line 95, where the update controller invokes:

newVersion := c.fuzzyGetter.Get(ctx, logger, pkg.PackageInfo,
    pkg.Package.Version, param.SelectVersion, param.Limit)

This single call initiates the entire pipeline, passing the package metadata and current version constraints down the stack.

GeneralVersionGetter Routing

The FuzzyGetter forwards requests to GeneralVersionGetter ([pkg/versiongetter/general.go](https://github.com/aquaproj/aqua/blob/main/pkg/versiongetter/general.go)), which acts as a router. Its internal get method selects the appropriate concrete implementation based on the package's Type field:

func (g *GeneralVersionGetter) get(pkg *registry.PackageInfo) VersionGetter {
    switch pkg.Type {
    case "cargo":    return g.cargo
    case "github":   // tag vs release decided later
    case "go":       return g.goGetter
    default:         return nil
    }
}

This design aggregates cargo, GitHub tag, GitHub release, and Go proxy getters into a unified interface.

Concrete Getter Implementations

Each concrete getter implements registry-specific logic:

For example, GitHubTagVersionGetter.Get lists tags, applies filters, and extracts the version using GetVersionAndPrefix (line 37 in github_tag.go).

Semantic Version Extraction

Raw tags often contain prefixes or non-standard formatting. The function GetVersionAndPrefix in [pkg/versiongetter/parse.go](https://github.com/aquaproj/aqua/blob/main/pkg/versiongetter/parse.go) handles normalization using the regex:


^(.*?)v?((?:\d+)(?:\.\d+)?(?:\.\d+)?(?:(\.|-).+)?)$

This pattern splits strings like "release-v1.2.3" into an optional prefix (release-) and a clean semantic version (1.2.3). The implementation uses github.com/hashicorp/go-version to parse the extracted version string:

a := versionPattern.FindStringSubmatch(tag)
if a == nil { return nil, "", nil }
v, err := version.NewVersion(a[2])

Errors propagate only when a version pattern is detected but malformed, ensuring that non-version tags are silently skipped rather than causing failures.

Handling Prefixed Versions

Some ecosystems embed contextual prefixes in tags (e.g., kubernetes-1.28.0 or release-v2.0.0). Aqua preserves these prefixes during parsing so that template variables like {{.Version}} can reconstruct the original tag format when necessary. The prefix (a[1] from the regex match) travels alongside the parsed version through the entire pipeline, enabling tools that require specific tag naming conventions while still supporting semantic version comparisons.

Code Examples

Parsing a Raw Tag Directly

You can utilize the parsing utility independently to extract versions from arbitrary strings:

import (
    "fmt"
    "github.com/aquaproj/aqua/v2/pkg/versiongetter"
)

func main() {
    version, prefix, err := versiongetter.GetVersionAndPrefix("release-v1.2.3")
    if err != nil {
        panic(err)
    }
    fmt.Printf("Version: %s, Prefix: %s\n", version, prefix)
    // Output: Version: 1.2.3, Prefix: release-
}

Resolving Versions Programmatically

In a controller-like context, you interact with the getter chain as follows:

func resolveVersion(ctx context.Context, logger *slog.Logger, pkgInfo *registry.PackageInfo, curVer string) (string, error) {
    // fuzzyGetter is typically injected via Wire (see pkg/controller/wire.go)
    version := fuzzyGetter.Get(ctx, logger, pkgInfo, curVer, false, 0) // no UI, unlimited limit
    if version == "" {
        return "", fmt.Errorf("could not determine version for %s", pkgInfo.Name)
    }
    return version, nil
}

Dependency Injection Configuration

The concrete getter chain is wired together using Google Wire. The generated [pkg/controller/wire_gen.go](https://github.com/aquaproj/aqua/blob/main/pkg/controller/wire_gen.go) (lines 115-121) illustrates the assembly:

func InitializeController(...) (*Controller, error) {
    cargoVersionGetter := versiongetter.NewCargo(client)
    gitHubTagVersionGetter := versiongetter.NewGitHubTag(repositoriesService)
    gitHubReleaseVersionGetter := versiongetter.NewGitHubRelease(repositoriesService)
    goGetter := versiongetter.NewGoGetter(goproxyClient)

    generalVersionGetter := versiongetter.NewGeneralVersionGetter(
        cargoVersionGetter,
        gitHubTagVersionGetter,
        gitHubReleaseVersionGetter,
        goGetter,
    )
    fuzzyGetter := versiongetter.NewFuzzy(fuzzyfinderFinder, generalVersionGetter)
    // … controller assembly continues …
}

Summary

  • Aqua manages tool versions through a layered VersionGetter pipeline that abstracts multiple registry types behind a unified interface.
  • The FuzzyGetter wrapper enables both interactive fuzzy-finding and automated resolution via the same underlying logic.
  • GeneralVersionGetter routes requests to specialized getters for Cargo, GitHub tags, GitHub releases, and Go modules based on package type.
  • Regex-based parsing in GetVersionAndPrefix extracts semantic versions from raw tags while preserving optional prefixes for template rendering.
  • The update controller in [pkg/controller/update/update.go](https://github.com/aquaproj/aqua/blob/main/pkg/controller/update/update.go) orchestrates the entire flow during aqua update operations.

Frequently Asked Questions

What is the VersionGetter interface in Aqua?

The VersionGetter interface ([pkg/versiongetter/version_getter.go](https://github.com/aquaproj/aqua/blob/main/pkg/versiongetter/version_getter.go)) defines the contract Get and List methods that all version sources must implement. It allows Aqua to treat GitHub repositories, Cargo crates, and Go modules interchangeably when resolving tool versions.

How does Aqua handle non-semver tags like "release-v1.2.3"?

Aqua uses the GetVersionAndPrefix function in [pkg/versiongetter/parse.go](https://github.com/aquaproj/aqua/blob/main/pkg/versiongetter/parse.go) to split raw tags into an optional prefix and a clean semantic version using a capturing regex. The prefix is preserved for template rendering while the version component undergoes strict semantic versioning checks via hashicorp/go-version.

Which package registries does Aqua support for version lookup?

According to the source code in [pkg/versiongetter/general.go](https://github.com/aquaproj/aqua/blob/main/pkg/versiongetter/general.go), Aqua supports Cargo (crates.io), GitHub tags, GitHub releases, and Go module proxy sources. The system routes to the appropriate concrete getter based on the package's type field in the registry configuration.

How does Aqua decide between GitHub tags and releases?

When processing GitHub-based packages, the GeneralVersionGetter may utilize both GitHubTagVersionGetter and GitHubReleaseVersionGetter depending on the package configuration. The specific logic determines whether to query the tags API or the releases API, allowing packages that distribute assets exclusively through releases to resolve correctly while others may use lightweight tag-based versioning.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →