distkit

package module
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 7, 2026 License: MIT Imports: 22 Imported by: 0

README

distkit

One Go module that owns binary distribution and self-update for the kfet family of tools — fir, harb, mintick, and the ACP relays.

It replaces four hand-written self-update implementations (each missing a different piece) and six divergent install.sh scripts.

  • No dependency on ACP, on any relay, or on fir. Every tool in the family must be able to import it, so it depends on nothing but the standard library.
  • Go 1.25.
go get github.com/kfet/distkit

Self-update in one call

import "github.com/kfet/distkit"

func main() {
    if len(os.Args) > 1 && os.Args[1] == "update" {
        os.Exit(distkit.Main(distkit.Config{
            Repo:        "kfet/zulip-acp",              // GitHub slug
            Binary:      "zulip-acp",                   // installed binary name
            AssetStem:   "zulip-acp",                   // asset is <stem>-<os>-<arch>
            Version:     version,                       // compiled-in current version
            RestartHint: "systemctl --user reload zulip-acp",
        }))
    }
    ...
}

That gives the tool a complete update subcommand:

zulip-acp update                       # install the latest release
zulip-acp update -check                # report only, install nothing
zulip-acp update -version v0.19.0      # install a specific release
zulip-acp update -repo kfet/other      # take releases from elsewhere
zulip-acp update -restart-cmd 'systemctl --user reload zulip-acp'

Exit codes: 0 success (including already up to date), 1 the update failed or was refused, 2 the flags did not parse, 3 a -check run found a newer release — so a timer or cron nag can act on the result without parsing stdout.

What it does, and why

Everything goes through the GitHub REST API — asset bytes included. The release is resolved with Accept: application/vnd.github+json, the asset is fetched from its API URL with Accept: application/octet-stream, and both carry a bearer token when one can be found. This is the only shape that works against a repo that is still private, and it works unauthenticated against a public one, so there is one code path rather than two. The token is taken from GITHUB_TOKEN, then GH_TOKEN, then gh auth token — a fleet host usually has gh configured but exports no token. It is worth having even against a public repo: GitHub's unauthenticated rate limit is per IP address, so a fleet behind one NAT exhausts it between them, and the resulting 403 reads like a permissions failure. A missing-token 404 and a spent-limit 403 are reported as the different problems they are.

The download is verified before anything moves. The asset's sha256 is computed inline as it streams to disk (never read back) and compared with the release's checksums.txt.

The swap is ETXTBSY-safe and atomic. The new binary is staged in a temp directory next to the running one — same directory, therefore same filesystem — and os.Renamed over it. A running executable cannot be written or truncated in place, but its directory entry can be replaced; the live process keeps its old inode mapped until it re-execs. That is why a swapped binary is inert until you recycle the service, and why RestartHint exists.

A Homebrew install is upgraded, not refused. The executable is resolved through symlinks; a Cellar component under one of the three standard prefixes (/opt/homebrew, /usr/local, /home/linuxbrew/.linuxbrew) — or under whatever brew --prefix reports — is a keg. The fully-qualified formula (kfet/ai/fir) comes from the keg's INSTALL_RECEIPT.json, falling back to brew info --json=v2, so brew upgrade cannot bind to a same-named formula from another tap. Then brew update && brew upgrade <formula> runs with no timeout (it may compile) and its output streamed through. Self-updating a keg would be silently reverted by the next brew upgrade, leaving a host that reports one version and runs another.

Detection is deliberately conservative: every uncertain case reports not brew, because a false positive breaks update on a non-brew host while a false negative only falls back to the normal path. The one case that is refused outright is a confirmed keg with brew absent from PATH.

