Skip to main content

Config as code

Keep an organization's checks in a file, in git, and let SolidPing reconcile against it: export the current state, edit it, validate it in CI, and apply it back. The file is the export document — there is no second schema to learn, and no translation layer to drift.

sp checks export --file config.yaml # bootstrap from live state
sp checks validate config.yaml # offline: no token, no network
sp checks diff config.yaml # has anything drifted?
sp apply -f config.yaml # reconcile (prints the plan first)

The round-trip guarantee​

A fresh export, fed straight back in, is a no-op. That sentence is the whole contract, and it is what makes everything else safe to build on:

export → validate = 0 issues
export → dry run = 0 created, 0 updated, 0 deleted, N unchanged

created=0 updated=0 deleted=0 is the machine-readable "the file matches the instance". It is what a CI job asserts after a merge, and what tells you a pull request really does change only what its diff shows.

The comparison is made on the normalized effective configuration, not on the text: region spellings are folded, expectedStatus and expectedStatusCodes are reconciled, periods are compared as durations, and document-level defaults are resolved on both sides. So a difference reported in a plan is a real difference.

An update entry names the fields that move:

{"slug": "api", "action": "update",
"changes": [{"field": "config.url",
"from": "\"https://acme.com/api\"",
"to": "\"https://acme.com/api-v2\""}]}

Secret values, and any value containing a ${param:…} / ${env:…} reference, are masked as *** — a plan gets pasted into tickets and CI logs, and must never be the thing that publishes a credential.

Two things a plan will tell you about​

unmanaged is drift, not agreement. It means the check exists but no manifest owns it yet. For an organization that has never run sp apply, every check is unmanaged — so an unmanaged entry carries its field diff like any other, and sp checks diff exits non-zero for a file that does not match, no matter who owns the checks.

escalationThreshold is reported but cannot be applied. The export carries it, but no write request does, so editing it in the tracked file changes nothing on the instance. The plan reports the difference and warns, naming the field and the checks — rather than answering unchanged for a file that genuinely differs. Everything else in the document applies normally.

Canonical spellings​

A file and a server can write the same thing two ways. Where that is possible, SolidPing accepts both and emits one:

Written by exportAlso accepted
Private location@paris@acme/paris
HTTP status expectationexpectedStatusCodes: [200]expectedStatus: 200

The folded @location is canonical. @acme/paris (the older fully-qualified form) is normalized to @paris on the way in when acme is your own organization, and refused when it names somebody else's. The two are the same region: a file written the long way plans as unchanged, never as a change.

expectedStatusCodes supersedes expectedStatus. Setting both is an error rather than a silent preference, because the loser would be dead config that still looks meaningful.

What secrets: stripped means​

Every export carries secrets: stripped, and it is a promise about two sets of keys:

  • Declared secrets — passwords, private keys, secretHeaders, basicAuth, a JS check's secrets map. These live encrypted and never appear in any API response either.
  • Export-redacted fields — keys that are perfectly ordinary at rest but must not reach a committed file: an email check's ingest token, and an SMTP probe's delivery_to (which contains one).

Both are restored on import, which is why a stripped export re-imports cleanly and why re-applying one never rotates a token or breaks a paired check. A file marked secrets: stripped is not an incomplete file, and the validator does not treat it as one.

A secret you do want in version control belongs in a reference:

config:
url: https://sso.acme.com/token
method: POST
body: "grant_type=password&username=probe&password=${param:sso-authtest-password}"

The reference is what is stored and what comes back out; the value is resolved when the check runs. Manage those values under Organization → Parameters or with sp params set.

Validating in CI​

POST /api/v1/orgs/:org/checks/validate accepts a whole document — JSON or YAML — and answers with every problem at once:

{
"valid": false,
"issues": [
{"slug": "api", "field": "regions", "code": "REGION_FORMAT",
"message": "region \"Paris!\" must be a slug or \"@private-location\""}
]
}

code is stable and message is prose, so a pipeline branches on the code and never on the wording. Any organization member may call it — validating changes nothing, and a job that only asks "is this file valid?" should not need a token that could delete checks. Add ?plan=true (admin) to get the reconcile plan back in the same call.

Offline, with no token at all:

sp checks validate config.yaml

This runs the server's own rules, shipped in the same binary — which is the point. A validator that re-implements the rules falls behind the day a check type is added, and then reports the server's own export as broken.

Two rules need your organization and so are answered only by the endpoint: does a ${param:…} reference resolve, and does the check already exist. The second matters because a secrets: stripped document legitimately omits a declared secret for a check that exists (the import restores it) and illegitimately for one that does not. Offline, sp checks validate assumes the check exists; post the file to the endpoint when you want the answer the write path will give.

Get sp into a pipeline from the release assets or the image — see the CLI page.

Reconciling, and deleting safely​

sp apply adds what import deliberately does not: delete-by-absence, inside a scope you opt into. Applying stamps every check it owns with a reserved label, and only checks carrying that label are ever deleted. Anything you created by hand shows up in the plan as unmanaged and is left alone.

Deleting further requires --prune, and is capped (10 by default) unless you pass --force — a bad manifest cannot quietly wipe a fleet.

sp apply -f config.yaml --dry-run # plan only
sp apply -f config.yaml # prompts, then applies
sp apply -f config.yaml --prune --yes # non-interactive, deletes by absence