Skip to content

Managing Configuration Versions

This is the hands-on companion to the versioning model — read that first if terms like latest vs running, declared base, or descendancy guard are new. The examples assume $VSS (API base URL), $TOKEN (session JWT), and $NSID (namespace); on the vsscli side, an active profile whose namespace is set (vsscli profile), so commands don’t need the namespace repeated. Storing versions needs config:write; promoting needs config:apply.

Terminal window
# List versions (newest first, 100/page; cursor in next_cursor)
curl -s "$VSS/v1/ns/$NSID/config" -H "Authorization: Bearer $TOKEN"
# Resolve a tag to its version
curl -s "$VSS/v1/ns/$NSID/config?tag=golden" -H "Authorization: Bearer $TOKEN"

The namespace head (GET /v1/ns/$NSID) reports running_version and latest_version — if they differ, a staged change is waiting.

Version tokens are interchangeable everywhere below: a ULID, latest, running, or a tag.

Terminal window
# Raw YAML (default); the ETag header is the version ULID — keep it,
# it's your If-Match base for the next write.
curl -s "$VSS/v1/ns/$NSID/config/running" \
-H "Authorization: Bearer $TOKEN" -o running.yaml -D headers.txt
# JSON envelope instead (version, parent_version, tag, description, yaml)
curl -s "$VSS/v1/ns/$NSID/config/latest" \
-H "Authorization: Bearer $TOKEN" -H "Accept: application/json"
# The effective (post-inheritance) document instead of what you authored
curl -s "$VSS/v1/ns/$NSID/config/running?view=effective" \
-H "Authorization: Bearer $TOKEN"

?view= accepts user (the default — your authored YAML) or effective (the merged document apply actually renders); anything else is a 400, so a typo can’t silently hand you the wrong view. In the JSON envelope the body arrives in effective_config_yaml instead of user_config_yaml — whichever view you asked for is the one populated.

To compare versions before acting:

There is no server-side diff — fetch both versions and diff locally (diff <(curl … running) <(curl … latest)).

Validate early and often — it’s the same compiler that gates storing, so a clean validate means the store will succeed.

Terminal window
# In-namespace: inheritance is merged in, references to inherited
# entries resolve. Requires config:write on the namespace.
curl -s -X POST "$VSS/v1/ns/$NSID/config/validate" \
-H "Authorization: Bearer $TOKEN" --data-binary @draft.yaml
# Standalone: no namespace context, no merge.
curl -s -X POST "$VSS/v1/ns/config/validate" \
-H "Authorization: Bearer $TOKEN" --data-binary @draft.yaml

Failures return a problem document whose validation_errors array lists every error, not just the first.

Terminal window
# The If-Match base is mandatory: prove you edited the current latest.
BASE=$(curl -sI "$VSS/v1/ns/$NSID/config/latest" \
-H "Authorization: Bearer $TOKEN" | awk -F'"' '/^ETag/{print $2}')
curl -s -X PUT "$VSS/v1/ns/$NSID/config" \
-H "Authorization: Bearer $TOKEN" \
-H "If-Match: \"$BASE\"" \
--data-binary @draft.yaml
# → 201 {"success":true,"new_version":"01JD…"} (Location + ETag headers)

A 412 means another writer got there first; the response’s current_latest field (and ETag header) name the new latest — re-fetch, reconcile your edit, and re-PUT against it. ?base=latest works instead of If-Match for quick interactive use, but a concrete ULID is what actually protects you: base=latest re-resolves at arrival time and so always passes, racing writers included.

For small changes, patch a version instead of re-uploading the document.

The API takes RFC 6902 JSON Patch (JSON or YAML encoded), applied to the authored document of the source version in the URL:

Terminal window
curl -s -X PATCH "$VSS/v1/ns/$NSID/config/latest" \
-H "Authorization: Bearer $TOKEN" \
-d '[
{"op":"test","path":"/lines/1001/line-profile","value":"standard"},
{"op":"replace","path":"/lines/1001/line-profile","value":"premium"}
]'
# → 201 {"success":true,"new_version":"…"}

Add ?rebase=true to replay the ops onto current latest instead of getting a 412 when your source is stale (the response then carries rebased_onto). A failing operation — including a test mismatch — returns 409 naming the op and path.

Terminal window
curl -s -X PUT "$VSS/v1/ns/$NSID/config/01JD…/tag" \
-H "Authorization: Bearer $TOKEN" -d '{"tag":"golden"}'
curl -s -X PUT "$VSS/v1/ns/$NSID/config/01JD…/description" \
-H "Authorization: Bearer $TOKEN" -d '{"description":"post-migration soak, verified 7d"}'
curl -s -X DELETE "$VSS/v1/ns/$NSID/config/01JD…/tag" \
-H "Authorization: Bearer $TOKEN"

Re-tagging with a name that exists moves the tag (it’s unique per namespace) — that’s the point of golden.

Terminal window
curl -s -X POST "$VSS/v1/ns/$NSID/config/latest/apply" \
-H "Authorization: Bearer $TOKEN"
# → {"success":true,"running_version":"…","previous_version":"…"}

The path token may be a ULID, latest, running (re-render repair), or a tag. ?force=true bypasses the descendancy and schema-version guards — reserve it for deliberate reverts and post-upgrade re-applies.

Watch the two guard refusals: not-descendant-of-running means the version you’re promoting isn’t downstream of what’s live (stale branch or deliberate revert — see below), and schema-version-mismatch appears after platform upgrades (re-store the document, or --force after review). A failed render rolls back automatically: the previous running version is re-rendered and reported, and your namespace keeps processing calls on the old config.

Roll-forward is the recommended shape — it keeps history linear and keeps latest matching what’s live:

Terminal window
# 1. Take the known-good version's document…
curl -s "$VSS/v1/ns/$NSID/config/golden" \
-H "Authorization: Bearer $TOKEN" -o rollback.yaml
# 2. …store it as a NEW version based on current latest…
BASE=$(curl -sI "$VSS/v1/ns/$NSID/config/latest" \
-H "Authorization: Bearer $TOKEN" | awk -F'"' '/^ETag/{print $2}')
NEW=$(curl -s -X PUT "$VSS/v1/ns/$NSID/config" \
-H "Authorization: Bearer $TOKEN" -H "If-Match: \"$BASE\"" \
--data-binary @rollback.yaml | jq -r .new_version)
# 3. …and promote it (no force needed: it descends from running).
curl -s -X POST "$VSS/v1/ns/$NSID/config/$NEW/apply" \
-H "Authorization: Bearer $TOKEN"

In a hurry, apply <old-version> --force reverts directly — but leaves latest ahead of running, so follow up with the roll-forward push once the incident is over (anyone applying latest later would otherwise silently re-deploy what you just reverted).

Response Meaning Do
412 on PUT/PATCH Your base is no longer latest — another writer landed Re-fetch latest, reconcile, retry (or --rebase)
400 with validation_errors Document failed compilation Fix every listed error; re-validate
409 on PATCH An operation failed to apply (test mismatch, missing path) Inspect the named op/path; re-fetch the source
apply refused: not a descendant Promoting a version outside running’s lineage Roll forward instead, or force for a deliberate revert
apply refused: schema version Version pre-dates a platform schema upgrade Re-store the same YAML (re-compiles under the new schema), then apply
apply failed, previous version restored Render failed; automatic rollback ran Read the error, fix the config or conflict, re-apply

One habit prevents most of these: fetch → edit → push against the fetched version — never write from memory, and never script around the precondition with base=latest.