Skip to content

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).

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
https://your-mantis/api/v1/config/export

The 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/, …).

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):

Terminal window
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).

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.

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.

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.

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 409 explaining 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. (The 409 protects 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}/webhook so a push reconciles immediately. Set a shared secret first (the Set webhook action on the Config Repositories page, or PUT .../webhook-secret); Mantis verifies the X-Hub-Signature-256 HMAC GitHub and Gitea send and rejects anything unsigned or mis-signed with 401. 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 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.