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 #
Existing GitHub workflows can also call actions/worker-image and
actions/unit-tests directly, without installing client configuration or vendoring scripts.
Both composites use the current source scripts from their pinned action revision. They require a
Windows container runner such as windows-2022; see
the composite example and contracts.
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.
Fork pull requests are analysis-only. Their jobs still run and upload report artifacts for
review, but they do not publish to the shared gh-pages branch because GitHub gives fork code a
read-only token. After a reviewed change merges, the main push reruns the pipeline with
repository permissions and publishes the canonical report and dashboard update. Collaborators who need
pre-merge publication should create branches in the repository, rather than use forks.
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>.json.
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. LUnit also runs on Linux. | Win Linux |
| Antidoc | Generates project documentation from the VI hierarchy using Wovalab's Antidoc CLI. | Win |
| VI Snapshots | 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.
Updates migrate the legacy snapshots-2 selection to the unified
snapshots capability, including browser reinstalls that keep current settings.
Snapshots remain selectable; updates do not enable them in clients that opted out.
The catalog's retiredFiles list identifies obsolete tooling workflows, with
ownership signatures checked before deletion. The retired build-toimages-image.yml
workflow is removed by the command-line installer; browser reinstalls offer it in the existing
stale-file cleanup preview. Unrecognized content is preserved. Command-line cleanup skips
symlinks and directories, and installation refuses source checkouts and forks carrying
source-only release or relocation files. Test installs belong in a separate scratch client.
GitLab CI
Pass --provider gitlab to the command-line installer, or choose GitLab in the browser
configurator. It copies the portable LabVIEW drivers under .github/, a native provider package
under .gitlab/labview-ci/, and a generated provider pipeline. A new repository receives a small
.gitlab-ci.yml root file that includes .gitlab/labview-ci/pipeline.yml. The browser
flow creates a GitLab merge request using a token with the api scope.
An existing root pipeline is never text-merged. It must already include that local provider pipeline; if it
declares stages, its list must contain prepare, verify, and
pages. Otherwise the installer stops before writing anything and explains the manual include.
The command-line --force option deliberately replaces the root pipeline only when that is the
operator's intention. The browser performs the same compatibility check before it opens a merge request.
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 the selected GitHub or GitLab distribution, 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: 4.17.4
source:
repo: ni/LabVIEW-CI-CD-Toolkit
ref: v4.17.4 # pinned to an IMMUTABLE tag, never "main"
distribution: # archive origin chosen by the bootstrapper
host: gitlab
repo: elijah286/ci-for-labview
ref: main
url: https://gitlab.com
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.
The nested source.distribution block records where the bootstrapper obtained that snapshot, so a
later bootstrap update can fetch from the same distribution rather than trusting a stale local installer.
Thin vs. fat installs
A thin install references the reusable workflow at a moving major tag
(uses: ni/LabVIEW-CI-CD-Toolkit/.github/workflows/labview-ci.reusable.yml@v4) 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.
GitLab-native provider
GitLab does not execute GitHub Actions syntax, so it uses a source-owned adapter instead of trying to
translate workflows at run time. The source package at
.github/labview-ci/providers/gitlab/ is copied unchanged to
.gitlab/labview-ci/. Its files.json manifest names every provider file and maps each
capability and operating system to a native GitLab template. The installer expands catalog
requires first, verifies those mappings, and fails rather than recording a capability that has no
native job. The source package is also vendored under .github/labview-ci/ so a local install can
later be re-run with --provider gitlab. Its files.json ownership manifest is copied
into the target package too, so an update prunes only files a prior provider version declared as its own (plus
the recognized dashboard-only scaffold from the original preview).
The generated pipeline.yml owns prepare, verify, and
pages stages, common job primitives, the Pages publisher, and only the selected native templates.
It exports GITHUB_REPOSITORY, GITHUB_SHA, and GITHUB_REF from GitLab
CI_* variables so existing portable report drivers keep their established contract. It makes no
GitHub API calls, posts no GitHub commit statuses, and does not depend on workflow dispatch.
| GitLab job type | Required runner | Notes |
|---|---|---|
| Windows LabVIEW jobs | Self-managed Windows shell runner with Docker | Set LVCI_WINDOWS_RUNNER_TAG when the default windows-docker tag is not suitable. |
| Linux LabVIEW jobs | Privileged Docker-in-Docker-capable runner | Uses docker:27-cli plus docker:27-dind; override LVCI_LINUX_RUNNER_TAG as needed. |
| Pages publication | Any Linux runner that can run the Python Alpine job | GitLab's Deploy › Pages view is the authority for the final URL. |
Native Windows jobs clear GIT_CLONE_PATH at job scope. This isolates them from a
legacy root-level custom clone directory that a runner may reject, without changing the checkout path for
any existing project job.
Custom workers log in through CI_REGISTRY, CI_REGISTRY_USER, and
CI_REGISTRY_PASSWORD, then publish to $CI_REGISTRY_IMAGE/labview. The Windows job
publishes a base plus windows-<sha> and windows-latest worker tags; Linux publishes
linux-<sha> and linux-latest. Let a first worker build finish before a job that
needs baked dependencies consumes it. On a fresh installation, the default-branch pipeline starts that worker
because the manifest changed; for a web/manual pipeline, start the relevant lvci:worker-* job
first, then run the dependent verification job. The Linux snapshot renderer requires that custom Linux worker or
an explicit LVCI_LINUX_IMAGE; without either it records a diagnostic and skips rather than pulling
a non-existent registry tag.
GitLab Pages is durable rather than a per-pipeline report folder. The serialized pages job restores
the latest default-branch Pages artifact with its job token, merges job-local snapshot stores, stages new
reports directly below public/, and publishes the resulting site. This preserves earlier report
history despite GitLab artifacts normally being scoped to one pipeline.
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 —
now hosted at ni/LabVIEW-CI-CD-Toolkit — 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.
The manifest writer preserves project-owned config blocks verbatim, including
unitTests, container, builds, and concurrency, while
refreshing the installer-owned LabVIEW version, branch, and OS fields. An existing image name is kept
unless explicitly overridden. This prevents an upgrade from silently disabling a native Caraya runner
or dropping its dependency selection. If an earlier upgrade already removed those blocks, restore them
from the last known-good manifest; preservation cannot reconstruct settings that are no longer present.
The legacy top-level unitTests block produced by Configure Pipeline is retained too.
Its JSON settings input accepts a single-line per-tool command; the workflow quotes it
safely and the runner decodes escaped paths and quotes before token substitution. Restore the native
wrapper command as well as enabling Caraya; enabling the framework alone selects its default scaffold.
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 after qualifying main-branch pushes (the manifest, dashboard
workflow, bundled page assets, or dependency declarations), relevant workflow_run completions,
its hourly schedule, and manual dispatch. Installed repositories stage the bundled pages from their selected
immutable source.ref; the source repository stages the triggering commit instead, so a page
change can deploy before its new release tag exists.
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
LUnit runs on Windows and Linux when the worker includes LUnit CLI 1.7.1.32 or newer. Caraya, VI Tester and NI UTF have Windows runners; Linux reports list them as unsupported. Antidoc remains Windows-only. Linux unit tests support per-revision Run; history backfill remains available only on Windows.
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": [...] } ] },
"retiredFiles": { "files": [ ... ], "signatures": { "workflow-path": ["ownership marker", "build context"] } },
"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.
The GitLab provider adds a second, explicit contract beside the catalog: its
providers/gitlab/files.json file lists the copied adapter files and capability-to-template map.
This prevents a new catalog capability from silently appearing installable on GitLab before a native job has
been supplied for every applicable platform.
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.
The reusable workflow loads config with
ni/LabVIEW-CI-CD-Toolkit/actions/load-config@v4 and checks out capability tooling from
ni/LabVIEW-CI-CD-Toolkit into _lvci at the manifest's explicit
source.ref. If that ref is empty or absent, or the manifest is missing, it falls back to
v4, the major alias that follows releases promoted to stable, rather than v1-era tooling.
An explicit immutable source.ref checks out that exact release, including a newer published
release that has not reached stable. The config reader still follows @v4 independently of
that capability pin. The latest published release can be newer than the deployed stable line;
publication alone does not promote stable or advance v4 to the new release
(see §18).
The ref resolver uses Python's standard library and Git, with no runtime YAML package dependency.
Python runs with -I -S so client-root modules, PYTHONPATH, and site packages
cannot supply imports to the resolver or forge its selected ref.
It accepts manifests with plain top-level keys and a block source mapping whose
ref is a single-line Git ref, optionally single- or double-quoted and followed by a comment.
Empty refs use the default. Refs under config or source.distribution are ignored
regardless of ordering. Duplicate source keys, inconsistent indentation, invalid refs, and unsupported
source syntax such as flow mappings, aliases, multiline refs, or escaped quoted refs stop the job with
a clear error instead of selecting another ref. Reusable Workflow Checks exercises this contract on
GitHub Actions; PyYAML is installed only for the test harness.
GitLab uses the same catalog selection but emits native template includes instead of a reusable workflow.
Its workflow: rules permits merge-request, default-branch, web, and scheduled pipelines; the
visible pages job uses when: always so a failed verification job does not suppress
publication of its report or the existing dashboard history.
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 standalone actions/worker-image composite builds only a client worker layer.
Callers explicitly supply a readable, qualified Windows base-image and matching LabVIEW year;
the action never builds or promotes the shared base. It resolves that reference to a digest and uses
the same digest for hashing, Docker FROM, and the dependency build. Even a client without project declarations builds and verifies its selected baseline tooling. It verifies
that the resolved base provides windows/amd64 before building or copying.
A fresh isolated context contains the current source VIPM installer, optional source
ci-tooling.vipc, and the caller's dependency files at the source worker's
.github/labview/vipm/ path. Its Dockerfile adds explicit LabVIEW/public-repository build
arguments, an 8 GB build memory limit, and required Dragon error handling.
Worker tags hash every staged filename and byte, the build arguments, base digest, and orchestration
code. An existing exact content tag is reused; only an explicit registry
MANIFEST_UNKNOWN or NAME_UNKNOWN response permits a build. Permission, authentication, rate-limit, and
transport errors fail the action. Publishing verifies a manifest digest before moving the year tag
and optional latest tag. These are client worker aliases, separate from toolkit release
channels. Concurrent writers to the same worker package should be serialized by the caller.
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.
On GitLab, native custom-image jobs build the local Dockerfile inputs and publish the finished worker to the
project's own Container Registry rather than GHCR. They use GitLab's per-job registry credentials and tags
keyed by CI_COMMIT_SHA, with moving windows-latest and linux-latest
tags for selected jobs. Each build reads the installed catalog's qualified containers.default
release and passes its pinned NI image tag to the Dockerfile; the Windows build also passes its paired NIPM
feed and VIPM installer. Set the GitLab CI/CD variable LVCI_CONTAINER_RELEASE to another
qualified catalog release id, such as 2026q1, when a project must remain on that release.
A project can set LVCI_WINDOWS_IMAGE or LVCI_LINUX_IMAGE to consume an approved
external worker instead.
Container releases — pinned, qualified, selectable
The tooling supports a set of NI LabVIEW container releases (LabVIEW 2026 Q1 and newer),
declared in the catalog's containers block. Each release is a pinned triple — NI container
tag, NIPM feed, and VIPM installer — that moves together and is qualified before clients can
select it: the source repo builds every release's base and runs a VIPM-startup probe inside it
(publish-base-images.yml), and the shared moving tags only advance after a full dependency bake
succeeds on the new base. Nothing floats on NI's latest-* tags — NI repointed
latest-windows from 2026 Q1 to 2026 Q3 on 2026-07-27 without notice, and a floating base
pin turned that into a fleet-wide bake outage. The configurator's LabVIEW version dropdown defaults to
the newest qualified release; choosing an older release pins the repository to it via the
LCWC_BASE_IMAGE / LCWC_LINUX_BASE_IMAGE Actions variables, which every image workflow
resolves first.
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 exact-version: a VIPC pin such aswovalab_lib_antidoc_cli-3.3.0.117is not satisfied by a different installed build. Only observed installed packages count; requested VIPC declarations and copied seed-image assumptions never establish satisfaction. A leadingvin an observed VIPM version is normalized. 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, installs its supported exact VIPM requirements through the shared verified installer, then checks the final requested LabVIEW inventory). 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 snapshot 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 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 compares the requested pins against this observed inventory.
The manifest’s vipc[].packages records requirements, not proof of installation.
Linux manifests also preserve dpkg_packages and raw dpkg-query output for native package status, including the installed ni-vialinux-labview-support VI Analyzer package. 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 honor each file’s monitorOn platforms. Windows declarations 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. Linux declarations use the selected target-verified VIPM inventory, or the selected
digest-pinned LVPM inventory with locally current freshness. Both require exact name/version matches.
Unsupported native/NIPM dependencies and stale or unavailable Linux evidence remain unknown/pending.
Files monitored on both platforms are satisfied only when both platforms are satisfied.
Complete dependency verification
The Linux VIPM worker uses the official 26.3.2 (4056) Debian installer, matching the legacy OGP compatibility fix required on Windows. Selected Dragon files with exact public VIPM pins are converted into deterministic VIPCs by the trusted staging helper. The input record preserves both the original source hash and generated VIPC hash. Unsupported NIPM requirements, custom feeds, embedded VIPC references, conflicting project pins, and mismatched LabVIEW targets fail before the build.
VIPC XML is parsed structurally, including compact single-line exports. The final Windows and Linux inventories must satisfy every effective tooling and project pin; explicit project pins take precedence over the tooling baseline. A successful install command alone is insufficient. The native validation workflow covers both example dependency files, the tooling baseline, and installed Antidoc dependencies on Windows. The captured VIPM inventories also pass through the production dashboard and Dependencies-page functions: every package cell, VIPC/Dragon status pill, and pending indicator must clear. It retains build and fresh-container evidence without publishing worker images. Its result does not establish that a published worker is active; publication and dashboard verification are separate steps.
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.
GitLab follows the same throwaway-container model. Its Windows shell runner invokes Docker directly, while its Linux job talks to the Docker-in-Docker service. Job artifacts carry raw output and staged reports to the serialized Pages publisher; there is no GitHub status or branch-push deploy step in that path.
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 #
Windows and Linux builds keep trusted tooling at reviewed HEAD while optionally reading committed dependency
inputs from dependency_sha, a full ancestor SHA. The expected_sha guard rejects a moved
reviewed HEAD. Selected configuration and dependency bytes participate in worker identity; staging rejects
escaping paths, symlinks and collisions. Linux uses vipm/linux-worker.py with the existing LVPM
planner for selection and safe input validation. The catalog explicitly ships this helper to Linux-only clients;
Windows helpers ship in the existing VIPM directory payload. These are installer/update payload changes, not
machine-local repairs.
Both VIPM installers capture structured, target-bound installed inventory and verify required project pins before
publishing active worker aliases. A requested file or a successful command alone is not installed-package
evidence. Every selected tooling and project requirement must be observed at its effective version;
only an empty dependency set can retain unavailable inventory without failing.
Separately tagged new workers do not move active aliases or the default Pages pointer. Registry
alias updates are not atomic; Windows attempts rollback on failure. Native Windows/Linux LabVIEW, VIPM,
registry publication and activity checks still require CI qualification; local fixtures do not establish them.
The revision picker compares that revision's declarations and monitoring choices with its selected worker. Saving in a client submits a configuration proposal; it is not applied until reviewed and merged. Windows and Linux checkboxes preserve the other platform's selection. The tooling source intentionally disables client Save on both platforms and explains that monitoring changes require a source PR; forks remain ineligible for the client installer. Existing LVPM candidate verification, digest pins and explicit activation are unchanged.
native-dependency-validation.yml runs only on its dedicated validation branch or manual dispatch.
Windows x64 VIPM and Linux x64 VIPM/LVPM jobs build the candidate with the real example VIPC and Dragon files,
the tooling baseline, and the Windows Antidoc capability. They retain source
hashes, build logs and installed-inventory evidence as artifacts. The jobs have read-only repository/package
permissions and never publish images, active aliases or Pages pointers. A passed native build verifies the
selected fixture and recorded scope, not every project or LabVIEW activity; failures retain diagnostic artifacts.
The fresh Windows container creates a genuine public Git checkout at the exact candidate commit before invoking VIPM Community Edition, verifies the staged VIPC hash, and deletes the baked inventory before querying again. Linux installation does not treat the original LabVIEW process exiting as proof of failure: after successful install commands, target-bound inventory must still verify every required exact package pin. Failed queries, missing packages, wrong versions and wrong LabVIEW targets remain fatal.
Windows best-effort tooling retries preserve project VIPC and Dragon version overrides, so a failed initial attempt cannot reinstall an older baseline pin over a required project dependency.
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:
Required Windows Dragon dependencies
The GitHub Windows push gate and staging share safe YAML monitoring semantics: quoted platform scalars,
flow/block lists, quoted paths and inline comments are supported; monitor must be a YAML boolean.
Invalid YAML or unsupported selection shapes fail explicitly. Explicit empty lists and platform opt-outs
exclude declarations without parsing them; no configured list retains legacy project discovery. The isolated
Python 3.13 workflow host installs PyYAML==6.0.3 and loads the trusted static TOML parser by explicit
path, excluding client working-directory and PYTHONPATH import/startup overrides. Declaration
files are never executed.
Every Windows producer stages selected source bytes and a normalized schema/platform/year/bitness/exact-root
plan. The composite worker-image action keeps its explicit flat deps-dir contract; GitLab Windows
keeps project discovery outside .github, rather than applying GitHub monitoring flags. Worker
identities bind source, plan, helper and installer
bytes with target inputs. This hash binding provides integrity, not signing or provenance. Missing, changed,
escaping or linked sources, unsupported install-affecting settings and contradictory pins fail closed.
Dragon-only and mixed VIPC/Dragon builds invoke the shared installer once, without an assumed
dragon apply executable. The container validates source hashes and the requested target before
bootstrapping VIPM.
Required Dragon roots cannot be bypassed with optional VIPC policy or
VIPM_ALLOW_MISSING_PACKAGES. After all later project/tooling installations, final inventory must
explicitly identify the requested LabVIEW year/bitness and contain every exact root/version. Later upgrades,
missing packages, wrong targets, command failures and unreadable/dead-engine inventory fail the build; the
installer does not choose newest versions or rewrite pins. -BootstrapOnly retains its early
no-package behavior.
install-dragon.ps1 hooks are retired. Nonempty
NIPM/feed requests, nonempty project.vipc, unsupported project settings and unpinned/ranged versions
fail rather than being delegated or ignored. Migrate to supported exact numeric VIPM string or inline
{ version = "..." } declarations and select VIPCs separately before updating. This is not a
backward-compatible patch. Local or hosted mock contracts do not prove a vendor bake succeeded: a real
Windows bake and final inventory remain required before claiming a published worker contains the repaired
dependencies.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). It clears the
runtime-only LV_RTE_HEADLESS default while VIPM starts, reads each VIPC's config.xml,
gives both VIPM operation and Desktop-liveliness checks a 900-second CI budget, and installs its
name@version package specs rather than calling VIPM's retired
apply_vipc verb. The hook forces VIPM phase progress in CI logs and fails the image build if a
package action fails; it publishes 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”.VIPM_NONINTERACTIVE controls VIPM CLI prompts; it does not suppress a third-party package's
post-install VI. The worker never patches or skips those scripts. A live GitLab Linux validation of
wovalab_lib_asciidoc_for_labview@1.12.1.492 downloaded and extracted the package, then timed out
at Executing PostInstall Custom Action. Its upstream shared component is a call-for-action dialog.
The worker records the VIPM phase plus LabVIEW/VIPM/Xvfb diagnostics and fails closed, so no incomplete image is
published. A package intended for Linux CI must make its install action headless-compatible, or be resolved
upstream before it can be baked.GitLab custom-worker jobs stage repository .vipc and .dragon declarations into the
same worker build context and use these Dockerfiles. The Linux Dockerfile declares its pinned NI base tag before
every build stage, so Docker can resolve it in the final worker FROM; the provider does not yet expose
the GitHub Dependencies page's interactive review/update policy. Dependency, catalog-release, and installed
custom-worker-template changes trigger the matching worker job; it can also be triggered or retried in GitLab.
The Windows template invokes the shared trusted Dragon staging helper with isolated Python (3.11+ with
tomllib, or a compatible interpreter with tomli installed in its isolated environment),
validates its configured LVCI_LABVIEW_VERSION against the selected base, hashes staged inputs
and target, and checks native
staging/build/push exit codes. A required failure stops subsequent publication.
- 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. - Keep package install actions headless — a package's own post-install VI is outside the
CLI prompt contract. The Linux hook forces
--show-progress --verboseso its phase is visible, reports focused process/display diagnostics on failure, and preserves the nonzero result instead of publishing a partial worker. - Verify both Windows VIPM executables before required installs — the shared
base/dependency bootstrap defaults to the official
VIPM 2026 Q3 f2/build 4056 installer.
The vendor release notes
identify the legacy package-file
Missing Name in Package sectionfix; this is not proof of container compatibility. The unmodified payload embeds CLI version26.3.2and Desktop file version2026.3.2.4056. The hook logs the actual selected paths, probes CLI--versionwithin 30 seconds, reads Desktop product/file identity, and requires Windows-trusted James Kring, Inc. Authenticode signatures on both. A missing/old executable triggers one verified upgrade and full rediscovery; stale preferred paths, unverifiable binaries, or an already-running Desktop at a different path fail closed. A supported newer pair is not downgraded. Mixed newer/old installations require an explicit paired upgrade rather than risking replacement of the newer half. - Keep installer overrides authoritative and bounded — when an upgrade is needed,
VIPM_INSTALLER_URLmust be HTTPS without embedded credentials and supply a Windows-trusted vendor-signed VIPM installer at least26.3.4056; both installed executable floors still apply. Custom URLs are not silently replaced; unreachable, old, untrusted, or unverifiable installers fail the build. A supported preinstalled pair leaves the URL unused. Downloads time out at 300 seconds; installation at 1200 seconds, with/exenoui /qn /norestart REBOOT=ReallySuppress. Nonzero and reboot-required (1641/3010) exits fail closed; timeout cleanup targets only the invoked process tree and temporary files. Signature trust, not an unpublished hash, checks integrity. Base Docker builds use the same hook's-BootstrapOnlymode, which exits before environment/headless changes, memory checks, Git/settings setup, engine/LabVIEW launches, or package operations. Both base-build workflows hash the bootstrap's actual file bytes into the immutable cache key. The catalog's2026q3.vipmInstallerUrlalso points to 4056 so catalog-driven overrides cannot silently retain 3954; other release records are unchanged. Old explicit overrides fail the floor when an upgrade is needed, without implying other-release qualification. - Require final Windows runtime evidence — static payload inspection and AST/mock tests do not prove legacy OGP installation. A reviewed real bake must verify required Antidoc/palette and the final target inventory. Recovered-batch validation alone is insufficient: the final gate verifies every selected project, Dragon and effective tooling requirement after all installs. Project pins override baseline pins consistently during retries and final verification. Package archives, integrity verification, target identity, and exact/minimum pin semantics are preserved.
- 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=900and LinuxVIPM_DESKTOP_LIVELINESS_TIMEOUT=900) — cold first-run headless LabVIEW and package downloads exceed the CLI's short defaults during a build, whereCI/GITHUB_ACTIONSenv hints are absent. - Never run the VIPM engine under a global headless default — the worker base bakes
LV_RTE_HEADLESS=1so g-cli/Antidoc launch LabVIEW headless at CI time, but the VIPM Desktop engine is itself a LabVIEW-runtime app: under that default it runs yet never completes the CLI's startup handshake, and every operation dies atwait for VIPM startup. This silently broke every Windows dependency bake from 2026-08-01 to 2026-08-21 (NI's unannouncedlatest-windowsrepoint to 2026 Q3 landed the same week and masked it as a version mismatch). The install hook now clears the variable for its own process tree on both Windows and Linux; the baked image ENV is untouched for runtime workflows. - Give the build container enough memory — the VIPM Desktop engine's package-list refresh
alone peaks over 1.3 GB on LabVIEW 2026 Q3, and a memory-capped Windows
docker buildcontainer starves it into the identicalwait for VIPM startupwedge. The image workflows build withdocker build -m 8GB, and the install hook's memory preflight fails fast with this diagnosis when less than ~2.5 GB is visible (VIPM_ALLOW_LOW_MEMORY=1overrides). - Fail fast on a dead Desktop engine — a refresh message containing
wait for VIPM startupproves package installs will wait on the same unresponsive process. The hook restarts headless LabVIEW and VIPM once (VIPM_MAX_REFRESH_RESTARTS=1by default), then stops before package installation if the handshake still fails. A workflow rerun starts from a clean worker build instead of consuming repeated 15-minute install timeouts. - Use the modern command shape —
vipm refresh --forceis a standalone command; package installs usevipm --labview-version 2026 --labview-bitness 64 install <name>@<version>after extracting the package declarations from the VIPC. The oldinstall --refreshform andapply_vipcverb are 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. - Resolve OpenG packaging releases without losing required roots — a VIPC-normalized
exact request such as
palette@1.1can select the indexed1.1-1packaging release; an explicitly release-pinned request remains exact, and minimum-version constraints retain their meaning. Public/local selection and installed-inventory verification use the same version predicate. A missing transitive dependency may still be assumed bundled, but is not marked resolved: if the VIPC explicitly requests it later, that required root must resolve or fail the install. - Do not repair legacy packages by changing their archives — the official
jki_rsc_toolkits_palette-1.1-1.ogpmatches its repository checksum but uses legacy[Package Name]metadata. VIPM build3954rejects it withMissing Name in Package section. The vendor's 2026 Q3 f2 release notes describe the package-file compatibility fix. Resolver correction alone does not upgrade the worker's VIPM or prove legacy installation works; that requires a separately reviewed runtime upgrade and Windows bake. Required Antidoc/palette packages remain required, and archive integrity checks stay enabled. - Verify local-file recovery, not earlier retry failures — VIPM can reject a legacy
transitive
.ogpwithMissing Name in Package section, then successfully install the requested packages during per-file retries. After local-file installation, the hook queriesvipm --labview-version <year> --labview-bitness <bits> list --installedand checks every requested package ID and version. A recovered set no longer fails solely because an earlier transitive file failed. Missing or wrong-version requested packages, an unreadable inventory, or an exhausted engine recovery still fail the install; required project dependencies and UTF/Caraya essentials remain fail-closed; tooling retries may continue, but the final complete-inventory gate still applies. - Batch recovery is not a final-image pin guarantee — a later best-effort tooling install can change versions already verified for a project VIPC. Validate the final target inventory as well as each recovered batch before claiming that all project pins hold in the completed image.
- 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. Opening a revision loads its committed monitoring settings without marking them
unsaved. When explicit VIPC or Dragon lists are absent, the existing default monitors discovered files on
both worker platforms; explicit empty lists and opt-outs retain their meaning. Only changing a monitoring
checkbox requires saving and merging a configuration PR before reviewing a worker update. Updates remain
restricted to the current default-branch revision.
After a worker image is built locally, the publish workflow captures both nipkg list-installed 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 target-verified inventory cannot establish installed package coverage. A requested dependency
rebuild applies and verifies the tooling baseline as well as the selected project dependencies.
NI package coverage uses installed-package inventory only: nipkg list lists available packages and cannot establish installation. Older Windows manifests without installed-inventory provenance show unknown native coverage until rebuilt. Windows VIPM is installed through the JKI bootstrap, so a successful target-verified CLI inventory establishes its presence even without an NI package named ni-vipm. The separate JKI Dragon application is not required: the toolkit parses Dragon declarations and installs their packages using VIPM or LVPM.
When all four committed VIPCs are selected together with example/Example.dragon,
experimental.vipc uses Antidoc CLI 3.4.0.126, already established by
the authoritative Antidoc capability archive. The experimental configuration retains its ID,
selection, flags and unrelated metadata, and embeds byte-identical CLI spec/icon metadata from
that archive; the informational Antidoc JSON agrees. This explicitly reconciles a stale
3.3.0.117 declaration, not a newest-wins resolver policy. Offline planning retains
all five sources and 35 exact roots, and rejects divergent pins without emitting a plan.
Archive identity and source-membership tests guard the alignment. Planning success is not
qualification of package installation, post-install hooks, a Linux worker bake, runtime
relinking or final inventory.
Manage dependencies one platform at a time
The Dependencies page has separate Windows and Linux tabs.
Windows keeps its VIPM workflow and never offers an LVPM installer choice. Linux offers VIPM or
LVPM. Links can select a tab with ?platform=windows or
?platform=linux; switching tabs preserves the inspected revision. Keyboard users can move
between platform tabs with the arrow, Home, and End keys.
Each tab shows only that platform’s default worker, package coverage, monitoring checkboxes, and update approval. The summary uses existing platform-specific inventory and declaration evidence, not aggregate missing-package counts. Missing packages and unavailable, malformed, or stale evidence require attention only on the affected platform. An empty monitored project list is neutral, not proof of installation; satisfied package inventory is not runtime verification. Legacy Windows-only Dragon status cannot establish Linux satisfaction. Explicit per-action overrides may select a different worker from the default shown here.
For VIPM, choose Review Windows VIPM update or Linux Update container image, inspect the selected platform’s package changes, then approve the build. Advanced update options are collapsed by default. Saving monitoring choices preserves the other platform’s selections and requests a configuration pull request; merge it and refresh before building committed dependencies. Tab or revision changes cancel outstanding approval/token prompts, and in-flight reads cannot retarget a dispatch. Late Linux responses do not dismiss a new token prompt or update status after the platform, revision, or monitoring selection changes. Switching away after a build is already queued does not cancel it.
Package rows, unresolved declaration details, native inventory, and candidate provenance are available on demand rather than shown as one large checklist. The aggregate dependency banner is replaced by the platform summary on Dependencies. Other dashboard pages name the affected platform(s), link to their tabs, and label combined unresolved counts as aggregate rather than attributing them to both workers. Dependency banners render text and navigation with DOM nodes, not parsed HTML; review links allow only HTTP(S) destinations.
The Linux tooling card has one primary action: Update container image. Clicking it
checks authenticated current-HEAD configuration and the installed workflow, comparing complete
monitoring choices (including opt-outs and the other platform), defaults, and activity overrides.
A historical view with equivalent committed settings can update the current committed SHA without
changing the inspected revision, URL, or choices. Unmerged choices, mismatches, unknown evidence,
or contradictory capability platform classification stop with a specific inline prerequisite and no
build request. An accepted Save is not proof of a proposal or merge; only demonstrably committed
matching settings can proceed. Reconfigure’s omitted monitorOn with
monitor: true means both platforms; false remains an opt-out. There are no Find/Open
navigation stages, automatic updates on page load, or background retries. Source repositories and
their forks must change monitoring through reviewed source PRs, not client Save or the installer.
Legacy implicit monitoring (omitted lists, not explicit empty lists) retains a clean-current path: authenticated HEAD must equal the fully loaded revision, with unchanged complete discovery inputs, worker settings, and no pending Save. Generated inventory requires exact repository/revision provenance; live loading requires successful manifest and file discovery. Overlapping revision loads publish inventory and provenance only from the still-current load; stale responses cannot replace newer choices. Historical or edited implicit settings cannot use this path. Unsupported or malformed configuration shapes remain unverified and do not dispatch.
The explicit update intent retains its page context across authentication, reads, VIPM approval,
and submission. Changes to platform, inspected revision, monitoring, or tooling invalidate it.
VIPM approval names the exact committed target and revalidates it before dispatch; LVPM builds
a candidate only. The request uses the default branch and expected_sha, not an
undocumented commit-SHA dispatch ref. This optional input accepts exactly 40 hexadecimal characters,
normalizes their case, and leaves existing manual/push/schedule behavior unchanged when empty.
Both Linux workflows reject an unexpected event SHA before checkout/setup/build, and checkout
is pinned to the run SHA. Older installed workflows lacking this
safeguard require a tooling update and are not dispatched. A moved HEAD is rejected without retry.
Windows behavior and separately reviewed activation/rollback are unchanged.
Dragon rows distinguish file present and parsed, file absent, parse error, and inspection unavailable from package status. Expand Declared packages to see each manager, package name, exact declared version, and the selected platform’s measured worker version/status. An unresolved package does not mean its declaration file is missing. Stale or unavailable evidence remains unverified; Windows-only evidence cannot establish Linux installation. This reporting does not install missing packages or repair runtime failures. LVPM satisfaction covers package files, not execution of CI activities.
Optional Linux LVPM environments
The Dependencies page’s Linux tab offers VIPM (the existing default) and LVPM
as alternative installers for Linux. Windows behavior is unchanged. Switching tooling views in the page
is exploratory, not a pipeline change: review the declarations, build a candidate, inspect its verification,
and explicitly request activation through a Reconfigure pull request. While exploring LVPM, the current
VIPM update controls are hidden; returning to the VIPM view does not change the active worker. If LVPM
is already selected, returning to the existing default selection requires the explicit reset PR under the
collapsed Current selection and rollback section. Reset removes only linuxUse,
retaining container.use and per-action overrides; a base or custom image may be selected,
so reset does not guarantee VIPM. The retained default ref is shown before requesting reset.
Never apply the client installer to
this tooling source repository or a fork to test this feature; use a separate scratch client repository.
Update container image uses the selected Linux tooling. VIPM still requires review and approval before dispatch. Candidate activation is a separate guarded action inside the collapsed candidate evidence section; rollback is available under Current selection and rollback. Building an LVPM candidate never changes the active worker. The single action clarifies submission and navigation, not package compatibility: conflicting pins or runtime installation failures can still stop the build. No successful image bake or CI activity run is established by enabling or submitting the button.
Linux requests ask GitHub for dispatch run details. An accepted request is not a successful build. When GitHub returns a run identity, the page checks the exact repository, workflow ID and path, dispatch event, branch, reviewed HEAD, authenticated actor, and first run attempt before showing queued/running/completed status with a direct log link. A Linux-only indeterminate progress bar appears while checking, submitting, awaiting run visibility, queued, or running; it does not invent a completion percentage or estimate package-install progress. Failure and cancellation stop the bar with a red label and retain the exact run link. Uncertain outcomes or stopped tracking show a warning, not success. Workflow success stops the bar with a review-required label, not a claim of candidate verification or activation. Reduced-motion preferences disable the animation. It never picks the latest run or infers ownership from timing. Delayed run visibility is retried; permission/network errors or mismatched identities stop tracking without automatically retrying the dispatch. Legacy HTTP 204 responses establish only acceptance: open the workflow runs and inspect them before refreshing or retrying. Repeated clicks are blocked while submitting, tracking, or the outcome is uncertain. Changing platform, revision, or monitoring invalidates the observer, not the remote run. Workflow success alone does not establish a published, verified candidate or activate it: refresh and review candidate evidence, then request and merge the activation PR separately.
- Review. The candidate uses the locked CI-tooling VIPC and project VIPC/Dragon files selected for Linux. One installer owns the environment; no per-package mixing of VIPM and LVPM occurs. Conflicting exact pins (including conflicts between tooling and project declarations), missing files, unsupported declaration settings, and Dragon NIPM requirements fail rather than being silently dropped.
- Build.
build-labview-lvpm-image.ymlis manually dispatched. The Linux custom-image capability must be installed first. The page checks workflow availability on the default branch before enabling Build and again before dispatch; unknown availability blocks dispatch rather than pretending the workflow is missing. It builds upstream Flydroid/lvpm at commit8e8bd7e390b7d4a77558af12cfa52f79079df409with its Cargo lockfile, as an external dependency; the toolkit does not vendor or fork its implementation. Candidates use separatelinux-lvpm-*image tags andworkers/linux-lvpm/manifests. They never replace the existing Linux VIPM, Windows, seed, or default worker tags. - Verify. The adapter first asks LVPM for read-only package discovery, then expands
explicitly declared embedded VIP subpackages into the checked dependency closure. It preserves original
archives and their SHA-256 values, and records separate hashes for normalized installation archives.
Legacy non-UTF-8 specs use ZIP's CP437 filename encoding; normalized specs and archive names use UTF-8
without replacing unknown characters or changing payload bytes. File groups explicitly restricted to
other LabVIEW versions are excluded with recorded evidence; package-level incompatibilities, unsupported
OS restrictions, unknown conditions, conflicts, and missing transitive dependencies remain errors.
Exact root pins are fetched separately with dependency traversal disabled, so upstream's
transitive-first resolution cannot silently upgrade a declared version. Transitive discovery cannot
replace those roots. Once every dependency constraint has been checked, both the local recheck and
actual install disable further dependency resolution and use only the frozen closure.
A final, local-only dry run must resolve the complete exact closure without unresolved dependencies or
missing archive entries before any real installation. No initial discovery warning counts as verification.
The checked closure is frozen to exact local package archives before the actual
install with hooks and relinking. Installation rejects non-Linux or non-x86-64 hosts before changing
packages. The exact digest-pinned image is inspected and must agree with the installed inventory on
linux/amd64. Final verification requires exact installed versions, recorded files on disk, no skipped applicable groups/hooks, and completed relinking. Declared system-package paths are preserved: file verification alone does not prove a Windows-shaped CLI operation works on Linux. SHA-256 values record the acquired package bytes; they are not publisher signatures. The initial indexes are requested over HTTPS. - Activate. Activation proposes a digest-pinned
config.container.linuxUseselection through the existing reviewable Reconfigure PR flow. Server-side checks require the canonical candidate manifest, explicitarchitecture: amd64, successful build provenance, matching registry digest, and current dependency fingerprint. Older manifests lacking architecture evidence require a rebuild. Freshness includes the consumed catalog default, qualification, LabVIEW year, and Linux base tag; release-note or version bookkeeping alone does not invalidate a candidate. Explicit existing per-action container overrides continue to win, thenlinuxUse, then the previous fallback. Windows never reads this Linux-only selection. Unrelated reconfiguration preserves it; resetting to the existing default selection removes onlylinuxUse, preservingcontainer.useand existing overrides without assuming the fallback uses VIPM. Reconfigure applies only explicitly requested settings to the parsed existing manifest: activation/reset preserves separate VIPC and Dragon lists, explicit dependency-policy values, platform settings, provenance, and unknown client fields. A no-op keeps the file byte-identical; an actual update may normalize YAML formatting.
Project declarations that intentionally duplicate CI-tooling roots (for example
example/Dependencies.vipc) must keep shared roots pinned to the exact same versions;
Linux planning rejects declaration drift instead of selecting a newer or transitive alternative.
The current OpenG File root pin (oglib_file-6.0.2.28) targets LabVIEW 2020+ on all
operating systems and supports automatic caller relinking into openg_file.lvlib.
Downgrading to older OpenG file packages can require manual relinking, which planning and
package-file verification do not prove at runtime.
verification.scope: package-files and activity_checks: not_run. Upstream LVPM
cannot yet observe a hook VI's internal error out; explicit hook failures are rejected, but a
completed hook is not proof of functional correctness. Validate the candidate against the project's actual
compile, test, build, and rendering activities before depending on it. Conditional file groups the adapter
cannot safely validate are rejected conservatively. Private package feeds, embedded Dragon VIPC references,
and NIPM installation are not supported by this initial LVPM adapter.These are global installs inside isolated containers, not developer virtual environments.
Snapshots retain their existing TOIMAGES_IMAGE or repository Linux-latest selection when
linuxUse is absent. After opting in, explicit snapshot action overrides precede
linuxUse; a verified LVPM pull failure never falls back to another worker.
Existing LabVIEW launchers therefore see the installed dependencies without venv activation or preference-file
changes. LVPM candidates are immutable selections: monitored dependency changes do not silently rebuild or
switch them. Only verified immutable LVPM workers skip the VIPM image wait; existing VIPM waiting and
NI-base behavior are unchanged. Linux execution checks the current requirements fingerprint and blocks stale selections until
a fresh candidate is built, verified, and explicitly activated. A failed candidate leaves the previous image
available but does not make stale requirements current. The selected environment drives Linux inventory;
an unused candidate does not create missing-dependency warnings for the active pipeline. LVPM inventories
VIP packages, not native NI packages: unmeasured native package coverage is unknown, not evidence of absence.
The dashboard separately reports local freshness for the exact inspected revision and
digest-pinned selection. It uses trusted action-distributed helper code, treating the client checkout only as
input data. Optional pinned Python prerequisites are installed in an isolated temporary environment only when
LVPM is selected; failure leaves the rest of the dashboard running and reports freshness as unknown.
Python startup is isolated so checkout modules and inherited import paths cannot replace trusted imports.
Changed declaration bytes invalidate freshness even when package pins are unchanged. Stale or unknown
freshness marks Linux dependencies pending and cannot produce green satisfaction. A local
current result does not verify registry access, build provenance, or runtime activities;
activation and execution still perform their server-side checks. Historical views cannot reuse another
revision's current result.
Dragon declaration inspection loads only the parser from the trusted action repository, resolved relative
to the dashboard generator itself; Python helpers in the inspected client checkout are never imported.
The parser requires Python 3.11+ with tomllib, or an available tomli backend.
Missing tooling/backend, missing monitored files, and malformed declarations produce per-file
inspection.status: unknown evidence instead of an empty installed inventory. Monitored unknowns
keep the dependency banner pending for their monitored platforms; unmonitored files do not create pending
work. The Dependencies page requires matching-revision declaration evidence and measured installed package
statuses before displaying installed. Update the trusted tooling or repair the declaration and regenerate
the dashboard; rebuilding a worker alone does not repair parser evidence. Per-dependency
platform_status records honor the file’s monitorOn list; the existing
status, installed_version, and message fields aggregate only applicable
monitored platforms. Installed requires all applicable platforms installed; conflict, missing, wrong
version, unknown, pending, and not attempted take precedence in that order. Windows-only files retain
measured Windows inference. Linux-only VIPM declarations can be satisfied by exact name/version in the
selected canonical LVPM inventory, only with locally current revision/selection-bound freshness.
Linux scope: package-files is not runtime verification; native/NIPM declarations remain unknown.
Pending package records include per-platform evidence and target only unresolved monitored platforms.
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 #
Standalone Windows worker and unit-test actions
For an existing workflow, grant contents: read and packages: write, check out
the project, and call these actions on windows-2022. Pin both actions to the same reviewed
immutable toolkit tag or commit. Replace REVIEWED_REF below after publication; this
example does not imply the actions exist in an earlier release.
permissions:
contents: read
packages: write
jobs:
tests:
runs-on: windows-2022
steps:
- uses: actions/checkout@v5
- uses: ni/LabVIEW-CI-CD-Toolkit/actions/worker-image@REVIEWED_REF
id: worker
with:
base-image: ${{ vars.LCWC_BASE_IMAGE }}
labview-version: '2026'
# Optional flat directory containing project VIPCs/Dragon files and referenced VIPs:
# deps-dir: dependencies
- uses: ni/LabVIEW-CI-CD-Toolkit/actions/unit-tests@REVIEWED_REF
id: tests
with:
image: ${{ steps.worker.outputs.image }}@${{ steps.worker.outputs.digest }}
framework: lunit
test-dir: tests
- uses: actions/upload-artifact@v6
if: always()
with:
name: unit-tests-report
path: ci-out/unit-tests/
if-no-files-found: warn
The worker's image input is an unqualified GHCR package name; the default is
<repo>-labview. Outputs are image, tag (a
win- content hash), verified digest, and built (also true for
a dependency-free registry copy). deps-dir must exist inside the workspace and contain
files directly: nested dependency files, links, duplicate names and reserved ci-tooling*
names fail rather than being silently skipped or overwritten. Exact .vip extensions are
hashed separately from .vipc. Loose VIPs are used only when a VIPC references them;
a VIP-only directory without a VIPC fails. Project VIPCs and the effective included tooling baseline are required in the final target inventory. include-ci-tooling: 'false' omits the tooling VIPC and disables
default UTF/JUnit/early framework packages. Supported exact Dragon VIPM declarations use the shared
host-generated target plan and verified VIPM installer, including Dragon-only builds; no separate Dragon CLI
is needed. The isolated action entry and content fingerprint include the trusted parser, normalized plan,
source bytes, installer and target inputs. Required Dragon versions are verified after later tooling.
NIPM/feed requests, unsupported settings and custom Dragon hooks fail explicitly; migrate before adopting
this incompatible change. A successful mock contract is not proof of a real Windows dependency bake.
The unit-test composite requires image and framework (Caraya,
vi-tester, utf, or lunit). Its optional command
is an intentionally executable, trusted cmd.exe template with {ver},
{dir}, {out}, {lv}, and {cli} tokens
({proj} replaces {dir} for UTF projects); custom
framework identifiers require it. A command overrides even the native UTF/LUnit runner.
test-dir accepts a workspace-relative directory, glob, or UTF project; empty means the
whole project. Inputs reach the container through a UTF-8 environment file, preserving empty values
and quotes without inserting input text into a shell script. Newlines are rejected.
The optional source-runner parameters -Framework, -TestDir, and
-Command (or LVCI_FRAMEWORK, LVCI_TEST_DIR, and
LVCI_COMMAND) select exactly one framework. Without a framework, existing config and
autodetection behavior remain intact. Installed LabVIEW/CLI path detection is preserved.
report-dir defaults to ci-out/unit-tests and must be a dedicated directory
inside the workspace. The action owns its results directory and removes old results and summaries
before running. Outputs are report-exists, tests, passed, and
failed (including errors). Docker pull/run errors, report-generation errors, absent XML,
empty tests, all-skipped tests, and runner errors fail the action. Reports are rendered after a
nonzero runner exit when Docker returns normally, so diagnostics remain available for artifact upload.
fail-on-test-failure: 'false' permits assertion failures only. The optional
pages-url controls navigation links; the caller owns artifact upload, Pages deployment,
and commit statuses. Python and the worker action's Go/crane runtime are installed explicitly.
Composite Action Checks runs portable regressions on Linux and Windows plus real Windows
PowerShell runner tests without LabVIEW; an actual Windows container bake/test remains a separate
acceptance check.
VIPM bootstrap contract checks read the catalog, workflows, Dockerfiles, and installer source as
UTF-8 rather than the Windows host's legacy code page. A portable regression exercises the affected
catalog/bootstrap assertion with simulated Windows-1252 defaults (None and
locale), confirming implicit reads fail and explicit UTF-8 preserves Unicode notes.
This validates source contracts, not a native Windows VIPM install or container bake.
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).
GitLab reuses the same drivers and report builders, but native templates stage their output as GitLab
artifacts before the Pages publisher aggregates it. The report paths remain Windows
<capability>/<sha>/ and Linux <capability>/<sha>/linux/. VIDiff's
GitLab layout is SHA-based rather than GitHub's pull-request-number path, and it does not yet post a merge
request note.
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
For modified VIs, the Linux runner requests LabVIEW’s XML comparison output and
renders it as static HTML with build-vidiff-report.py. This avoids native HTML report
generation crashes reproduced in LabVIEW 26.3. Functional, cosmetic, and VI attribute changes
retain their descriptions and all available before/after screenshots; when LabVIEW supplies no image
for a change, the report explicitly says so. The source comparison.xml is retained for
inspection. These static reports do not include the native HTML renderer’s interactive sub-VI
hotspots. Added/deleted VIs still use PrintToSingleFileHtml.
The Linux runner preserves per-VI cli-attempt-1.log (and, when retried,
cli-attempt-2.log) in the report artifact. CLI error 66 (connection lost during an operation)
or −350000 (VI Server connection failure) resets the container’s LabVIEW process
using pkill and retries the same VI once. An exhausted retry also resets the session
before processing the next VI, but still fails the job; other operation errors are not retried.
A successful CLI exit without a nonempty report, or malformed comparison XML, is also treated as failure.
- Reports land at
vidiff/<pr-N|push-sha>/<platform>/vidiff/with achanges.jsonmanifest ({modified, added, deleted}).vidiff-deploy.ymldeploys trusted reports to Pages and comments the diff on the PR (it runs even on failure). The deploy workflow derives SHA/PR lineage and platform from the trustedworkflow_runevent, then fail-closes ifvidiff-meta.jsondisagrees or falls outside strict SHA/PR/platform/outcome validation. Artifact extraction is path-safe and rejects ZIP entries with absolute paths, parent traversal, or symlink members before writing to disk. The VIDiff installer payload bundles both helper scripts (extract-vidiff-artifact.pyandvalidate-vidiff-deploy-metadata.py) on Windows, Linux, and combined installs. For pull-request events whereworkflow_run.pull_requestsis empty, it resolves PR association from the commit-to-PR API and requires exactly one trusted match. For manually dispatched historical diffs, it keeps the artifact’s validated target SHA (instead of silently retargeting toworkflow_run.head_sha) but only publishes when producer identity is trusted. For push/manual producers this requires a canonical producerhead_repositoryplus producer-head ancestry on the repository default branch; manual targets also require target-SHA ancestry there. Same-repository writers remain trusted for Pages publication because they already hold equivalent repository privileges to modifygh-pagescontent directly. Fork PR artifacts never publish on the shared Pages origin; the PR comment and commit status link to the Actions artifact/run URL instead. - 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 (Windows) and .github/labview/run-unit-tests.sh (Linux) — run each enabled framework headless, write 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; unit-tests-linux-container.yml on ubuntu-latest. |
| 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 with commit-status context CI / Unit Tests; the Linux run deploys to unit-tests/<sha>/linux/ with CI / Unit Tests (Linux). |
| Platform | Windows; LUnit also runs on Linux. Every framework depends on baked VIPM/UTF tooling (§15). |
The four frameworks & their headless commands
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) | A project-owned runner with an explicit test inventory | native LabVIEWCLI RunVI through command: | Use a wrapper that disables Caraya's interactive dialog, writes JUnit to {out}, checks its assertion inventory, and propagates CLI failures (example below). The legacy g-cli ... caraya default is an unverified extension scaffold, not provided by the Caraya package itself. |
| 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 |
On Linux, LUnit runs with the same default command and tokens ({lv} is
labviewprofull, {cli} is LabVIEWCLI), so one command: line
can work on both platforms when the executable and flags are portable. The Linux runner requires Python 3
(already installed in the worker). It splits a command into arguments before substituting tokens, so spaces,
quotes and shell metacharacters in paths remain literal; shell operators in overrides are not evaluated.
Invoke a project-owned script explicitly for more complex commands. Keep -Headless as the last argument of any override: on
Linux, LabVIEWCLI swallows whatever follows it, and the Linux driver warns when an override doesn’t
end with it.
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. The driver reads indentation relative to unitTests:, including the
canonical config.unitTests.tools nesting, preserves quoted command arguments, and stops at
sibling blocks.
Unconfigured fallback. When .github/labview-ci.yml has no
unitTests: block at all (the unit-tests activity is installed but the Unit Testing
settings page was never saved), the driver auto-detects every framework present and runs them all:
NI UTF (empty locations) when any .lvproj references a .lvtest /
TestItem; Caraya, LUnit and VI Tester when a .vi’s linker strings name
Caraya.lvlib, LUnit.lvlib or TestCase.lvclass respectively, using the
outermost directories holding those VIs as locations (.github, ci-out,
build and .git are skipped). The Caraya signature was checked against real Caraya test
VIs; the LUnit and VI Tester signatures have not yet been checked against real projects. Any explicit
unitTests: block — even one with every tool disabled — is respected as-is and produces
the “No enabled config.unitTests.tools” runner error instead.
A native Caraya integration can use this override under config.unitTests.tools:
caraya:
enabled: true
locations:
- "tests"
command: powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "{dir}\Invoke-CoreTests.ps1" -ResultsPath "{out}" -LabVIEWPath "{lv}" -Headless
Invoke-CoreTests.ps1 is supplied by the project, not generated by the toolkit. Its
Run Tests.vi must implement the installed RunVI connector contract and explicitly
select test VIs; fixtures and the runner are not independent tests. Local headless validation against the
Measurement Utility Reference working tree passed eight suites / 44 assertions on 2026-10-23 without
g-cli. This does not certify a clean container. The historical 6d69ccd worker failed with
LabVIEW error 1003; its mass-compile report identified missing Caraya runner VIs and Linked
Network Actor dependencies. Bake both the test framework and the project's full dependency closure,
including authorized private/offline dependencies that are not in the public VIPM indexes.
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 Linux run
unit-tests-linux-container.yml follows the same step order on ubuntu-latest,
with the Linux image selection used by the other Linux activities (config.container.actions.unit-tests
→ config.container.use → the shared Linux worker :linux-latest;
base/none selects nationalinstruments/labview:latest-linux). It runs the
driver in a fresh container:
docker run --rm -v "$GITHUB_WORKSPACE:/workspace" "$LABVIEW_CONTAINER_IMAGE" \
bash /workspace/.github/labview/run-unit-tests.sh /workspace /workspace/ci-out/unit-tests/results
- Python isolation. The Linux driver uses
python3 -I -S -so client-root modules,PYTHONPATHand site startup hooks cannot replace its standard-library imports. - LUnit. The driver runs enabled LUnit locations and records other enabled frameworks as
unsupported-on-linuxin_tooling.json. Reports show this as information, separately from missing tooling, and update the notice when switching platforms. - Fail closed. The driver clears stale XML, logs and tooling markers, and writes a runner-error JUnit case for missing CLI/executable, unresolved locations, a nonzero command exit, or absent output. Runner failures exit nonzero even when a partial XML file exists. GitHub and GitLab also fail empty, malformed and all-skipped reports; assertion failures come from JUnit. Unsupported tools do not fabricate test cases: a run containing only unsupported tools has zero tests and fails.
- Report generation. A failed, skipped or cancelled report step fails the final status even if passing JSON or a partial HTML file exists. Only successful report generation permits artifact upload or Pages deployment; report failures link to the workflow run for diagnostics.
- Autodetection. With no
unitTests:block, the Linux runner uses the same framework signatures and tooling/build exclusions as Windows. Any explicit block, including all tools disabled, suppresses autodetection. Bothconfig.unitTestsand the legacy top-level block are read. - Worker selection. The workflow uses the current LVPM resolver and verifies immutable candidate evidence before selecting an LVPM image. A selected candidate skips waiting for a VIPM build.
- History backfill has no Linux workflow yet; use the per-revision Run button.
The GitLab provider installs templates/unit-tests-linux.yml for Linux clients. The unit-test
catalog capability now advertises Linux support; the installer selects the OS-specific runner
and workflow without changing existing client configuration. Windows-only installations omit the Linux runner.
The dashboard Run button dispatches the Linux workflow with commit_sha; historical dispatches
restore current driver/report/publishing scripts from the default branch before execution.
The bundled VIPC upgrades only LUnit CLI from 1.6.1.23 to 1.7.1.32. Rebuild the shared Linux worker before
clients use it, then rebuild client workers that copy that base. Existing consumer package pins remain owned
by the consumer and must be updated explicitly if older than this minimum. Linux VIPM installation preserves
package revision suffixes, for example jki_rsc_toolkits_palette-1.1-1 becomes
jki_rsc_toolkits_palette@1.1-1; dropping the revision makes VIPM reject the pinned package.
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*).- Runner diagnostics — a failed command, unresolved location, missing CLI, or absent
report produces
<tool>-runner-<n>.xmlwith a clearly labelled runner-error case, not a fabricated test assertion.<tool>-<n>.logretains command output and the LabVIEW CLI session log when available. The driver ownsResultsDirand clears prior XML/log files before each run, continues other configured tools after a command failure, and exits nonzero. 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→ failure, “No unit tests found”.- All cases skipped, or the container runner failed → failure, even if a partial JUnit file exists.
failed + errored == 0→ success, “N passed”.- otherwise → failure, “N failed, M passed”.
Malformed, empty, non-JUnit, or count-inconsistent XML is represented as an explicit report-validation
error. An existing empty report renders its zero counts instead of claiming the Windows report is absent.
Runner/report errors contribute to summary.errored; assertion results remain separate cases.
The workflow fails the job as well as the commit status, and test-script/inventory edits under
tests/ trigger a run. Regression checks run without LabVIEW in the Windows
Unit Test Runner Checks workflow. Linux offline checks run in its Ubuntu job and cover
autodetection, disabled configs, missing tooling, stale results, safe argument substitution, installation
selection and VIPM revision suffixes. Locally run python .github/labview/test-unit-tests.py
and python .github/labview/test-linux-unit-tests.py. Real LabVIEW and container execution
still require worker validation.
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.” On Linux the same banner appears when
LUnit CLI is missing (-350053 or -350006). 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 #
Populate history respects the selected revisions. The snapshot workflow receives
an explicit space-separated shas list, oldest first, and renders only those commits in one run.
Choosing only the newest revision does not walk older history; Fill excludes completed cells, while Re-run
forces rendering only within the selection. A single cell’s Run action also passes that revision’s SHA.
The workflow’s run name carries the selected SHAs so queued, running, and failed badges belong only to
those rows. Direct workflow dispatches without shas retain the manual head and
unrestricted backfill modes, with max_commits applying only to the latter.
Update installed tooling to receive both the dashboard and the workflow’s new input together; an older
workflow rejects the input rather than silently expanding the requested scope.
The Populate history chooser offers an advisory banner when older, unselected revisions are missing snapshots for an enabled snapshot activity and selected platform. It counts revisions once, excludes hidden dependency-only revisions and browser-tracked queued work, and explains that Fill reuses snapshots for unchanged file content. The banner recommends All revisions plus Fill while noting that more revisions may start more workflows and that other enabled activities follow the same revision scope. It never changes the selection or queues work. Keep selected revisions dismisses the tip for that dialog session; opening the chooser again enables it again.
Snapshots are interactive views of a VI's front panel and block diagram. Pan and zoom the code, switch between the front panel and block diagram, and navigate cases, events, and sequences directly on the diagram without opening LabVIEW. The same viewer is used in the VI Browser, standalone links, and report previews. Snapshots and Diff are the only viewer choices.
Rendering and reuse
Snapshot Checks: Windows contract tests read repository text and scratch-client fixtures as UTF-8, including Unicode release notes, rather than relying on the host's legacy code page. A regression exercises catalog loading with a simulated Windows-1252 default. These checks validate installer and viewer contracts; they do not establish successful LabVIEW rendering or a container bake.
The VI Snapshots capability installs the browser, revision metadata, and frame renderers.
Configure selects the Windows and/or Linux render platforms under config.viBrowser.platforms.
Windows uses vi-snapshots-json-windows.yml; Linux uses vi-snapshots-json.yml
and the render engine baked into the Linux worker. The gallery workflow is vi-snapshots.yml.
The gallery workflow renders Windows frames directly; platform render workflows can fill missing frames for a selected revision.
Frames are content-addressed by git blob SHA: by-blob/<ab>/<blob>.windows.json
on Windows and by-blob/<ab>/<blob>.json on Linux. Unchanged VIs reuse frames across
revisions. Each job keeps a warm container to avoid per-VI startup costs. The dashboard counts frame
coverage, not exported HTML files.
Browsing and missing snapshots
VI file rows show a brown, keyboard-accessible Diff pill when a side-by-side report exists for that revision. Clicking it opens the report in the viewer, independently of snapshot availability, with the existing Linux/Windows report fallback. Snapshot and New pills remain omitted. Once the frame indexes confirm that neither platform has a snapshot, a small red X beside the filename indicates No snapshot available, with the same accessible label and hover text. Unknown or still-loading availability has no marker. The X disappears when polling finds frames from either platform. Folder change indicators and the toolbar’s Diff control remain available.
When both platforms have frames, the browser initially prefers Windows; a platform-specific shared link preserves its selected render. Front Panel and Block Diagram controls choose the pane, and share links preserve the revision, VI, pane, and platform. Coaching tips can be dismissed locally.
A VI without frames shows a render-needed, running, or failed state with a link to the render workflow. There is no static-image fallback. Use Populate history to generate snapshots for older revisions. Older snapshot links and saved configuration are accepted and open the interactive viewer. Installing the capability makes the renderer workflows available for the selected operating systems; it does not queue historical renders or expand a selected revision scope.
16.7 SBOM Generation #
At a glance
| Driver | .github/labview/run-sbom-generation.ps1 Windows |
|---|---|
| Report builder | .github/labview/build-sbom-report.py |
| Workflow | generate-sbom-windows-container.yml windows-2022 |
| Output & status | sbom/<sha>/; commit status CI / SBOM Generation |
The Windows worker invokes the VIPM CLI to inventory the revision’s resolved LabVIEW package
dependencies and preserves its native CycloneDX document at
sbom/<sha>/results/sbom.json. The host report builder normalises that inventory into
results.json and a navigable index.html, then the workflow uploads the whole
directory as an Actions artifact and publishes it to GitHub Pages.
The dashboard has a per-revision SBOM column. Its status pill links to the Pages report
inside the shared report viewer; the viewer’s CycloneDX JSON action opens the native
machine-readable document. Empty cells can dispatch SBOM generation for that revision, and SBOM participates
in Populate history, the activity switcher, revision navigation, and regeneration controls. Historical
CI / SBOM statuses remain readable, while new runs use the catalog-declared
CI / SBOM Generation context.
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 |
unit-tests/<sha>/linux/results.json | Unit-test report builder (Linux) | Dashboard cell (Windows / Linux split) + report |
builds/<sha>/summary.json | Builds report builder | Dashboard cell (succeeded/failed split) + report |
sbom/<sha>/results.json | SBOM report builder | Dashboard report availability + HTML report |
sbom/<sha>/results/sbom.json | VIPM CLI | Native CycloneDX download |
vi-snapshots/<sha>/manifest.json | Snapshot gallery builder | VI Browser |
dependencies/index.json | Dashboard publisher | Dependencies page install-time inventory |
columns[].freshness in dependencies/index.json | Trusted dashboard LVPM helper | Local status, reason, inspected revision, and exact selection; stale/unknown block satisfaction |
dependencies/index.json: dragon.files[].inspection | Trusted Dragon parser in the action repository | Parsed or unknown declaration evidence with a reason; monitored parse/tooling failures cannot report installed |
dragon.files[].dependencies[].platform_status / dragon.install_set[].platform_status | Monitored Windows inventory and selected, locally current Linux LVPM inventory | Per-platform status, installed_version and message; Linux scope is package-files. Aggregate status cannot be installed when an applicable platform is missing or unknown. |
deps-pending.json | Dashboard publisher | Persistent pending banner, including local LVPM freshness under linux_lvpm and monitored Dragon inspection failures as dragon[].status: unknown |
workers/<platform>/<version>/manifest.json | Worker image workflows | Dependencies page installed-package status |
manifest.json schema 3: vipm_inventory, vipm_packages, dependency_sha, labview_bitness | Shared worker manifest producer | Target-verified observed packages, explicit unavailable/unverified evidence, and separate tooling/input revisions. Raw lists and vipc[].packages remain diagnostic/declaration data, never installed inventory. |
workers/<platform>/<version>/observed-inventory.json | VIPM worker builds | Schema 1 evidence: platform, LabVIEW year/bitness, boolean target verification and required-project flags, source, status, raw output, and exact package names/versions. Malformed or wrong-target evidence blocks manifest generation. |
workers/linux-lvpm/<tag>/manifest.json | LVPM candidate build | Digest-bound candidate inventory, activation and Linux runtime freshness checks |
workers/linux-lvpm/latest.json | LVPM candidate build | Alternative candidate discovery only; never selects an active worker |
catalog.json (root) | Installer / integrate-deploy | Version badge, What's New |
GitLab publishes these same report paths directly below the Pages public/ root, not below an
extra reports/ directory. The provider restores the previous default-branch Pages artifact before
merging the current pipeline's output, and a resource_group prevents two publishers from racing;
report links therefore retain the same stable paths across pipelines.
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 runs for every eligible contributor push to main. It derives the next
version from published tags, writes the matching catalog entry, and cuts the immutable tag and GitHub Release
in the same run when the catalog push is permitted. On protected branches it opens a catalog pull request
instead and defers publication until that PR is reviewed and merged. The minor alias advances to the newest patch on its line; the major alias advances only when
a release is promoted to stable. Release notes are generated from history.releases[0].
release-catalog-pr.py handles the protected-branch fallback for both release and promotion.
It opens a lvci-release/ branch only for GitHub's explicit pull-request-required rejection;
authentication and other rule failures remain errors. Enable Allow GitHub Actions to create and
approve pull requests in the repository's Actions settings (subject to organization policy).
The workflows request pull-requests: write but never approve or merge their own PRs and
do not change branch protection.
If an earlier preparation already occupies lvci-release/<kind>-<version> and
rejects the new catalog with a non-fast-forward error, the helper retries on a branch suffixed with
the full prepared commit SHA. It never force-pushes or rewrites the older review branch. It creates
or reuses the PR for that exact retry branch, and reports other push failures with their Git output.
Close superseded catalog PRs after checking that the new proposal includes the current release notes;
merge only the current proposal. Release-routing regressions run in the Catalog Source Sync PR gate.
Generated catalog PR titles describe release-note content first, with the version as secondary context, and bodies identify the update type, baseline, unpublished status, and exact client update guidance. Existing bot PR metadata is refreshed when reused; meaningful missing notes stop generation rather than producing a version-only placeholder. Catalog Source Sync rejects known generic titles on creation and edits. Reconfigure titles name actual changed settings or explicit LVPM activation/reset. Metadata-only release PRs describe already-merged capabilities without claiming to implement them again.
After merging a prepared catalog PR, Release verifies that it was created by
github-actions[bot] in this repository and changes only the catalog and release-note fragments.
It publishes the prepared version without generating another bump. If the merge does not trigger Release,
run it manually on the default branch immediately after the merge, before other changes land. Review one
pending catalog PR at a time; regenerate a stale or conflicting proposal rather than changing historical
tags. Both release workflows must run on the default branch.
Knowing which release contains a fix
Every pull request includes a Release impact section after its feature summary:
an explicit target vMAJOR.MINOR.PATCH, update type, published baseline, availability,
and client update guidance. Major identifies incompatible changes;
minor identifies backward-compatible new capabilities; and
incremental (patch) identifies backward-compatible fixes or documentation/maintenance
updates. The corresponding release:major, release:minor, or
release:patch label records the intended bump; no label still defaults to patch.
A projected version can change with merge order. A prepared catalog PR is not a published release. After publication, the feature PR must link the verified first containing immutable release. Client owners should select that exact tooling version once available and confirm the installed version after updating. A rolling channel or alias is not proof that a fix is included: check its resolved release separately. Publication does not promote beta or stable. Explicitly skipped releases have no assigned containing version until a subsequent release is verified.
Canonical source and GitLab distribution
GitHub is the canonical source of implementation and releases. When the optional GitLab mirror is enabled,
the release workflow runs sync-gitlab-mirror.sh after GitHub has published an immutable tag. It
first pushes immutable vMAJOR.MINOR.PATCH tags without force, then force-updates the mirror's
main to that released commit and updates rolling aliases such as
vMAJOR, beta, stable, and dev. The GitLab project
elijah286/ci-for-labview is therefore a one-way distribution mirror, not a contribution branch
or a second codebase.
The mirror does not publish arbitrary GitHub main work: it advances only from a canonical
release tag. Set the GitHub Actions variable GITLAB_MIRROR_ENABLED=true and the write-capable
secret GITLAB_MIRROR_TOKEN (optionally GITLAB_MIRROR_USERNAME) to enable the
post-release push. The token needs GitLab write_repository access, and a protected mirror
main branch must permit that automation identity to force-update the release snapshot. An enabled
mirror failure produces a warning without invalidating the published release. If it needs recovery,
the source-only Sync GitLab Distribution workflow accepts an immutable tag; it never
manufactures a release or moves an immutable tag.
A GitLab-only command-line install starts from the mirror's raw bootstrapper with
--source-host gitlab; the bootstrapper follows its GitLab source.json
distribution entry and downloads its archive through the GitLab API. It writes a
source.distribution block in the target manifest. The browser configurator remains served from
GitHub Pages and records GitHub as its origin even when it creates a GitLab target, leaving the command-line
installer as the GitHub-free route without maintaining two implementations.
To update a GitLab-distribution target, run its install.sh --update or
install.ps1 --update bootstrapper. It reads source.distribution, fetches fresh
tooling from GitLab, and then runs the archive-sourced install.py --update to refresh
.gitlab/labview-ci/. Direct install.py --update operates only on the source
checkout already on disk. The GitHub-only one-click Update now workflow and Dependabot flow are not GitLab
update mechanisms.
GitHub 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 (re-asserting the git push credentials from that token first, so the commit never stalls on a missingactions/checkoutcredential with “could not read Username … terminal prompts disabled”) — 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.
Tooling updates retain a selected legacy snapshots-2 activity as unified
snapshots and clean recognized retired image-build tooling without enabling an
opted-out capability. Reconfigure migrates the activity alias when activities are explicitly
submitted; a blank request preserves the existing manifest unchanged.
Reconfigure applies only explicitly requested settings to the parsed existing manifest rather than regenerating the entire file. Separate VIPC and Dragon declarations, explicit false dependency policies, operating systems, provenance, and unknown client fields are preserved. Blank or unchanged requests keep the file byte-identical, including comments; actual updates may normalize YAML formatting. Invalid YAML or a non-mapping configuration path is rejected before the manifest is written.
Changed manifests use the layout expected by existing tooling readers: indented dependency lists,
path-first dependency records, config-first VI Analyzer rules, double-quoted numeric strings, and inline OS lists. Dependencies also
recognizes adjacent VIPC, Dragon, and action sections without dropping a header. Scalar values are not
line-wrapped, and explicit none dependency selections remain configured empty lists rather
than falling back to automatic discovery. Scoped updates detach
shared YAML mappings before editing so aliases cannot change unrelated client settings. A no-op remains
byte-identical. If a prior update left an older dashboard unable to read the formatting, the saved values
are still present: update tooling, then make an intentional settings change through the GitHub Reconfigure
workflow to regenerate compatible formatting. Do not rely on a misread UI to overwrite monitored lists.
Upgrade preservation covers the complete existing project configuration, not just runner concurrency.
Unit Test Runner Checks exercises manifest upgrades alongside execution and report regressions,
and is triggered by installer changes as well as unit-test runtime changes.
The major alias @v<major> tracks the newest release promoted to stable, not the
newest release published. Every merge to the source repo is tagged and released, but a release only reaches
@v4 once the maintainer blesses it — so consumers on the major alias receive reviewed work, not
the development tip. Pin @beta for release candidates, @dev for every build, or
@v<major>.<minor> to follow one line. The major alias never moves backwards, so an
automatic update can never downgrade a consumer.
Published release notes and workflow summaries distinguish publication from promotion. A successful
release does not itself move the stable or beta channel. To test a newly published version before
promotion, select its immutable version tag explicitly rather than assuming @v4 has advanced.
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 or opens a reviewable catalog PR when it is protected. Promotion remains pending until
that PR merges and Release publishes it; the same buttons un-mark to roll a channel back to the previous build in that tier.
Relocation pointer
.github/labview-ci/source.json names the canonical NI repository, ni/LabVIEW-CI-CD-Toolkit,
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.
The source dashboard, documentation, FAQ, and installer are published at
ni.github.io/LabVIEW-CI-CD-Toolkit.
GitHub repository redirects do not redirect the old GitHub Pages site, so published links must use this
new address. The pointer's GitHub distribution, the catalog's source.repo, and this repository's
install manifest all name the same NI source. That matching manifest makes the source dashboard publish
its current commit's page bundle rather than an older installed release; consumer dashboards still use
their opted-in tooling ref.
Immutable releases made before the transfer retain their original source metadata. Publish a release containing the updated metadata and promote it to the desired install channel so new default installs use the NI source. Keep legacy GitHub repository redirects reachable for existing clients following the relocation pointer; do not rewrite historical tags or assume container packages moved with the repo.
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.
GitLab projects are not currently part of that GitHub search-based discovery index. Installing the GitLab provider does not report back to this source repository or expose project metadata to the Clients page.
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: LUnit supports Windows and Linux; Caraya, VI Tester and UTF have Windows runners. Antidoc remains Windows-only. Workers must include the corresponding tooling.
- SBOM now fails fast on VIPM errors. The SBOM driver treats a non-zero
vipm sbomexit (or missing output file) as fatal, and the workflow no longer usescontinue-on-erroron that step, so failed scans do not publish replacement output artifacts. The generated SBOM JSON is preserved in its native CycloneDX form, and the HTML report now renders the component supplier/vendor field from that source data instead of rewriting it to a generic label. VIPM still auto-detects and explicitly targets the container's installed LabVIEW version (via--labview-version) to prevent auto-detection from project metadata; this avoids errors when a project targets a different year than the container provides. - 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 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 frameworks depend on baked VIPM/UTF tooling. |
| Run Unit Tests — Linux Container | ubuntu-latest | PR / push (LabVIEW source, test scripts), dispatch | LUnit CLI 1.7.1.32+; report under unit-tests/<sha>/linux/. |
| 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) | Builds revision metadata and queues interactive snapshot rendering. |
| VI Snapshots JSON (VI Browser, Linux) | ubuntu-latest | workflow_run after snapshots, dispatch | Gate job decides; auto-runs only when positionAware includes linux. |
| VI Browser 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 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 | Publishes trusted in-repo/default-history reports to Pages; fork PR and untrusted dispatch targets stay on Actions artifact/run links in PR comments/statuses. |
| 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).