Add Doctor Check
Register custom diagnostic checks so doctor validates your feature's health alongside the built-in checks.
Important
Doctor checks are read-only diagnostics.
They must not modify state. Return a setup.CheckResult describing what was found.
Step 1: Write your check functions¶
Each check receives a context.Context and *props.Props and returns a setup.CheckResult.
package myfeature
import (
"context"
"gitlab.com/phpboyscout/go-tool-base/pkg/props"
"gitlab.com/phpboyscout/go-tool-base/pkg/setup"
)
func checkMyService(_ context.Context, props *props.Props) setup.CheckResult {
endpoint := props.Config.GetString("myfeature.endpoint")
if endpoint == "" {
return setup.CheckResult{
Name: "My Service",
Status: "warn",
Message: "endpoint not configured",
}
}
// Perform a health check...
return setup.CheckResult{
Name: "My Service",
Status: "pass",
Message: "reachable at " + endpoint,
}
}
Step 2: Register checks via the feature registry¶
Use setup.RegisterChecks in your package's init() block. The CheckProvider function receives *props.Props and returns a slice of setup.CheckFunc, allowing you to conditionally include checks based on configuration.
package myfeature
import (
"gitlab.com/phpboyscout/go-tool-base/pkg/props"
"gitlab.com/phpboyscout/go-tool-base/pkg/setup"
)
func init() {
setup.RegisterChecks(props.FeatureID("myfeature"),
[]setup.CheckProvider{
func(p *props.Props) []setup.CheckFunc {
return []setup.CheckFunc{
checkMyService,
}
},
},
)
}
Step 3: Import your package¶
As with initialisers, ensure your package is imported somewhere in the dependency graph: typically via a blank import in your command package:
When the feature is enabled via props.Tool.IsEnabled(), the doctor command will automatically discover and run your checks.
Combining with Initialisers¶
If your feature already has an initialiser, you can register checks in the same init() block:
func init() {
setup.Register(props.FeatureID("myfeature"),
[]setup.InitialiserProvider{...},
[]setup.SubcommandProvider{...},
[]setup.FeatureFlag{...},
)
setup.RegisterChecks(props.FeatureID("myfeature"),
[]setup.CheckProvider{
func(p *props.Props) []setup.CheckFunc {
return []setup.CheckFunc{checkMyService}
},
},
)
}
Status Constants¶
Use the following status strings in your CheckResult:
| Status | Meaning |
|---|---|
"pass" |
Check succeeded |
"warn" |
Non-fatal issue, feature may work |
"fail" |
Critical problem |
"skip" |
Check not applicable |
A failing check fails the run; a warning only does so if you say it should.
Set Gating: true on a CheckResult when a warning is a policy violation
rather than advice:
return CheckResult{
Name: "Credential storage",
Status: "warn",
Gating: true, // this is a policy failure, not advice
Message: "2 literal credential(s) in config",
}
Most warnings are advice. "No AI provider configured" is a perfectly good state
for a tool that does not use AI, and failing its pipeline over that would turn a
diagnostic into a tripwire, so an advisory warning never fails a run, at any
threshold. Gating has no effect on a pass, a skip, or a fail: a failed check
gates regardless, because there is nothing advisory about one.
doctor exits non-zero when a check is as bad as the run's threshold, or
worse:
| threshold | fails on | when it applies |
|---|---|---|
warn |
gating warn, and any fail |
the default under CI |
fail |
fail |
the default interactively |
none |
nothing | only when asked for |
gtb doctor # warn gates in CI; a failure gates anywhere
gtb doctor --fail-on=warn # opt a local run in
gtb doctor --fail-on=none # escape hatch for a pipeline not ready yet
skip never fails a run, whatever the threshold. A check that could not run has
not found a problem, and failing a pipeline because something was unavailable is
the fastest way to have the gate switched off. If your check cannot reach what
it needs, return skip and say why. Never pass.
The report always prints in full before the exit code is decided, so a gated run still shows what gated it.
Tip
Look at the built-in checks in pkg/cmd/doctor/checks.go for reference implementations.