# Why one IOC instance touches eight repositories Converting a single IOC is fast. Getting the result into a state where it can actually run is not, and the difference is worth understanding before you plan a beamline conversion. This page uses one real conversion — `BL15I-VA-IOC-02`, August 2026 — as a worked example. The specifics will date; the shape of the chain is the point. ```{raw} html

The same material as a one-page chart

One IOC, eight repos maps the dependency graph, the fifteen forced steps and the friction inventory visually, on a single page. It is the better starting point if you are presenting this or skimming it; this page carries the analysis and what it suggests we change.

``` ## The short version The conversion itself took seconds and produced correct output: 338 XMLbuilder elements became 335 ibek entities, with every entity type round-tripping. Fifteen defects then had to be fixed across seven support modules in three repositories, two of those modules turned out to be absent from the generic IOC image altogether, two releases had to be cut, and the chain stopped three times waiting for a human to merge a request. The last of those defects was the expensive one. The IOC generated cleanly, passed validation and passed CI while every vacuum space in it linked to a PV that was never created — 1360 records that should not have existed. It was found by comparing the expanded database against XMLbuilder's own output, and by nothing else. None of that work was avoidable and none of it was wasted — every fix benefits every IOC that uses the same module. But almost all of it was *discovered* by running the conversion, rather than known in advance. The end state is worth recording too: 12,025 records in the original and 12,025 in the generated IOC, none missing, none extra, and 29 records differing by a field for reasons that are understood. ## The dependency graph Support metadata sits three tiers below the thing you are trying to build, and below that again sits a tier nobody in this workflow owns. **Tier 0 — the module sources.** `digitelSpc`, `mks937b`, `vacuumSpace` and their siblings are DLS support modules written for the DLS production build. They are not forked: each is patched declaratively during the image build through its `*.install.yml`, which is the pattern to keep — a fork would have to be maintained forever, whereas a two-line `comment_out` or `patch_blocks` entry is legible and rebases itself onto the next module release. **Tier 1 — support metadata.** `ibek-support` (public, on GitHub) holds community entity models. `ibek-support-dls` (on Diamond's GitLab) holds the DLS-specific ones. `ibek-runtime-support` (GitHub) holds vendored runtime patterns. A given module lives in exactly one of them, and which one is not always obvious. **Tier 2 — consumers.** `builder2ibek` pins the first two as submodules, and so does each generic IOC repo — `ioc-dlslinuxvac` in this example. Generic IOC repos additionally track `ioc-template` through copier. **Tier 3 — instances.** The beamline services repos, `i15-services` here, which hold the `ioc.yaml` you set out to write. A fix in tier 1 has to be committed, merged, pinned, and released before tier 3 can use it. That climb is the cost. ## Why the sequence is forced The steps below cannot be reordered, and several cannot be started until the previous one has *landed* rather than merely been written. 1. **Convert.** `xml2yaml` produces `ioc.yaml`. 2. **Generate, and find out what is missing.** `ibek runtime generate2` is the only mechanism that reports gaps in entity models. In this case: an entity model absent entirely (`dlsPLC.NX102_femto`), three macros missing from another, a required parameter that almost no IOC supplies, a module with only one of its eight record types modelled (`ether_ip`), and a model demanding four inputs it should have derived (`mks937b.mks937bGaugeEGU`). 3. **Fix, in whichever repo owns the module.** Two of those modules were in the public repo and two in the DLS repo — identical work, split by ownership, through two different review processes on two different forges. 4. **Deal with whatever the fix invalidates.** A `vacuumSpace` defect surfaced here that was passing XMLbuilder short-names such as `GAUGE1` into database macros instead of device PV names, so every gauge, img and pirg link in every vacuum space group pointed at a PV that could never connect. It had also silently affected an IOC converted earlier the same day and reported clean, and it changed five committed CI samples. 5. **Bump the pins in `builder2ibek`,** then refresh the vendored copy of `ibek-support-dls` that CI uses (GitHub Actions cannot reach DLS GitLab), then regenerate the samples. The order is forced: the refresh tool refuses to run unless the pin is already committed, and the guard tests read `git ls-tree HEAD`, so staging is not enough. See [](../how-to/contribute.md) and the testing notes under `.claude/skills/shared/testing-and-ci.md`. 6. **Bump the same pins in the generic IOC repo and release it.** Only then can an instance's `values.yaml` point at an image containing the fix. The two consumers must agree: `builder2ibek` converts the XML and `ioc-dlslinuxvac` builds the container that runs the result, so if their pins drift the IOC is converted against one version of the entity models and executed against another. The symptom is a generate failure in the cluster, not a diff anyone reviewed. 7. **Add whatever support modules the image is missing.** The generated IOC referenced `mks937b` and `digitelSpc`; neither was in the Dockerfile. The module list is per-image and hand-maintained, and nothing reconciles it against what the IOCs using that image actually reference. 8. **Fix the module builds.** Adding those two exposed four defects in the upstream sources — a dbd ordering that only works when StreamDevice is built without calc, a script the disabled `etc/makeIocs` build was supposed to generate, and two modules still asking for a vxWorks cross-compiler. All four were fixed in `*.install.yml`, none by forking. See [](../how-to/create-generic-ioc-repo.md). 9. **Expand the database and compare it.** `builder2ibek db-compare` against the original `_expanded.db` is the only check that catches a converted IOC which is structurally wrong rather than invalid. Here it found 1360 extra records: the `vacuumSpace` `space` and `space_b` models instantiated all five group templates unconditionally, where XMLbuilder counts the devices of each type and creates a group, a dummy, or nothing at all. Fixing that meant rewriting the converter and reconverting the instance — which is cheap precisely because the instance is pure converter output. See [](../how-to/verify-with-devcontainer.md). Steps 3, 5, 6 and 8 above each end at a push to a protected branch. Three of them are GitLab repositories, and those are where the chain stops and waits for another person to merge; step 5 is a pull request on `builder2ibek`, which is on GitHub and which the author can merge themselves. What keeps this tractable is that none of it has to be published to be tried. The generic IOC's devcontainer builds the developer image locally with every support module compiled and the `ibek-support` repos as submodules inside it, so entity-model edits, module build fixes, conversion, `generate2` and `db-compare` can all be exercised in one shell against a real EPICS build. Publishing an image and driving CI in two repositories is the last step, not a step per iteration. ## The traps that do not announce themselves Four classes of problem here fail silently, and they are the ones worth designing against. **Output that is wrong without being invalid.** The `vacuumSpace` space expansion is the important example: the entity models produced a database that was structurally valid, generated without a warning and passed CI, in which every space linked at a `:GAUGEG` / `:IMGG` / `:IONPG` / `:PIRGG` / `:VALVEG` prefix that no template had created. An IOC in that state starts and is dead — every space PV points at something that does not exist. Validation cannot catch this, because nothing in the entity model is malformed; only a record-by-record comparison against XMLbuilder's own output can. Treat `db-compare` as part of converting an IOC, not as an optional extra. **Gaps that only exist until someone needs them.** There is no inventory of which templates have entity models. In this example a sweep afterwards found fifteen `dlsPLC` templates with no model at all — not broken, just absent, waiting for whichever IOC needs one first. Every new IOC is therefore a discovery exercise of unpredictable length. **Changes that build green and fail in the cluster.** The generic IOC image was two `ioc-template` versions behind, and the newer template carries the only two things that make a vendored runtime pattern work: `ibek runtime place-files` in `start.sh`, and the instance config folder on msi's include path. Without both, the image builds cleanly and the IOC dies at container start. Nothing reported the drift; it surfaced from a side question about a version pin. The same shape applies to an `ibek` requirement below the floor `start.sh` needs. See [](../how-to/runtime-support.md). **Upstream changes not validated against converted IOCs.** A four-line addition to the `positioner` entity model added a required parameter that nothing consumes, no template accepts, and no builder XML can supply. It broke six conversion samples immediately. Support-model repos are shared with hand-written IOCs, whose authors can simply add the new parameter; converted IOCs cannot. ## What this suggests The conversion is not the bottleneck and does not need attention. The chain around it does, and the candidates in rough order of how much of the chain they would shorten: - **Make `db-compare` routine.** It is the only check that catches a conversion which is wrong rather than invalid, and the one defect it caught here would have reached the beamline without it. It needs no more than the generic IOC's devcontainer and the original `_expanded.db`. - **Make entity-model coverage knowable in advance.** A sweep comparing a module's `db/*.template` files against its entity models would turn every per-IOC discovery into a one-off per-module task. The data is already mechanically available. - **Reconcile the image's module list with the IOCs that use it.** The two missing modules here were discoverable mechanically: every entity in an `ioc.yaml` names the module it needs, and the Dockerfile names the modules the image builds. Nothing compares the two. - **Shorten the distance from a tier-1 fix to a usable image.** Four of the chart's fifteen steps exist only to propagate a pin. - **Fail at build time, not boot time.** A check that the image's `ibek` and `start.sh` can actually serve the patterns an instance vendors would have caught the template drift immediately. - **Reduce the number of copies of support metadata.** There are three: the public repo, the DLS repo, and a vendored copy so CI can run without DLS network access. A guard exists specifically to catch them drifting. - **Validate support-model changes against the conversion samples,** so that a change which no converted IOC can satisfy is caught where it is made. Three of the chart's fifteen steps stop for a human purely because `main` is protected on the GitLab repositories. That is a permissions question rather than a design one, but it is the only part of the chain that no amount of tooling can cross. ## Related - [](../how-to/convert-ioc-instance.md) — the conversion workflow itself - [](../how-to/verify-with-devcontainer.md) — `db-compare` against the original expanded database, the check this page argues should be routine - [](../how-to/runtime-support.md) — vendoring templates instead of building them - [](../how-to/create-generic-ioc-repo.md) — the generic IOC and its submodule pins - [](../tutorials/create-support-yaml.md) — authoring the entity models this page describes as frequently missing