An install we do not own is refused up front, with the command to use instead — not three network round-trips later with permission denied. That means a package-manager prefix (/usr/bin, /usr/sbin, /bin, /sbin, the Homebrew trees) or an install directory belonging to another user. Directory ownership is what matters: the swap creates a temp file there and renames it, so a directory owned by someone else fails regardless of the binary's mode. Refusals are *distkit.ErrManaged (use errors.As), so a caller can tell "you must not" from "it went wrong". /usr/local/bin is deliberately not treated as package-manager owned — it is where this project's own install.sh puts binaries — so a root-owned install there is refused by the ownership check, which knows to suggest sudo <tool> update rather than "use your package manager".

Off unix the whole design (directory ownership, rename over a running executable, ETXTBSY) does not hold, so checkOwnership refuses outright rather than performing an unverified overwrite. The module still compiles for GOOS=windows.

Config

field required default notes
Repo ✓ GitHub owner/name
Binary ✓ installed file name; also the name looked for inside an archive
Version ✓ compiled-in version, with or without a leading v
AssetStem Binary the {stem} in the asset name
RestartHint printed after a successful swap, e.g. systemctl --user reload zulip-acp
AssetTemplate {stem}-{os}-{arch} placeholders below; an archive suffix triggers unpacking
ChecksumsAsset checksums.txt name of the manifest in the release
SkipChecksums false install unverified (do not)
ArmSuffix armv6 the asset token for 32-bit ARM, where GOARCH is just arm
DisableBrew false turn off Homebrew handling
Token discovered GITHUB_TOKEN → GH_TOKEN → gh auth token
APIBase https://api.github.com GitHub Enterprise, or a test double
HTTPClient see below
StallTimeout 2 min abandon a download that makes no progress for this long
Stdout / Stderr the process's
ExecPath os.Executable test seam
Args os.Args[2:] flags after the update word
TargetVersion latest pin a release
CheckOnly false resolve and report only

AssetTemplate placeholders: {stem}, {binary}, {os}, {arch}, {version}, {version_no_v}. A rendered name ending in .tar.gz, .tgz or .zip is unpacked — the file named Binary is pulled out at any depth, so both a flat archive and harb's harb-0.4.0-linux-arm64/harb layout work.

The default HTTP client has no total request deadline: a release binary is tens of megabytes and a Pi on a slow link legitimately takes minutes. Only the parts that can actually hang are bounded — a 30 s ResponseHeaderTimeout for a server that accepts the connection and never answers, and StallTimeout for a transfer that dies mid-body. Slow is fine; no progress at all is not.

A dev build is refused. Version: "dev" (or unknown, (devel), snapshot, …) can never equal a tag, so every run would report an update available and then rename a release binary over the developer's own build. distkit.IsDevBuild is exported if you want to hide the subcommand instead.

Version comparison is exact tag equality, not "is newer". With TargetVersion pinned the target may be older than what is installed, and a deliberate downgrade is a thing an operator is allowed to ask for.

The pieces, without the CLI

Main is a thin shell over exported functions, for callers (fir) that want their own UI:

st, err := distkit.Check(ctx, cfg)          // resolve + compare; touches no disk
dir, err := distkit.StagingDir(exe, "fir")  // temp dir beside the binary
defer os.RemoveAll(dir)
staged, err := distkit.Download(ctx, cfg, st.Release, dir) // fetch + verify + unpack
err = distkit.Apply(staged, exe)            // the atomic rename

err = distkit.CheckWritable(exe, "fir")     // the refusal, on its own
inst, err := distkit.DetectBrewInstall(ctx) // nil, nil when not brew-managed
err = distkit.UpgradeViaBrew(ctx, inst, "fir", os.Stdout, os.Stderr)

rel, err := distkit.FetchRelease(ctx, cfg, "v1.2.3") // rel.Assets is []distkit.Asset
tok := distkit.DiscoverToken()
sum, err := distkit.LookupChecksum(manifest, "fir-linux-amd64")

distkit.Update(ctx, cfg) is the whole flow, and is what Main calls.

install.sh: generated and drift-checked

Shell cannot be imported, so the honest form of "shared" is generated-and-verified. One canonical template lives in installsh/template.sh; each consumer repo holds a small spec and a generated install.sh at its root (it must be at the root for curl …/main/install.sh | sh to resolve), plus a dev-only check that fails when the checked-in copy has drifted.

