Skip to main content

Importing a library

POST /import takes a whole library — tag groups, tags, library sets, variables and values — in one request, one transaction and one save. That is the point of it: importing a file with hundreds of variables should not be hundreds of requests assembled by the page.

Permission​

BatchProcessingImport

One permission that stands for seventeen. It declares a dependency on every tag group, tag and variable permission there is, because an import creates all three and a replacing import deletes all three - plus BatchProcessingView, the permission that shows the Batch section Export also sits under - and the middleware requires the whole closure. See API permissions.

VariableReadSecrets is not among them. An import writes values, sensitive ones included, and is never shown one back.

The file​

The payload is shaped after the file rather than after the models. It says "library sets" where the API says variable sets, and nothing in it carries an ID — names are the only identity a file has, so names are what an import matches on.

library.json
{
"tagGroups": {
"Environment": ["Production", "Staging", "Development"],
"Region": ["EU", "US"]
},
"librarySets": {
"common": {
"description": "Defaults shared by every service",
"variables": {
"LogLevel": {
"values": [
{ "value": "Debug", "scopes": [] },
{ "value": "Warning", "scopes": ["Environment/Production"] }
]
}
}
},
"orders-service": {
"slug": "orders-service",
"description": "Config for the orders service",
"parent": "common",
"variables": {
"DatabaseConnection": {
"values": [
{ "value": "Server=localhost;Database=orders", "scopes": [] },
{ "value": "Server=prod.internal;Database=orders", "scopes": ["Environment/Production"] },
{ "value": "Server=eu-prod.internal;Database=orders", "scopes": ["Environment/Production", "Region/EU"] }
]
},
"ApiToken": {
"sensitive": true,
"values": [
{ "value": "sk-live-9f2a7c4e18b6d05", "scopes": ["Environment/Production"] }
]
},
"MaxConnections": {
"type": "Number",
"values": [
{ "value": 100, "scopes": [] }
]
}
}
}
}
}
Field
tagGroupsGroup name → list of tag names
librarySetsSet name → its slug, description, parent and variables
slugOptional. Defaults to one derived from the set's name, the same as POST /variablesets does
descriptionOptional
parentOptional. The slug of the set this one inherits from — either one already in the organization, or another this same file defines
variablesVariable name → its type, sensitivity and values
typeOptional, one of String (the default), Multiline, Number, Boolean, Json, Certificate — see Library sets and variables
sensitiveOptional, defaults to false. Marks every value of the variable a secret; the hint and hash are derived on import. Refused for a Boolean variable
valuesThe variable's values
valueA string, a number or a boolean, matching type
scopesThe tags this value applies under, as Group/Tag strings

Note scopes, not tags — the file's own word for it. A value whose JSON kind does not match the variable's type — a string given for a Number variable, say — is refused with 400, the same as a missing value is.

Parent library sets​

slug, description and parent only take effect when a set is being created. A set matched against one that already exists by name is left exactly as it is — only the variables it doesn't already hold are added, the same as before this file could name any of the three.

parent is looked up by slug rather than by name, because a parent is what the rest of the product already points at a set by, and a slug — unlike a name — is guaranteed to identify exactly one set. It can name a set already in the organization, or one this same file also defines; which one appears first in the file does not matter.

A parent that names no set, names itself, or would otherwise put a set above itself somewhere up its own chain is refused with 400, the same as POST /variablesets refuses it. See Parent library sets for what a parent then does when a set's values are resolved.

Merging​

The default. POST /import with no query parameter.

Anything already present by name is left alone and reported as skipped; anything new is created. Nothing reaches inside a variable that already exists, so re-importing an unchanged file is a no-op that reports itself as one.

curl -X POST "$LIBRARY_API/import" \
-H "x-tagshape-auth: $JWT" \
-H "Content-Type: application/json" \
--data @library.json

Replacing​

curl -X POST "$LIBRARY_API/import?replaceExisting=true" \
-H "x-tagshape-auth: $JWT" \
-H "Content-Type: application/json" \
--data @library.json
This deletes everything first

The organization's entire library — every tag group, tag, library set, variable and value — is removed before the file is applied. Once emptied there is nothing left to recognise, so a replacing import matches against nothing and numbers everything from one.

The deletion is done in the database rather than by loading and removing entity by entity. Both roots cascade all the way down, and the rows joining a value to its tags cascade from either side, so nothing is orphaned. Loading it all first would mean fetching every secret the organization holds only to count it and throw it away.

The summary​

200 OK
{
"replaced": false,
"tagGroupsCreated": 2, "tagGroupsSkipped": 0,
"tagsCreated": 5, "tagsSkipped": 0,
"librarySetsCreated": 2,"librarySetsSkipped": 0,
"variablesCreated": 3, "variablesSkipped": 0,
"valuesCreated": 7,
"tagGroupsDeleted": 0, "tagsDeleted": 0, "librarySetsDeleted": 0,
"variablesDeleted": 0, "valuesDeleted": 0,
"notes": []
}

The Deleted counts are what a replacing import took away; they are zero on a merge. notes explains what was skipped and why.

Failure​

A rejected file is refused with 400 and a list of reasons, and the transaction is rolled back rather than merely not committed — so a rejected replacing import leaves the emptied library exactly as it found it.

Auditing​

One entry for the whole import, with the action Merge or Replace, the organization as the entity, and the counts as the after state. Not one entry per entity: a file with hundreds of variables in it would spend longer being audited than imported, and what happened was one act, which the counts describe in full.

The notes are left out of the audit entry. Re-importing an unchanged file produces a note per entity left alone — worth telling the caller, not worth shipping to another service as a record of nothing having changed.

From the management UI​

The Batch section's Import subsection does the same thing with a file picker, a summary of what the selected file holds (tag groups, tags, library sets, variables and how many are sensitive), and a replace toggle. It shows the same summary the API returns once the import has run. See The management UI.

Practical notes​

  • Export first if you are replacing. There is no undo. Read your library sets via GET /variablesets and keep the result.
  • Merging is safe to repeat. It is the right choice for a library kept in version control and applied on every pipeline run.
  • Tags must exist or be declared. A scopes entry naming a tag that is neither in the file nor already in the library is skipped, which produces a value that matches less specifically than intended rather than an error.
  • Secrets in a file are secrets in a file. If your library holds sensitive values, the file holds them in plaintext. Treat it accordingly.