Skip to main content
Version: v5.7.0

Template and template version

templates

JSON:API types: template and template_version. One page covers both: a template is the project-scoped definition (name, type, git remote), and each of its versions pins a git revision assets are templated from.

The v2 OpenAPI document declares this resource's schema; the resource-field coverage gate holds this page to it.

Endpoints​

Reads dispatch through the node-path router under the project:

VerbPathBehaviour
GET/api/projects/{p}/templatesIndex — each template gated by read on its own rule path (<project path>/templates/<code>); meta.authorisations reports update/delete on the project's …/templates path.
GET/api/projects/{p}/templates/{id}Show (404 on a miss, an out-of-project template, or a read denial).
POST/api/projects/{p}/templatesCreate — project readable plus update on …/templates, 404-on-deny. 201, or 422.
GET/api/projects/{p}/templates/{id}/versionsVersion index (ordered by updated_at); readability inherits the template's. Filterable with filter[<attribute>_eq]=<value> over the serialized attributes plus filter[created_at_gt]=<ts> — the asset screen's new-template probe sends created_at_gt + state_eq + archived_eq — and sortable with filter[sorts][]=<attr> asc|desc over the serialized attributes (the versions screen sends created_at desc); a sort attribute outside the wire shape is a 400.
GET/api/projects/{p}/templates/{id}/versions/{version}Version show, addressed by the version label, not a row id.
PATCH/PUT/api/projects/{p}/templates/{template_id}/versions/{version}Upsert: creates the version when the label is new, updates it otherwise. Requires update on the template's rule path, 404-on-deny. Request meta flags: fetch_revision (resolve the revision now) and ignore_in_use (override the in-use guards). Write attribute pack_pins (array of {name, version}) sets the version's explicit per-version pack pins — see sealed_packs below.
POST…/versions/{version}/assign_to_assetsRepoints the named assets (asset_paths) at this version — requires update on the template's path plus update on every named asset path. A broken version, or one without a resolved git commit, refuses ("cannot be assigned as it is broken or has not resolved a Git commit"). 200 with the version re-serialized.
PATCH/PUT/api/projects/{p}/templates/{id}Update — permitted attributes name, description, git_remote_name (re-resolved within the project), archived, disabled; a JSON null archived is ignored. Requires update on the template's own rule path, 404-on-deny. Archiving or disabling refuses with 422 (Can't archive/disable templates that are in use) while any unarchived node uses the template; a supplied archived also refuses when it matches the stored state (Template is already archived/active) or while the template's commits carry active changes. 200 with the updated template.
DELETE/api/projects/{p}/templates/{id}Delete — only a template with no versions and no pinned assets (the templates create --version rollback lane); a referenced template refuses by name with 422. Requires project read plus delete on the project's …/templates path. 204.

Every template and template version is created with its empty owned properties and settings documents (the upstream PropertiesOwner/SettingsOwner persistence) — the serializer's properties/settings relationship ids are therefore always populated. A one-shot idempotent boot backfill heals owners created before that port.

template attributes​

AttributeTypeMeaning
codestringUnique (case-insensitively) among the project's unarchived templates; ^[a-z0-9_]+$, max 50. Also the tail of the template's RBAC rule path.
namestringDisplay name, unique per (project, template_type) among unarchived.
descriptionstring, nullableNull when never set.
archivedbooleanArchive flag.
disabledbooleanDisable flag.
template_typestringThe template's kind. Create on this server accepts only asset (the default when blank); any other supplied value answers "Template type is not included in the list". The has_mintmodel version attribute is conditional on this type.
created_at / updated_atstring (ISO8601, 6 fractional digits)Row timestamps.

Relationships: properties, settings (to-one documents with related links), git_remote (to-one — the project remote the versions fetch from, required at create and resolved by name within the project), template_versions (to-many), nodes (to-many — the assets templated from it, filtered to nodes the principal can read), project (to-one). Links: self, template_versions (= self + /versions), project.

Every relationship is includable on the index and the show with the standard ?include= parameter. In particular include=git_remote sideloads the remote serialized exactly as its own show document, masked credentials included (previously the serializer exposed the relationship but the include engine could not resolve it, so the CLI re-fetched the git_remotes index to map the id to a name).

template_version attributes​