install.sh.json in the consumer repo:

{
  "repo": "kfet/harb",
  "binary": "harb",
  "asset_template": "{stem}-{version_no_v}-{os}-{arch}.tar.gz",
  "brew_formula": "kfet/tap/harb",
  "version_flag": "-version",
  "next_steps": ["harb init    # bootstrap config + password",
                 "harb serve   # start the server"]
}

Two Make targets:

install.sh: install.sh.json
	go run github.com/kfet/distkit/cmd/distkit-installsh -o $@

check-installsh:
	go run github.com/kfet/distkit/cmd/distkit-installsh -check

check-installsh belongs in CI and in make check. It is dev-only and never runs on a user's machine. Or do it from a test:

func TestInstallShIsNotDrifted(t *testing.T) {
    spec, err := installsh.LoadSpec("install.sh.json")
    if err != nil { t.Fatal(err) }
    if err := installsh.CheckDrift("install.sh", spec); err != nil { t.Fatal(err) }
}

installsh.FromConfig(cfg) derives the spec from the same distkit.Config the binary self-updates with, so asset naming has exactly one definition per project.

What the generated script does

The template is the union of the six hand-written scripts it replaces:

  • curl or wget; only one of them needs to exist.
  • uname mapping for linux/darwin (optionally freebsd), amd64/arm64 (optionally 386), and all three 32-bit ARM spellings — armv6l, armv7l, armv8l — which collapse to the single GOARM=6 asset that every one of them can execute.
  • Version resolution through the GitHub API, honouring GITHUB_TOKEN, so the same script works against a private repo. Without a token it uses the plain releases/download URL (one redirect, no JSON parsing); with one it picks the asset's API URL out of the release JSON, because the download host 404s a private repo whatever you send it.
  • sha256 verification against checksums.txt (sha256sum or shasum -a 256).
  • Optional tarball unpacking, finding the binary at any depth.
  • BIN_DIR, with PREFIX accepted as the legacy alias ($PREFIX/bin) — the two schemes the old scripts disagreed on — defaulting to /usr/local/bin when writable and $HOME/.local/bin otherwise.
  • Install via a sibling temp file and mv -f, so a running copy is replaced atomically; sudo escalation when the target directory is not writable.
  • A $PATH warning, optional runtime-dependency warnings (acp-tmux needs tmux at runtime), an optional version smoke test, and optional next-step hints.

Spec fields: repo, binary, asset_stem, asset_template, arm_suffix, no_checksums, checksums_asset, darwin, freebsd, arch_386, brew_formula, runtime_deps ([{name, message}]), version_flag, next_steps, example_tag. Unknown fields are rejected — a silently ignored typo means a silently wrong installer.

Values that are interpolated into shell source (repo, binary, arm_suffix, …) are rejected if they contain shell metacharacters. Values that are prose (next_steps, runtime_deps[].message) are single-quoted instead, so "tmux is not on $PATH" is a legal message.

Env overrides the generated script honours: VERSION, BIN_DIR, PREFIX, REPO, OS, ARCH, GITHUB_TOKEN, GITHUB_API, GITHUB_HOST.

til's installer is deliberately not in scope: it clones a repo and installs through pipx, which is a different program that happens to share a file name.

Development

make check      # gofmt, vet, tests
make cover

The install.sh tests generate the script and actually run it against a fake GitHub over httptest, for the raw-binary and tarball shapes, the anonymous and tokenised download paths, PREFIX, and a checksum mismatch. sh -n alone proves the script parses, not that it installs.

Documentation

Overview

Package distkit owns binary distribution and self-update for the kfet family of tools: fir, harb, mintick, and the ACP relays.

