go-selfupdate

repository·main·Indexed 40 days ago

https://github.com/creativeprojects/go-selfupdate

A Go library that enables applications to automatically detect, download, and update themselves using GitHub, Gitea, or GitLab as source providers. It supports semantic versioning, automatic rollback, and integrity verification via SHA256 and ECDSA signatures. The package includes CLI tools such as detect-latest-release, get-release, and serve-repo for testing and binary management.

Tokens
17.8K
Snippets
66
Records
87
Agent score
100%

What's inside go-selfupdate

  1. How go-selfupdate works

    main

    The library automates the detection and replacement of binaries by:

    • Detecting the latest release via a source provider (GitHub, Gitea, or GitLab).
    • Matching the running OS and architecture to the appropriate asset.
    • Downloading and replacing the binary with built-in rollback support.
    • Supporting various archive formats: .zip, .tar.gz, .gzip, .xz, .bzip2.
    • Supporting semantic versioning (semver) for comparison.
    • Handling different ARM architectures (e.g., armv6, armv7) by attempting to find the specific architecture, then falling back to lower versions or a generic arm tag.
  2. Naming rules for versions (Git Tags)

    main

    The library uses Git tags to determine version numbers.

    • Format: It expects semantic versioning (e.g., 1.2.3 or v1.2.3).
    • Prefixes: Common prefixes like ver1.2.3 or release-1.2.3 are automatically stripped to find the version number.
    • Exclusions: Tags that do not contain a version number (e.g., nightly) or releases marked as pre-release in the provider are ignored.
  3. Use get-release to update a binary

    main

    The get-release command works similarly to go get. You provide the package path of the GitHub repository, and the tool will attempt to find the latest release tag and install the corresponding binary to your $GOPATH/bin.

    Note: The package must follow standard Git tag naming rules and released binary naming conventions as defined in the main project documentation.

    $ get-release github.com/tcnksm/ghr
    # Example output:
    # Command was updated to the latest version 0.5.4: /Users/you/.go/bin/ghr
  4. Verify integrity using ECDSA signatures

    main

    To use ECDSA verification, generate a signature and save it in a file with the same name as the original file plus the .sig suffix. The private key must be compatible with FIPS 186-3 as the library uses Go's internal crypto package.

    Example for a file named foo.zip using OpenSSL:

    openssl dgst -sha256 -sign Test.pem -out foo.zip.sig foo.zip
  5. Implement self-updating in your Go application

    main

    To allow your application to update itself, use selfupdate.DetectLatest to find the newest version from your provider (GitHub, Gitea, or GitLab) and selfupdate.UpdateTo to download and replace the current binary.

    Key steps:

    1. Use selfupdate.ParseSlug or selfupdate.NewRepositorySlug to define your repository.
    2. Call selfupdate.DetectLatest(ctx, repository) to get the latest Release information.
    3. Compare the latest version with your current version using latest.LessOrEqual(currentVersion).
    4. Locate your current executable path using selfupdate.ExecutablePath().
    5. Execute the update with selfupdate.UpdateTo(ctx, latest.AssetURL, latest.AssetName, exePath).

    Note: The library handles rollback automatically if the update fails.

    func update(version string) error {
    	// 1. Detect latest release
    	latest, found, err := selfupdate.DetectLatest(context.Background(), selfupdate.ParseSlug("creativeprojects/resticprofile"))
    	if err != nil {
    		return fmt.Errorf("error occurred while detecting version: %w", err)
    	}
    	if !found {
    		return fmt.Errorf("latest version could not be found")
    	}
    
    	// 2. Check if update is needed
    	if latest.LessOrEqual(version) {
    		log.Printf("Current version (%s) is the latest", version)
    		return nil
    	}
    
    	// 3. Get current executable path
    	exe, err := selfupdate.ExecutablePath()
    	if err != nil {
    		return errors.New("could not locate executable path")
    	}
    
    	// 4. Perform the update
    	if err := selfupdate.UpdateTo(context.Background(), latest.AssetURL, latest.AssetName, exe); err != nil {
    		return fmt.Errorf("error occurred while updating binary: %w", err)
    	}
    
    	log.Printf("Successfully updated to version %s", latest.Version())
    	return nil
    }
  6. How asset matching and suffixes work

    main

    The Updater identifies the correct download asset by constructing potential suffixes based on the current os and arch settings.

    Suffix Construction Logic:

    • Separators: It tries both _ and - (e.g., linux_amd64 and linux-amd64).
    • Extensions: It checks for common compression formats: .zip, .tar.gz, .tgz, .gzip, .gz, .tar.xz, .xz, and .bz2.
    • Windows Support: On Windows, it specifically looks for the .exe extension (e.g., windows_amd64.exe.zip).
    • Filtering: If you have defined custom filters on your Updater, the library will use those regex/string filters to match assets instead of the default suffix-based matching.
  7. Executable name matching logic

    main

    The library uses a specific pattern to match the target executable within archives. It looks for a filename that matches the cmd name, optionally followed by a SemVer version, and optionally followed by OS and architecture identifiers.

    Matching Pattern: ^<cmd>([_-]v?<semver>)?([_-]<os>[_-]<arch>)?(\.exe)?$

    Key behaviors:

    • The .exe suffix is stripped from the cmd parameter before matching.
    • It supports separators like underscores (_) or hyphens (-).
    • It supports versioning using the SemVer pattern.
    • It supports OS and Architecture suffixes (e.g., myapp_linux_amd64).
  8. Use PatternValidator for multi-step validation

    main

    The PatternValidator allows you to route different files to different validators based on glob patterns. It is a RecursiveValidator, meaning it can handle complex scenarios like validating a checksum file with a signature, and then using that checksum file to validate the actual release assets.

    Key Methods:

    • Add(glob string, validator Validator) *PatternValidator: Maps a glob pattern to a specific validator.
    • SkipValidation(glob string) *PatternValidator: Ensures a specific pattern is ignored (useful for signature files to prevent infinite validation loops).

    Example: Validating assets via a SHA256SUMS file and its PGP signature:

    // 1. The 'SHA256SUMS' file is validated by the PGP signature 'SHA256SUMS.asc'
    // 2. The signature file itself is skipped to prevent loops
    // 3. All other files are validated by looking up their hash in 'SHA256SUMS'
    new(PatternValidator).
        Add("SHA256SUMS", new(PGPValidator).WithArmoredKeyRing(key)).
        SkipValidation("*.asc").
        Add("*", &ChecksumValidator{UniqueFilename: "SHA256SUMS"})
    new(PatternValidator).
        Add("SHA256SUMS", new(PGPValidator).WithArmoredKeyRing(key)).
        SkipValidation("*.asc").
        Add("*", &ChecksumValidator{UniqueFilename: "SHA256SUMS"})