Status: decided 2026-10-20; built (DirXMLDev 0.49.0).
This note is about one tree deployed to every environment, with what differs kept in overrides. The other way, a tree per environment with changes promoted between them, is promotion.md; DirXMLDevWeb's git-workflow.md compares the two.
The usual case is not a clean start. A customer has a production tree and one or more others (QA, test, dev), each changed by hand over the years: drivers added in one and not the other, the same driver under two names, hot fixes made in production and never carried back, work in QA never promoted, host names typed into policies. The tool has to take that on without asking anyone to clean it up first, and it has to help decide what genuinely needs resolving.
A customer's QA and production driver sets, read from two LDIF exports (2026-10-20):
| Production | QA | |
|---|---|---|
| objects in the driver set | 1,143 | 1,542 |
| drivers | 41 | 43 |
| servers | 4 | 2 |
| driver set DN | …,ou=SERVICES,o=… |
…,ou=services,o=… |
Compared driver by driver (QA against production, production as the tree):
- Drivers in one only: 7 in production only, 9 in QA only. Among them one is plainly the same driver renamed (20 of 20 policies in common, names one character apart); one production driver has four QA counterparts (one QA certificate-revocation driver per CA where production has one); and two drivers look alike because each pushes to the environment below (production→QA, QA→dev), which only a person can call the same driver or not.
- Values: 81 scalar values differ: 69 shim parameters, 5 engine control values, 4 driver GCVs, 3 driver-set GCVs. These are what overrides are for.
- Everything else: 879 differences. 52 differ only in letter case (
OU=servicesagainstOU=SERVICES; eDirectory reads them as the same) and are set aside. The other 827:
| count | what it suggests | |
|---|---|---|
| only in QA | 402 | QA-only objects; 350 are role-based entitlement policies |
| only in production | 57 | production has them, QA never got them |
| lines only production has | 37 | a change made in production, not carried back |
| lines only QA has | 26 | work in QA not yet promoted |
| package versions differ | 13 | the newer side is known |
| cosmetic (descriptions, comments, display names) | 6 | align freely |
| environment text inside content | 7 | a changed line names an environment or server: if it is a literal in a policy, it belongs in a GCV |
| both sides changed | 268 | needs a person; mostly User Application objects and PRDs |
Two more things the data shows. Jobs differ by schedule (production runs them on a schedule, QA doesn't), which an override can't carry yet. And jobs, the driver set's server list and per-server settings name servers by DN. Tree names and server names are globally unique, so two environments never share a server: every environment but the base needs its servers mapped, here four production servers onto two in QA.
One environment at a time, the production tree first.
- Base. Import production into
main.mainis what production runs; everything any other environment has beyond it shows up as a change to look at, not as something already on its way to production. (built) - Connect the next environment under Connections, and add it to the project.
- Set up that environment: a guided read (Vault → Environments → Set up QA…, proposed) that
proposes, for review, everything it needs. Nothing is written until you apply; you commit the
result.
- Match drivers. Same name: matched. Otherwise a likely match by shim class, name and policies
in common, which you confirm (a
drivers/<d>.nameoverride) or reject. Proposals are one-to-one; one production driver is never matched to two. (matching proposed; the name override is built) - Membership. A driver only that environment has is marked as its own
(
environments="QA"indriver.xml); a tree driver that environment lacks is marked as not deployed there. (built: the marks, compare, deploy and update follow them) - Values. Each differing value becomes that environment's override, ticked by default (the tree holds production's value). (built: recorded one at a time from the Environments tab; bulk proposed)
- Servers. Map the tree's servers to that environment's (several to one allowed), so jobs and per-server settings line up. Required for every environment but the base, since no two environments share a server. (proposed: see "Servers")
- What's left: analyse, then decide (next section).
- Match drivers. Same name: matched. Otherwise a likely match by shim class, name and policies
in common, which you confirm (a
- Compare again. What remains is what you decided to leave, and from then on only new differences show.
Each remaining difference gets a likely cause from the comparison itself, as in the table above, and the page groups them by driver with counts, cause first, so that 827 differences read as a short list of drivers with a few kinds of problem each. For each you choose:
- Production is right: leave the tree as it is; a deploy to QA (when you choose to) brings QA in line. Suggested for only in production, lines only production has and an older package in QA.
- QA is ahead: take QA's version into a branch (Update this branch from QA…, ticking those files), to promote through the normal flow. Suggested for lines only QA has and a newer package in QA.
- Needs fixing: both sides wrong or a design problem, such as a host name typed into a policy. Opens the file; the suggestion names the fix (move the literal into a GCV, then override it).
- Different on purpose: this environment's content genuinely differs and no value can carry it. Recorded as known (D1).
- Decide later: recorded as known, with who and when, so it stops hiding new differences but stays listed.
What's known about a driver helps decide. When the repository holds notes on a driver (a summary,
known defects; for example docs/drivers/<driver>.md from an earlier analysis), the setup shows them
beside that driver's differences. In the customer data above, production carries an extra rule that
vetoes every user event on one driver's subscriber channel, and QA doesn't. Whether that is a stopgap
for a defect the earlier analysis found is the kind of question only such context can help answer.
Resolving is then ordinary work on branches: each decision either leaves the tree, changes it on a branch, or records the difference as known.
An item that only some environments get may be waiting (built in QA, not yet promoted to production) or permanent (a test loopback, a feed that only exists in QA). The tree says which, beside the list:
<driver name="Slack Driver" environments="QA" planned="PROD"> <!-- waiting for promotion to PROD -->
<driver name="Test Loopback" environments="QA"> <!-- QA's own, for good -->The same two attributes go on jobs, role-based entitlement policies and notification templates. A
compare against production lists the planned ones as waiting for promotion and leaves the permanent ones
quiet; promoting moves the environment from planned to environments, one reviewed change; a plan for
production names what is planned for it and not promoted yet.
Keeping a lower environment's data current from production is common: an eDirectory-to-eDirectory link whose sending end is a driver in production (for example "VaultProdToVaultQA") and whose receiving end is a driver in QA ("VaultQAToVaultProd"); QA may feed dev the same way ("VaultQAToVaultDev"). Each end is its own driver. Two ways to keep them:
- Each end its environment's, for good (
environments="PROD",environments="QA"): the simple default, and right when the ends differ in more than where they point. - One tree driver per role, named per environment: "send to the environment below" is the same
configuration in production (VaultProdToVaultQA) and QA (VaultQAToVaultDev), aimed at a different tree.
One tree driver with
drivers/<d>.nameper environment and the remote tree's address as each environment's override keeps a fix to one end in step with the other.
Names that carry environment names, and a remote address that is another environment of the project, are how the setup recognises a feed; it proposes, and a person decides. Keep a feed's remote address an override in every environment that gets the driver, so a deploy never points one environment's feed at another's target. (A validation check for this is a possible follow-up; the setup flags feeds today.)
A file per environment beside the overrides, overrides/<env>.known.json, listing differences
accepted as they are: the object's path, a fingerprint of both sides, the decision, who and when. A
compare shows them collapsed under "known" and shows anything whose fingerprint changed as new. A
deploy to that environment still writes what the tree holds; the plan says which of its changes
overwrite a known difference, so nobody flattens one by accident.
Server names are globally unique, so the server DNs in the tree (the driver set's server list, each
driver's servers/<server>/ settings, jobs' servers) are the base environment's. Every other
environment maps them in its overrides file, one line per tree server. The key names the tree's server by
its name (the servers/<name>/ folder's), since the file splits a line at its first =; the value is the
environment's server's DN:
# overrides/QA.properties
servers.vault01 = cn=vault01qa,ou=servers,o=acme
servers.vault02 = cn=vault01qa,ou=servers,o=acme
servers.vault03 = cn=vault02qa,ou=servers,o=acme
servers.vault04 = cn=vault02qa,ou=servers,o=acme
The tree's servers act as roles. Compare, plan and deploy against QA read and write each role on the
server it maps to; an import from QA maps the other way. Two roles mapped to one server must agree
on that server's settings, or the setup flags the conflict. A server of QA that no role maps to is
reported, as is a role left unmapped (validation: server-unmapped). The guided setup proposes the
map by name and by which drivers run where, for you to confirm.
- D1 — Known differences: recorded per environment in
overrides/<env>.known.json, as above. - D2 — Driver-set objects get an environments list like drivers: role-based entitlement policies, notification templates and driver-set jobs. Compare and deploy leave them out of other environments; an update from an environment marks the ones new to the tree as its own.
- D3 — Servers: required, mapped in each environment's overrides file (
servers.<tree server's name> = <env server's DN>). Fewer servers outside production is the normal case, so several tree servers mapping to one is the rule. A driver whose settings differ between two tree servers that map to the same server is flagged, and the person picks which that server takes (servers.<env server's name>.prefer = <tree server's name>). - D4 — Job schedules, enabled state and trace are override keys:
drivers/<d>.job.<job>.schedule(empty = no schedule),.disabled,.trace-level,.trace-file,.trace-size-limit, anddriverset.job.<job>.…for the set's jobs. The comparison lists them as values. In the customer data, two production jobs run on a schedule QA's don't, and job trace levels and files differ. - D6 — A new driver's server, chosen at deploy: promoting a driver to an environment with several
servers asks which one runs it (default: the one the deploy connects through). That server gets the
driver's start option, manual, and every other server of the set gets disabled
(
vault.deploy --driver-server "D=<server>"; Runs on in the plan). A deploy choice, not a tree value: it matters only when the driver is created there. The step waits for the driver to replicate to that server first. - D5 — A guided Set up … flow proposing 3.1–3.5 together, reviewed before anything is written, then committed.