It replaces four hand-written self-update implementations, each of which was missing a different piece, and it carries the union of what they got right:

  • Every byte moves through the GitHub REST API, asset payloads included (`Accept: application/octet-stream`), with a bearer token when one can be discovered. This is the only shape that works against a repo that is still private, and it works unauthenticated against a public one, so there is one code path rather than two.
  • sha256 of the downloaded asset is checked against the release's `checksums.txt` before anything is moved into place.
  • The replacement is an ETXTBSY-safe atomic swap: stage in a temp dir NEXT TO the running binary (same directory → same filesystem) and os.Rename over it. A running executable cannot be written or truncated in place, but renaming a sibling over it replaces the directory entry while the live process keeps its old inode mapped.
  • A Homebrew-managed install is detected and UPGRADED THROUGH BREW rather than merely refused: self-updating a keg would be silently reverted by the next `brew upgrade`.
  • An install this process does not own — a package-manager prefix, a directory belonging to another user — is refused up front, with the command to use instead, rather than failing three network round-trips later with "permission denied".

The package depends on nothing outside the standard library, and on nothing from ACP, any relay, or fir: every tool in the family must be able to import it.

The usual consumer wires it up in one call:

distkit.Main(distkit.Config{
    Repo:        "kfet/zulip-acp",
    Binary:      "zulip-acp",
    AssetStem:   "zulip-acp",
    Version:     version,
    RestartHint: "systemctl --user reload zulip-acp",
})

Check, Download, Apply, DetectBrewInstall and UpgradeViaBrew are exported separately for callers that want the pieces without the CLI.

Index

Constants

View Source
const DefaultAPIBase = "https://api.github.com"

DefaultAPIBase is the GitHub REST API root.

View Source
const DefaultAssetTemplate = "{stem}-{os}-{arch}"

DefaultAssetTemplate names a raw-binary release asset, which is what goreleaser's `formats: [binary]` produces. See Config.AssetTemplate.

View Source
const DefaultChecksums = "checksums.txt"

DefaultChecksums is the release asset listing sha256 sums, in the `sha256sum` output format that goreleaser writes.

View Source
const DefaultStallTimeout = 2 * time.Minute

DefaultStallTimeout is how long a download may make no progress at all before it is abandoned. See Config.StallTimeout.

View Source
const ExitUpdateAvailable = 3

Main implements the `<tool> update` subcommand and returns the process exit code. It is the whole integration surface for a consumer:

case "update":
    os.Exit(distkit.Main(distkit.Config{
        Repo:        "kfet/zulip-acp",
        Binary:      "zulip-acp",
        Version:     version,
        RestartHint: "systemctl --user reload zulip-acp",
    }))

Flags parsed from Config.Args (default os.Args[2:], i.e. everything after the subcommand word):

-check              report whether an update is available; install nothing
-version <tag>      install a specific release instead of the latest
-repo <owner/name>  override the source repository
-restart-cmd <sh>   run this after a successful update

Exit codes: 0 success (including "already up to date"), 1 the update failed or was refused, 2 the flags did not parse, and ExitUpdateAvailable (3) for a -check run that found a newer release. ExitUpdateAvailable is the exit code of a `-check` run that found a release different from the one running. It is distinct from 0 ("already up to date") so a scheduled check can act on the result without parsing stdout.

Variables

This section is empty.

Functions

func Apply

func Apply(staged, target string) error

Apply replaces target with the file at staged.

This is the ETXTBSY-safe swap. A running executable cannot be opened for writing or truncated — the kernel returns ETXTBSY — but its *directory entry* can be replaced. os.Rename does exactly that, atomically, and the process that is still running keeps the old inode mapped until it re-execs. That is why staged must be a sibling of target: a rename across filesystems degrades to copy-then-unlink, which is neither atomic nor permitted here.

func CheckWritable

func CheckWritable(path, binary string) error

CheckWritable reports whether the resolved binary at path may be replaced by this process: not under a package-manager prefix, and in a directory owned by the effective user.

