Validate config in CI before prod
Catch the broken config in the pull request, not in the 2am page. Lint the structure, validate references with --strict, and assert the values are sane — three commands that turn config mistakes into a red CI check.
blog/validate-config-ciThe worst config bugs aren't the loud ones. They're the typo in a replica count, the environment name nobody defined, the variable referenced but never set — the kind that lints fine, deploys fine, and then quietly does the wrong thing in production at 2am.
The fix is boring and it works: make the config prove itself in the pull request. Three commands, three failure modes caught, all before merge.
Three checks, three failure modes
Each command catches a different class of mistake. lint catches shape and syntax. validate --strict catches references that won't resolve. A check task asserts the actual values are within bounds.
name: config-check
defaults:
stage: staging
image: ghcr.io/acme/api:1.6.0
replicas: "3"
tasks:
# orchstep run check
check:
desc: "Assert required config is present and sane"
steps:
- name: image_set
func: assert
args:
condition: '{{ ne vars.image "" }}'
message: "image must be set"
- name: stage_known
func: assert
args:
condition: '{{ or (eq vars.stage "staging") (eq vars.stage "production") }}'
message: "stage must be staging or production"
- name: replicas_set
func: assert
args:
condition: '{{ ne vars.replicas "" }}'
message: "replicas must be set"
- name: ok
func: shell
do: echo "config valid for {{ vars.stage }} -> {{ vars.image }} x{{ vars.replicas }}"
# the task CI validates structurally with: orchstep validate deploy --strict
deploy:
steps:
- name: deploy
func: shell
do: echo "deploying {{ vars.image }} to {{ vars.stage }} ({{ vars.replicas }} replicas)"The assert steps fail the run with a clear message when a value is wrong. stage_known is the interesting one: pass --var stage=qa and the gate fails because qa isn't a stage you defined — exactly the typo that would otherwise sail through.
Wire it into the PR
The CI step is three lines. Each exits non-zero on failure, so a broken config turns the check red:
orchstep lint
orchstep validate deploy --strict
orchstep run checklint checks the workflow's structure. validate deploy --strict statically resolves the deploy task's references and fails on anything left unresolved — a variable you referenced but never defined, for instance. run check then asserts the values themselves are in range.
Prove the gate actually fails
A gate you've never seen fail is a gate you don't trust. Feed it a bad value on purpose:
$ orchstep run check
config valid for staging -> ghcr.io/acme/api:1.6.0 x3
Result: success$ orchstep run check --var stage=qa
[FAIL] stage_known
stage must be staging or production
Result: failedThe bad value exits non-zero, the PR check goes red, and the typo never reaches a cluster. That's the whole point: the failure happens in CI, where it costs a re-push, not in prod, where it costs an incident.
What you gained
| Failure mode | Without a gate | With the gate |
|---|---|---|
| Malformed YAML | deploy-time error | orchstep lint |
| Unresolved reference | wrong value at runtime | validate --strict |
| Out-of-range value | quiet prod bug | assert in check |
| Unknown environment name | silent fallthrough | red PR check |
| When you find out | 2am page | code review |
Where to stop
Don't try to assert every conceivable property — a config check that takes longer to maintain than the config it guards is a net loss. Pin the handful of values that have actually burned you: the environment names, the must-be-set secrets references, the counts that must be positive. That's where the leverage is.
Where to go next
- CLI reference —
lintandvalidate --strictin detail - Running in CI — wiring the gate into your pipeline
- The assert function — conditions and messages
The project above is a runnable, echo-only demo. Run orchstep run check --var stage=qa to watch the gate fail on purpose.
curl -fsSL https://orchstep.dev/install.sh | sh