Neutral trees
A standard local tree is faithful to one account: files carry their _ids and every cross-reference is a raw id. That is right for one environment per repository, and wrong for keeping two environments on two git branches — the ids differ everywhere, so git merge sandbox into prod conflicts on every file even when the configuration is identical.
There are two answers. celigo promote writes a standard tree into another account or environment through a committed id map. This page describes the other: celigo pull --neutral pulls an environment-neutral tree, in which the tracked files contain nothing environment-specific — the ids are the environment, the content is the configuration — so one permanent git branch per environment merges cleanly.
What is neutralized
In every tracked document:
_idis removed. Identity lives in.celigo/manifest.json, which maps each file path to this environment's id.Server-managed fields (
_sourceId, template and connector lineage,lastExecutedAt, …) are removed. A storage stub keeps only its name and MIME type.Every reference — declared paths, hook
_scriptIdand_stackIdslots at any depth, and other_xxxIdfields — is rewritten from a raw id to the target's tree path, the same placeholder form push accepts for new files:"_exportId": "exports/Get Orders/export.json". A script reference points at the.js.Links omit the target
_idin.link.jsonpointer files; the path identifies the target.
Two environments that hold the same configuration therefore write byte-identical files.
The binding layer
Connections, iClients, stacks, and on-premise agents are legitimately different in each environment — different hosts, different credentials — and their secrets never travel anyway. In a neutral tree their documents are not tracked as files at all. A reference to one becomes a stable binding key derived from the resource's name:
"_connectionId": "@bindings/connection/shopify"The key → id resolution lives in .celigo/bindings.json — untracked, per environment, human-readable, and auto-populated by every pull from the account's actual resources:
{
"note": "Per-environment bindings for this neutral tree …",
"bindings": {
"connection/shopify": { "id": "6a1b2c3d4e5f6a7b8c9d0e1f", "name": "Shopify" }
}
}Each environment's copy maps the same keys to its own ids, which is the whole trick: the tracked reference is identical on every branch. celigo resolve @bindings/connection/shopify prints the current environment's id and name.
Pins. Edit an id by hand to point a key at a different resource — for example when production's connection is named Shopify (Prod):
A pin is durable: re-pulls preserve it (only entries whose id no longer exists refresh from the account), and re-renders keep writing the pinned key rather than the resource's own name-derived one, so the tracked files keep the shared symbol. The durable organizational fix is same-named connections in every environment, which makes pins unnecessary.
Collisions. Two resources whose names slugify to the same key collide; the second gets an id-suffixed key (…--bbf193) and the pull warns, because an id-suffixed key is environment-specific. Rename one resource so the key derives cleanly.
What stays environment-specific
Ids embedded inside URI strings and ids hardcoded in script source are not rewritten — mangling a URI or rewriting code is worse than honesty. The pull reports each one as env-specific residue (envResidue in --format json) so a cross-environment merge knows what still needs review. Prefer Handlebars placeholders or script parameters over embedded ids where you can. Server-populated content that legitimately differs per environment — the http._httpConnector* metadata a connector-backed connection attaches, for instance — stays in the files too; restore your side's version after a merge that brought the other environment's (git checkout ORIG_HEAD -- <files>).
The mode is decided once
--neutral is valid only on a tree's first pull (or with --force on a manifest that tracks nothing). It is recorded in .celigo/manifest.json and permanent: every later pull, status, diff, push, and lint honors it with no flag, and a standard tree can never be converted in place — re-pull into a fresh directory. The pull also writes a .celigo/.gitignore of *, keeping the manifest and the bindings — both per environment by definition — out of the repository. This is the opposite of a standard tree, whose manifest is committed.
A neutral pull fetches the whole account even when scoped: the symbol table needs every path and binding name. On a large account budget tens of seconds per pull.
Push and diff in a neutral tree
Path references and @bindings/ keys resolve to this environment's ids just before the wire; _id for an update comes from the manifest, matched by path. A push that meets a binding with no id in this environment refuses that item, naming the key and .celigo/bindings.json as the fix — or run celigo pull to auto-populate:
Creating a connection, iClient, stack, or on-premise agent from a file is refused: create it with its own command (celigo connections create), then pull to record its binding. Write-backs after a create render the neutral form, so a freshly pushed file is byte-identical to what the next pull would write; diff neutralizes the server copy before comparing, so only genuine content differences appear. An update whose pasted document carries a standard _id is still refused when that _id is not the tracked resource's.
One branch per environment
Each environment lives on a permanent branch, pulled from its own account with its own profile, in its own git worktree — a worktree keeps its own untracked .celigo/ (manifest and bindings for that account), which is why worktrees beat git switch here.
Use --no-symlinks for git-tracked trees; the style is sticky.
Feature branches are cut from the environment branch of the account you develop against, in a third worktree:
git worktree add -b feat/thing ../wt-feature env/sandbox. No.celigo/is needed until you push.PR checks. From a worktree that has a
.celigo/for the target account, check out the candidate content and run the two read-only gates:celigo lintandceligo push --dry-run.Deploy = merge + push + a post-deploy commit. Merge the feature into the environment branch,
celigo push, then commit again: a push that creates resources normalizes the authored files (server defaults added, quick forms folded), and the next pull adds link files for new references. After that commit, re-pulls report everything unchanged. Branch protection that requires a pull request before merging refuses this commit; run the deploy from CI or from a bot account exempted from the protection.Promote with a merge commit.
git merge --no-ff env/sandboxfrom the prod worktree, so every promotion is a recorded merge even when git could fast-forward, thenceligo pushfrom the prod worktree resolves the same tracked symbols against prod's.celigo/. Because the tracked files are identical wherever the configuration is identical, the merge carries only real edits — a flow enabled, a mapping changed — never id churn. Cherry-picking a single fix across environments works the same way.The first promotion PR looks bigger than the merge. GitHub's "Files changed" view is a three-dot diff against the merge base, which on the first
env/sandbox→env/prodPR can be the shared root commit, so files prod already carries byte-identically are displayed as new. Git's own diff is the authoritative one; every later promotion PR has a recent merge base.Cross-environment connections. If prod has no connection named like the binding key, the push refuses the item naming the key. Pin it in the prod worktree's
.celigo/bindings.json(above).
Neutral trees are not promotable with celigo promote in this version; the branch workflow above is their multi-environment story.
Related
Pull a tree —
--neutral,--no-symlinks,--force.Push changes — path placeholders and creates.
Promote to another account — the standard-tree alternative.
Teams, git, and CI — the rules a standard tree follows instead.
Last updated
Was this helpful?