The directory ownership matters rather than the file's: the swap creates a temp file in the install directory and renames it over the binary, so a directory belonging to someone else fails no matter what the file's mode says. Checking it here turns a confusing "permission denied" after three network round-trips into an immediate, explainable refusal.

func DiscoverToken

func DiscoverToken() string

DiscoverToken finds a GitHub token the way the surrounding tooling does: the standard environment variables first, then a logged-in `gh`. A fleet host usually has gh configured but rarely exports a token. Returns "" when there is none, which is fine against a public repo.

func Download

func Download(ctx context.Context, cfg Config, rel *Release, dir string) (string, error)

Download fetches the release asset for the current platform into dir, verifies its sha256 against the release's checksum manifest, unpacks it if it is an archive, and returns the path of the executable ready to be installed.

dir must be on the same filesystem as the binary that will be replaced for Apply to be atomic; StagingDir creates such a directory. The caller owns dir and is responsible for removing it.

Verification is not optional in any sane configuration: the bytes are only trusted once the manifest agrees, and the manifest is fetched over the same authenticated API call chain as the asset itself.

func EnsureV

func EnsureV(s string) string

EnsureV prepends "v" to a version string that lacks it, so that a tag written "0.4.1" and one written "v0.4.1" compare equal.

func IsDevBuild

func IsDevBuild(v string) bool

IsDevBuild reports whether v is a placeholder rather than a released tag.

func LookupChecksum

func LookupChecksum(manifest []byte, asset string) (string, error)

LookupChecksum returns the hex sha256 recorded for asset in a manifest in `sha256sum` format ("<hex> <name>"), as written by goreleaser.

func Main

func Main(cfg Config) int

func StagingDir

func StagingDir(target, binary string) (string, error)

StagingDir creates a temporary directory in the same directory as target, which is what makes the subsequent Apply an atomic same-filesystem rename. The caller must os.RemoveAll it.

func UpgradeViaBrew

func UpgradeViaBrew(ctx context.Context, inst *BrewInstall, binary string, stdout, stderr io.Writer) error

UpgradeViaBrew runs `brew update` followed by `brew upgrade <formula>`, streaming both commands' output to the supplied writers so a multi-minute upgrade is not swallowed into silence.

No timeout is imposed: a brew upgrade can legitimately take many minutes (it may compile). Cancellation is the caller's ctx.

Types

type Asset

type Asset struct {
	// Name is the file name, e.g. "zulip-acp-linux-amd64".
	Name string `json:"name"`
	// URL is the asset's API URL. Fetching it with
	// Accept: application/octet-stream returns the bytes; without that
	// header it returns the asset's JSON metadata.
	URL string `json:"url"`
}

Asset is one file attached to a release.

type BrewInstall

type BrewInstall struct {
	// ExePath is the running executable with symlinks resolved — the real
	// file inside the Cellar.
	ExePath string
	// Prefix is the Homebrew prefix the Cellar belongs to, e.g. "/opt/homebrew".
	Prefix string
	// Formula is the name to hand to `brew upgrade`. Fully qualified when we
	// could resolve it (e.g. "kfet/ai/fir"), otherwise the bare keg name.
	Formula string
	// BrewPath is the resolved `brew` executable, empty when brew is not on
	// PATH. An install with a non-empty Prefix and an empty BrewPath is the
	// one case we must refuse to self-update.
	BrewPath string
}

BrewInstall describes a detected Homebrew-managed installation.

func DetectBrewInstall

func DetectBrewInstall(ctx context.Context) (*BrewInstall, error)

DetectBrewInstall reports whether the running executable is managed by Homebrew. It returns (nil, nil) when the install is not brew-managed — including every case where we are merely unsure, since a false positive breaks `<tool> update` on non-brew hosts while a false negative only leaves us on the pre-existing self-update path.

type Config

