Skip to content

Repository files navigation

envprobe

CI

Check that a machine has the tools and services you expect — a laptop, a CI runner, or a test box.

envprobe reads a config file that names what should be there, then verifies it: executables on PATH (with their versions), TCP ports that answer, the Docker daemon, environment variables, and files and directories. There are no built-in defaults. What you write in the config is exactly what gets checked.

$ envprobe doctor
using /Users/you/envprobe.yaml
✓  git            2.50.1   9ms
✓  Java           21.0.11  93ms
✓  Go             1.26.5   6ms
✗  terraform               0s
✓  postgres                2ms
✗  redis                   1ms  (connection refused)
✓  docker-daemon           160ms
✓  AWS_PROFILE             0s
✗  ssh config              0s  (not a file)
6 of 9 checks passed

Install

Download a binary from the latest release, or build from source:

go install github.com/nchatzak/envprobe@latest

Linux and macOS. Windows is out of scope.

Usage

Write a starter config, then run it:

envprobe config init     # writes ./envprobe.yaml
envprobe doctor          # run every check

doctor looks for envprobe.yaml in the current directory, then your home directory, then ~/.config/envprobe, and uses the first one it finds. The file it chose is printed to stderr, so you always know which config produced the output.

Checks run concurrently, each with a 5-second timeout, so one unreachable host does not hold up the rest. A check that times out is reported as a failure.

A row says why in parentheses when the ✗ alone would leave you guessing — redis above was refused rather than slow. terraform gets none because missing from PATH is the whole story.

Commands

Command What it does
envprobe doctor Run every configured check and print a table
envprobe doctor --json Same, as JSON on stdout
envprobe doctor --ci Exit non-zero when a check fails (see below)
envprobe config init Write an example config to ./envprobe.yaml (-o to change the path, --force to overwrite)
envprobe config validate [file] Parse a config without running anything
envprobe config example Print the annotated example config to stdout
envprobe completion <shell> Print a shell completion script (bash, zsh, fish)
envprobe --version Print the version (-v works too)

Shell completion

envprobe completion <shell> prints a script — it does not install one. Until that script is where your shell looks, TAB does nothing and nothing says why. On macOS with zsh:

envprobe completion zsh > $(brew --prefix)/share/zsh/site-functions/_envprobe

Then restart your shell; the session that wrote the file will not pick it up. envprobe completion <shell> --help has the paths and setup steps for each shell — zsh, bash and fish, on both macOS and Linux — including the bash-completion package that bash needs.

Two things that help does not mention:

  • zsh ignores the file unless its completion system is on, silently. Check with (( $+functions[compdef] )) && echo on || echo off. If that prints off, add autoload -Uz compinit && compinit to ~/.zshrc — but check first, as many tools run it for you when sourced. To confirm envprobe itself loaded: print -l ${(k)_comps} | grep envprobe.
  • The script contains no command or flag names — it asks the binary on your PATH as you type, so upgrades bring their own and it never needs regenerating.

JSON output

Results go to stdout, diagnostics to stderr, so --json stays pipeable:

$ envprobe doctor --json | jq '.[] | select(.found | not) | .name'
"terraform"
"redis"

Each result carries name, found and duration_ms. version and path appear only when a check has one to report, so a port check has neither. A binary check drops path when the executable is missing; a path check keeps it either way, since the expanded target is the thing worth seeing when the check failed.

problem carries the same cause the table prints in parentheses, under the same rule. It is not limited to failures: wrong version_args give found: true with problem: version command failed, unless version_constraint is set — then there is nothing to compare and the check fails.

The 5 of 7 checks passed line is a diagnostic, so it goes to stderr in both formats — stdout stays exactly the results array. It prints on every run that had checks to run, whether or not --ci is set, so one grep finds the tally in any log.

Configuration

Every check has the same three fields:

  • name — the label in the output and the key under --json. Must be unique.
  • type — binary, port, docker-daemon, env, or path.
  • with — the payload for that type.

Keys on a check and keys inside with are both validated: an unknown key is an error that names the key, so a typo fails loudly instead of being ignored. A key beside with is caught before any check is built, so it is reported on its own — fix it and the next run reports whatever is wrong inside with.

The checks: key itself is not. A file that misspells it parses as a file with no checks in it, so doctor warns and exits 0 while --ci exits 2.

The five types:

Type Checks with:
binary The executable is on PATH, and its version if you ask for one target (defaults to name), version_args (optional), version_constraint (optional)
port Something is listening, which proves a service is running rather than merely installed target, required, as "host:port"
docker-daemon docker info answers, which the binary check alone cannot tell you none
env An environment variable is set, and matches a pattern if you ask for one target (defaults to name), matches (optional regexp)
path A file or directory exists, for the things a tool reads rather than the tool itself target, required, kind (optional, file or dir)

A binary check with version_constraint: ">= 1.24" fails when the tool is older than that, rather than reporting a version and leaving the judgement to you. It needs version_args, since there is otherwise nothing to compare, and omitting them is a config error. The key is spelled apart from the version field in --json, which is what the machine reported rather than what you asked for.

A two-component version counts as its .0 release, so 1.24 satisfies >= 1.24. A version with more than three components — 1.2.3.4, or output holding no version at all — cannot be compared, and fails the check with problem: cannot compare version rather than passing unverified. Such a tool can still be checked for presence, just not constrained.

A prerelease suffix is dropped before the comparison, so 1.2.3-rc1 compares as 1.2.3 and satisfies >= 1.2.3 — the version such a constraint is most likely written to exclude. The row prints 1.2.3 as well, so nothing on screen says the judgement was made on a truncated string.

An env check with no matches asks only whether the variable is set, so an empty value passes. Write matches: "." to require a non-empty one.

A path check takes no default from name, since a label like ssh config is not a path. A leading ~ expands to your home directory; anywhere else it is a literal character, so ~backup stays ~backup. A relative target resolves against the directory you run envprobe from, not the one the config file sits in. Symlinks are followed, so kind: file passes on a link to a file. Without kind, a file and a directory both pass.

For a worked config with every type and the pitfalls annotated — which tools print their version to stderr, which want no dashes at all:

envprobe config example    # print it
envprobe config init       # write it to ./envprobe.yaml

An empty checks: list runs nothing and exits 0, with a warning on stderr.

Exit codes

Code Meaning
0 Every check passed, or there was nothing to check
1 The checks ran and at least one failed (--ci only)
2 envprobe could not check: no config file, no checks under --ci, a config it could not build, or a bad flag

Without --ci, a failing check is information, not an error: doctor reports it and exits 0. --ci turns the same run into a gate. The 1/2 split matters there — 1 says the environment is wrong, 2 says envprobe never got far enough to form an opinion, and a pipeline should treat those differently.

- name: Verify toolchain
  run: envprobe doctor --ci

Development

./scripts/check.sh       # gofmt, build, vet, test -race, lint — what CI runs
./scripts/exit-codes.sh  # drives the built binary to verify the exit-code contract

Design rationale lives in docs/decisions.md.

License

MIT. See LICENSE.

About

Check that a machine has the tools and services you expect - a laptop, a CI runner, or a test box. One config file, one command, no built-in defaults.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages