Skip to content

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 revision from Busroot. It cannot be calculated from a local copy of the manifest.
  • lastChange describes the most recent change. Its source is 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. repo is 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.
  • sourceRef is the source's own reference for the change. For repo it is the commit SHA. It is null whenever source is null or unknown.
  • GET /api/manifest?state=true returns revision and lastChange without 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

  1. Pull the live manifest. Save the document as manifest.json and its revision as manifest.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
    
  2. 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.

  3. 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 revision equals manifest.lock. The repository was pulled from the manifest that is live now.
  • lastChange.source is repo and lastChange.sourceRef is 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.

  1. Pull the live manifest again, as in step 1 above.
  2. Merge it with the change you wanted to make, and commit manifest.json and the new manifest.lock together.
  3. Run the pipeline. It passes because the live revision equals manifest.lock again.

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.