Managing the Manifest from a Repository¶
An account manifest can be kept in a git repository and applied by a pipeline, so that configuration changes are reviewed and versioned like code. The admin pages keep working while an account is managed this way. Busroot does not lock them. Instead, it makes sure that a change made by hand is never overwritten without someone seeing it.
Whole-manifest writes are available to Output Industries operators only. A pipeline can
call POST /api/manifest/apply with the Output Industries admin API key for the account, or
with an API key that an Output Industries user created in the customer account:
an API key has the role of the user who created it. A key that a customer user created is
refused. From this release, every such caller must send If-Match (see Applying a
manifest).
Revisions¶
Every stored manifest has a revision: an identifier that changes whenever the manifest
changes, whoever changed it. GET /api/manifest returns it with the document:
{
"isSuccess": true,
"result": {
"manifest": { "version": "v1", "account": { "...": "..." } },
"revision": "9f2c…",
"lastChange": {
"source": "repo",
"sourceRef": "4be1a0c7…",
"userId": null,
"at": "2026-10-05T10:00:00.000Z"
}
}
}
- Read
revisionfrom Busroot. It cannot be calculated from a local copy of the manifest. lastChangedescribes the most recent change. Itssourceis one of:null: a manual change, made in the admin pages or through another API endpoint.- The value the caller declared for a whole-manifest write.
repois the only value Busroot gives a meaning to: a pipeline that applies the manifest from a git repository. It is what the admin pages' notice and the pipeline check look for. unknown: the manifest was changed without a record of who or what changed it, for example by a Busroot upgrade that rewrites stored manifests.
sourceRefis the source's own reference for the change. Forrepoit is the commit SHA. It isnullwheneversourceisnullorunknown.GET /api/manifest?state=truereturnsrevisionandlastChangewithout the document.
Applying a manifest¶
POST /api/manifest/apply replaces the whole manifest. The request must say which revision
its copy was based on:
| Header | Value |
|---|---|
If-Match |
The revision the copy was read at, quoted or not. * overwrites the manifest whatever its revision. One revision only: a list of several is rejected with 400. |
X-Manifest-Source |
Optional. Where the change comes from, 1 to 32 characters of a-z, 0-9 and -. A pipeline sends repo. |
X-Manifest-Source-Ref |
Optional, and only accepted together with X-Manifest-Source. The source's reference for the change, up to 255 characters. A pipeline sends the commit SHA. |
| Response | Meaning |
|---|---|
200 |
The manifest was stored. The body carries the new revision. |
400 |
The manifest failed validation, or a header was not valid: several revisions in If-Match, a malformed X-Manifest-Source, or X-Manifest-Source-Ref without X-Manifest-Source. Nothing was stored. |
412 |
The manifest has changed since the revision in If-Match. Nothing was stored. The body carries the current revision and lastChange. |
428 |
The request had no If-Match header. Nothing was stored. |
The Account Manifest admin page sends the revision it loaded. If someone else saves a change while the page is open, the save is rejected and the page asks for a reload.
Moving an account to a repository¶
-
Pull the live manifest. Save the document as
manifest.jsonand its revision asmanifest.lock, and commit both.curl -fsS -H "x-api-key: $BUSROOT_API_KEY" "$BUSROOT_URL/api/manifest" > live.json jq '.result.manifest' live.json > manifest.json jq -r '.result.revision' live.json > manifest.lock -
Run the pipeline. On its first run the live revision still equals
manifest.lock, so the pipeline knows that the repository is based on the live manifest, and applies it. - Edit in the repository from then on. Each later run finds that the last change came from an earlier commit of the same repository, and applies the new one.
No setting marks the account as repository-managed. While the last change came from a repository, the admin pages show a notice that says so.
To go back to managing the account in the admin pages, stop the pipeline. Nothing else has to be undone.
The pipeline check¶
The pipeline applies the manifest when either of these is true, and fails otherwise:
- The live
revisionequalsmanifest.lock. The repository was pulled from the manifest that is live now. lastChange.sourceisrepoandlastChange.sourceRefis an ancestor of the commit being applied. The last change was an earlier run of this pipeline.
#!/bin/sh
set -eu
api() { curl -fsS -H "x-api-key: $BUSROOT_API_KEY" "$@"; }
live=$(api "$BUSROOT_URL/api/manifest?state=true")
revision=$(echo "$live" | jq -r '.result.revision')
source=$(echo "$live" | jq -r '.result.lastChange.source // ""')
ref=$(echo "$live" | jq -r '.result.lastChange.sourceRef // ""')
if [ "$revision" = "$(cat manifest.lock)" ]; then
echo "The live manifest matches manifest.lock."
elif [ "$source" = "repo" ] && git merge-base --is-ancestor "$ref" HEAD; then
echo "The live manifest was last applied from commit $ref."
else
echo "The live manifest was changed outside this repository:"
echo "$live" | jq '.result.lastChange'
exit 1
fi
api -X POST "$BUSROOT_URL/api/manifest/apply" \
-H "Content-Type: application/json" \
-H "If-Match: \"$revision\"" \
-H "X-Manifest-Source: repo" \
-H "X-Manifest-Source-Ref: $CI_COMMIT_SHA" \
--data-binary @manifest.json
The job needs the full git history for the ancestor check (in GitLab CI, GIT_DEPTH: 0).
The If-Match header covers the time between the check and the write: if the manifest
changes in between, the write is rejected with 412.
When the pipeline fails¶
A failed check means that the live manifest holds a change the repository does not have. This includes everyday changes: SKUs, reasons and Tasks are part of the manifest, so adding a SKU in the admin pages or through the API stops the next run.
- Pull the live manifest again, as in step 1 above.
- Merge it with the change you wanted to make, and commit
manifest.jsonand the newmanifest.locktogether. - Run the pipeline. It passes because the live revision equals
manifest.lockagain.
A pipeline for an older commit also fails, because the commit that was applied last is not one of its ancestors. This stops a re-run of an old job from undoing newer configuration.