How LabVIEW CI works
A complete guide to continuous integration for LabVIEW — written for working LabVIEW developers, not software engineers. The early sections explain what CI is and the handful of concepts it rests on, in plain language and with diagrams. Later sections go all the way down to the implementation, so you can evaluate, operate, customise, or extend the stack. You do not need prior experience with Git, Docker, or GitHub Actions to read this — everything is introduced from first principles.
Living document — kept in lock-step with the tooling. When a capability is added or a behaviour changes, this page changes with it.
Start here
1. Why continuous integration? #
Picture the way most LabVIEW code is checked today. You finish a feature, run it on your own machine, click through a few cases, and commit. A week later a teammate opens the project and a VI is broken — a subVI moved, an add-on was missing, or a change two branches over quietly broke something you never thought to re-test. Nobody did anything wrong; there was simply no automatic, repeatable check that the whole project still loads, compiles, passes its tests, and meets your standards every time the code changes.
Continuous integration (CI) is that automatic check. Think of it as a tireless robot teammate. Every time anyone saves code to the shared repository, the robot quietly opens the entire project on a clean, identical machine and runs the work you would otherwise do by hand:
- compiles every VI and control and reports anything broken or any missing dependency;
- runs your unit tests and collects the pass/fail results;
- checks code against NI's VI Analyzer rules for correctness, performance, and style;
- produces a visual, side-by-side diff of every VI that changed;
- renders a browsable picture of every VI and generates project documentation;
- and publishes all of it to a single web dashboard that anyone on the team can read.
It does this without you remembering to, without tying up your machine, and in exactly the same way every time — so a result you can trust replaces "well, it worked when I tried it."
Why this has always been hard for LabVIEW
CI is routine for text-based languages, but LabVIEW resists the usual recipes. LabVIEW needs a licensed graphical IDE, usually a specific year version, and often a set of add-on packages (VIPM) just to open a project — and it traditionally wants a desktop with a display. A typical cloud build machine has none of that. LabVIEW CI removes every one of those obstacles by running LabVIEW headless (no visible UI) inside a container (a pre-built, self-contained machine image with LabVIEW and your add-ons already installed). Sections 2 and 13–15 explain exactly how.
Is it for you?
CI pays off as soon as more than one person, more than one branch, or more than one release is involved — and it is genuinely useful even for a solo developer who wants a dependable record of project health over time. The setup is largely automated (see §6), and on public repositories the compute is free. If your project lives on GitHub (or you are willing to put it there), you can be running in a few minutes.
2. Key concepts in plain language #
LabVIEW CI is built on a small number of GitHub building blocks. You can use the tooling without mastering any of them, but a one-paragraph mental model of each makes everything that follows click into place. Skip ahead if these are already familiar.
- Repository (“repo”)
- The home of one project — your VIs, controls,
.lvproj, and its complete history. On GitHub it lives atgithub.com/<you>/<project>. Analogy: a project folder that also remembers every version of itself. - Git
- The version-control system underneath GitHub. It records each change so you can review history, work in parallel, and never lose anything. Analogy: an infinite, reliable Undo history shared with your whole team.
- Commit
- One saved snapshot of the project with a short message. Committing is the LabVIEW-equivalent of “save a labelled checkpoint of the whole project.”
- Branch · push · pull request (PR)
- A branch is a parallel line of work;
mainis the trunk everyone shares. You push to upload your commits to GitHub. A pull request proposes merging your branch intomain; it is where CI checks run and teammates review before the change lands. - GitHub Actions · workflow · job
- GitHub's built-in automation. A workflow is a recipe (a
.ymlfile) that runs one or more jobs when something happens — a push, a pull request, a schedule, or a button press. LabVIEW CI is, at its core, a set of these recipes. - Runner
- The computer that actually executes a job. GitHub lends you clean, disposable Windows and Linux machines (“GitHub-hosted runners”); you can also register your own (“self-hosted”) — see §7. Analogy: a fresh rental laptop that is wiped after every job.
- Container · image · Docker
- A container is a lightweight, throwaway virtual computer. Its blueprint is an image. We bake LabVIEW and your add-ons into an image once; each CI job boots a fresh container from it, does its work, and is deleted. Docker is the technology that runs them. Analogy: the image is a saved disk; a container is booting a brand-new copy of that disk that you discard when done.
- GHCR (GitHub Container Registry)
- Where your built images are stored, attached to your own repository as packages.
- GitHub Pages
- Free static-website hosting served straight from a repository. LabVIEW CI publishes your dashboard
and every report here, at
https://<you>.github.io/<project>/. - Commit status
- A small pass / fail / pending marker GitHub attaches to a commit. Each CI activity posts one; the dashboard reads them to fill in its grid — this is how a result travels from a workflow to a cell.
- Headless LabVIEW
- LabVIEW running with no visible front panel, driven from the command line / VI Server, so it can run unattended on a server inside a container.
- CI / CD
- Continuous Integration = automatically build and test every change. Continuous Delivery = automatically publish the results and artefacts. This stack does both: it tests every commit and delivers a live web report.
Here is how those pieces connect for a single change:
Everything in this stack is just an elaboration of that one line. The rest of the document fills in what runs, when, and why each choice was made.
3. What LabVIEW CI is #
LabVIEW CI is a portable, repository-agnostic CI/CD pipeline for LabVIEW projects. It runs LabVIEW headless inside Docker containers on GitHub Actions runners, performs a set of code-quality activities on every commit, and publishes the results as a static GitHub Pages site with a unified dashboard. Nothing runs on the tooling author's infrastructure or on NI's — every container executes inside your GitHub Actions environment, under your account's minutes and limits.
Four design principles shape almost every decision in the codebase. Knowing them up front explains a lot of the “why” in later sections:
- Catalog-driven. A single
catalog.jsonis the source of truth for which capabilities exist, which files they need, what they depend on, and how they are branded. The configurator UI and the installer both read it, so adding a capability is a one-entry change rather than edits scattered across UI, installer, and dashboard code. Why it matters: the system stays small and consistent as it grows. - Reproducible isolation. Every action, on every revision, on every platform, gets a
fresh container (
docker run --rm). Containers are never reused across actions or revisions. The only intentional exception is the snapshot renderer, which keeps one container warm within a single job and deduplicates work by content hash. Why it matters: a result is never contaminated by a previous run. - Logic travels with workflows; heavy artefacts are built once. The multi-gigabyte worker images are built once and pulled on demand; the scripts that drive them are small and ship inside the workflows/actions, so consumers need no local copy. Why it matters: improvements reach every repo without a re-download of LabVIEW.
- Derive, don't rewrite. Functional wiring (image name, LabVIEW version, Pages URL) resolves at runtime from GitHub context and Actions variables with safe fallbacks. The installer only rewrites cosmetic branding strings, never functional configuration — which keeps installs robust and upgrades trivial.
4. The big picture #
A push (or pull request) triggers the reusable workflow, which gates a set of per-capability container
jobs on your configuration. Each job pulls a worker image, runs LabVIEW headless inside a throwaway
container, writes a report, deploys it to the gh-pages branch, and posts a commit status. A
separate dashboard workflow reacts to those statuses and rebuilds the Pages dashboard. Read the diagram
top to bottom — it is the same journey as §2, with the real file and job names.
Three deploy destinations live side by side on the gh-pages branch (all written with
keep_files: true so they never clobber each other):
- The dashboard + tooling pages at the Pages root (
index.html,vi-snapshots/,configure.html,whats-new.html, this page, etc.). - Per-revision reports under content paths keyed by commit SHA
(
masscompile/<sha>/,vi-analyzer/<sha>/,vidiff/<segment>/). - Content-addressed snapshots under
vi-snapshots/by-blob/<ab>/<blob>.html.
Where things actually run Nothing in this picture runs on the tooling author's servers or NI's. The runner is GitHub's (or yours, if self-hosted); the images live in your registry; the website is your GitHub Pages. You own and can audit every part.
Using it
5. The activities & when to use each #
LabVIEW CI is a menu of independent activities (also called capabilities). You enable the ones that fit your project; each runs on its own and writes its own report. Here is the menu, what each one is for, and when it earns its keep. The deep mechanics of each are in §16.
| Capability | What it does | Platforms |
|---|---|---|
| Mass Compile | Compiles every VI/CTL and flags broken VIs and missing dependencies. | Win Linux |
| VI Analyzer | Runs NI's static-analysis test suite for correctness, performance, style and documentation. | Win Linux |
| VIDiff | Generates visual, side-by-side front-panel + block-diagram diffs of changed VIs between commits. | Win Linux |
| Unit Tests | Runs Caraya / LUnit / VI Tester / NI Unit Test Framework headlessly and merges JUnit output into one report. | Win |
| Antidoc | Generates project documentation from the VI hierarchy using Wovalab's Antidoc CLI. | Win |
| VI Snapshots / Browser | Renders every VI to a content-addressed HTML snapshot gallery (the classic VI Browser). | Win |
| VI Snapshots 2.0 | Position-aware, in-place VI Browser frames (JSON) rendered by the cross-platform toimages engine. | Win Linux |
| Dashboard | Aggregates everything above into a single status page on GitHub Pages. | runner host |
Which to turn on, and when
- Start with Mass Compile + the Dashboard. Mass Compile is the single highest-value check: it proves the whole project still loads and compiles with all dependencies present. If you enable nothing else, enable this. The Dashboard is required to see results in one place.
- Add VIDiff once more than one person touches the code. Graphical diffs are hard to do by eye in a normal pull request; VIDiff makes a block-diagram change reviewable like a text diff.
- Add Unit Tests if you have (or want) automated tests. It runs Caraya, VI Tester, and the NI Unit Test Framework and gives you a single pass/fail trend you can gate merges on.
- Add VI Analyzer to enforce standards. Ideal for teams adopting a style guide or
chasing performance/correctness issues; you author the rule set (
.viancfg) in the desktop VI Analyzer and commit it. - Add VI Snapshots / Browser for visibility and reviews. A browsable picture of every VI at every revision is invaluable for code review, onboarding, and explaining changes to non-developers.
- Add Antidoc when you need shipped documentation. It is heavier, so it runs on pushes to the default branch rather than on every pull request.
6. Installing LabVIEW CI #
There are two ways in, and you do not need a command line for either. The fastest is the
browser-based Apply to New Repo wizard (a button on the live site); the scriptable
alternative is the install.py command-line installer. Both read the same catalog and produce
the same result.
What you need first
- A GitHub repository for your LabVIEW project (it can already have history).
- Permission to add files and workflows to it, and to enable GitHub Pages.
- For the web installer, a fine-grained access token with Contents, Pull requests, and Workflows set to read/write — the wizard links you to the exact token-creation page and checks the token before doing any work, so an under-scoped token fails fast instead of half-way through.
- Public repository recommended to start: GitHub Actions minutes and GitHub Pages are free on public repos. Private repos work too, but Pages on a private repo requires a paid GitHub plan — see §7.
The installer in three layers
| Layer | File(s) | Role |
|---|---|---|
| Entry point | install.sh / install.ps1 | Fetch the tooling from GitHub, locate Python, forward to install.py. |
| Brain | install.py (stdlib only) | Resolve the file set from the catalog, apply text substitutions, write the manifest. |
| Result | the target repo | .github/workflows/*.yml, .github/labview/*, and the .github/labview-ci.yml manifest. |
The browser-based Apply to New Repo flow also records each started install in this browser so you can
leave and return. On return it reconciles the saved record against GitHub and clears anything that is no
longer in flight: an open install pull request stays as “pull request open”, a merged one that
is still building shows “publishing dashboard”, and a record clears the moment its dashboard is
live. A record with a saved tooling version waits for the target dashboard's published catalog.json
to reach that version; because the dashboard a repo runs can publish a different version line than the
installer page is on, a record whose dashboard has simply been live for a while past the install clears
anyway, so a finished install never lingers as “publishing dashboard”. Publishing records are
rechecked from the public dashboard URL even if the install token has since been forgotten.
Before starting a new install, the flow also checks whether one is genuinely already in flight, and treats
live GitHub state as authoritative: it warns only when there is an open install pull request, or the
ci/install-labview-ci branch exists and its pull request has not yet merged or closed.
GitHub does not delete a head branch on merge by default, so a finished, merged install leaves that branch
behind — it is treated as harmless debris (the next install fast-forwards it), not as an install still in
progress. A local record only blocks a re-install while its dashboard is genuinely still publishing (verified
against the live catalog.json); any other stale record that GitHub contradicts is forgotten so it
stops nagging on future installs.
What gets written into your repo
- Text files (
.yml .ps1 .sh .py .html .md .json .xml .viancfg) have the catalog'ssubstitutionsapplied — purely cosmetic rebranding (e.g. the source slug and the Pages host) so links point at your repo. - Binary files (
.viand friends) are copied byte-for-byte. - The installer's own files under
.github/labview-ci/are protected from substitution so they keep pointing at the source repo for later re-runs and upgrades.
The per-repo manifest — .github/labview-ci.yml
This one file is the control panel for your pipeline. Everything the Configure and Dependencies dialogs change ends up here, and every workflow reads it at run time.
schemaVersion: 1
installedVersion: 3.24.5
source:
repo: elijah286/LabVIEW-CI-with-Containers
ref: v3.24.5 # pinned to an IMMUTABLE tag, never "main"
config:
labviewVersion: "2026"
os: [windows, linux]
container:
use: latest # tag every action uses by default
vipc:
- { path: Dependencies.vipc }
actions: # per-action image overrides
masscompile: base # bare NI image
vidiff: "2026" # pinned tag
activities: [dashboard, masscompile, vi-analyzer, vidiff]
Because source.ref is pinned to an immutable version tag, your pipeline
never changes unless you explicitly update (see §18). That is a
deliberate safety property: a new release of the tooling cannot silently alter how your project is checked.
Thin vs. fat installs
A thin install references the reusable workflow at a moving major tag
(uses: …/labview-ci.reusable.yml@v3) and inherits improvements automatically. A
fat install copies all workflow files locally and updates them deliberately via
install.py --update. Both are valid; thin is the lowest-maintenance path and the right
default for most teams.
After it installs
The wizard enables GitHub Pages, opens (and can auto-merge) the install pull request, and dispatches the first dashboard build so your site goes live within a minute of the merge — no CI run is required to see the dashboard. A fresh repo with existing history shows a Populate the dashboard with your history card so you can backfill results for past revisions on demand.
7. Private, self-hosted & custom setups #
The defaults target public repositories on GitHub-hosted runners because that path is free and zero-maintenance. But the architecture was designed to be adapted. This section covers the common “my situation is different” cases.
Private repositories
Everything works on a private repo with one caveat: GitHub Pages on a private repository requires a paid GitHub plan (Pro, Team, or Enterprise). Without one, every CI activity still runs and every report is still produced and uploaded as a workflow artifact — you simply cannot serve the live dashboard website. Your options are to make the repo public, upgrade the plan, or read reports from the Actions artifacts. The web installer detects a private repo and tells you which applies; if Pages cannot be enabled the install finishes with an honest amber warning rather than a false “done.” Use a fine-grained token scoped to only the target repository (the token-creation link cannot pre-select a single repo, so set Repository access → Only select repositories yourself).
Self-hosted & on-prem runners
The worker container runs on the runner, so the runner is where you decide CI executes. Both
runner labels default to GitHub-hosted but can be pointed at self-hosted machines you
register (your own Windows and Linux boxes, on-prem or in your cloud) via the reusable workflow's
windows-runner input or repository Actions variables. Reasons to self-host:
- Speed. A self-hosted runner keeps a warm Docker layer cache, so the multi-gigabyte worker image is not re-pulled on every job — by far the biggest single time saving.
- Licensing / compliance. Run inside your own network where your LabVIEW licensing and data-handling rules apply.
- Special hardware. If a test needs particular hardware or drivers, run it on a machine that has them.
Custom container registries & base images
By default your worker images are published to your GHCR
(ghcr.io/<owner>/<repo>-labview) and seeded from the shared LCWC base. If your
organisation standardises on a different registry or a hardened LabVIEW base image, the image name and tag
every action uses are derived at runtime from the manifest and Actions variables, not hard-coded
— so you can point actions at a pre-existing internal image by setting config.container.use (or
a per-action override) to a literal image tag, or by overriding the image variable. The dependency-baking
Dockerfiles (§15) are ordinary multi-stage Dockerfiles you can fork to start
FROM your own approved base.
Offline / restricted-network VIPM dependencies
Worker builds install add-on packages from public VIPM/NI indexes by default. In a locked-down network the build cannot reach those. Three supported escape hatches, in order of preference:
- A committed offline VIPC that embeds the package files themselves, so no download is needed at build time.
- Direct package-file URLs to an internal mirror (the resolver-bypass fallback can
install local
.vip/.ogpfiles). - A pre-populated VIPM resolver cache baked into your custom base image.
Private or custom VIPM repositories are not auto-resolved from the public indexes; one of the three above is required. The mechanics are in §15.
Custom LabVIEW versions & add-ons
The LabVIEW year is set once in the manifest (config.labviewVersion) and threaded through the
whole pipeline; the worker base image is built for that year. Your project's add-on packages are declared
in .vipc (and optionally JKI .dragon) files and baked into the worker on demand
from the Dependencies page — that is the supported way to add anything LabVIEW needs that
is not in the base image.
Migrating or rehoming the project
A small relocation pointer (.github/labview-ci/source.json) lets the tooling's source move —
for example to an official NI repository — without breaking existing installs. If it names a different
repo than the current one, the bootstrappers and the update workflow follow it automatically and re-pin a
consumer's manifest to the new home on the next update. See §18.
8. Everyday use & best practices #
Once installed, CI mostly takes care of itself. This section is the operating manual: when it runs, how to work with it day to day, and the handful of habits that get the most value for the least cost.
When does CI run?
The activities trigger automatically on three kinds of event. Knowing them avoids surprise (and surprise runner-minute bills):
Path filters keep CI honest: a commit that only touches documentation or the tooling under
.github/ does not fan out the whole LabVIEW matrix. A dependency rebuild is separate
and opt-in — changing a .vipc flags that dependencies need installing but
does not rebuild the heavy worker image until you ask it to (see §13).
A recommended workflow
The pattern that gets the most out of CI mirrors normal GitHub flow:
- Create a branch for your change.
- Commit and push. CI runs on the branch.
- Open a pull request into
main. The activities post pass/fail checks and VIDiff gives you a reviewable visual diff. - Review the dashboard / checks, fix anything red, and merge once green.
This keeps main always-compiling and gives every change an automatic, reviewable safety net.
Solo developers benefit too: even pushing straight to main, you get a continuous record of
project health and instant notice when something breaks.
Reading the dashboard
Each row is a commit; each column an activity. A coloured cell is a finished result you can click into; a spinning Queued / Running cell is live; a plain run arrow is an empty cell you can fill on demand. The version badge tells you whether newer tooling is available. §9 covers the dashboard in detail.
Managing dependencies
When you add or change the add-on packages your VIs need, declare them in a .vipc (or
.dragon) file and commit it. The dashboard compares your declarations against what is baked
into the current worker and shows a dependencies need to be installed banner if anything is
missing. Applying it from the Dependencies page is a deliberate, one-click worker rebuild —
so a dependency change never silently triggers a slow rebuild in the middle of your day.
Keeping costs & concurrency sane
- Enable only the activities you read. Every enabled activity is runner minutes.
- Concurrency is shared across your whole account, not per repo — so the per-repo runner cap defaults conservatively. Raise it deliberately.
- Antidoc and full backfills are heavy. Antidoc runs on default-branch pushes only; backfills are opt-in from the Populate-history dialog.
- Self-host if you run CI a lot — the warm image cache is the biggest single saving.
Staying up to date
The version badge in the header tells you when newer tooling exists. Update now applies it in one click and preserves your configuration; it never rebuilds your containers. Because your manifest is pinned to an immutable tag, updates are always something you choose, never something that happens to you. See §18.
9. The dashboard & Pages site #
The dashboard is built by the actions/dashboard composite action (Python,
dashboard.py) and deployed by dashboard-pages.yml. The action bundles all the
tooling pages — the dashboard, VI Browser, report viewer, Configure, Apply, What's New, this Documentation
page — so a consumer needs no local copy of any of them.
dashboard-pages.yml rebuilds on three kinds of trigger: a push that changes the page assets,
the GitHub status event (every capability posts statuses, so the dashboard reacts to all of
them with no per-workflow enumeration and no polling — and since it only reads statuses, it can
never self-trigger), and workflow_run completions for workflows that publish page artifacts
without a status.
dashboard.py reads each commit's statuses (keyed by the catalog's
statusContext), classifies whether a commit touched real LabVIEW source vs. CI tooling,
aggregates per-activity data from the deployed JSON (e.g. masscompile/<sha>/summary.json,
the snapshot by-blob index), and renders one dashboard row per recent commit. To keep the
project's own revisions visible even after many CI/tooling commits have piled up on main, it
keeps paging back through history — past the recent window it retains only project revisions —
until it has collected the project's revisions or reaches a large safety cap on how many commits it will
classify. Reading that history is resilient to a transient GitHub API failure: rate-limited
(HTTP 403/429), server, and network errors are retried with backoff
(honoring Retry-After/X-RateLimit-Reset), and if a call still fails so the
revision list would be incomplete, the build refuses to publish and exits — the
previous good dashboard stays live and rebuilds automatically once the API recovers, so a burst of
concurrent CI activity can never overwrite it with a blank or truncated table. It also stages the
bundled tooling pages and writes the VI Browser's files.json index.
On a fresh install with project revisions but no CI results yet, dashboard.py emits a
Populate the dashboard with your history card above the table. The card opens the Populate
history chooser, can be dismissed per repository in local storage, and hides as soon as that dashboard has
queued one of its own runnable cells or a later dashboard rebuild sees a running/finished activity from a
revision or pull request. Queued-run state is matched against the current table's cells by capability and
target revision (or globally for the all-history snapshot backfill) so another repository on the same Pages
origin cannot suppress the fresh-install card.
Each activity in the chooser is set independently to Skip, Fill (queue only
the revisions missing that result), or Re-run (queue every selected revision, replacing
existing results). An activity is offered for Re-run when any selected revision has a result, an empty cell, or
an in-flight queued/running cell — the latter are tagged cidash-active-cell so a re-run
replaces the in-flight run via concurrency instead of greying out as “none in scope.” The optimistic
“Queued” badge overlays that in-flight cell too, so re-running the latest revision's still-running
activity shows its Queued spinner immediately rather than silently doing nothing. A cell whose only state is a
browser-side optimistic overlay — a cidash-queued-cell that replaced an empty run glyph when you
dispatched it, including the sticky red “Failed” badge for a dispatch that failed — is gathered the
same way, so a failed run stays re-runnable here instead of disappearing as “none in scope.” Clicking
that red “Failed” badge opens its manage dialog, which now also offers Re-run (dispatch
it again for that commit) and, when the failed run still deployed a report, a View report link
straight to it — a hard failure that produced no report keeps just the “View run” link to the Actions
log. Queuing a large batch
— more than ten workflow runs across the selected revisions and activities — first asks for
confirmation, so an accidental “all revisions” selection can't fire dozens of runs without a prompt.
The chooser's revision picker (the All / range / specific selectors) mirrors the
dashboard table: dependency-only revisions — those whose only change is an external
dependency (.vipc/.vip) — are hidden by default and revealed with a
“Show dependency-only revisions” toggle (shown only when such a revision exists),
defaulting to whatever the dashboard's own dependency-only filter is currently set to. A search
box narrows the picker by commit title or short SHA across all three scopes, so you can queue the
latest change to LabVIEW code without scrolling past dependency-only commits. CI-only revisions never appear,
because the history feeding the chooser already contains project revisions only.
The same searchable revision picker backs every other “select a revision”
control (the Dependencies page, the Unit Testing project picker and the per-revision report viewer’s
Revision selector). It is a shared widget in lvci-header.js (window.lvciRevPicker)
that enhances a native <select> in place — the select stays the source of truth, so
existing behaviour (deep links, steppers, change handlers) is unchanged — and adds a filter box that
searches by commit title, build number or short SHA. By default it shows only revisions that change LabVIEW
source, hiding CI/tooling and dependency-only (.vipc/.vip) revisions
behind a “Show CI & dependency-only revisions” toggle, so picking “the latest
revision” anywhere land On the Dependencies page this picker now lives in the shared header
context bar — the same place the report viewer and VI Browser put their Revision
selector (built by the header from window.LVCI.revPicker and exposed as
window.lvciRevBar) — instead of a separate in-page dropdown.s on the latest LabVIEW-code change. The classification is published per revision in
dependencies/index.json and vi-snapshots/files.json, so every picker filters
identically to the dashboard.
The Populate-history chooser offers an opt-in "keep one container warm across revisions"
checkbox, shown only when at least one selected activity has a warm-container backfill workflow installed
(Windows Mass Compile, VI Analyzer, Unit Tests, Antidoc). It is always off by default and
carries a red warning that container state carries over between revisions (see §14).
When ticked, the chooser collapses every batch-capable (activity, platform) into a
single workflow_dispatch carrying the whole oldest→newest SHA list instead of one
dispatch per revision; non-batch platforms still queue per revision. The chooser also shows a live, history-driven
time estimate comparing fresh vs warm-batch compute: the per-activity work time is the median
of the durations the dashboard already loads from each summary.json/results.json, and
the startup time is the median run total (from the Actions API) minus that work time, falling back to per-platform
defaults until a repository has run history. Warm-batch is serial inside one container, so the estimate notes the
wall-clock vs total-cost trade-off.
The shared site-wide header is lvci-header.js: every page declares a tiny
window.LVCI config and loads the script, which injects a consistent navigation chrome
(Dashboard / VI Browser nav, Settings & Help menus, version badge, live activity pill, theme toggle).
Reports stay immutable; the header updates the moment the shared script is redeployed.
10. Common errors & troubleshooting #
Most problems fall into a few recognisable buckets. Each entry below gives the symptom, the cause, and the fix. When you need the raw detail, open the Actions tab of your repository and click into the failing run — the job log shows the exact step and error. (Note: GitHub returns no log for a step while it is still in progress; wait for the step to end or cancel it to read the log.)
The dashboard shows a placeholder or never goes live
Cause & fix. Usually GitHub Pages is not enabled, or the repo is private on a free
plan (Pages needs a paid plan — see §7). Check Settings → Pages and set
the source to the gh-pages branch. On a brand-new install, many workflows publish to
gh-pages at once and one deploy can briefly lose a push race; the dashboard deploy retries
automatically and self-heals on the next publish, so a momentary placeholder right after install is normal.
“Update now” seems stuck
Cause & fix. The update opens (or directly pushes) a change to your repo. If it opened a pull request that is just waiting, simply merge it — a tooling update PR changes only config/tooling files, so it has no checks to wait for. If no PR appeared, the built-in token either cannot push a workflow-file change or is not allowed to open PRs here; the What's New dialog offers a one-time token to finish the update. See §18.
A worker image build fails
Worker builds bake add-on packages with the VIPM CLI, which has several non-obvious failure modes (covered in depth in §15). The most common:
- Unit-test runs fail with error
-350053(“missing or bad files in the operation folder”). The UTF JUnit reporter is not baked in; it is a required package — rebuild the worker so it is installed. - “Package not found” for a public package. The Server Core VIPM resolver can come up empty; the build automatically falls back to downloading the public index and installing the package files directly. If it still fails, the package is in a private/custom repo — provide an offline VIPC or a direct URL (§7).
- Build “succeeds” but a package is missing. Inside the container VIPM can return success while printing No packages were installed; the tooling treats that as a failed apply and continues to its fallbacks, but a heavy add-on that wedges the engine is logged as a warning and the build still publishes a working, UTF-capable image.
A LabVIEW activity errors with -350000
Cause & fix. LabVIEW 2026+ needs the -Headless flag for VI Server
operations inside a Windows container; without it, VI Server raises -350000. The bundled
drivers already pass it — if you forked a driver, restore the flag.
A dispatched run for an older commit fails instantly
Cause & fix. This was a historical bug (a missing origin/main tracking
ref during a checkout of a past commit) that current tooling fixes. If you are on older tooling, the
workaround is to re-dispatch with the commit box blank (which targets the latest commit),
then update your tooling.
Relatedly, the Determine target SHA step no longer calls the GitHub API when the dispatched
commit_sha is already a full 40-character SHA (the dashboard always passes one), and it falls
back to the raw input instead of aborting if a short-SHA lookup is rate-limited — so a transient
HTTP 403 API rate limit exceeded can no longer make a dispatched report run fail before it
starts (which otherwise showed on the dashboard as an empty/failed activity, e.g. “no unit tests”).
The first run on a fresh runner is very slow
Cause & fix. The worker image is multi-gigabyte and a GitHub-hosted runner re-pulls it every job. This is expected. A self-hosted runner with a warm Docker layer cache removes the repeated pull (§7).
Windows jobs fail to start a container
Cause & fix. Every Windows job pins windows-2022 on purpose:
windows-latest now resolves to Windows Server 2025, whose Docker daemon is broken for these
LabVIEW containers. If you changed a runner label, set it back to windows-2022.
Unit Tests / Antidoc “missing” on Linux
Not a bug. Unit Tests and Antidoc are Windows-only today (they depend on baked VIPM tooling). They are not offered for Linux in Configure or Populate history. VI Analyzer, by contrast, now runs on Linux too — enable it per platform on the Configure page.
A new install does not appear on the Clients page
Not a bug. Discovery is a daily pull-based scan plus GitHub's own search-index lag, so a brand-new public install can take up to a day to appear (§19). Private repos are never listed.
Under the hood
11. The catalog — single source of truth #
Everything starts at .github/labview-ci/catalog.json. It is a versioned manifest that both the
browser configurator and the Python installer consume. Its key sections:
{
"schemaVersion": 1,
"version": "3.24.5", // must equal history.releases[0].version
"history": { "releases": [ { "version": "...", "title": "...", "highlights": [...] } ] },
"base": { "files": [ ... ] }, // always installed (dashboard chrome, manifest, …)
"capabilities": [
{
"id": "vi-analyzer",
"name": "VI Analyzer",
"status": "stable", // planned | advanced | stable → drives UI visibility
"recommended": true,
"supportsOs": ["windows"],
"requires": ["dashboard"], // hard dependencies
"recommends": ["dashboard"],
"statusContext": "CI / VI Analyzer", // GitHub commit-status context
"files": { "any": [...], "windows": [...] }
}
],
"substitutions": { "ordered": [ { "find": "...", "replaceWith": "{ownerRepo}" } ] },
"userConfig": { "files": [ ".github/labview-ci.yml", ".github/labview/vipm/ci-tooling.packages.json" ] }
}
The status field controls how a capability surfaces: planned is shown but
disabled, advanced is opt-in, stable is offered by default. The
statusContext string is the GitHub commit-status context the capability's workflow posts and
the dashboard reads — that one string is how a result flows from a workflow to a dashboard cell.
12. Workflow orchestration #
The heart is .github/workflows/labview-ci.reusable.yml. It implements
config-driven job gating: a single config job reads the consumer's
.github/labview-ci.yml once (via the actions/load-config composite action) and
exposes the settings as job outputs. Every activity job then gates on both a capability flag and a platform
flag.
config:
runs-on: ubuntu-latest
outputs:
has-masscompile: ${{ steps.cfg.outputs.has-masscompile }}
os-windows: ${{ steps.cfg.outputs.os-windows }}
tooling-ref: ${{ steps.ref.outputs.ref }}
# …
mass-compile:
needs: config
if: ${{ needs.config.outputs.has-masscompile == 'true'
&& needs.config.outputs.os-windows == 'true' }}
runs-on: ${{ inputs.windows-runner }} # default windows-2022
Disable a capability in Configure and its flag flips to false, skipping the job. Unselect a
platform and the platform flag does the same. The tooling-ref output lets the consumer's
manifest pin which version of the tooling scripts are checked out at run time (ref: in the
config), decoupling capability versioning from GitHub's moving major-tag alias.
Runners
Windows container jobs run on windows-2022; Linux jobs on ubuntu-latest. Both
default to GitHub-hosted runners but can be pointed at self-hosted labels via workflow inputs / Actions
variables. The worker container runs on the runner, so choosing a runner is how you choose where CI
executes — a self-hosted runner additionally gives you a persistent Docker layer cache so the multi-gigabyte
image is not re-pulled every job.
Concurrency & parallel deploys
Each report-producing workflow runs in its own concurrency group keyed by activity and revision
(${{ github.workflow }}-${{ inputs.commit_sha }}, cancel-in-progress: false), so
different activities — and the same activity on different revisions — run in parallel and never
cancel each other; re-dispatching the same activity for the same revision simply queues behind the in-flight
run. Several jobs still push to the same gh-pages branch, so races are handled at the deploy
step: the shared publish-gh-pages action is retry-safe — it re-fetches gh-pages
and retries the push (up to six times with back-off) so every parallel publish lands instead of one losing a
non-fast-forward cannot lock ref 'refs/heads/gh-pages' push. (Earlier versions serialised
all report workflows through a single report-pages-deploy group; that made unrelated activities
wait on — and, because GitHub keeps only one pending run per group, cancel — each other,
so it was replaced by per-activity groups once the deploy became retry-safe.) The dashboard build uses a
separate group (dashboard-pages, cancel-in-progress: true) so a burst of status
events collapses into a single latest-wins rebuild.
The dashboard surfaces this line live. It reads GitHub's in-progress, pending, waiting/requested, and queued
runs and shows each enqueued activity as a spinning Queued cell on the revision it targets. Because
a manual re-run is dispatched on the default branch ref, the run's head_sha is the workflow-file
commit, not necessarily the revision being analyzed. Per-revision dispatch workflows therefore include the
target SHA (inputs.commit_sha, or inputs.head_sha for VIDiff) in their run name, and
dashboard.py keys active runs on that target SHA rather than the dispatch ref. Snapshot backfills
are one global run, so they are treated as active for every snapshot cell still awaiting coverage. When several
runs are waiting for an available runner,
each queued cell also reports how many jobs are ahead of it (for example Queued · 1 ahead).
A capability that runs on two platforms (Windows and Linux) writes a single shared result for the commit, so the faster platform finishes first. The dashboard checks for an active run on the commit + capability before rendering that result, so while the slower platform is still running the cell keeps its live Running badge instead of reverting to a finished-looking result — the indicator only clears once every platform's run for that capability has completed.
13. Worker container images #
The worker image is where LabVIEW and your baked-in dependencies live. The source/template repo
(LCWC) owns and publishes one shared base image from
.github/docker/labview-ci-base.Dockerfile — LabVIEW with VI Analyzer support, optional UTF
support, VIPM, and Git already installed. Every client repo gets its worker from that base in one of two ways,
and which one runs is the key distinction between installation and an upgrade.
Installation vs. upgrade — how containers are managed
- Installation always copies the base. When a repo installs LabVIEW CI, its worker is
seeded by copying the shared LCWC base straight into the repo's own GHCR — there is no
rebuild. This is its own dedicated workflow,
copy-labview-image.yml(“Copy LabVIEW CI Image”), kept separate from the dependency build so the Actions tab clearly shows an install seed versus a dependency bake. It usescrane copy/crane tagto copy the base intoghcr.io/<you>/<repo>-labviewat the registry level. Cross-repo blob mounting is server-side only when the token already has access to the source package, so a client install actually transfers the multi-GB layers through the runner — which is why the copy workflow always runs onubuntu-latest, whose fast network finishes the copy in a minute or two instead of the several minutes a Windows runner takes (and far quicker than a full rebuild either way). A fresh install bakes no project dependencies; the worker is plain NI + LabVIEW. - Upgrade adds or replaces dependencies. Later you add or change project
.vipc/.dragonfiles that declare the add-on packages your action runners need. The dashboard compares those declarations against what is baked into the current worker manifest; if any are missing it shows a persistent “dependencies need to be installed” banner, and the Dependencies page’s update panel itemizes exactly which files and packages are pending. The comparison is version-tolerant: a VIPC pins a specific build (e.g.wovalab_lib_antidoc_cli-3.3.0.117), but VIPM installs one version per package, so a baked…-3.4.0.126satisfies the request — matching is on the versionless base identifier, so a present-but-different build reads as installed instead of a false missing. Applying it is opt-in: from the Dependencies page you dispatch the dependency build (build-labview-image.yml, “Build LabVIEW CI Image”), which runs on a Windows runner and this time startsFROMthe base and applies a thin dependency layer (.github/docker/labview-ci.Dockerfile) that bakes your monitored.vipcpackages (and, when a monitored.dragonfile is selected, applies it through the JKI Dragon CLI the base image provides). Dependency monitoring is configured on the Dependencies page, not Configure. Every discovered project.vipcand.dragonis checked by default on a fresh repo, while.github/labview/vipm/ci-tooling.vipcis always checked and locked. Monitoring is per worker container: the Dependencies table shows a Windows and a Linux column, and each file carries a Monitor checkbox for each. Selections are saved in.github/labview-ci.yml(container.vipc[]/container.dragon[]) as amonitorflag plus an optionalmonitorOnlist of platforms (for examplemonitorOn: windows);monitor: truewith nomonitorOnmeans every worker platform, for backward compatibility. A file monitored for a platform is baked into that worker and produces dependency warnings there; platforms it is not monitored for are shown as unmonitored and do not turn missing packages into warnings. Installation remains separate: dependencies are not installed before actions unless the Configure page's automatic dependency installation policy is enabled, or you dispatch the dependency build yourself. On a push, each platform'sgaterebuilds its worker only when a changed dependency file is monitored for that platform; files not monitored for a platform (or saved withmonitor: false) do not trigger an automatic worker update there. The dashboard also writesdependencies/index.jsonduring every publish so a fresh install or revision change can immediately show the dependency table from metadata alone: checked boxes mean a worker manifest proves the package is baked, a yellow bang means the platform supports a monitored dependency but the current worker is missing it or has not published its manifest yet, andN/Ais reserved for dependency/platform combinations that are not supported or files that are not monitored. Each generated VIPC entry carries its role (coretooling vs.projectdependency), monitor state, and tooling lock state, so built-in CI tooling packages can stay neutral while monitored project packages are marked as missing until a worker update bakes them in. - Tooling upgrade is separate. Updating the LabVIEW CI tooling itself (the Update dialog /
apply-tooling-update.yml) refreshes the workflow scripts and preserves your config; it does not rebuild containers. See §18.
Linux builds are handled by build-labview-linux-image.yml on the same model. The Linux worker and
the standalone VI Browser 2.0 render image also bake a set of X11 core and TrueType fonts (DejaVu,
Liberation, Noto, and the 75/100 dpi xfonts-* families) — NI’s minimal Linux base
ships almost none, and without them LabVIEW’s UI controls and rendered VI text (VI Browser snapshots
and the Debug Run desktop) come out blank. Images are pushed to your GitHub Container
Registry (GHCR) as packages attached to your repo:
ghcr.io/<owner>/<repo>-labview # your LabVIEW worker (copied from the base, or base + your deps; the Linux worker also bakes the VI Browser 2.0 render engine)
ghcr.io/<lcwc-owner>/<lcwc-repo>-labview-base # the shared base, owned & published by the source repo
The image inherits your repository's visibility and is authenticated with the job's built-in
GITHUB_TOKEN. GHCR rejects uppercase names, so the owner/repo are lowercased at build time.
Content-addressed tagging
Every build computes a worker version — a SHA-256 over the layered Dockerfiles, the VIPC install
script, and every baked .vipc — and publishes four tags so consumers can choose their
stability/freshness trade-off:
| Tag | Example | Stability |
|---|---|---|
:latest | :latest | Always the newest build. |
:<year> | :2026 | Moves as that LabVIEW year rebuilds. |
:<year>-<date> | :2026-20250620 | Dated snapshot, stable until the next rebuild. |
:<hash> | :win-a1b2c3d4e5f6 | Fully immutable — same inputs always reproduce the same tag. |
Each build also publishes a human/machine-readable worker manifest to
/workers/<platform>/<version>/manifest.html listing the exact nipkg and
VIPM packages baked in, so a worker image is fully auditable.
The machine-readable manifest.json records installed VIPM packages in
vipm_packages (name, version, and VIPC-style label) plus
the raw vipm list --installed output in vipm_raw. The Dependencies page unions that
installed-package inventory with the manifest’s own vipc[].packages list when deciding what is
baked. Because VIPM records versions with a leading v (e.g. v0.5.0.1) while VIPC files
declare the bare form (0.5.0.1), the comparison normalizes that v prefix — without
it, correctly-baked packages would be reported as missing and the “dependencies need to be installed”
banner would never clear.
Dragon dependencies are reconciled against the current Windows worker manifest (the worker
rebuilt by build-labview-image.yml). If a manifest has not yet published a dedicated
dragon.items status block, the dashboard falls back to the installed-package inventories
(vipm_packages / nipkg_packages) so already-baked dependencies are marked installed
instead of staying stuck at not_attempted.
When images rebuild
A worker rebuild is a heavy, opt-in operation. It runs when you apply a dependency update from the dashboard's
Review & update dependencies dialog, on the monthly cron (to pick up base-image security updates),
or by manual dispatch. Adding or changing a project .vipc never triggers a rebuild on its own —
the dashboard simply flags that dependencies need installing until you choose to run the update.
Dependency gate (activities wait for the rebuild)
VIPC files are the source of truth for a repo's project dependencies, so the container-based activities must
never run against a worker image that predates a dependency update. While a worker rebuild is in progress, the
activity workflows — VI Analyzer, Mass Compile, Unit Tests,
Antidoc, and VIDiff — call .github/labview/wait-for-worker-images.sh before they pull the image
and block until the worker image build(s) for that revision finish. The wait outlasts a cold
first build (pulling the multi-gigabyte NI base and baking the project VIPC runs ~80–100 minutes), so a
fresh install's first dispatched activity waits rather than failing while the image is still building. In the
reusable workflow a
single gate-images job does this and the dependency-consuming jobs need it. If the
rebuild fails the activities do not run; if nothing worker-affecting changed the gate returns immediately. The
snapshot renderer is intentionally exempt because it renders on the bare NI base image.
Manual VIPC base-image bake
bake-vipc-windows-base.yml is a focused proof-and-promote workflow for a single VIPC. It runs on
windows-2022, pulls :2026 of the shared -labview-vipm-base image (or
builds and pushes it once when missing or explicitly requested), stages only the selected VIPC (default:
example/Dependencies.vipc) into the thin Windows dependency layer, verifies that VIPM reports the
VIPC packages as installed inside the resulting container, and only then pushes the verified image to GHCR. On
success it promotes the
normal project tag (for example :2026), an operator-selected moving tag (default
:vipc-base), and an immutable VIPC-hash tag.
ghcr.io/<owner>/<repo>-labview-vipm-base:2026. VIPC rebuilds then pull that base and apply
only the dependency layer. The verified project image for the example VIPC reported all seven expected OpenG
packages from vipm list --installed before GHCR tags were pushed.Per-action image routing
Image selection is a three-level lookup against .github/labview-ci.yml:
- Per-action override —
config.container.actions.<id>(e.g. pin VIDiff to a frozen build while Mass Compile trackslatest). - Project-wide default —
config.container.use. - Built-in default — the bare NI base image.
Special values: base/none force the NI base image (no baked dependencies),
latest selects the custom image's :latest tag, and anything else is treated as a
literal tag.
14. Container lifecycle & headless LabVIEW #
A container job follows the same shape on every platform. The Windows Mass Compile job is representative:
docker pull "$IMAGE"
docker run --rm `
-e "GITHUB_REPOSITORY=$repo" -e "GITHUB_SHA=$sha" `
-v "${{ github.workspace }}:C:\workspace" ` # your repo, bind-mounted in
-v "${{ github.action_path }}:C:\ci-action" ` # the script ships with the action
"$IMAGE" `
powershell -NonInteractive -ExecutionPolicy Bypass `
-File "C:\ci-action\masscompile.ps1" `
-WorkspaceRoot "C:\workspace" `
-ReportDir "C:\workspace\ci-out\masscompile" `
-LabVIEWPath "C:\Program Files\National Instruments\LabVIEW 2026\LabVIEW.exe"
Key properties:
--rm— the container is destroyed the instant the action finishes. Nothing persists inside it; the image is reused, the container instance never is.- Two bind mounts — your workspace (so reports written to
ci-out/…appear back on the runner) and the action directory (so consumer repos need no local copy of the driving script). VIDiff mounts the base checkout at a second path (C:\workspace-base) as well. - Headless invocation — Windows uses
LabVIEWCLI.exe; Linux uses thelabviewcli/g-clientry points on PATH. LabVIEW 2026+ on Windows requires the-Headlessflag to avoid VI Server error-350000in a container.
Warm-container history backfills (opt-in)
Populating a long history one revision at a time pays the multi-GB image pull and the slow Windows LabVIEW
cold-start once per revision. The per-capability backfill workflows
(masscompile-backfill-windows.yml, vi-analyzer-backfill-windows.yml,
unit-tests-backfill-windows.yml, antidoc-backfill-windows.yml, plus the existing
VIDiff and snapshot backfills) instead start one long-lived container
(docker run -d … sleep) and process every requested revision through it with
docker exec, turning N × (startup + work) into startup + N × work. Each
revision is checked out into a throwaway git worktree, compiled/analyzed/tested inside the
container, its report docker cp'd back out to the host (Windows bind-mount output is unreliable),
and the host report builder + shared Pages publisher run exactly as in the per-commit job. The runs are
resumable: a skip-list built from the deployed gh-pages tree and a TimeBudgetMinutes
cap let a batch stop before the 6-hour job limit and continue on the next dispatch.
This is strictly opt-in and never the default, because reusing one container means its mutable state — temp files, the LabVIEW object cache, the registry — carries over between revisions, so a revision whose code modifies the system can influence a later revision's result. The container image is never modified; only the short-lived instance accumulates state, and it is destroyed when the batch ends. The dashboard's Populate-history chooser exposes this as a clearly-warned checkbox — see §9.
Report generation runs on the host, not in the container
The container emits raw output (a log, a native HTML report, JUnit XML, …). After it exits, a small Python
builder on the runner host turns that into the navigable report — grouping problems by VI, adding the shared
navigation chrome, and writing the machine-readable summary.json/problems.json
that the dashboard reads. The report is then uploaded as an artifact and deployed to
gh-pages under its per-SHA path, and a commit status is posted with the report URL.
When a report workflow is manually dispatched for an older commit, it checks out that target revision but
restores the current CI runtime from the repository default branch before running: .github/labview,
.github/workflows, and the local .github/actions/publish-gh-pages action. That keeps
history backfills compatible with commits that predate the current report builders or shared Pages publisher.
Debug Run — remote into a worker over VNC
Every activity above runs the container headless. When a run misbehaves in a way that is hard to diagnose from logs (a VI that loads broken only inside the image, a dependency that resolves differently than expected), Tools › Debug Run on the dashboard boots the same worker for a chosen revision with a full graphical desktop you can remote into, so you can open the project in the LabVIEW IDE and watch it happen.
The dialog is a single form over a live list. You pick a revision and a session length and
press Start debug session; it boots the debug desktop for that revision with the
checkout’s project (*.lvproj) already open in the LabVIEW IDE (it sets the
open_source input, which the container script uses to open the first project it finds under the
workspace) and dispatches debug-session.yml. You no longer pre-select activities in the dashboard
— once you are inside the desktop you choose what to run from an on-screen menu (see below). The dialog
makes it explicit that each debug session occupies one of the project’s Actions runners for its
whole lifetime and will block other CI jobs if no runner slots are free, so end sessions as soon as you
are done. The form stays put; each session you start appears in a scrollable Existing debug
sessions list beneath it, so you can launch several (extra ones queue and start as earlier sessions
finish). Every row shows a live progress bar and status — Queued, Booting with the
current step, or Live — plus Open remote desktop and End, and the
list is driven by the Actions API so it works from any device (not just the browser that started the session).
Mechanically, the job runs the Linux worker (.github/labview/debug/debug-session.sh) which starts
a virtual display (Xvfb) and window manager, launches the LabVIEW UI (opening the project), and serves it as a
browser noVNC session (x11vnc + websockify) on port 6080. The runner
exposes that port through an ephemeral Cloudflare quick tunnel (no account or secret required)
and publishes the one-time connect URL to gh-pages under debug/<run-id>.json;
the dialog polls for it and auto-opens it. Inside the desktop an on-screen terminal shows an
operations menu: the first option opens the project in LabVIEW, and the rest run CI operations
(Mass Compile, Builds, VIDiff, …) via .github/labview/debug/run-debug-actions.sh, which
invokes the same Linux entrypoints CI uses (e.g. masscompile.sh) and streams output to the window
so you watch each step execute. It is a run-and-observe loop — run one operation, return to the menu, run
another. The session self-tears-down after the chosen minutes, on End session, or when the run is
cancelled — deleting the published connect file.
No license secret is needed: LabVIEW prompts for interactive login/activation in the session, so you activate it yourself; the activity runners then execute in that same activated session. Security note: the tunnel URL and its one-time password are unguessable and short-lived, but on a public repository anyone who can see the Actions run could read the connect file while the session is live. Use Debug Run on private repositories for sensitive code; it is off by default, dispatch-only, and never runs in normal CI. Interactive Debug Run is Linux only: Windows containers run in session 0 with no interactive desktop or DWM compositor, so a screen-scraping VNC view of a Windows container is blank — a Microsoft container limitation, not a setup issue. (Windows headless CI — VI Analyzer, snapshots, mass-compile, builds — is unaffected.)
15. VIPM dependency baking — lessons from the trenches #
This section is the single hardest part of the whole stack, and it is documented in detail precisely because it was painful to get right. If you ever fork the worker build or debug a baking failure, read it carefully — every numbered point below is a real problem that was hit and solved.
Third-party LabVIEW packages (Antidoc, Caraya, VI Tester, LUnit, the UTF JUnit reporter, and project packages
declared in staged .vipc files) are baked into the Windows worker image at build
time so they are available headless during CI. Getting the modern VIPM CLI to install packages
non-interactively inside a docker build required solving several non-obvious problems,
documented in .github/labview/vipm/README.md and implemented by
install-vipc.ps1:
labview-ci-linux.Dockerfile) installs VIPM from the native Debian package and
applies the same staged repository .vipc files during its build via
install-vipc-linux.sh (VIPM CLI under Xvfb + headless LabVIEW), publishing a
workers/linux manifest just like Windows. So the Dependencies page shows real per-package Linux
coverage and its columns are first-class, not “pending”.- Use VIPM 2026 Q3 from the JKI CDN — the older NI-feed CLI times out on
library_listwith no diagnostic. - Run without requiring VIPM Pro — the script does not force
VIPM_COMMUNITY_EDITION=1, but it does satisfy the Free/Community public-repo checks by running from a real shallow clone (or a minimal fallback.git) of the public repository. - Non-interactive env (
VIPM_NONINTERACTIVE=1,VIPM_ASSUME_YES=1,NO_COLOR=1) so the CLI never blocks on a prompt. - Seed
Settings.inibefore installing — a fresh image lacksC:\ProgramData\JKI\VIPM\Settings.ini, without which the CLI aborts with an IO error. - Run LabVIEW headless first — the modern CLI installs into a running LabVIEW over
VI Server, so the script launches
LabVIEW.exe --headlessand waits for port3363. - Extend timeouts (
VIPM_TIMEOUT=900) — cold first-run headless LabVIEW exceeds the CLI's short defaults during a build, whereCI/GITHUB_ACTIONSenv hints are absent. - Use the modern command shape —
vipm refresh --forceis a standalone command; package installs usevipm --labview-version 2026 --labview-bitness 64 install <name>@<version>orvipm install -y project.vipc. The oldinstall --refreshform is rejected by VIPM 26.3. - Bypass the empty Free/Community resolver when needed — JKI's public Docker example notes
that container use currently requires Pro activation and that Free/Community support is still being fixed.
Our tests match that: desktop Free refresh downloads the NI Tool Network and VIPM Community indexes, but
Windows Server Core refresh can report success without populating the install resolver. When by-name installs
fail with package-not-found, the script downloads the public
.vipr/.ogpdindexes directly, resolves the VIPC package set plus transitive dependencies, downloads the public.vip/.ogpfiles, validates them, and installs those local files with VIPM. - Treat direct VIPC no-op applies as failures — in the container VIPM can return exit
0fromvipm install -y project.vipcwhile printingNo packages were installed. The hook treats that text as a failed apply and continues to the package-spec and local-file fallback paths.
.vipc in the repo is discovered recursively and listed on the Dependencies page. That page
owns the monitored-file selection written back as config.container.vipc entries; Configure only
owns the global dependency policy. The tooling also ships a base configuration —
.github/labview/vipm/ci-tooling.vipc — that is always monitored and locked on, while a project's
own discovered VIPC files can be checked or unchecked. Checked project files produce warnings when their
packages are missing from the worker; unchecked project files are tracked in config with monitor: false
and do not produce warnings or automatic dependency-update triggers. When this template repo updates the base,
consumers receive the new base on their next tooling update and the next container rebuild re-applies base +
project together. Because each repo publishes to its own GHCR namespace, those rebuilds produce that repo's own
unique worker image.
After a worker image is built locally, the publish workflow captures both nipkg list and
vipm list --installed from inside the image and writes them into the worker manifest. Fresh-install
seed copies use a fast registry copy and do not pull the multi-GB image back just to inspect it; seed manifests
without a captured VIPM list are treated conservatively by the dashboard, with only the known
Windows core tooling VIPC inferred as installed.
install-vipc.ps1 installs the UTF JUnit essentials
(ni_lib_utf_junit_report + its ni_lib_junit_results_api /
ni_lib_simple_xml deps) first as required, then applies project
.vipc files (also required). Tooling VIPCs (ci-tooling*.vipc, e.g. Antidoc / Caraya /
VI Tester / LUnit) are best-effort: if a heavy add-on wedges the headless VIPM engine it is logged as a warning
and the build still publishes a working, UTF-capable image. Override the required set with
VIPM_REQUIRED_PACKAGES.sf://opengtoolkit URLs and the legacy
jki_vi_tester alias. Private/custom VIPM repositories still require either a populated VIPM
resolver cache, a committed offline VIPC that embeds package files, or direct package-file URLs..vip anywhere outside .github/; the
build stages every repo *.vip into .github/labview/vipm/ next to the VIPCs (the glob
*.vip does not match *.vipc, and the worker-version hash matches the exact extension so
VIPCs are not double-counted). Reference the package by id + version in an applied .vipc and keep
VIPM's canonical export name <package-id>-<version>.vip so its id + version parse. When that
.vipc is applied, install-vipc.ps1 uses the committed .vip in preference to
the public mirror, and it is the only way a no-index package resolves at all. The .vipc must still
enumerate the package's dependency closure (OpenG, etc.) — deps are not parsed from the committed .vip
— and a committed .vip that no applied .vipc references is never installed on its own.LabVIEWCLI -OperationName RunUnitTests links against
ni_lib_utf_junit_report. Without it baked in, headless UTF runs fail with error
-350053 ("missing or bad files in the operation folder").The committed tooling VIPC is the source of truth
The worker dependency intent is the committed ci-tooling.vipc itself. It must remain a real,
VIPM-openable file and is the file applied during image builds. The Reconfigure workflow validates the
committed VIPC but does not regenerate it, so a UI-authored VIPC is not overwritten by automation.
build-tooling-vipc.py remains available as an optional helper: it can assemble a genuine
VIPM-openable VIPC from JSON/package-index inputs (real spec + icon assets from public indexes), but that
generated output is only authoritative once committed as ci-tooling.vipc.
16. Capabilities in depth #
Every capability is built to the same template, so once you understand one you understand them all. §16.4 (Unit Tests) is documented end-to-end as the fully worked example you can copy from.
The shared anatomy of a capability:
- An in-container driver (
.github/labview/<activity>.ps1/.sh) that invokes headless LabVIEW inside the worker container and writes raw output. - A host report builder (
.github/labview/build-<activity>-report.py) that normalises that raw output intosummary.json/results.jsonplus a navigable HTML report. - A gated workflow (
<activity>-*-container.yml) that pulls the image,docker run --rms the driver, builds the report on the host, deploys it to<activity>/<sha>/ongh-pages, and posts a commit status. - One catalog entry (id,
statusContext,supportsOs,files) that wires it into the configurator, installer, and dashboard generically (§20).
16.1 Mass Compile #
At a glance
| Driver | .github/labview/masscompile.ps1 Windows / masscompile.sh Linux |
|---|---|
| Report builder | .github/labview/build-masscompile-report.py |
| Workflows | masscompile-windows-container.yml windows-2022, masscompile-linux-container.yml ubuntu-latest; backfill masscompile-backfill-windows.yml |
| Output & status | masscompile/<sha>/ (Linux: …/<sha>/linux/); commit status CI / Mass Compile |
What it runs (headless command)
A single LabVIEWCLI MassCompile pass recursively compiles every VI/CTL in one LabVIEW session:
LabVIEWCLI -LogToConsole TRUE -OperationName MassCompile `
-DirectoryToCompile "C:\workspace" `
-LabVIEWPath "C:\Program Files\National Instruments\LabVIEW 2026\LabVIEW.exe" `
-Headless
-Headless is required on LabVIEW 2026+ in a Windows container (VI Server error -350000
otherwise). Output is tee’d to masscompile.log.
Runner composition
docker run --rm `
-e "GITHUB_REPOSITORY=${{ github.repository }}" -e "GITHUB_SHA=${{ steps.sha.outputs.sha }}" `
-v "${{ github.workspace }}:C:\workspace" `
$Env:LABVIEW_CONTAINER_IMAGE `
powershell -NonInteractive -ExecutionPolicy Bypass `
-File "C:\workspace\.github\labview\masscompile.ps1" `
-WorkspaceRoot "C:\workspace" -ReportDir "C:\workspace\ci-out\masscompile" `
-LabVIEWPath "C:\Program Files\National Instruments\LabVIEW 2026\LabVIEW.exe"
Output contract
- The raw
masscompile.logis parsed for two failure signals:### Bad VI(VIs LabVIEW cannot load) andSearch failed to find …(unresolved subVI dependencies). - The builder enumerates the repo’s own VIs (excluding
.github/,ci-out/,build/) as the denominator and emitsproblems.json(per-VI, grouped by top-level folder) andsummary.json({total, ok, bad, percent, status, missing_deps}). - Windows and Linux deploy to disjoint paths (
masscompile/<sha>/summary.jsonvs…/<sha>/linux/summary.json), so the dashboard shows a two-numberWindows% / Linux%pill (worst colour wins) when both ran; a single platform shows one number.
0 = all compiled; 3 = partial (some bad VIs, not a container failure); any other
non-zero = a real container/configuration error. The reader also handles UTF-16 BOMs that Windows
Tee-Object can emit.Dashboard badge thresholds
The Mass Compile column colours the compiled-percentage badge: green at or above
greenAtLeast (95%), yellow at or above yellowAtLeast (80%),
red below redBelow (80%). A true compile failure (status: failed,
percentage zeroed) is drawn as a solid, bright-red filled pill. Override in .github/labview-ci.yml:
massCompile:
thresholds:
compiledPercent:
greenAtLeast: 95
yellowAtLeast: 80
redBelow: 80
16.2 VI Analyzer #
At a glance
| Driver | run-vi-analyzer.ps1 Windows / run-vi-analyzer.sh Linux |
|---|---|
| Report builder | build-analyzer-report.py |
| Workflows | run-vi-analyzer-windows-container.yml; backfill vi-analyzer-backfill-windows.yml |
| Output & status | vi-analyzer/<sha>/ (CI / VI Analyzer); Linux …/<sha>/linux/ (CI / VI Analyzer (Linux)) |
What it runs (headless command)
Each analysis pass runs one configuration over one scope:
LabVIEWCLI -LogToConsole TRUE -OperationName RunVIAnalyzer `
-ConfigPath "<config.viancfg>" -ReportPath "<report.html>" `
-ReportSaveType HTML `
-LabVIEWPath "...\LabVIEW.exe" -Headless
Before analysing, the driver runs a headless Mass Compile pre-pass
(-OperationName MassCompile -DirectoryToCompile …) to upgrade the project’s VIs to the
image’s LabVIEW year — VI Analyzer silently skips VIs saved in an older version (“0 VIs
analyzed”). The pre-pass compiles each top-level project folder individually and
excludes the CI’s own tooling under .github/ (plus .git/,
actions/, ci-out/, build/) — e.g. the VI Browser render engine under
.github/labview/toimages — so a broken tooling VI can’t fail the compile before project
VIs are upgraded.
Runner composition
docker run --rm `
-e "GITHUB_REPOSITORY=${{ github.repository }}" -e VIA_FILES -e VIA_CONFIG `
-v "${{ github.workspace }}:C:\workspace" `
$Env:LABVIEW_CONTAINER_IMAGE `
powershell -NonInteractive -ExecutionPolicy Bypass `
-File "C:\workspace\.github\labview\run-vi-analyzer.ps1" `
-WorkspaceRoot "C:\workspace" -ReportDir "C:\workspace\ci-out\vi-analyzer" `
-LabVIEWPath "...\LabVIEW.exe"
VIA_FILES / VIA_CONFIG are passed by name (not spliced onto the command line), so a
single-VI re-run’s paths and config never break argument parsing; empty means a normal whole-revision run.
Which config runs
- Directory mode (default suite). Passing the mounted workspace directory makes LabVIEWCLI apply every built-in test to every VI under it.
- Committed
.viancfg. Applies its ownTestConfigData; an emptyTestConfigDataruns zero tests, so when a project commits a config but selects nothing, the pipeline auto-picks the first repo.viancfg— excluding CI-internal dirs (.github/,actions/,ci-out/,build/) so the bundledvia-config-default.viancfg(emptyTestConfigData) is never chosen. - Analyze-Project rewrite. For the default pass the runner rewrites the generated config to
VI Analyzer’s native Analyze Project mode (
AnalyzeProject=TRUEpointed at the detected.lvproj) while keeping the selected tests, because a config whoseItemsToAnalyzeis a list of explicit VIs runs zero tests headlessly. - Safety net. If the project pass still executes no tests (or there’s no
.lvproj), it re-runs in directory mode so the report is never blank. - Per-path rules. The manifest’s
config.viAnalyzercan name adefaultconfig and a list of per-pathrules(strict forsrc/critical/**, relaxed fortests/**).
Report & dashboard
- Runs on both Windows and Linux: the Linux driver mirrors Windows (same pre-compile, same
directory / Analyze-Project handling with a directory-mode fallback) and deploys under
vi-analyzer/<sha>/linux/. The dashboard column shows both platforms’ finding counts, and a segmented Windows | Linux toggle in the report switches between them (the other platform’s button is disabled when it hasn’t run for that commit). - The builder maps each failed test to a category (correctness / performance / style / documentation) for the
dashboard’s coloured breakdown, preserves the LabVIEW-native HTML as
raw.html(linked as “Download raw report”), and writessummary.json. - Each VI card embeds its rendered front panel & block diagram snapshot inline, lazily
pulled from
vi-snapshots/<sha>/manifest.jsonwhen expanded; “Open full view” opens the larger snapshot drawer. - When VI Analyzer reports testing errors (VIs it couldn’t load/analyze, e.g. LabVIEW
Error 14216), the banner names each affected VI and its message, and tags CI-tooling VIs (under
.github/) separately so it’s clear when the errors are in CI helper VIs rather than your project code.
16.3 VIDiff #
At a glance
| Driver | vidiff.ps1 / vidiff.sh; history walker vidiff-backfill.ps1 / .sh |
|---|---|
| Deploy + PR comment | vidiff-deploy.yml |
| Workflows | vidiff-windows-container.yml windows-2022, vidiff-linux-container.yml ubuntu-latest; backfill vidiff-backfill-windows.yml |
| Output & status | vidiff/<pr-N|push-sha>/<platform>/vidiff/; status CI / VIDiff (windows|linux) |
What it runs (headless commands)
Per changed VI, one of two operations runs (a magic-byte check at offset 8 confirms a real LabVIEW resource first):
# Modified — side-by-side comparison
LabVIEWCLI -LogToConsole TRUE -OperationName CreateComparisonReport `
-VI1 "<base.vi>" -VI2 "<head.vi>" -ReportType html -ReportPath "<out>\index.html" `
-LabVIEWPath "...\LabVIEW.exe" -Headless
# Added / deleted — single-VI snapshot via the bundled custom operation
LabVIEWCLI -OperationName PrintToSingleFileHtml `
-AdditionalOperationDirectory "<PrintToSingleFileHtml op>" `
-VI "<path.vi>" -OutputPath "<out>\index.html" -o -c `
-LabVIEWPath "...\LabVIEW.exe" -Headless
Runner composition
The job mounts two checkouts — head at C:\workspace, base at
C:\workspace-base — and passes the changed-file list by env var:
docker run --rm `
-v "${workspace}\head:C:\workspace" -v "${workspace}\base:C:\workspace-base" `
-e CHANGED_FILES=$Env:CHANGED_FILES -e "GITHUB_REPOSITORY=${{ github.repository }}" `
$Env:LABVIEW_CONTAINER_IMAGE `
powershell -NonInteractive -ExecutionPolicy Bypass `
-File "C:\workspace\.github\labview\vidiff.ps1" `
-BaseDir "C:\workspace-base" -HeadDir "C:\workspace" `
-ReportDir "C:\workspace\ci-out\vidiff" -LabVIEWPath "...\LabVIEW.exe"
Output & dashboard
- Reports land at
vidiff/<pr-N|push-sha>/<platform>/vidiff/with achanges.jsonmanifest ({modified, added, deleted}).vidiff-deploy.ymldeploys them to Pages and comments the diff on the PR (it runs even on failure). - The backfill variant walks
git log --reverse -- '*.vi' '*.ctl', creates detached worktrees per commit, renders through a warm container, and is resumable via a skip-list. - The dashboard VIDiff column summarises each revision as a three-number diff stat —
different /
new /
deleted VI/CTL counts versus the parent — computed by
dashboard.pyfrom the two git trees (each file’s blob SHA is the content fingerprint: same path + changed blob = different, a path the parent lacked = new, a path the parent had = deleted; a rename reads as one new + one deleted). Zero parts are dimmed, a tip strip spells the counts out, and the cell links into the VI Browser filtered to this revision’s changed VIs.
.github/labview/PrintToSingleFileHtml/ is a binary
.lvclass sourced from NI's labview-for-containers examples; it renders a single VI
(front panel + block diagram) to a standalone HTML file and powers both added/deleted diffs and snapshots.16.4 Unit Tests — a fully worked example #
Unit Tests is documented end-to-end here as the reference capability. Every other activity shares the anatomy above, so if you can reproduce Unit Tests from the detail below — the frameworks, the exact CLI commands, the container invocation, and the output contract — you can reproduce any of them.
At a glance
| In-container driver | .github/labview/run-unit-tests.ps1 — runs each enabled framework headless, writes one JUnit XML per tool. |
|---|---|
| Host report builder | .github/labview/build-unittest-report.py — merges the JUnit XML into results.json + the navigable HTML report. |
| Per-commit workflow | unit-tests-windows-container.yml on windows-2022. |
| History backfill | unit-tests-backfill-windows.yml + unit-tests-backfill.ps1 — one warm container across many revisions. |
| Output & status | Report deploys to unit-tests/<sha>/ on gh-pages; commit-status context is CI / Unit Tests. |
| Platform | Windows only — every framework depends on baked VIPM/UTF tooling (§15). |
The four frameworks & the exact headless command each runs
Each enabled tool is driven by a per-tool command template that run-unit-tests.ps1
applies verbatim after token substitution. Any template can be overridden per tool with a command:
key in the config, so a precise invocation can be corrected on a real worker with no script change.
| Framework | Test unit it discovers | Runs via | Default command template |
|---|---|---|---|
| Caraya (JKI) | VIs containing Caraya assertions, under a directory | g-cli | g-cli --lv-ver {ver} -- caraya -- --directory "{dir}" --junit "{out}" |
| LUnit (Astemes) | Test Case classes (.lvclass), under a directory | native LabVIEWCLI | "{cli}" -LogToConsole TRUE -OperationName LUnit -Path "{dir}" -ReportPath "{out}" -LabVIEWPath "{lv}" -Headless |
| VI Tester (JKI) | xUnit TestCase classes, under a directory (scaffold) | g-cli | g-cli --lv-ver {ver} -- vitester -- --directory "{dir}" --junit "{out}" |
| NI UTF | .lvtest items inside a .lvproj | native LabVIEWCLI | "{cli}" -LogToConsole TRUE -OperationName RunUnitTests -ProjectPath "{proj}" -JUnitReportPath "{out}" -LabVIEWPath "{lv}" -Headless |
Tokens the driver substitutes: {ver} = LabVIEW year, {dir} = a resolved test-root
directory, {proj} = a resolved .lvproj, {out} = the JUnit output path,
{lv} = LabVIEW.exe, {cli} = LabVIEWCLI.exe.
Directory-based tools (Caraya, LUnit, VI Tester) run once per resolved test-root. UTF runs a whole
.lvproj, so each location resolves to its owning project and is robust to however
a repo is laid out: a location may be a .lvproj path, a folder that contains one or more test-bearing
projects, or a folder of .lvtest files whose .lvproj lives in a parent directory (the
driver walks up to the nearest owning project). An empty locations list discovers
and runs every test-bearing .lvproj anywhere in the repo, so a fresh install reports results
for whatever projects exist.
Configuring which frameworks run
The configurator writes a unitTests block into .github/labview-ci.yml; the driver
reads it directly. Enable a tool, point it at one or more locations, and optionally override its
command or the dashboard pass-rate thresholds:
unitTests:
thresholds:
passedPercent: # colours the dashboard Unit Tests badge
greenAtLeast: 100
yellowAtLeast: 80
redBelow: 80
tools:
utf:
enabled: true
locations:
- "example/User-Defined Test/" # a dir, a glob, or a .lvproj (empty = whole repo)
caraya:
enabled: false
locations: []
lunit:
enabled: false
locations: []
# command: '...' # optional per-tool override
vi-tester:
enabled: false
locations: []
Only tools with enabled: true run. A location may be a directory (recursed), a glob
/ extension pattern (each match’s parent directory becomes a test root), or — for UTF — a
.lvproj path.
How the workflow composes the runner (per commit)
The whole job runs on one windows-2022 runner and follows a fixed step order (it mirrors VI Analyzer so the two behave identically):
- Determine the target SHA — from the
workflow_dispatchinput, the PR head, orgithub.sha. - Post a
pendingstatus (CI / Unit Tests) so the dashboard cell spins immediately. - Check out the target SHA (
fetch-depth: 0); on a dispatched historical run, restore the current CI runtime (.github/labview,.github/workflows,publish-gh-pages) from the default branch so old commits use today’s builders. - Resolve the container image —
config.container.actions.unit-tests→config.container.use→ the shared worker:latest(base/noneforces the bare NI image). - Ensure Docker, log in to GHCR, and wait for the worker-image build (
wait-for-worker-image.sh) when this commit changed a monitored dependency. - Pull the image, then run the driver inside a throwaway container (below).
- Build the report on the host (Python), upload the artifact, deploy to
gh-pagesunderunit-tests/<sha>/, and post the final commit status.
The container invocation is the heart of it — a single docker run --rm that bind-mounts the
checkout and launches the driver under PowerShell:
docker run --rm `
-e "GITHUB_REPOSITORY=${{ github.repository }}" `
-v "${{ github.workspace }}:C:\workspace" ` # the checkout, mounted in
$Env:LABVIEW_CONTAINER_IMAGE `
powershell -NonInteractive -ExecutionPolicy Bypass `
-File "C:\workspace\.github\labview\run-unit-tests.ps1" `
-WorkspaceRoot "C:\workspace" `
-ResultsDir "C:\workspace\ci-out\unit-tests\results" `
-LabVIEWVersion "2026" `
-LabVIEWPath "C:\Program Files\National Instruments\LabVIEW 2026\LabVIEW.exe"
Because ci-out/… lives under the bind-mounted workspace, the per-tool JUnit XML the driver
writes appears back on the runner host for the report step. (The backfill path cannot rely on that
— see below.)
Invoking LabVIEW headless inside the container
run-unit-tests.ps1 prepares the headless environment before running any framework:
- Resolves the toolchain — newest
LabVIEW.exeunderC:\Program Files\National Instruments\LabVIEW *, plusLabVIEWCLI.exeandg-clifrom PATH. - Refreshes PATH from the registry (
Sync-PathFromRegistry) — a Windows container’s process PATH is baked into the image ENV layer and does not pick up a VIPM-installedg-cli, so the machine/user PATH is re-read and merged; this makes a freshly-bakedg-clivisible without an image rebuild. - Reads the VI Server port from
LabVIEW.ini(server.tcp.port, default3363). - Passes
-Headlessto everyLabVIEWCLIoperation — mandatory on LabVIEW 2026+ in a Windows container or VI Server fails with-350000. - Runs each command via
cmd.exe /c, capturing exit code and output; on a failed UTF/LUnit run it echoes the LabVIEW CLI session log (the console error is generic) and records a tooling finding.
The output contract
The container emits raw JUnit XML; the host builder normalises it. Two files matter:
<ResultsDir>/<tool>-<n>.xml— one JUnit file per tool per test-root. The report builder infers the framework from the filename (caraya*,lunit*,vi-tester*/vitester*,utf*).unit-tests/<sha>/results.json— the merged model: per-tool suites, per-case status, and asummaryof{tests, passed, failed, errored, skipped}. Each failing case deep-links the test VI (and, when derivable by stripping affixes likeFoo Tests.vi → Foo.vi, the VI under test) into the VI Browser for that revision.
The commit status is derived straight from results.json so it always matches the report:
tests == 0→ success, “No unit tests found”.failed + errored == 0→ success, “N passed”.- otherwise → failure, “N failed, M passed”.
The dashboard colours the Unit Tests badge from the passedPercent thresholds above (green at or
above 100%, yellow at or above 80%, red below 80%). When Linux results are present the cell shows the same
two-number Windows/Linux pill as Mass Compile.
Warm-container history backfill
Backfilling a long history one revision at a time would pay the multi-GB pull and the slow Windows LabVIEW
cold-start once per revision. unit-tests-backfill.ps1 instead:
- starts one long-lived container (
docker run -d … while ($true) { Start-Sleep 3600 }); - checks out each revision into a throwaway
git worktreeunderC:\wtand runs the driver throughdocker exec; docker cps each revision’s results out of the container — Windows bind-mount output is unreliable, so results are written to a container-internal dir and copied back — then builds the report on the host exactly as the per-commit job does;- is resumable — a skip-list from the deployed
gh-pagestree plus aTimeBudgetMinutescap lets a batch stop before the 6-hour limit and continue on the next dispatch.
This is the opt-in warm path from §14: reusing one container means its mutable state carries across revisions, so it is never the default.
-350053 (“missing or bad files in the operation folder”). The driver records this in
_tooling.json and the report renders an explicit “the selected container is missing this
dependency” banner instead of a bare “no tests found.” The UTF JUnit reporter in particular is
mandatory — see §15.16.5 Antidoc #
At a glance
| Driver | run-antidoc.ps1; report build-antidoc-report.py |
|---|---|
| Workflow | run-antidoc-windows-container.yml windows-2022; backfill antidoc-backfill-windows.yml |
| Trigger | push to the default branch + dispatch (no per-PR run — it’s a heavier render) |
| Output & status | antidoc/<sha>/; status CI / Antidoc |
| Dependencies | committed .github/labview/antidoc/antidoc.vipc (the CLI + wovalab_lib_antidoc_add_on_labview_project_documentation) |
What it runs (headless command)
Wovalab’s Antidoc CLI, driven through g-cli against the project’s .lvproj
(auto-detected), producing AsciiDoc with embedded diagram images:
g-cli -v --lv-ver 2026 --timeout <ms> antidoc -- `
-addon lvproj -pp "<project.lvproj>" -t "<title>" -out "<docdir>"
The tool is invoked as the g-cli tool antidoc (renamed from the older antidoccli) with
-addon lvproj, so the LabVIEW-project documentation add-on must be baked into the worker alongside
the CLI. Four implementation details make it work headless:
- Headless via
LV_RTE_HEADLESS=1. g-cli launchesLabVIEW.exedirectly (notLabVIEWCLI -Headless), which NI’s container otherwise brings up at the activation wizard so the tool VI never runs. NI’s global headless override makes every launch headless and activation-free — which is why the LabVIEWCLI activities always worked but Antidoc didn’t until this was set (in the base image, and re-set by the runner as a fallback). - Registry PATH refresh — merges the Windows registry PATH so a VIPM-installed
g-cliis visible without an image rebuild. - Generous
--timeout(10 min, overrideANTIDOC_CONNECT_TIMEOUT_MS) so a cold container launching LabVIEW and loading Antidoc’s large VI hierarchy doesn’t trip the connect handshake (“No connection established with application”). - Always emits a safety-net report even on failure, so the dashboard never links to a 404.
Runner composition
docker run --rm `
-e "GITHUB_REPOSITORY=${{ github.repository }}" -e "GITHUB_SHA=${{ steps.sha.outputs.sha }}" `
-v "${{ github.workspace }}:C:\workspace" `
$Env:LABVIEW_CONTAINER_IMAGE `
powershell -NonInteractive -ExecutionPolicy Bypass `
-File "C:\workspace\.github\labview\run-antidoc.ps1" `
-WorkspaceRoot "C:\workspace" -ReportDir "C:\workspace\ci-out\antidoc" `
-LabVIEWPath "...\LabVIEW.exe"
The report
The report page is a two-pane in-browser viewer: a file navigator (main document, its include::
sections, every rendered image) beside a pane that shows the fully rendered document by default —
Asciidoctor.js resolves Antidoc’s includes (fetched up front) and pins imagesdir at the
deployed image folder — or raw source / an image preview on demand. If the renderer CDN is unreachable the
raw source stays visible and every file remains downloadable.
Dependency intent is the committed .github/labview/antidoc/antidoc.vipc. As with
ci-tooling.vipc, this file is authoritative and must remain VIPM-openable; optional JSON metadata
may assist tooling but does not override the committed VIPC.
16.6 VI Snapshots & VI Browser #
At a glance
| Drivers | build-snapshots.ps1 (orchestrator) → render-snapshots.ps1 (per-VI); gallery build-gallery.py; 2.0 engine toimages / lvctl toimages |
|---|---|
| Workflows | vi-snapshots.yml windows-2022; 2.0 vi-snapshots-json.yml Linux / vi-snapshots-json-windows.yml Windows |
| Output & status | vi-snapshots/ (content-addressed by-blob/<ab>/<blob>.html|.json); status CI / VI Snapshots |
This is the one place that deliberately reuses a warm container. Snapshots are content-addressed by git blob SHA, so each unique VI is rendered exactly once — ever — and reused across every commit that contains it. If an older revision references a snapshot or report document not yet published, the VI Browser and framed report viewer show an in-page missing-document state with a Populate history link instead of framing a GitHub Pages 404.
The warm-container pattern
Both phases start one long-lived container and push every VI through it with docker exec —
no per-VI or per-commit container churn:
docker run -d --name <name> ... $Image powershell -NoProfile `
-Command "while ($true) { Start-Sleep -Seconds 3600 }"
docker exec <name> powershell -File C:\ops\render-snapshots.ps1 ... # per worklist entry
Phase 1 — HTML snapshots and Windows 2.0 frames Windows
Orchestrated by build-snapshots.ps1, which starts a single long-lived container
(docker run -d … while true sleep) and renders many commits through it via
docker exec calling render-snapshots.ps1 — no per-VI or per-commit container
churn. A TSV worklist (<blob>\t<rel-path>) drives rendering;
render-snapshots.ps1 skips any blob already present in the deployed
by-blob store (seeded from gh-pages) and writes a placeholder HTML for any VI that
fails to render, so the gallery is never broken by a single bad VI. When
config.viBrowser.positionAware includes windows and the bundled
PrintToImagesJson operation is available, the same workflow also writes Windows 2.0 frame
JSON beside the HTML as <blob>.windows.json. build-gallery.py emits the
per-commit manifest.json (VI → by-blob HTML), the rolling commits.json, and the
Windows JSON index used by the in-place browser.
Phase 2 — position-aware JSON (VI Snapshots 2.0) Windows Linux
Phase 2 is the separate VI Snapshots 2.0 capability, selectable on Windows and Linux (the classic gallery above is Windows-only). The vi-snapshots-json.yml (Linux) and vi-snapshots-json-windows.yml (Windows) workflows render
position-aware frame JSON for the in-place browser. The render engine is baked into the Linux worker image (and runs in the stock NI image on Windows) — a Go batch runner
(main.go) reads the same TSV worklist and shells out to lvctl toimages — a native
Go VI-Server TCP client that launches LabVIEW under Xvfb, captures the front panel and block diagram as
positioned frames, and emits a JSON array. Output is written to
by-blob/<ab>/<blob>.json, deployed alongside the 1.0 HTML so any VI without JSON
gracefully falls back to the classic view.
Both 2.0 renderers auto-run on every new revision via a workflow_run trigger after the snapshot pipeline finishes, each behind a gate job that reads config.viBrowser.positionAware: the Linux workflow runs when it includes linux, the Windows workflow when it includes windows (the configurator default). Because renders are content-addressed by git blob SHA, the worklist skips every VI whose blob already has a render — unchanged VIs reuse their existing frames with no work — and queues only the new/changed blobs, so a VI a revision actually changed gets a fresh render automatically.
The browser now remembers its viewer mode and 2.0 platform choice in local storage, defaults to 2.0 when a VI has a 2.0 render (preferring the Windows render when both platforms exist), and shows dismissible coaching tip strips for the block-diagram toggle and the case/event selector arrows. The 2.0 auto-default is now enforced on open whenever a 2.0 render exists (unless the URL explicitly pins viewer/plat2). Those tips are browser-local too: dismissing one with Never show this again hides it only in that browser profile, and the dismissal keys are versioned so a new coaching rollout can deliberately surface the tips once more for everyone.
17. Report data contracts #
Each capability writes a small, stable JSON document that the dashboard and the shared header consume. The most useful ones to know when extending or debugging:
| File | Produced by | Read by |
|---|---|---|
masscompile/<sha>/summary.json | Mass Compile report builder | Dashboard cell (pass %, counts) |
vi-analyzer/<sha>/summary.json | VI Analyzer report builder | Dashboard cell |
vi-analyzer/<sha>/linux/summary.json | VI Analyzer report builder (Linux) | Dashboard cell (Windows / Linux split) |
vidiff/<segment>/<platform>/vidiff/changes.json | VIDiff | Dashboard + VIDiff report |
unit-tests/<sha>/results.json | Unit-test report builder | Dashboard + report |
builds/<sha>/summary.json | Builds report builder | Dashboard cell (succeeded/failed split) + report |
builds/<sha>/summary.json | Builds report builder | Dashboard cell (succeeded/failed split) + report |
vi-snapshots/<sha>/manifest.json | Snapshot gallery builder | VI Browser |
dependencies/index.json | Dashboard publisher | Dependencies page install-time inventory |
workers/<platform>/<version>/manifest.json | Worker image workflows | Dependencies page installed-package status |
catalog.json (root) | Installer / integrate-deploy | Version badge, What's New |
A per-revision report is considered "available" when its summary.json exists at
<prefix>/<sha>/. The header's re-run action ("Re-run analysis", "Regenerate report",
etc.) opens the dashboard's Populate history chooser pre-scoped to exactly the report on
screen — that activity set to Re-run, every other activity to Skip, and the revision
scope narrowed to the viewed commit (resolved via currentRevisionSha(), with the platform when the
activity is platform-split). On the dashboard the chooser opens inline; on a report page it opens in a floating
overlay iframe that loads the dashboard with ?lvci-populate=1&lvci-embed=1&cap&sha&platform
— embed mode hides the dashboard chrome so only the dialog shows, and the user stays on the report page
(the framed dashboard posts lvci:hist-close when the dialog closes, dismissing the overlay).
Queuing dispatches the capability's workflow with inputs.commit_sha; the dashboard's optimistic
queued bridge keys on <cap>|<sha>. The pre-select matches the viewed revision against
the dialog's history via histIdxIn(HIST, sha). These conventions are codified in the
DOCTYPES table in lvci-header.js — adding a new report type is a single entry
there plus emitting the matching window.LVCI config from its generator.
18. Versioning & updates #
release.yml fires when catalog.json changes on main. It cuts an
immutable tag (v3.24.5) and moves the major/minor aliases (v3, v3.24)
so consumers pinned at @v3 receive improvements automatically, while consumers pinned at an
exact version never change unless they act. Release notes are generated from
history.releases[0].
Clients update in one of two ways:
- One-click "Update now" (
apply-tooling-update.yml, launched from the What's New dialog) reads the manifest, follows the relocation pointer if present, fetches the latest tooling, runsinstall.py --update(preserving user config, refreshing tooling files, and pruning vendored tooling files the source has since removed so an obsolete file can't linger and break a build), and commits to the default branch with the built-in token — falling back to a PR with auto-merge if the branch is protected. - Dependabot opens a PR whenever the pinned major alias tag moves; merging it is the opt-in.
The browser Apply to New Repo flow (re-applied on top of an existing install) mirrors the same prune: when it re-vendors a tooling directory it deletes any target file the source no longer ships, except the consumer's own preserved config — so re-applying after a file is removed upstream stays in sync with Update now and never leaves an orphan (e.g. an obsolete Go source) that breaks a build.
reconfigure.yml backs the Configure Pipeline dialog: it rewrites
.github/labview-ci.yml from a workflow_dispatch form and opens a reviewable pull
request rather than committing directly.
Release channels (Dev / Beta / Stable)
Every published build is a Dev build by default (the tooling ships many a day). The
maintainer can then mark a build up two tiers: Beta (a release candidate — “might
be good enough”) and Stable (a release, ready for clients). The catalog carries this as
two optional top-level pointers — betaVersion and stableVersion — plus a
"beta": true / "stable": true flag on each marked release entry. Marking never builds
anything: it blesses an already-published, immutable v<version> release, and
release.yml force-moves a rolling beta and stable git tag to the marked
commits.
The install and What's New dialogs offer a cumulative channel picker — Release only
(stable), Release + beta (beta), or Release + beta + dev
(dev) — stored per browser in lvci_install_channel /
lvci_update_channel. The header's update indicator and the What's New compare resolve the channel
target the same way: dev = latest published; beta = newest of
betaVersion/stableVersion; stable = stableVersion.
Everything gracefully falls back to the latest published version while a tier hasn't been marked yet. The
install dialog fetches the target version's own catalog so its version, capabilities, and vendored files are
all consistent at the tag being installed.
The maintainer marks builds from the per-release Mark as beta / Mark as stable
buttons on their own source repo's What's New page. They dispatch promote-release.yml (owner-only
— GitHub requires Actions: write on the source repo) with a tier input, which
verifies the v<version> tag exists, runs the catalog/source-sync gate, edits the catalog via
promote-release.py --tier (set the pointer, flag the entry, bump the version), and commits to the
default branch; the same buttons un-mark to roll a channel back to the previous build in that tier.
Relocation pointer
.github/labview-ci/source.json lets the project migrate (for example to an official NI repo)
without breaking installs: if it names a different repo than the current one, the bootstrappers and the
update workflow follow it automatically, and a redirected update re-pins the consumer's manifest to the new
home.
19. Client discovery #
The Clients page is populated by discover-clients.yml / discover-clients.py, which
run only in the source repository on a daily schedule (or on demand). Discovery is
deliberately pull-based — your repo is never modified and never phones home. It queries
GitHub's public search index for three signals an install necessarily leaves behind:
- the
labview-cirepository topic (the primary, structured signal); - this repo's slug as
source.repoin a client'scatalog.json; - a
uses:reference back to this repo in a client's workflows.
Every candidate is verified (public, and catalog actually points back here) before listing, and only public metadata GitHub already exposes is shown. A fork is listed when it is a genuine install (a fork of another project that added this stack); a fork of this framework repo itself is not, since it carries our own catalog verbatim. Private repositories are never indexed or listed. See the FAQ discovery entry for the privacy details.
20. Extending the system #
Adding a capability is intentionally cheap. Using "Unit Tests" as the worked example:
- Write the workflow(s) — e.g.
unit-tests-windows-container.yml(and a Linux variant if applicable), following the pull-image →docker run --rm→ host report → deploy → post-status shape. - Write the driver + report builder — e.g.
.github/labview/run-unit-tests.ps1andbuild-unittest-report.py, emitting asummary.json/results.jsonunder<prefix>/<sha>/. - Add one catalog entry — id, name,
status,supportsOs,requires,statusContext, and thefilesmap. The configurator, installer, and dashboard pick it up generically. - Register the report type — add a single entry to
DOCTYPESinlvci-header.jsand emit the matchingwindow.LVCIfrom the report generator so the header's revision picker, regenerate action, and status text work.
No edits to UI rendering, installer logic, or the dashboard generator are required — the catalog and the
DOCTYPES/statusContext conventions carry the wiring.
21. Security model #
- Least-privilege tokens. Each job declares the minimal permissions it needs
(
contents: writefor report deploys,statuses: writeto post results,packages: readonly where a custom GHCR image is pulled). The built-inGITHUB_TOKENcovers the common path; a separateTOOLING_UPDATE_TOKENis only requested when an update needs to touch workflow files. - Your images, your registry. Worker images are packages on your own GHCR, inheriting your repo's visibility. The NI base layer comes from NI's public registry. Nothing is stored on third-party infrastructure.
- No phone-home. Discovery is pull-based from the source repo only; consumers transmit no telemetry.
- Reproducible, auditable builds. Content-addressed image tags and the published worker manifest mean you can verify exactly which packages a given run used.
22. Constraints & gotchas #
- Concurrency is per account, not per repo. GitHub's job-concurrency limit is shared across all your repositories, which is why the per-repo runner cap defaults conservatively.
- Unit Tests & Antidoc are Windows-only today (they depend on baked VIPM tooling).
- First container pull is slow. The worker image is multi-gigabyte; GitHub-hosted runners re-pull every job. A self-hosted runner with a warm Docker layer cache is the cure.
- LabVIEW 2026+ needs
-Headlessfor Windows container operations, or VI Server raises-350000. - Search-index lag. A brand-new install can take up to a day (plus GitHub's own indexing delay) to appear on the Clients page.
23. Workflow & runner reference #
Every job in this system runs on exactly one of two GitHub-hosted runner images, and the choice is mechanical: a job runs on windows-2022 only if it drives LabVIEW inside a Windows Docker container; everything else — orchestration, gating, Pages deploys, releases, Linux containers, discovery, and maintenance — runs on ubuntu-latest, which is faster to start and cheaper. The lightweight decision/publishing work is deliberately kept off the Windows runner so the slow, expensive Windows image only spins up for the one thing that genuinely needs it.
windows-latest now resolves to Windows Server 2025, whose Docker daemon is broken for these
LabVIEW containers. Every Windows job pins windows-2022 explicitly.Which runner OS a job uses
Activity fan-out — the reusable workflow gates every job
labview-ci.reusable.yml is where repo settings decide which jobs run. A single config
job (Ubuntu) reads .github/labview-ci.yml once and exposes capability + platform flags; a
gate-images job (Ubuntu) then blocks the dependency-consuming activities until the worker image
for this commit is rebuilt. Each activity job runs only when both its capability flag and its platform
flag are true — toggling either in Configure Pipeline turns the job on or off with no workflow
edits.
Image build — gate, coordinate, then the heavy lift
The worker image build (build-labview-image.yml, “Build LabVIEW CI Image”) is a
three-job run that bakes project dependencies — it is distinct from the
copy-labview-image.yml install seed described earlier, which only copies the base. It runs on an
explicit dispatch (the dashboard’s “Review & update dependencies” dialog or a manual run),
the monthly base-image cron, or a push that changes a
monitored .vipc / .dragon file. The gate job enforces this
opt-in: on a push it rebuilds only if a changed dependency file is flagged monitor: true in
.github/labview-ci.yml, and skips otherwise. The staging step follows the same list: after a monitor list exists, only monitor: true entries are baked; before any list exists, a manual dependency build discovers all project .vipc / .dragon files so the fresh-repo default selections can be applied. The two Ubuntu jobs decide
whether to rebuild and safely when; the final build-and-push job always runs on
windows-2022 (it needs the Windows Docker daemon to bake the dependency layer), falling back to a
plain crane copy of the base on that same runner if a dispatch declares no dependencies at all.
ubuntu-latest (Linux containers
build on a Linux runner). The install seed is its own workflow, copy-labview-linux-image.yml, which
the installer dispatches to crane-copy the shared LCWC Linux base (…-labview:linux-2026)
into the client's own package — a fast registry copy, not a rebuild, so a Linux install never scales with
the number of .vipc / .dragon files. build-labview-linux-image.yml is
bake-only: a project dependency change does not rebuild the worker by default
(the “dependencies need to be installed” banner prompts an on-demand update), and a push rebuilds
automatically only when it changes a monitored .vipc — exactly the opt-in
behavior of the Windows worker. It also accepts the same update_mode dispatch input
(replace rebuilds the moving linux-latest/linux-<year> tags in place;
new publishes a fresh <tag>-deps-<date> tag alongside without moving them).
(The Linux Beta builds directly on ubuntu-latest with no separate gate; the VI Browser 2.0 render engine is now baked into the Linux worker, so there is no separate render image.)build-labview-image.yml for Windows and/or
build-labview-linux-image.yml for Linux, each with the selected update_mode, and its
live progress bar tracks every dispatched run at once. The deps-pending.json contract drives the
defaults: the dashboard compares each project VIPC’s packages — and each installed
capability VIPC (e.g. Antidoc), limited to the platform(s) that capability’s worker runs on
(Antidoc = Windows) — against the per-platform baked
worker manifest, so its containers array lists windows and/or linux
independently (a Linux gap is reported only when a real, non-base Linux worker manifest resolved for the
revision), and vipcs is the union of the files missing packages on either worker. The dependency
table evaluates each column the same way: a package that is monitored for a platform but not
yet baked into that worker shows as missing in that column (Linux is evaluated exactly like
Windows), while a package that is simply not monitored for the platform stays N/A. Package matching is
version-tolerant — a baked build satisfies a differently-versioned VIPC request of the same
package (VIPM installs one version per package) — and each VIPC file’s header row rolls the same
per-package result up into an N missing / installed pill, so you can see which file is
short a package without expanding it. A push that adds or changes
any .vipc/.dragon file now rebuilds the dashboard as well, so a newly declared —
but not-yet-baked — package surfaces as missing and the “dependencies need installing” banner
appears immediately, without waiting for a worker build or a commit status.Build LabVIEW CI Image / Build LabVIEW CI Image - Linux) is
in the dashboard’s workflow_run triggers, so finishing a container update rebuilds the
dashboard and regenerates deps-pending.json against the freshly published worker manifest —
the “dependencies need installing” banner clears on its own once the update lands, with no manual
refresh. (Publishing the worker manifest to gh-pages does not itself trigger a rebuild; the
build workflow completing is the signal.) VIPC files under .github/ are treated as
internal tooling — the build’s “Stage repo VIPC files” step never bakes them, so they
are shown as N/A on the worker columns rather than as a false missing dependency.Complete runner inventory
Every workflow in the repository, the runner(s) it uses, what triggers it, and the conditions under which it runs or is skipped. windows-2022 = Windows container work; ubuntu-latest = everything else.
Container activities
| Workflow | Runner | Triggers | Runs when / skips |
|---|---|---|---|
| Mass Compile — Windows Container | windows-2022 | PR / push on VI source (**.vi/.ctl/.lvproj/.lvlib/.lvclass, excl. .github/), dispatch | Compiles every VI; skipped when only tooling under .github/ changed. |
| Mass Compile — Linux Container | ubuntu-latest | same as above | Linux counterpart; deploys under masscompile/<sha>/linux/. |
| Run VI Analyzer — Windows Container | windows-2022 | PR / push VI source, dispatch | Full .viancfg suite incl. per-path rule subsets. |
| Run VI Analyzer — Linux Container | ubuntu-latest | same as above | Linux counterpart; deploys under vi-analyzer/<sha>/linux/. |
| VIDiff Report — Windows Container | windows-2022 | PR / push **.vi/.ctl, dispatch | Diffs changed VIs; runs on the bare NI image (no custom worker). |
| VIDiff Report — Linux Container | ubuntu-latest | PR / push **.vi/.ctl, dispatch | Linux counterpart. |
| Run Unit Tests — Windows Container | windows-2022 | PR / push (VIs, **.lvtest), dispatch | Windows-only (depends on baked VIPM/UTF tooling). |
| Run Antidoc — Windows Container | windows-2022 | push to default branch, dispatch (no PR) | Windows-only; heavier doc render, so no per-PR run. |
| VI Snapshots and VI Browser | windows-2022 | push VI source (excl. .github/), dispatch (head / backfill) | Renders HTML snapshots + Windows 2.0 frames in one warm container. |
| VI Snapshots JSON (VI Browser 2.0, Linux) | ubuntu-latest | workflow_run after snapshots, dispatch | Gate job decides; auto-runs only when positionAware includes linux. |
| VI Browser 2.0 render (Windows container) | windows-2022 | workflow_run after snapshots, dispatch | Gate job decides; auto-runs only when positionAware includes windows (the configurator default). Additive Windows frames via COM in the stock NI image. |
Image builds
| Workflow | Runner | Triggers | Runs when / skips |
|---|---|---|---|
| Build LabVIEW CI Image (Windows worker) | ubuntu-latest gate + coordinate, windows-2022 build | dispatch (dashboard “Review & update dependencies” or manual), monthly cron, and a push that changes a monitored .vipc/.dragon | Dependency bake. On a push the gate rebuilds only if a changed dependency file is monitor: true; otherwise it skips. Always builds on windows-2022; with no dependencies declared it falls back to a plain crane copy of the base. |
| Copy LabVIEW CI Image (install seed) | ubuntu-latest | dispatch (the installer dispatches it on a fresh install) | Seeds a fresh worker by copying the LCWC base into the repo’s GHCR with crane — no LabVIEW build; the install-time counterpart to the dependency build above. |
| Build LabVIEW CI Image — Linux | ubuntu-latest (gate + build) | dispatch (dashboard or manual), monthly cron, and a push that changes a monitored .vipc | Dependency bake (mirrors the Windows worker); also bakes the VI Browser 2.0 render engine into the worker. On a push the gate rebuilds only if a changed .vipc is monitor: true; otherwise it skips. |
| Copy LabVIEW CI Image — Linux (install seed) | ubuntu-latest | dispatch (the installer dispatches it on a fresh install) | Seeds a fresh Linux worker by copying the LCWC Linux base into the repo’s GHCR with crane — no build; the Linux counterpart to the Copy workflow above. |
| Bake VIPC Windows Base Image | windows-2022 | dispatch only | Proof-and-promote: verifies a VIPC installs before pushing tags. |
Publishing & release
| Workflow | Runner | Triggers | Runs when / skips |
|---|---|---|---|
| CI Dashboard — GitHub Pages | ubuntu-latest | push page assets, status event, workflow_run completions, dispatch | Latest-wins rebuild; reacts to every capability's status with no enumeration. |
| Integrate Configurator — Pages | ubuntu-latest | push configurator / catalog.json, dispatch | Publishes Apply / Configure / What's New + catalog. |
| VIDiff Deploy — Pages + PR Comment | ubuntu-latest | workflow_run after VIDiff reports | Deploys the diff and comments on the PR (runs even on failure). |
| Catalog Source Sync | ubuntu-latest | PR / push to catalog, docker, or build workflow; dispatch | Fails if installer file lists drift from the source-owned worker files. |
| Release | ubuntu-latest | push catalog.json on main, dispatch | Tags v<x.y.z> and moves the v<major>/v<major.minor> aliases. |
Maintenance & diagnostics
| Workflow | Runner | Triggers | Runs when / skips |
|---|---|---|---|
| Discover Clients | ubuntu-latest | daily cron, dispatch | Source repo only (if: github.repository == source). |
| Apply LabVIEW CI update | ubuntu-latest | dispatch (the What's New "Update now") | Runs install.py --update; commits to default branch or opens a PR. |
| Reconfigure LabVIEW CI | ubuntu-latest | dispatch (Configure Pipeline) | Rewrites labview-ci.yml and opens a reviewable PR. |
| VIDiff Backfill — Windows Container | windows-2022 | dispatch | Walks VI history rendering diffs through a warm container; resumable. |
| VIDiff Backfill — Linux Container | ubuntu-latest | dispatch | Linux counterpart of the backfill. |
| Example project CI (composite-action demo) | ubuntu-latest config + windows-2022 activities | dispatch | Dogfoods the local ./actions path against the example project. |
| toimages COM probe (diagnostic) | windows-2022 | dispatch | Confirms COM-driven rendering in the stock NI image; touches nothing in production. |
windows-runner input or Actions variables. A self-hosted runner additionally keeps a
warm Docker layer cache, so the multi-gigabyte worker image is not re-pulled every job (see §22).