AttributeTypeMeaning
versionstringThe version label (the URL segment), max length enforced on upsert.
descriptionstring, nullableNull when never set; an omitted key preserves the stored value.
statestringThe version's readiness. This server writes ready (set at create and whenever a revision resolves) and treats broken as the unassignable state; the full upstream state vocabulary is not declared in this repository, so no exhaustive list is documented here.
created_bystringUsername of the creating user.
archivedbooleanArchive flag. Cannot be set on create ("archived on create" refusal); archiving a version that assets still use refuses unless ignore_in_use.
initialization_errorJSON, nullableThe stored initialisation failure, served verbatim; null when none.
lockedbooleanVersion lock: a locked version refuses modification ("Template version cannot be modified as the version is currently locked/archived"-shaped 422s) and float_git_rev cannot be enabled on it. Changing git_rev on an in-use version likewise refuses unless ignore_in_use.
float_git_revbooleanWhether the version follows its git_rev's moving tip instead of staying on the commit it sealed. A floating version repins on every fetch of its remote — the periodic fetch sweep (the remote's effective periodic_fetch_interval; opschain docs resources/git_remote, "The periodic fetch") or a manual Fetch revision (templates version upsert --fetch): when the tip differs from the sealed commit the version takes the manual refetch path — pack pins re-resolved (explicit pins kept), the pinned assets' active windows rolled onto the new commit, their action catalogs refreshed — and the sweep records info:template_version:rev_repinned. Locked and archived versions never float (the flag cannot be enabled on a locked version, and the sweep skips both); a version with a refresh in progress waits for the next pass. The repin runs as the version's creator, so a break-glass account cannot own a floating version: creating or switching a version to floating as one answers 422 on float_git_rev (A floating revision is re-resolved in the background as its creator, and break-glass accounts are interactive-only: pin the revision, or create this version as an everyday user) and records warn:template_versions:floating_refused.
float_refresh_errorJSON object, nullableThe last failure of the periodic sweep's automatic repin of this floating version ({message, at} — the stored revision no longer resolves, the refetch was refused by name, the git identity policy refused the version's creator, or the version is owned by a break-glass account, in which case message names the remedy: pin the revision, or create the version as an everyday user), served verbatim and mirrored by an error:template_version:float_refresh event; cleared to null by the next successful repin. Null when none, and always null for a non-floating version.
has_mintmodelboolean, conditionalRendered only for asset-type templates (the key is absent otherwise): whether the version's repository carries a MintModel.
created_at / updated_atstring (ISO8601, 6 fractional digits)Row timestamps.
git_revstring, nullableThe requested revision (branch/tag/sha). On upsert, an omitted or "" git_rev falls back to the version's current revision.
commit_shastring, nullableThe resolved commit, null until resolution.
git_remote_namestring, nullableThe owning template's git remote name.
sealed_packsarray of {name, version, mount, decided_by}The installed pack versions this version's pinned set sealed when it resolved (installed: declarations in .opschain/packs.yaml), pack name then mount ascending — name is the pack store name, version the exact installed version, mount the name the declaration mounted it under, and decided_by how that version was decided: declared (the declaration carries version: — the file seals it, nothing moves it but a change to the file), admin_pin (the pack store's pin decided an unversioned declaration), highest_installed (no pin: the highest installed semver decided it) or explicit (the per-version pin an operator set on this template version — see below); "" on a pin written before provenance was recorded. Always an array, empty when the version mounts no installed pack; git-sourced and bundled packs are not listed. The pack store shows the other direction as referenced_by (opschain docs resources/pack). How the list moves: a fetch of the revision re-resolves every pin against the commit's declarations and the estate default, but a mount with an explicit pin keeps it (re-validated as installed — an uninstalled one refuses the fetch by name); a locked version is never refetched and never repinned; a re-resolve cuts a new version whose list moves — this one's stays. Setting the pin: the upsert write attribute pack_pins ([{name, version}]; v2 UpsertTemplateVersion.pack_pins, CLI templates version upsert --pack <name>=<version>, the GUI's Sealed packs panel) sets version as the explicit pin on every mount of installed pack name whose declaration carries no version:; an empty version clears the explicit pin back to the estate default (the admin pin, else the highest installed version). Refused by name: a locked or archived version ("cannot be modified as the version is currently locked"), a pack whose every declaration carries version: (pack <name> is set in .opschain/packs.yaml …; v2 ERROR_REASON_PACK_DECLARATION_SEALED), a version that is not installed, a pack the version's commit does not declare, and a version assets are pinned to unless ignore_in_use ("Template version's pack pins cannot be changed as the version is currently in use") — with ignore_in_use the pinned assets' action catalogs refresh the way a fetch refreshes them. Every move is audited info:template_version:pack_repinned.
invalid_action_filesarray of {file, action, error}The declarative action files at the version's pinned commit that failed validation or compilation at the version's last catalog resolution — file the repository path (.opschain/templates/<template>/actions/…, .opschain/actions/…, or a mounted pack's actions/… path), action the identity the file would have listed under, error the validation error verbatim. Each listed action appears in the asset catalog as an invalid entry (invalid on the action resource), a change created from it is refused with the same error, and a workflow step that reaches it ends system_error with that error as its last_error_message. The resolution that records the list also emits warn:template:action_file_invalid once per file (opschain docs events). Always an array, empty when every file compiles or the version was never resolved.

Relationships: properties, settings, project, template, and pending_commit (to-one commit while a resolution is pending; data: null otherwise). Links: self (…/templates/<template_id>/versions/<version>), template.

Assets and versions​

An asset pins exactly one template version (template_version / template relationships on the asset, see opschain docs resources/node). Repointing happens from either end: the asset update's template_version attribute, or this page's assign_to_assets — both close the active template-version-history window, open the new one, and carry the previous successful generated actions forward.

MintModel versions​

A version whose pinned commit carries the template's MintModel document (MintModel/templates/assets/<template code>.json.erb, stored as has_mintmodel at revision resolution) renders that document on every generation request as the first phase of the derive job itself:

  • The server converges the asset's properties (the repository-committed document, overlaid by the converged mintpress.mintmodel_overrides, then by the mintpress subtree with those overrides removed) and the derive job stages the upstream render payload in its workspace — mintmodel.tar holding the commit's MintModel/ folder (already materialised by the snapshot fetch) plus properties.json and mp_context.json, with the mintmodel_request.json document (erb_file, parent codes) — under OPSCHAIN_MINTMODEL_DATA_PATH, the data-directory contract of the opschain-mintmodel-render CLI.
  • The CLI is co-packaged in the mintpress-runner image (beside the :7001 transformer, which serves only the MintModel diff lane) and runs before opschain-template-actions in the same job — no daemon dependency, so a render works even while the transformer is down, and a runner image without the CLI fails the generation naming the missing binary. This server never renders ERB or derives step trees itself; the result document rides the job-result channel back.
  • A successful render stores the concrete model in mintmodels, deduplicated by the SHA-256 of its canonical JSON (the upstream find_or_create_by_sha semantics — re-rendering an identical model reuses the row), and the result's actions — full-path enriched exactly as upstream (name→step_name, duplicate paths uniquified case-insensitively) — in asset_mintmodel_actions, keyed on the asset, the pinned commit and the SHA-256 of the staged properties, and stamped with the git tree id of the commit's MintModel/ folder.
  • The reuse rule — a render runs only when the MintModel changed. The render gate matches the asset's stored successful renders on the MintModel/ tree id at the pinned commit plus the properties fingerprint, not on the commit itself: a new commit that touches nothing under MintModel/ (an actions.rb edit, a README, a floating version repinned on a fetch) reuses the stored concrete model and step trees — the row is materialised for the new commit so the catalog, the templated-change validity gate and mintmodel_valid resolve it — and the job runs the actions.rb discovery alone, staged with the stored formatted trees. A commit that changes anything under MintModel/, or a change to the converged properties, re-renders; a commit that dropped the folder runs as a version that is not MintModel-backed (no render, the plain discovery). Rows stored before the tree id was recorded still reuse on their own commit and are stamped on the way. A concretise request (mintmodels/generate) always re-renders. The task records which path it took — rendered, reused or absent — and its step log announces it (=== MintModel rendered, === MintModel reused (tree unchanged)).
  • The asset's action catalog merges those stored step trees with the actions.rb and declarative-action-file sources (actions.rb is only applicable to existing MintPress customers and those using the MintPress SDK or the packs:mintpress feature), and the runner discovery workspace receives the formatted trees as ~/.opschain/mintmodel_actions.json for actions.rb's OpsChain.mintmodel_actions — staged through the job's prepare spec with no size limit on current runners (a runner build predating protocol 3 receives them base64-encoded inside the job command instead, where a catalog over the 96 KiB budget degrades to an empty list with a warn:mintmodel:actions_unavailable event) — and an actions.rb that consumes them after a fresh render is re-exported once against the stored result.
  • A render failure fails the generation request with MintModel actions could not be derived: <cause> on the task's error_message (the actions history serves it), never runs the template-actions phase, and caches the cause as the row's generation_error; the next generation request re-renders.

Browsing a version's content​

A version seals a commit, and the sealed tree is readable without a clone: TemplateService/ListTemplateVersionContent lists the entries directly under one directory of the tree (TemplateVersionContentEntry: path, name, kind — file, dir, link or submodule, size, readable), ReadTemplateVersionContent returns one UTF-8 text file (path, text, sha256, bytes, commit; 1 MiB cap — a binary file, a symbolic link or a submodule is refused by name). The CLI twins are opschain templates version browse and cat; the GUI's template version page carries Browse beside the Sealed packs panel. Both reads are authorised like the version read plus the repository-content rule path /git/content/projects/<project>/<path> with the caller's own authority — the same family a workflow's content() read crosses (opschain docs expressions) — so a listing marks a file readable only where a rule grants it, lists a directory only when something readable sits beneath it, and a file read without the grant answers permission_denied naming the rule path. A version whose commit has not resolved yet answers failed_precondition. Links are never followed.

API v2​

TemplateService on /api/v2 (opschain docs api-v2) serves the same catalog with the same authorisation split and refusals, audited with the same api:templates:* / api:template_versions:* events:

  • ListTemplates(project_code) (typed archived/template_type filter, per-row read on each template's rule path) and GetTemplate(id). Every template get carries its git_remote relation — the equivalence: the same fields the v1 git_remotes show serializes, the secret-bearing ones (user, ssh_key_data, password, passphrase) masked as {{hidden}} when stored and empty when not, never a value. TEMPLATE_VIEW_FULL adds the version rows (newest first; also on list rows) and the pinned nodes on the get.
  • CreateTemplate / UpdateTemplate / DeleteTemplate — the v1 permit lists as typed optional fields, the same in-use and referenced-template refusals.
  • ListTemplateVersions(template_id) (newest first; typed archived/state/created_after), GetTemplateVersion by label, and UpsertTemplateVersion with the v1 payload plus ignore_in_use / fetch_revision as typed flags and pack_pins (repeated TemplateVersionPackPin — name, version; the explicit per-version pack pins above). The v2 TemplateVersion row carries sealed_packs (repeated SealedPack — name, version, mount, decided_by) with the v1 meaning.
  • ListTemplateVersionContent(template_id, version, path) and ReadTemplateVersionContent(template_id, version, path) — the sealed tree and one text file ("Browsing a version's content" above).
  • DeleteTemplateVersion(template_id, version) (the ruling — upstream DestroyTemplateVersionCommand semantics): an actively assigned version refuses ("currently in use"); one assigned in the past — or still referenced by another record — is archived instead, answered as archived: true with the row (the v1 206); otherwise the version and its owned documents are destroyed. Requires delete on the template's rule path; audited api:template_versions:destroy.
  • CancelTemplateVersionRefresh(template_id, version) (upstream CancelTemplateVersionRefreshCommand semantics): refuses unless the version is initializing ("No SHA refresh is in progress…"); a cancelled version drops its pending commit and returns to ready on its resolved commit — or becomes broken when none resolved — recording the info|error:template_version:refresh_cancelled event. Requires update on the template's rule path; audited api:template_versions:cancel_refresh. Revision resolution is synchronous in this server, so only a row adopted mid-refresh can be cancellable.

The v1 facade still serves neither route (the route-parity waivers stand for v1); both verbs live on the v2 surface only. The catalog read model is PostgreSQL-only: the embedded store answers the typed ERROR_REASON_STORE_POSTGRES_REQUIRED posture.

TemplateVersionHistoryEntry​

One template-version-history window of a templated node, served by AssetService.ListTemplateVersionHistory(node_path) (the v1 template_version_history serializer's attributes plus the identities its relationships carried). A window opens when the node pins a version and closes when it is repointed. The GUI shows the windows on the node's Actions page as version activity, with links to the tasks that ran under each.

FieldTypeMeaning
idstringThe window row's identity.
node_idstringThe owning node's row id.
node_kindstringThe owning node's kind.
node_pathstringThe owning node's path.
created_bystringUsername of the identity that opened the window (the pin or repoint).
active_fromtimestampWhen the window opened.
active_totimestampWhen the window closed; unset while the window is active.
template_idstringThe pinned template's identity.
template_version_idstringThe pinned template version's row id.
template_versionstringThe pinned template version's label.
task_idsstring arrayThe background tasks that ran under the window, newest first (opschain docs resources/task, template_version_history_id).
  • opschain docs resources/node — the asset side of the pin.
  • opschain docs resources/git_remote — where versions fetch from.