Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,14 @@ nfpms:
bindir: /usr/bin

homebrew_casks:
# skip_upload: cask files are still built into dist/ so this stays testable, but
# nothing is pushed until harness/homebrew-tap exists and HOMEBREW_TAP_GITHUB_TOKEN
# is set — see docs/publishing-to-homebrew.md. Remove once both are in place.
- name: harness
# Installs the core `harness` binary only — modules/plugins are pulled in
# afterwards with `harness install module <name>`.
- name: harness-cli
ids: [harness-core]
binaries: [harness]
skip_upload: true
# "auto" publishes on stable tags and skips prerelease tags (v1.2.3-rc1),
# so an rc never becomes what `brew install` hands out.
skip_upload: auto
repository:
owner: harness
name: homebrew-tap
Expand All @@ -106,6 +107,10 @@ homebrew_casks:
if OS.mac?
system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/harness"]
end
system_command "#{staged_path}/harness",
args: ["--post-install"],
env: { "HARNESS_INSTALL_TYPE" => "brew" },
must_succeed: false

checksum:
name_template: "{{ .ProjectName }}_{{ .Version }}_checksums.txt"
Expand Down
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,14 @@ irm https://raw.githubusercontent.com/harness/cli/main/install.ps1 | iex
$env:HARNESS_NONINTERACTIVE=1; $env:HARNESS_CORE_ONLY=1; irm https://raw.githubusercontent.com/harness/cli/main/install.ps1 | iex
```

### Homebrew (macOS)

```sh
brew install --cask harness/tap/harness-cli
```

Installs the core `harness` binary only. Modules/Plugins are managed by the CLI, not by Homebrew.

### Manual install

Prefer to install by hand? Download an archive from [GitHub Releases](https://github.com/harness/cli/releases), place `harness` on your `PATH`, and register the bundled `har` plugin with `harness install plugin`. Unix bundles are `tar.gz`; Windows bundles are `zip`. Published for `linux_amd64`, `linux_arm64`, `darwin_amd64`, `darwin_arm64`, `windows_amd64`, and `windows_arm64`.
Expand Down Expand Up @@ -148,6 +156,9 @@ harness install cli --core-only # skip module updates
| `--check` | Print the resolved version without installing; exits 1 if not found |
| `--core-only` | Only install the core binary, skip module updates |

> [!NOTE]
> If you installed with Homebrew, upgrade with Homebrew: `brew upgrade --cask harness/tap/harness-cli`.

External modules are managed the same way:

```sh
Expand Down Expand Up @@ -751,7 +762,7 @@ Add the built binaries to your `PATH` for the duration of the session:
source local-setup.zsh
```

For details on the release process and Homebrew publishing, see [`BUILD.md`](BUILD.md) and [`docs/publishing-to-homebrew.md`](docs/publishing-to-homebrew.md).
For details on the release process, see [`BUILD.md`](BUILD.md).

---

Expand Down
1 change: 1 addition & 0 deletions go.work.sum
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bits-and-blooms/bitset v1.24.6/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
Expand Down
20 changes: 18 additions & 2 deletions modules/core/mgmt/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,24 @@ func InstallCLIHandler(ctx *cmdctx.Ctx) error {
}
installDir = hbase.ExpandHomeDir(installDir)

if err := checkRunningFromInstallDir(installDir); err != nil {
return err
// --check only reports versions, so it stays useful on a brew install.
if !force && !check {
if path, ok := hbase.BrewManagedBinary(); ok {
return fmt.Errorf(
"harness was installed by Homebrew (%s)\n"+
"Upgrade it with: brew upgrade --cask %s\n"+
"Pass --force to install a separate copy anyway",
path, hbase.BrewCaskRef,
)
}
}

// --force means the caller wants a copy in installDir regardless of where
// the running binary lives, which is the only way off a brew install.
if !force {
if err := checkRunningFromInstallDir(installDir); err != nil {
return err
}
}

platform, err := detectPlatform()
Expand Down
35 changes: 35 additions & 0 deletions pkg/hbase/hbase.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,41 @@ func GetHarnessBinDir() string {
return filepath.Join(GetHarnessHomeDir(), "bin")
}

// BrewCaskRef is the tap-qualified cask this CLI is published as, suitable for
// `brew install --cask` / `brew upgrade --cask`.
const BrewCaskRef = "harness/tap/harness-cli"

// brewCaskDirName is the directory Homebrew stages cask payloads under:
// $HOMEBREW_PREFIX/Caskroom/<token>/<version>/<binary>, with a symlink to it
// from $HOMEBREW_PREFIX/bin. Matching a resolved path on this segment is
// prefix-independent (works for /opt/homebrew, /usr/local, and a custom
// HOMEBREW_PREFIX) and cannot false-positive on a manual install into
// /usr/local/bin, which a bare prefix match would.
const brewCaskDirName = "Caskroom"

// BrewManagedBinary reports whether the running binary was installed by a
// Homebrew cask, returning the resolved path when it was. Homebrew owns that
// path, so self-update must defer to `brew upgrade` rather than write over it.
func BrewManagedBinary() (string, bool) {
if runtime.GOOS == "windows" {
return "", false
}
exe, err := os.Executable()
if err != nil {
return "", false
}
resolved, err := filepath.EvalSymlinks(exe)
if err != nil {
resolved = exe
}
for _, seg := range strings.Split(resolved, string(filepath.Separator)) {
if seg == brewCaskDirName {
return resolved, true
}
}
return "", false
}

// EnsureHarnessHome creates ~/.harness with 0700 permissions if it does not exist.
// Returns an error if the directory cannot be created or if the path exists but is not a directory.
func EnsureHarnessHome() error {
Expand Down
6 changes: 5 additions & 1 deletion pkg/release/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,11 @@ func NagIfDue(currentVersion string) {
if err := writeCache(c); err != nil {
return
}
fmt.Fprintf(os.Stderr, "\nA new version of the Harness CLI is available: %s → %s\nRun: harness install cli\n\n", currentVersion, c.LatestVersion)
upgradeCmd := "harness install cli"
if _, ok := hbase.BrewManagedBinary(); ok {
upgradeCmd = "brew upgrade --cask " + hbase.BrewCaskRef
}
fmt.Fprintf(os.Stderr, "\nA new version of the Harness CLI is available: %s → %s\nRun: %s\n\n", currentVersion, c.LatestVersion, upgradeCmd)
}

// shouldUpdateCheck returns false for all gating conditions that mean we skip entirely.
Expand Down
7 changes: 4 additions & 3 deletions pkg/rootcmd/rootcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ func MaybeRunBackgroundUpdateCheck() {
}
}

// postInstallFlag is the hidden flag install.sh invokes right after placing a
// fresh binary on disk, purely to fire a cli_installed telemetry event.
// postInstallFlag is the hidden flag the installers (install.sh, install.ps1,
// the Homebrew cask's postflight hook) invoke right after placing a fresh
// binary on disk, purely to fire a cli_installed telemetry event.
const postInstallFlag = "--post-install"

// MaybeRunPostInstall exits if this invocation is install.sh's post-install
// MaybeRunPostInstall exits if this invocation is an installer's post-install
// telemetry ping. Respects the same opt-out as every other event.
func MaybeRunPostInstall() {
for _, arg := range os.Args[1:] {
Expand Down
13 changes: 7 additions & 6 deletions pkg/telemetry/telemetry.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ type CommandError struct {
Env Env
}

// InstallEvent is emitted once by install.sh, via the hidden --post-install
// InstallEvent is emitted once by an installer, via the hidden --post-install
// flag, right after a fresh binary is placed on disk.
type InstallEvent struct {
RunID string
Expand All @@ -180,23 +180,24 @@ type InstallEvent struct {
Env Env
}

// InstallType values. Add new install methods (e.g. "brew") here as they're
// wired up, and have the installer set [hbase.EnvInstallType] accordingly.
// InstallType values. Add new install methods here as they're wired up, and
// have the installer set [hbase.EnvInstallType] accordingly.
const (
InstallTypeScript = "script"
InstallTypeBrew = "brew"
InstallTypeUnknown = "unknown"
)

// installTypeWhitelist is every value ResolveInstallType may return.
var installTypeWhitelist = map[string]bool{
InstallTypeScript: true,
InstallTypeBrew: true,
InstallTypeUnknown: true,
}

// ResolveInstallType reads [hbase.EnvInstallType], defaulting to
// [InstallTypeScript] when unset (install.sh is currently the only caller of
// --post-install) and falling back to [InstallTypeUnknown] for any value
// outside the whitelist.
// [InstallTypeScript] when unset and falling back to [InstallTypeUnknown] for
// any value outside the whitelist.
func ResolveInstallType() string {
v := os.Getenv(hbase.EnvInstallType)
if v == "" {
Expand Down