type Config struct {
	// Repo is the GitHub "owner/name" releases are taken from, e.g.
	// "kfet/zulip-acp". Required.
	Repo string

	// Binary is the installed file name, e.g. "zulip-acp". It is also the
	// name searched for inside an archive asset. Required.
	Binary string

	// AssetStem is the "{stem}" of the release asset name. Defaults to
	// Binary, which is right for every consumer today.
	AssetStem string

	// Version is the compiled-in version of the running binary, with or
	// without a leading "v". Required.
	Version string

	// RestartHint is the command that makes a swapped binary take effect,
	// e.g. "systemctl --user reload zulip-acp". A replaced binary is inert
	// until the process re-execs — the running one still has the old inode
	// mapped — so this is printed after every successful update. Optional:
	// a one-shot CLI has nothing to recycle.
	RestartHint string

	// AssetTemplate names the release asset. Placeholders:
	//
	//	{stem}        AssetStem
	//	{binary}      Binary
	//	{os}          runtime.GOOS
	//	{arch}        release arch suffix (see ArmSuffix)
	//	{version}     resolved tag, with leading v
	//	{version_no_v} resolved tag, without leading v
	//
	// Defaults to DefaultAssetTemplate ("{stem}-{os}-{arch}"), a raw
	// binary. When the rendered name ends in .tar.gz, .tgz or .zip the
	// asset is treated as an archive and Binary is extracted from it —
	// harb publishes "{stem}-{version_no_v}-{os}-{arch}.tar.gz".
	AssetTemplate string

	// ChecksumsAsset is the name of the checksum manifest in the release.
	// Defaults to DefaultChecksums.
	ChecksumsAsset string

	// SkipChecksums installs the asset without verifying it. No consumer
	// should set this: the bytes are only trustworthy once the manifest
	// agrees. It exists for a project whose release process genuinely
	// publishes no manifest.
	SkipChecksums bool

	// ArmSuffix is the arch token used in asset names for 32-bit ARM,
	// where GOARCH is the uninformative "arm". The family builds at
	// GOARM=6 and publishes "armv6"; that is the default. A project whose
	// assets say plain "arm" sets it to "arm".
	ArmSuffix string

	// DisableBrew turns off Homebrew detection. With it set, a keg install
	// is refused by the managed-prefix guard instead of being upgraded
	// through `brew upgrade`. Only useful for a binary that is never
	// distributed as a formula.
	DisableBrew bool

	// Token is a GitHub token. Empty means discover one from the
	// environment (GITHUB_TOKEN, GH_TOKEN) and then from a logged-in `gh`.
	Token string

	// APIBase is the GitHub API root; empty means DefaultAPIBase. Tests
	// point it at an httptest server.
	APIBase string

	// HTTPClient overrides the default client. The default deliberately
	// has NO total-request deadline: a release binary is ~10 MB and a Pi
	// on a slow link legitimately takes minutes. It bounds the part that
	// can actually hang — a server that accepts the connection and then
	// never answers — with ResponseHeaderTimeout.
	HTTPClient *http.Client

	// StallTimeout bounds how long a download may make NO progress before it
	// is abandoned. It is not a total deadline — a slow link is fine, a
	// stalled one is not — and it is what stops a dropped connection
	// mid-body from wedging a systemd timer forever. Defaults to
	// DefaultStallTimeout; a negative value disables the check.
	StallTimeout time.Duration

	// Stdout and Stderr default to the process's. Progress and results go
	// to Stdout, diagnostics to Stderr.
	Stdout io.Writer
	Stderr io.Writer

	// ExecPath overrides os.Executable. Tests use it; production does not.
	ExecPath func() (string, error)

	// Args are the CLI arguments AFTER the "update" subcommand word. nil
	// means take them from os.Args[2:], which is what a consumer that
	// dispatches `<tool> update ...` straight into Main wants. A consumer
	// with global flags BEFORE the subcommand (`tool -config x update -check`)
	// must set this explicitly — os.Args[2:] would be wrong there.
	Args []string

	// TargetVersion pins the release to install, e.g. "v0.19.0". Empty
	// means latest. The -version flag sets it when going through Main.
	TargetVersion string

	// CheckOnly resolves and reports without downloading or replacing
	// anything. The -check flag sets it when going through Main.
	CheckOnly bool
	// contains filtered or unexported fields
}

