Supply credentials in CI¶
Use an environment variable. The keychain is unavailable on most runners, and literals are refused outright.
Reference a variable from config¶
Then inject the secret as that variable in the job. In GitLab CI, a masked project variable of the same name is already in the environment:
In GitHub Actions:
Remove literals from any config that runs in CI¶
A literal key plus CI=true fails with LiteralRefusedInCi.
The failure is not softened by having other sources configured. When a
literal is present and refused, resolution stops — it does not
continue to fallback_env, even when that variable is set:
# fails in CI despite the fallback being available
anthropic:
literal: sk-ant-...
fallback_env: ANTHROPIC_API_KEY
Delete the literal key. There is no flag that makes the fallback take
over.
Confirm your platform sets CI to the value the check expects¶
The refusal triggers only on the exact lowercase string true. CI=1
and CI=TRUE do not trigger it, so on those platforms a literal secret
will be read and used in the pipeline.
If you are relying on the refusal, set the variable explicitly:
Do not configure a keychain for a CI job¶
CI runners rarely have a working keychain, and a keychain backend error
aborts resolution rather than falling through to the legs below it. A
config carrying both a keychain and a fallback_env key can therefore
fail in CI even though the fallback variable is set, because the
keychain leg is tried second and never returns a clean "not found". Only
the ref's own env key escapes this, being the leg tried first.
Keep CI configuration to env alone.
Turn the refusal off for a local run¶
A developer shell that exports CI=true for something else will find
literals refused. Any other value restores them:
Check what a pipeline will do before running it¶
Resolver::probe reports which source would win without printing the
secret, which makes it safe to run in a job log:
use rtb_credentials::{ResolutionOutcome, Resolver};
match resolver.probe(&config.anthropic).await? {
ResolutionOutcome::Resolved(source) => println!("resolves via {source:?}"),
ResolutionOutcome::LiteralRefusedInCi => println!("literal refused under CI"),
ResolutionOutcome::Missing => println!("nothing configured resolves"),
_ => println!("unrecognised outcome"),
}
ResolutionOutcome is #[non_exhaustive], so the wildcard arm is
required.