holdon 0.1.0

Wait for anything. Know why if it doesn't.
Documentation
holdon-0.1.0 has been yanked.

holdon

crates.io docs.rs CI MSRV license

Wait for anything. Know why if it doesn't.

A next-gen "wait for service ready" CLI in Rust. One static binary, parallel by default, protocol-aware, with diagnostic failures that actually tell you what broke.

$ holdon postgres://db:5432 redis://cache:6379 https://api/health
✓ ready postgres://db:5432 · 27ms
✓ ready redis://cache:6379 · 14ms
✗ failed https://api/health · 5.0s · ▁▂▄▆█ · 510ms
├ dns ✓ 2ms
├ tcp ✓ 3ms
└ http ✗ status 503
hint: service may still be initializing
→ 2/3 ready · 5.1s

Install

Method Command
Cargo cargo install holdon
Prebuilt binaries GitHub Releases
Docker (planned) docker pull ghcr.io/imjustprism/holdon
Homebrew (planned) brew install imjustprism/holdon/holdon

Minimum supported Rust version: 1.85.

Quickstart

holdon :5432                              # wait for localhost:5432
holdon :5432 :6379 :3000                  # several ports in parallel
holdon :5432 -- npm run migrate           # exec a command once ready
holdon https://api.local/health -t 60s    # http with custom timeout
holdon postgres://user:pw@db/app          # postgres handshake
holdon exec:///usr/local/bin/check.sh     # custom readiness command

Protocols

Scheme What it checks
tcp://, :port, host:port DNS resolve, TCP connect
http://, https:// TCP, TLS, HTTP request (-H, --method)
dns:// Hostname resolves
file:///path Path exists (?mode=absent inverse)
postgres://, postgresql:// Connect + SELECT 1 (TLS by default)
redis://, rediss:// Connect + PING (rediss:// for TLS)
exec://program?arg=... External command, ready iff exit 0

Feature flags

Defaults (http + json-output) cover most CI use cases. Database probes are opt-in to keep the default binary small.

Feature Adds
http HTTP / HTTPS probes (rustls)
postgres Postgres probe via tokio-postgres + rustls
redis Redis probe via redis crate + rustls
json-output --output json line-delimited events
all-databases postgres + redis
full Everything above

Exit codes

Code Meaning
0 All targets ready
2 CLI misuse or parse error
124 Overall timeout elapsed (GNU timeout convention)
126 Exec'd child not executable
127 Exec'd child binary not found
130 Interrupted by SIGINT (Ctrl-C)
143 Interrupted by SIGTERM

Output modes

  • Plain (default). Live spinner, colored status, sparklines on stderr. Auto-disabled in non-TTY environments.
  • JSON (--output json). Line-delimited events on stdout, stable schema documented in docs/json-schema.md.
  • Quiet (-q). Only the exit code.

Library

use std::time::Duration;
use holdon::{Runner, Target};
use holdon::runner::RunnerConfig;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let targets = vec![
        "postgres-host:5432".parse::<Target>()?,
        "redis-host:6379".parse::<Target>()?,
    ];
    let cfg = RunnerConfig::default().timeout(Duration::from_secs(30));
    let report = Runner::new(cfg).run(targets, None).await;
    report.assert_all_ready()?;
    Ok(())
}

See the examples directory and the API docs.

Security notes

  • TLS is rustls only. No OpenSSL anywhere in the tree.
  • Postgres and Redis use rustls with webpki roots out of the box. Postgres opportunistically upgrades to TLS unless the URL passes ?sslmode=disable.
  • HTTP redirects are followed up to 5 hops and refuse https → http downgrades.
  • --insecure (HTTP only) disables TLS verification for HTTP probes and prints a stderr warning. Do not use in production.
  • exec:// runs whatever command you point it at. Treat target strings as code.
  • Symlinks are not followed by file:// probes.
  • URL passwords are redacted in Display, Debug, and error chains.

See SECURITY.md for the full threat model and disclosure instructions.

License

Dual MIT or Apache-2.0, at your option.