The fleet's one typed reader for the CI export-sink and variable context — so no
tool re-implements "which CI am I in, and where do exports go?" with scattered
os.Getenv checks.
A Reader (built with functional options) whose Read() returns a typed
Context: the detected Provider (GitHub Actions / GitLab CI / Bitbucket
Pipelines / Buildkite, or unknown locally), the export Sink (the
provider-specific env/output/path/summary file paths), and the captured
variable namespace. It is the reader half of the CI-export pattern; its
sibling cisink-write-go is the
writer that consumes a Sink to emit variable-set/output lines in the right
dialect.
Detection is conservative — a provider is reported only when its canonical
marker variable is present. A forced provider missing its required sink path
becomes a typed, code-carrying error via errors-go (cisink_missing_sink /
cisink_unsupported_provider).
Every CI-aware tool starts by asking the same two questions: am I under CI, and
which one? Splitting that read into one typed primitive means a tool that merely
needs to branch on CI does not pull the emitters, and the writer library has one
agreed source of truth for the sink paths — never a hand-rolled os.Getenv
ladder again.
go get github.com/pleme-io/ci-sink-go
ctx, err := cisink.Read()
if err != nil { return errs.Exit(err) }
if ctx.InCI() {
log.Printf("running under %s", ctx.Provider)
// ctx.Sink → hand to cisink-write-go to emit a variable.
}
// Hermetic / forced:
r := cisink.New(cisink.WithForcedProvider(cisink.ProviderGitHub))
ctx, err = r.Read()A typed Config (yaml-tagged) drives detection declaratively when a tool
already loads a config file via shikumi-go. Load once at main, then
cisink.FromConfig(cfg.CISink):
force_provider: github # optional; empty → autodetectFromConfig consumes the already-loaded sub-struct and never calls
shikumi.Load itself (BOREALIS §3.5).
Pull-model (Go modules): an annotated vX.Y.Z tag is the release; pkg.go.dev
indexes it. See the GSDS module delivery FSM.