Config describes one consumer's distribution setup. Repo, Binary and Version are required; everything else has a working default.

func (*Config) AssetName

func (c *Config) AssetName(tag string) string

AssetName renders Config.AssetTemplate for the current platform and the given resolved tag.

type ErrManaged

type ErrManaged struct {
	// Path is the resolved binary.
	Path string
	// Reason explains what makes it unsafe.
	Reason string
	// Hint is the command to use instead, if there is one.
	Hint string
}

ErrManaged is returned when the install is owned by something other than this process. Callers can errors.As it to distinguish "you must not" from "it went wrong".

func (*ErrManaged) Error

func (e *ErrManaged) Error() string

type Release

type Release struct {
	TagName string  `json:"tag_name"`
	Assets  []Asset `json:"assets"`
}

Release is the subset of the GitHub release JSON this package uses.

Asset.URL is the *API* URL (…/releases/assets/123), not the browser_download_url: the API URL serves the bytes to a bearer token, which is the only thing that works while the repo is private.

func FetchRelease

func FetchRelease(ctx context.Context, cfg Config, tag string) (*Release, error)

FetchRelease resolves a release through the GitHub API: the latest one when tag is empty, otherwise that exact tag.

func (*Release) AssetURL

func (r *Release) AssetURL(name string) (string, error)

AssetURL returns the API URL of the named asset.

type Result

type Result struct {
	// Current is the version that was running, with a leading v.
	Current string
	// Target is the version resolved for installation, with a leading v.
	// Empty when ViaBrew is set (brew resolves the version itself) and
	// when the run failed before the release was resolved.
	Target string
	// Updated is true iff the binary was actually replaced (or upgraded
	// through brew).
	Updated bool
	// ExecPath is the resolved path of the binary, symlinks followed.
	ExecPath string
	// ViaBrew is true when the work was done by `brew upgrade` rather than
	// by downloading a release asset.
	ViaBrew bool
	// Available is set by a CheckOnly run that found a different release.
	// It is the machine-readable form of what -check prints.
	Available bool
}

Result reports what an update run did, so the caller can decide follow-up actions such as recycling a service.

func Update

func Update(ctx context.Context, cfg Config) (Result, error)

Update runs the whole flow: locate the running binary, hand a Homebrew install to brew, refuse an install we must not touch, then resolve, download, verify and swap.

It returns Updated=false with a nil error for the two ordinary non-outcomes: already up to date, and CheckOnly.

type Status

type Status struct {
	// Current is the running version, with a leading v.
	Current string
	// Target is the resolved release tag, with a leading v.
	Target string
	// Available reports whether Target differs from Current. With
	// TargetVersion pinned this may be a downgrade — the field says
	// "different", not "newer", because installing a pin is legitimate.
	Available bool
	// Release is the resolved release, carrying the asset API URLs.
	Release *Release
}

Status is what Check resolved, without touching the installed binary.

func Check

func Check(ctx context.Context, cfg Config) (*Status, error)

Check resolves the release that `update` would install and compares it with the running version. It performs one GitHub API call and touches nothing on disk, so it is safe to call from a status command or a background nag.

The comparison is exact-match on the tag, not "is newer": with Config.TargetVersion pinned the target may legitimately be older than what is installed, and calling a deliberate downgrade "no update available" would refuse to do a thing the operator explicitly asked for.

Directories

Path Synopsis
cmd
distkit-installsh command
Command distkit-installsh generates a repo's root install.sh from the canonical distkit template, or checks the checked-in copy for drift.
Command distkit-installsh generates a repo's root install.sh from the canonical distkit template, or checks the checked-in copy for drift.
Package installsh generates a consumer repo's root install.sh from one canonical template.
Package installsh generates a consumer repo's root install.sh from one canonical template.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL