Config as Code
Config as Code lets you export your Mantis deployment configuration — solutions, sequences, actions, environments, tags, variables, variable sets, schedules, deployment freezes, promotion lifecycles, and notification channels — to human-readable, version-controlled Nickel files. Keep your deployment definitions in Git alongside your application code, review changes in pull requests, and reproduce configuration across instances.
It covers your deployment definitions, not runtime state (deployment history, logs, target status) or platform identity (users, roles, certificates).
Exporting your configuration
Section titled “Exporting your configuration”curl -H "Authorization: Bearer $TOKEN" \ https://your-mantis/api/v1/config/exportThe response is a multi-file document — a map of repository-relative paths to file contents — plus a summary of what was exported:
{ "schema_version": "1.0.0", "files": { "environments/production.ncl": "{ \"slug\" = \"production\", \"name\" = \"Production\", \"sort_order\" = 1 }", "tags/team-platform.ncl": "{ \"key\" = \"team\", \"value\" = \"platform\" }" }, "summary": { "environments": 1, "tags": 1 }}Write each files entry to the given path in your repository to get a config repo whose
layout mirrors the Mantis UI hierarchy (environments/, solutions/, actions/, …).
Importing your configuration
Section titled “Importing your configuration”Send the same multi-file shape back to import it. Use dry_run to preview without
changing anything, and conflict_strategy to control what happens to entities that
already exist (fail, skip, or upsert):
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ https://your-mantis/api/v1/config/import \ -d '{ "files": { "environments/staging.ncl": "{ \"slug\" = \"staging\", \"name\" = \"Staging\", \"sort_order\" = 2 }" }, "dry_run": true, "conflict_strategy": "upsert" }'The response reports per-type counts plus anything the import deliberately ignored:
{ "dry_run": true, "created": 1, "updated": 0, "skipped": 0, "unsupported": [], "ignored_immutable": [] }.
POST /config/validate is the same as an import with dry_run forced on. Import is
atomic — if any document fails, the whole import is rolled back — and tenant-confined:
imported entities always land in your own tenant scope.
You can only import an entity type you are allowed to create and update (e.g.
environments:create + environments:update); secrets are never read from the files (a
sensitive variable’s value is re-supplied separately, never taken from Git).
Immutable versions
Section titled “Immutable versions”Versioned definitions — solutions, sequences, and actions — treat each published version as
immutable: once a version (a SemVer like 1.2.0) exists, its content is frozen. An
upsert import only ever adds new versions; it never rewrites a version that already
exists. This is what makes a deployed release reproducible — solution@1.2.0 always means
the same thing.
So if you edit a published version in place in Git (change its steps or payload without
bumping the version) and import, that change is not applied — the existing version wins.
To avoid losing the edit silently, the import reports every such version in the
ignored_immutable field of the response, as "<kind>/<slug>@<version>":
{ "created": 0, "updated": 1, "skipped": 0, "unsupported": [], "ignored_immutable": ["actions/deploy@1.0.0"]}When you see an entry there, bump the version (add a new SemVer with your changes) and re-import. Two-way Git sync and Git-authoritative reconcile apply the same rule, so an in-place edit pushed to a repository is surfaced the same way rather than quietly dropped.
Deleting a definition (and what happens to history)
Section titled “Deleting a definition (and what happens to history)”When you remove a definition’s file from the Git repository and sync (or delete it through the API), Mantis removes that definition. Two guarantees make this safe:
Your history is never destroyed. Operational and historical records — deployment
history, environment-state history, and rollback variable snapshots — survive the deletion
of the definition they refer to. The link to the now-deleted definition is cleared, but the
record keeps a denormalized copy of the definition’s name captured at the time it ran,
so audit history stays readable. (This mirrors how Octopus Deploy decouples a project’s
deployment history from the project’s current configuration.) History whose definition has
since been deleted is listed at GET /api/v1/environment-states/orphaned.
A delete that would strand in-flight work is refused. To avoid orphaning operations you
still care about, Mantis blocks a deletion (surfaced as a sync conflict, or 409 over
the API) when:
- a solution still has a pending or approved promotion request — resolve or cancel the promotion first; or
- an environment still has a pending or approved promotion request targeting it — resolve or cancel the promotion first; or
- an environment is still referenced by a promotion-lifecycle rule — remove the rule first.
A blocked Git-side deletion does not silently revert your change: it is held back as a conflict for you to resolve, and the file stays deleted in Git until you do. As a safety net against a misconfigured or partially-cloned repository, a sync that would delete a large share of one entity type at once (more than half, and more than a few) is treated as a bulk-wipe and held back rather than applied. Normal deletions — even removing the last entity of a type — always go through.
What you can export
Section titled “What you can export”You can only export configuration you are allowed to read. Export composes your
existing per-resource read permissions: an environment is included only if you can read
environments (environments:read), a tag only if you can read tags (tags:read), and so
on. If you cannot read any exportable resource, the endpoint returns 403. Results are
scoped to your tenant — you see your own configuration plus shared/global configuration.
Secrets are never exported
Section titled “Secrets are never exported”Sensitive values never leave Mantis. A sensitive (encrypted) variable is exported as
metadata only, with its value shown as the placeholder "<ENCRYPTED>":
{ "name": "db-password", "variable_type": "sensitive", "value": "<ENCRYPTED>", "is_prompted": false }The real value stays in the Mantis database and is re-supplied when the configuration is imported. Plaintext and ciphertext are never written to the exported files, and no secret-bearing field (passwords, tokens, keys, credential-carrying webhook URLs) is ever included. This matches how other config-as-code systems (e.g. Octopus Deploy) keep sensitive variables out of version control.
File format
Section titled “File format”Files are native Nickel (records use key = value) and
validate against the published Mantis schemas. Entities reference each other by their
stable slug (e.g. a solution references a sequence by slug), never by internal ID, so
the files are portable and readable.
Git as the source of truth (authoritative mode)
Section titled “Git as the source of truth (authoritative mode)”By default a config repository syncs two-way: you can edit definitions in Mantis or in
Git, and conflicts are detected and resolved. If you’d rather make Git the single source
of truth — the GitOps model — switch the repository to Git-authoritative mode (requires the
config:set_authoritative permission, held by admins and tenant admins; from the Config Repositories page or PUT /config/repositories/{id}/authoritative).
In authoritative mode:
- Git always wins. A reconciler pulls Git → Mantis on a schedule; changes you push to the repository are applied to Mantis, and Mantis is never pushed back to Git.
- Managed definitions are read-only. Solutions, sequences, actions, environments,
schedules, freezes, and promotion lifecycles owned by the repository are edited in Git
(ideally via a pull request), not in Mantis — any attempt to change one (via the API, or by
saving an edit from the UI) is rejected with a
409explaining where to edit. Lens shows the repository as Git-authoritative (a badge on the Config Repositories page) and a read-only banner on each managed solution/sequence/action/environment detail page — the edit, delete, and add-version controls are hidden, with a link to the owning repository. (The409protects every kind regardless of where the edit is attempted, banner or not.) - Secrets stay in Mantis. Sensitive values still live only in the database (never in Git), so you can still set/rotate a managed entity’s secret values — only the non-secret configuration is Git-owned.
- Deployments are pinned. Each deployment of a Git-managed definition records the exact reconciled commit it ran, so “what config did this deploy use?” is always answerable.
- Turning it on is guarded. Enabling first shows a divergence preview (what the first reconcile would create/update) and asks you to confirm before Git overwrites the Mantis copy. Turning it off releases the definitions so they’re editable again; a repository that still owns managed definitions can’t be deleted until you disable the mode (so nothing silently becomes editable).
- Push-to-reconcile webhooks. Instead of waiting for the scheduled sync, point your Git
host’s webhook at
POST /api/v1/config/repositories/{id}/webhookso a push reconciles immediately. Set a shared secret first (the Set webhook action on the Config Repositories page, orPUT .../webhook-secret); Mantis verifies theX-Hub-Signature-256HMAC GitHub and Gitea send and rejects anything unsigned or mis-signed with401. A burst of pushes is debounced so it can’t spin the reconciler. - Staleness is visible. An authoritative auto-sync repository whose last successful sync
has aged past 3× its sync interval (or that has never synced) is flagged Degraded
on the Config Repositories page and in the repository API (
degraded: true), so a stalled reconcile doesn’t go unnoticed.
This mirrors how Octopus Deploy treats a version-controlled project, with one difference:
Mantis keeps managed definitions read-only in the UI rather than committing your UI
edits back to Git for you. You can also preview a repository’s configuration (read-only)
via GET /config/repositories/{id}/preview?ref=…, where ref is a tag/branch/commit
reachable from the repository’s configured branch (only that branch is fetched; an
unreachable ref returns an error); it defaults to the configured branch.
