This documentation describes the LabVIEW-CI-CD-Toolkit tooling, maintained by NI. Looking for short answers instead? See the FAQ.

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:

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."

In plain terms CI catches mistakes minutes after they are introduced, on a clean machine, before they reach a colleague or a release — instead of days later when they are expensive and confusing to track down.

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 at github.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; main is the trunk everyone shares. You push to upload your commits to GitHub. A pull request proposes merging your branch into main; 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 .yml file) 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:

flowchart LR DEV["You, in LabVIEW<br/>edit + commit"] --> PUSH["git push<br/>(upload to GitHub)"] PUSH --> REPO["GitHub repository<br/>stores code + history"] REPO --> ACT["GitHub Actions<br/>starts the workflow"] ACT --> RUN["Runner (clean PC)<br/>pulls your image"] RUN --> CON["Container<br/>headless LabVIEW runs the checks"] CON --> REP["Reports + commit status"] REP --> PG["GitHub Pages<br/>dashboard + reports"] PG --> YOU["You read the result<br/>in a browser"]

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:

4. The big picture #

A push (or pull request) triggers the reusable workflow, which gates a set of per-capability container jobs on your configuration. Each job pulls a worker image, runs LabVIEW headless inside a throwaway container, writes a report, deploys it to the gh-pages branch, and posts a commit status. A separate dashboard workflow reacts to those statuses and rebuilds the Pages dashboard. Read the diagram top to bottom — it is the same journey as §2, with the real file and job names.

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.

flowchart TD CP["commit / PR"] --> RW["labview-ci.reusable.yml<br/>config job reads .github/labview-ci.yml"] RW -->|"has-masscompile and os-windows"| MC["masscompile<br/>windows-2022"] RW -->|"has-vi-analyzer and os-linux"| VA["vi-analyzer<br/>ubuntu-latest"] RW -->|has-vidiff| VD["vidiff"] RW -->|has-snapshots| SN["vi-snapshots"] MC --> JOB["each job<br/>1. pull image (GHCR / NI)<br/>2. docker run --rm<br/>3. LabVIEW headless<br/>4. build report on host<br/>5. deploy to gh-pages<br/>6. post commit status"] VA --> JOB VD --> JOB SN --> JOB IMG["images<br/>ghcr.io/owner/repo-labview — build-labview-image (Windows) / build-labview-linux-image (Linux, incl. VI Browser 2.0 engine)<br/>nationalinstruments/labview — base layer (public)"] -.->|"pulled by"| JOB JOB -->|"commit status"| ST["status event"] ST --> DP["dashboard-pages.yml<br/>actions/dashboard"] DP --> GHP["gh-pages root<br/>index.html, reports, snapshots"] GHP --> PAGES["GitHub Pages<br/>your dashboard + reports"]

Three deploy destinations live side by side on the gh-pages branch (all written with keep_files: true so they never clobber each other):

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.

CapabilityWhat it doesPlatforms
Mass CompileCompiles every VI/CTL and flags broken VIs and missing dependencies.Win Linux
VI AnalyzerRuns NI's static-analysis test suite for correctness, performance, style and documentation.Win Linux
VIDiffGenerates visual, side-by-side front-panel + block-diagram diffs of changed VIs between commits.Win Linux
Unit TestsRuns Caraya / LUnit / VI Tester / NI Unit Test Framework headlessly and merges JUnit output into one report.Win
AntidocGenerates project documentation from the VI hierarchy using Wovalab's Antidoc CLI.Win
VI Snapshots / BrowserRenders every VI to a content-addressed HTML snapshot gallery (the classic VI Browser).Win
VI Snapshots 2.0Position-aware, in-place VI Browser frames (JSON) rendered by the cross-platform toimages engine.Win Linux
DashboardAggregates everything above into a single status page on GitHub Pages.runner host

Which to turn on, and when

You can change your mind any time Every activity is a checkbox in the Configure Pipeline dialog (and Windows/Linux is a per-activity toggle). Turning one off simply skips its job; nothing is uninstalled and no history is lost.

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.

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

The installer in three layers

LayerFile(s)Role
Entry pointinstall.sh / install.ps1Fetch the tooling from the selected GitHub or GitLab distribution, locate Python, forward to install.py.
Braininstall.py (stdlib only)Resolve the file set from the catalog, apply text substitutions, write the manifest.
Resultthe 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

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: …/labview-ci.reusable.yml@v3) and inherits improvements automatically. A fat install copies all workflow files locally and updates them deliberately via install.py --update. Both are valid; thin is the lowest-maintenance path and the right default for most teams.

After it installs

The wizard enables GitHub Pages, opens (and can auto-merge) the install pull request, and dispatches the first dashboard build so your site goes live within a minute of the merge — no CI run is required to see the dashboard. A fresh repo with existing history shows a Populate the dashboard with your history card so you can backfill results for past revisions on demand.

7. Private, self-hosted & custom setups #

The defaults target public repositories on GitHub-hosted runners because that path is free and zero-maintenance. But the architecture was designed to be adapted. This section covers the common “my situation is different” cases.

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 typeRequired runnerNotes
Windows LabVIEW jobsSelf-managed Windows shell runner with DockerSet LVCI_WINDOWS_RUNNER_TAG when the default windows-docker tag is not suitable.
Linux LabVIEW jobsPrivileged Docker-in-Docker-capable runnerUses docker:27-cli plus docker:27-dind; override LVCI_LINUX_RUNNER_TAG as needed.
Pages publicationAny Linux runner that can run the Python Alpine jobGitLab'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 Snapshots 2.0 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.

Current GitLab parity boundary Native templates cover custom images, Mass Compile, VI Analyzer, Unit Tests, Antidoc, SBOM Generation, Builds, VIDiff, classic Snapshots, and Snapshots 2.0. The GitLab dashboard is intentionally a basic static index, not the GitHub dashboard's live status/configuration UI. GitLab currently does not add VIDiff merge request comments, GitHub-style history backfills or manual configuration screens, or GitHub Release/generic package promotion for Builds artifacts.

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:

Self-hosted runner security A self-hosted runner executes whatever a workflow tells it to. On a public repository, treat self-hosted runners with care (a fork's pull request can propose workflow changes); GitHub recommends self-hosted runners primarily for private repositories, ephemeral runners, and required approvals for outside contributors.

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:

  1. A committed offline VIPC that embeds the package files themselves, so no download is needed at build time.
  2. Direct package-file URLs to an internal mirror (the resolver-bypass fallback can install local .vip/.ogp files).
  3. 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):

flowchart TD E{"What happened?"} -->|"push that touches<br/>a VI / CTL / project file"| R1["The matching activities run<br/>on that commit"] E -->|"pull request<br/>with VI changes"| R2["Activities run on the PR<br/>(results shown as checks)"] E -->|"you press Run<br/>on the dashboard"| R3["That activity is dispatched<br/>for the chosen revision"] E -->|"only .github/ tooling<br/>changed"| R4["Skipped — no LabVIEW source changed"]

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:

  1. Create a branch for your change.
  2. Commit and push. CI runs on the branch.
  3. Open a pull request into main. The activities post pass/fail checks and VIDiff gives you a reviewable visual diff.
  4. 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

Staying up to date

The version badge in the header tells you when newer tooling exists. Update now applies it in one click and preserves your configuration; it never rebuilds your containers. Because your manifest is pinned to an immutable tag, updates are always something you choose, never something that happens to you. See §18.

9. The dashboard & Pages site #

The dashboard is built by the actions/dashboard composite action (Python, dashboard.py) and deployed by dashboard-pages.yml. The action bundles all the tooling pages — the dashboard, VI Browser, report viewer, Configure, Apply, What's New, this Documentation page — so a consumer needs no local copy of any of them.

dashboard-pages.yml rebuilds 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.

dashboard not live? └─ Settings → Pages source set to gh-pages? → no → set it └─ repo private on a free plan? → yes → make public or upgrade └─ just installed (< a minute ago)? → yes → wait; the deploy retries └─ still stuck? → → re-run the dashboard workflow

“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:

A LabVIEW activity errors with -350000

Cause & fix. LabVIEW 2026+ needs the -Headless flag for VI Server operations inside a Windows container; without it, VI Server raises -350000. The bundled drivers already pass it — if you forked a driver, restore the flag.

A dispatched run for an older commit fails instantly

Cause & fix. This was a historical bug (a missing origin/main tracking ref during a checkout of a past commit) that current tooling fixes. If you are on older tooling, the workaround is to re-dispatch with the commit box blank (which targets the latest commit), then update your tooling.

Relatedly, the Determine target SHA step no longer calls the GitHub API when the dispatched commit_sha is already a full 40-character SHA (the dashboard always passes one), and it falls back to the raw input instead of aborting if a short-SHA lookup is rate-limited — so a transient HTTP 403 API rate limit exceeded can no longer make a dispatched report run fail before it starts (which otherwise showed on the dashboard as an empty/failed activity, e.g. “no unit tests”).

The first run on a fresh runner is very slow

Cause & fix. The worker image is multi-gigabyte and a GitHub-hosted runner re-pulls it every job. This is expected. A self-hosted runner with a warm Docker layer cache removes the repeated pull (§7).

Windows jobs fail to start a container

Cause & fix. Every Windows job pins windows-2022 on purpose: windows-latest now resolves to Windows Server 2025, whose Docker daemon is broken for these LabVIEW containers. If you changed a runner label, set it back to windows-2022.

Unit Tests / Antidoc “missing” on Linux

Not a bug. Unit Tests and Antidoc are Windows-only today (they depend on baked VIPM tooling). They are not offered for Linux in Configure or Populate history. VI Analyzer, by contrast, now runs on Linux too — enable it per platform on the Configure page.

A new install does not appear on the Clients page

Not a bug. Discovery is a daily pull-based scan plus GitHub's own search-index lag, so a brand-new public install can take up to a day to appear (§19). Private repos are never listed.

Under the hood

11. The catalog — single source of truth #

Everything starts at .github/labview-ci/catalog.json. It is a versioned manifest that both the browser configurator and the Python installer consume. Its key sections:

{
  "schemaVersion": 1,
  "version": "3.24.5",            // must equal history.releases[0].version
  "history": { "releases": [ { "version": "...", "title": "...", "highlights": [...] } ] },
  "base":   { "files": [ ... ] }, // always installed (dashboard chrome, manifest, …)
  "capabilities": [
    {
      "id": "vi-analyzer",
      "name": "VI Analyzer",
      "status": "stable",          // planned | advanced | stable  → drives UI visibility
      "recommended": true,
      "supportsOs": ["windows"],
      "requires": ["dashboard"],   // hard dependencies
      "recommends": ["dashboard"],
      "statusContext": "CI / VI Analyzer",   // GitHub commit-status context
      "files": { "any": [...], "windows": [...] }
    }
  ],
  "substitutions": { "ordered": [ { "find": "...", "replaceWith": "{ownerRepo}" } ] },
  "userConfig": { "files": [ ".github/labview-ci.yml", ".github/labview/vipm/ci-tooling.packages.json" ] }
}

The status field controls how a capability surfaces: planned is shown but disabled, advanced is opt-in, stable is offered by default. The statusContext string is the GitHub commit-status context the capability's workflow posts and the dashboard reads — that one string is how a result flows from a workflow to a dashboard cell.

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.

Why this matters for extension Because the configurator and installer iterate the catalog generically, you never touch UI code to add a capability. See §20 for the full recipe.

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.

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 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

flowchart TD BASE["Source repo (LCWC)<br/>labview-ci-base.Dockerfile<br/>publishes the shared -labview-base image"] INS["Installation<br/>a repo onboards LabVIEW CI"] --> COPY["copy-labview-image.yml<br/>crane copy + crane tag on ubuntu-latest<br/>copy the base into<br/>ghcr.io/you/repo-labview<br/>a minute or two, no rebuild"] BASE -.->|"copied from"| COPY COPY --> READY["Worker ready<br/>NI + LabVIEW, no project deps yet"] READY -.->|"later"| UPG UPG["Upgrade<br/>add or replace .vipc / .dragon files"] --> CHK{"Declared deps already<br/>baked into the current<br/>worker manifest?"} CHK -->|"yes"| OK["Nothing to do"] CHK -->|"no, some missing"| BAN["Dashboard banner<br/>dependencies need to be installed"] BAN --> APPLY["You open Review and update<br/>opt-in workflow dispatch"] APPLY --> BUILD["build-labview-image.yml<br/>FROM base + thin labview-ci.Dockerfile<br/>bakes your VIPC / Dragon packages"] BUILD --> MODE{"Update mode"} MODE -->|"replace (default)"| REP["Rebuild current tags in place<br/>actions pick it up automatically"] MODE -->|"new"| NEW["Publish dated tag year-deps-date<br/>select per action in Configure"]

Linux builds are handled by build-labview-linux-image.yml on the same model. The Linux worker and the standalone VI Browser 2.0 render image also bake a set of X11 core and TrueType fonts (DejaVu, Liberation, Noto, and the 75/100 dpi xfonts-* families) — NI’s minimal Linux base ships almost none, and without them LabVIEW’s UI controls and rendered VI text (VI Browser snapshots and the Debug Run desktop) come out blank. Images are pushed to your GitHub Container Registry (GHCR) as packages attached to your repo:

ghcr.io/<owner>/<repo>-labview      # your LabVIEW worker (copied from the base, or base + your deps; the Linux worker also bakes the VI Browser 2.0 render engine)
ghcr.io/<lcwc-owner>/<lcwc-repo>-labview-base   # the shared base, owned & published by the source repo

The image inherits your repository's visibility and is authenticated with the job's built-in GITHUB_TOKEN. GHCR rejects uppercase names, so the owner/repo are lowercased at build time.

Content-addressed tagging

Every build computes a worker version — a SHA-256 over the layered Dockerfiles, the VIPC install script, and every baked .vipc — and publishes four tags so consumers can choose their stability/freshness trade-off:

TagExampleStability
:latest:latestAlways the newest build.
:<year>:2026Moves as that LabVIEW year rebuilds.
:<year>-<date>:2026-20250620Dated snapshot, stable until the next rebuild.
:<hash>:win-a1b2c3d4e5f6Fully immutable — same inputs always reproduce the same tag.

Each build also publishes a human/machine-readable worker manifest to /workers/<platform>/<version>/manifest.html listing the exact nipkg and VIPM packages baked in, so a worker image is fully auditable.

The machine-readable manifest.json records installed VIPM packages in vipm_packages (name, version, and VIPC-style label) plus the raw vipm list --installed output in vipm_raw. The Dependencies page unions that installed-package inventory with the manifest’s own vipc[].packages list when deciding what is baked. Because VIPM records versions with a leading v (e.g. v0.5.0.1) while VIPC files declare the bare form (0.5.0.1), the comparison normalizes that v prefix — without it, correctly-baked packages would be reported as missing and the “dependencies need to be installed” banner would never clear.

Dragon dependencies are reconciled against the current Windows worker manifest (the worker rebuilt by build-labview-image.yml). If a manifest has not yet published a dedicated dragon.items status block, the dashboard falls back to the installed-package inventories (vipm_packages / nipkg_packages) so already-baked dependencies are marked installed instead of staying stuck at not_attempted.

When images rebuild

A worker rebuild is a heavy, opt-in operation. It runs when you apply a dependency update from the dashboard's Review & update dependencies dialog, on the monthly cron (to pick up base-image security updates), or by manual dispatch. Adding or changing a project .vipc never triggers a rebuild on its own — the dashboard simply flags that dependencies need installing until you choose to run the update.

Dependency gate (activities wait for the rebuild)

VIPC files are the source of truth for a repo's project dependencies, so the container-based activities must never run against a worker image that predates a dependency update. While a worker rebuild is in progress, the activity workflows — VI Analyzer, Mass Compile, Unit Tests, Antidoc, and VIDiff — call .github/labview/wait-for-worker-images.sh before they pull the image and block until the worker image build(s) for that revision finish. The wait outlasts a cold first build (pulling the multi-gigabyte NI base and baking the project VIPC runs ~80–100 minutes), so a fresh install's first dispatched activity waits rather than failing while the image is still building. In the reusable workflow a single gate-images job does this and the dependency-consuming jobs need it. If the rebuild fails the activities do not run; if nothing worker-affecting changed the gate returns immediately. The snapshot renderer is intentionally exempt because it renders on the bare NI base image.

Manual VIPC base-image bake

bake-vipc-windows-base.yml is a focused proof-and-promote workflow for a single VIPC. It runs on windows-2022, pulls :2026 of the shared -labview-vipm-base image (or builds and pushes it once when missing or explicitly requested), stages only the selected VIPC (default: example/Dependencies.vipc) into the thin Windows dependency layer, verifies that VIPM reports the VIPC packages as installed inside the resulting container, and only then pushes the verified image to GHCR. On success it promotes the normal project tag (for example :2026), an operator-selected moving tag (default :vipc-base), and an immutable VIPC-hash tag.

Validated Windows base split The successful 2026 proof path stores VIPM and Git in 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:

  1. Per-action override — config.container.actions.<id> (e.g. pin VIDiff to a frozen build while Mass Compile tracks latest).
  2. Project-wide default — config.container.use.
  3. 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:

Container lifecycle in one sentence A fresh container is created for each action × revision × platform, started when the action begins and destroyed when it ends; it is shared across all files in one action's run but never across actions or revisions. The snapshot renderer is the sole always-on exception — see §16.6 — and the opt-in warm-container history backfills below are the other.

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 #

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:

Linux bakes the same dependencies The Linux worker (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”.
Package scripts remain package-owned 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.

  1. Use VIPM 2026 Q3 from the JKI CDN — the older NI-feed CLI times out on library_list with no diagnostic.
  2. 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.
  3. Non-interactive env (VIPM_NONINTERACTIVE=1, VIPM_ASSUME_YES=1, NO_COLOR=1) so the CLI never blocks on a prompt.
  4. Keep package install actions headless — a package's own post-install VI is outside the CLI prompt contract. The Linux hook forces --show-progress --verbose so its phase is visible, reports focused process/display diagnostics on failure, and preserves the nonzero result instead of publishing a partial worker.
  5. Seed Settings.ini before installing — a fresh image lacks C:\ProgramData\JKI\VIPM\Settings.ini, without which the CLI aborts with an IO error.
  6. Run LabVIEW headless first — the modern CLI installs into a running LabVIEW over VI Server, so the script launches LabVIEW.exe --headless and waits for port 3363.
  7. Extend timeouts (VIPM_TIMEOUT=900 and Linux VIPM_DESKTOP_LIVELINESS_TIMEOUT=900) — cold first-run headless LabVIEW and package downloads exceed the CLI's short defaults during a build, where CI/GITHUB_ACTIONS env hints are absent.
  8. Never run the VIPM engine under a global headless default — the worker base bakes LV_RTE_HEADLESS=1 so 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 at wait for VIPM startup. This silently broke every Windows dependency bake from 2026-08-01 to 2026-08-21 (NI's unannounced latest-windows repoint 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.
  9. 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 build container starves it into the identical wait for VIPM startup wedge. The image workflows build with docker 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=1 overrides).
  10. Fail fast on a dead Desktop engine — a refresh message containing wait for VIPM startup proves package installs will wait on the same unresponsive process. The hook restarts headless LabVIEW and VIPM once (VIPM_MAX_REFRESH_RESTARTS=1 by 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.
  11. Use the modern command shape — vipm refresh --force is a standalone command; package installs use vipm --labview-version 2026 --labview-bitness 64 install <name>@<version> after extracting the package declarations from the VIPC. The old install --refresh form and apply_vipc verb are rejected by VIPM 26.3.
  12. 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/.ogpd indexes directly, resolves the VIPC package set plus transitive dependencies, downloads the public .vip / .ogp files, validates them, and installs those local files with VIPM.
  13. Treat direct VIPC no-op applies as failures — in the container VIPM can return exit 0 from vipm install -y project.vipc while printing No packages were installed. The hook treats that text as a failed apply and continues to the package-spec and local-file fallback paths.
Discovery & the base/project stack Every .vipc in the repo is discovered recursively and listed on the Dependencies page. That page owns the monitored-file selection written back as config.container.vipc entries; Configure only owns the global dependency policy. The tooling also ships a base configuration — .github/labview/vipm/ci-tooling.vipc — that is always monitored and locked on, while a project's own discovered VIPC files can be checked or unchecked. Checked project files produce warnings when their packages are missing from the worker; unchecked project files are tracked in config with monitor: false and do not produce warnings or automatic dependency-update triggers. When this template repo updates the base, consumers receive the new base on their next tooling update and the next container rebuild re-applies base + project together. Because each repo publishes to its own GHCR namespace, those rebuilds produce that repo's own unique worker image.

After a worker image is built locally, the publish workflow captures both nipkg list and vipm list --installed from inside the image and writes them into the worker manifest. Fresh-install seed copies use a fast registry copy and do not pull the multi-GB image back just to inspect it; seed manifests without a captured VIPM list are treated conservatively by the dashboard, with only the known Windows core tooling VIPC inferred as installed.

Required essentials vs. best-effort tooling install-vipc.ps1 installs the UTF JUnit essentials (ni_lib_utf_junit_report + its ni_lib_junit_results_api / ni_lib_simple_xml deps) first as required, then applies project .vipc files (also required). Tooling VIPCs (ci-tooling*.vipc, e.g. Antidoc / Caraya / VI Tester / LUnit) are best-effort: if a heavy add-on wedges the headless VIPM engine it is logged as a warning and the build still publishes a working, UTF-capable image. Override the required set with VIPM_REQUIRED_PACKAGES.
Public-index fallback The resolver-bypass fallback covers packages in the built-in NI LabVIEW Tools Network and VIPM Community repositories, including old OpenG 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.
Baking a package on no VIPM repository A package published on no public index (e.g. an in-house framework) can be baked without a private mirror by committing its .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.
Why the UTF JUnit reporter is mandatory for unit tests The built-in LabVIEWCLI -OperationName RunUnitTests links against ni_lib_utf_junit_report. Without it baked in, headless UTF runs fail with error -350053 ("missing or bad files in the operation folder").

The committed tooling VIPC is the source of truth

The worker dependency intent is the committed ci-tooling.vipc itself. It must remain a real, VIPM-openable file and is the file applied during image builds. The Reconfigure workflow validates the committed VIPC but does not regenerate it, so a UI-authored VIPC is not overwritten by automation.

build-tooling-vipc.py remains available as an optional helper: it can assemble a genuine VIPM-openable VIPC from JSON/package-index inputs (real spec + icon assets from public indexes), but that generated output is only authoritative once committed as ci-tooling.vipc.

16. Capabilities in depth #

Every capability is built to the same template, so once you understand one you understand them all. §16.4 (Unit Tests) is documented end-to-end as the fully worked example you can copy from.

The shared anatomy of a capability:

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
Workflowsmasscompile-windows-container.yml windows-2022, masscompile-linux-container.yml ubuntu-latest; backfill masscompile-backfill-windows.yml
Output & statusmasscompile/<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

Exit-code semantics 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

Driverrun-vi-analyzer.ps1 Windows / run-vi-analyzer.sh Linux
Report builderbuild-analyzer-report.py
Workflowsrun-vi-analyzer-windows-container.yml; backfill vi-analyzer-backfill-windows.yml
Output & statusvi-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

Report & dashboard

16.3 VIDiff #

At a glance

Drivervidiff.ps1 / vidiff.sh; history walker vidiff-backfill.ps1 / .sh
Deploy + PR commentvidiff-deploy.yml
Workflowsvidiff-windows-container.yml windows-2022, vidiff-linux-container.yml ubuntu-latest; backfill vidiff-backfill-windows.yml
Output & statusvidiff/<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

PrintToSingleFileHtml The custom operation under .github/labview/PrintToSingleFileHtml/ is a binary .lvclass sourced from NI's labview-for-containers examples; it renders a single VI (front panel + block diagram) to a standalone HTML file and powers both added/deleted diffs and snapshots.

16.4 Unit Tests — a fully worked example #

Unit Tests is documented end-to-end here as the reference capability. Every other activity shares the anatomy above, so if you can reproduce Unit Tests from the detail below — the frameworks, the exact CLI commands, the container invocation, and the output contract — you can reproduce any of them.

At a glance

In-container driver.github/labview/run-unit-tests.ps1 — runs each enabled framework headless, writes one JUnit XML per tool.
Host report builder.github/labview/build-unittest-report.py — merges the JUnit XML into results.json + the navigable HTML report.
Per-commit workflowunit-tests-windows-container.yml on windows-2022.
History backfillunit-tests-backfill-windows.yml + unit-tests-backfill.ps1 — one warm container across many revisions.
Output & statusReport deploys to unit-tests/<sha>/ on gh-pages; commit-status context is CI / Unit Tests.
PlatformWindows only — every framework depends on baked VIPM/UTF tooling (§15).

The four frameworks & the exact headless command each runs

Each enabled tool is driven by a per-tool command template that run-unit-tests.ps1 applies verbatim after token substitution. Any template can be overridden per tool with a command: key in the config, so a precise invocation can be corrected on a real worker with no script change.

FrameworkTest unit it discoversRuns viaDefault command template
Caraya (JKI)VIs containing Caraya assertions, under a directoryg-clig-cli --lv-ver {ver} -- caraya -- --directory "{dir}" --junit "{out}"
LUnit (Astemes)Test Case classes (.lvclass), under a directorynative LabVIEWCLI"{cli}" -LogToConsole TRUE -OperationName LUnit -Path "{dir}" -ReportPath "{out}" -LabVIEWPath "{lv}" -Headless
VI Tester (JKI)xUnit TestCase classes, under a directory (scaffold)g-clig-cli --lv-ver {ver} -- vitester -- --directory "{dir}" --junit "{out}"
NI UTF.lvtest items inside a .lvprojnative LabVIEWCLI"{cli}" -LogToConsole TRUE -OperationName RunUnitTests -ProjectPath "{proj}" -JUnitReportPath "{out}" -LabVIEWPath "{lv}" -Headless

Tokens the driver substitutes: {ver} = LabVIEW year, {dir} = a resolved test-root directory, {proj} = a resolved .lvproj, {out} = the JUnit output path, {lv} = LabVIEW.exe, {cli} = LabVIEWCLI.exe.

Directory-based tools (Caraya, LUnit, VI Tester) run once per resolved test-root. UTF runs a whole .lvproj, so each location resolves to its owning project and is robust to however a repo is laid out: a location may be a .lvproj path, a folder that contains one or more test-bearing projects, or a folder of .lvtest files whose .lvproj lives in a parent directory (the driver walks up to the nearest owning project). An empty locations list discovers and runs every test-bearing .lvproj anywhere in the repo, so a fresh install reports results for whatever projects exist.

Configuring which frameworks run

The configurator writes a unitTests block into .github/labview-ci.yml; the driver reads it directly. Enable a tool, point it at one or more locations, and optionally override its command or the dashboard pass-rate thresholds:

unitTests:
  thresholds:
    passedPercent:            # colours the dashboard Unit Tests badge
      greenAtLeast: 100
      yellowAtLeast: 80
      redBelow: 80
  tools:
    utf:
      enabled: true
      locations:
        - "example/User-Defined Test/"   # a dir, a glob, or a .lvproj (empty = whole repo)
    caraya:
      enabled: false
      locations: []
    lunit:
      enabled: false
      locations: []
      # command: '...'                    # optional per-tool override
    vi-tester:
      enabled: false
      locations: []

Only tools with enabled: true run. A location may be a directory (recursed), a glob / extension pattern (each match’s parent directory becomes a test root), or — for UTF — a .lvproj path.

How the workflow composes the runner (per commit)

The whole job runs on one windows-2022 runner and follows a fixed step order (it mirrors VI Analyzer so the two behave identically):

  1. Determine the target SHA — from the workflow_dispatch input, the PR head, or github.sha.
  2. Post a pending status (CI / Unit Tests) so the dashboard cell spins immediately.
  3. 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.
  4. Resolve the container image — config.container.actions.unit-tests → config.container.use → the shared worker :latest (base/none forces the bare NI image).
  5. 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.
  6. Pull the image, then run the driver inside a throwaway container (below).
  7. Build the report on the host (Python), upload the artifact, deploy to gh-pages under unit-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:

The output contract

The container emits raw JUnit XML; the host builder normalises it. Two files matter:

The commit status is derived straight from results.json so it always matches the report:

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:

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.

If the worker lacks a framework’s tooling A framework whose CLI add-on is not baked into the image fails to load: UTF/LUnit surface LabVIEW CLI error -350053 (“missing or bad files in the operation folder”). The driver records this in _tooling.json and the report renders an explicit “the selected container is missing this dependency” banner instead of a bare “no tests found.” The UTF JUnit reporter in particular is mandatory — see §15.

16.5 Antidoc #

At a glance

Driverrun-antidoc.ps1; report build-antidoc-report.py
Workflowrun-antidoc-windows-container.yml windows-2022; backfill antidoc-backfill-windows.yml
Triggerpush to the default branch + dispatch (no per-PR run — it’s a heavier render)
Output & statusantidoc/<sha>/; status CI / Antidoc
Dependenciescommitted .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:

Runner composition

docker run --rm `
  -e "GITHUB_REPOSITORY=${{ github.repository }}" -e "GITHUB_SHA=${{ steps.sha.outputs.sha }}" `
  -v "${{ github.workspace }}:C:\workspace" `
  $Env:LABVIEW_CONTAINER_IMAGE `
  powershell -NonInteractive -ExecutionPolicy Bypass `
    -File "C:\workspace\.github\labview\run-antidoc.ps1" `
    -WorkspaceRoot "C:\workspace" -ReportDir "C:\workspace\ci-out\antidoc" `
    -LabVIEWPath "...\LabVIEW.exe"

The report

The report page is a two-pane in-browser viewer: a file navigator (main document, its include:: sections, every rendered image) beside a pane that shows the fully rendered document by default — Asciidoctor.js resolves Antidoc’s includes (fetched up front) and pins imagesdir at the deployed image folder — or raw source / an image preview on demand. If the renderer CDN is unreachable the raw source stays visible and every file remains downloadable.

Dependency intent is the committed .github/labview/antidoc/antidoc.vipc. As with ci-tooling.vipc, this file is authoritative and must remain VIPM-openable; optional JSON metadata may assist tooling but does not override the committed VIPC.

16.6 VI Snapshots & VI Browser #

At a glance

Driversbuild-snapshots.ps1 (orchestrator) → render-snapshots.ps1 (per-VI); gallery build-gallery.py; 2.0 engine toimages / lvctl toimages
Workflowsvi-snapshots.yml windows-2022; 2.0 vi-snapshots-json.yml Linux / vi-snapshots-json-windows.yml Windows
Output & statusvi-snapshots/ (content-addressed by-blob/<ab>/<blob>.html|.json); status CI / VI Snapshots

This is the one place that deliberately reuses a warm container. Snapshots are content-addressed by git blob SHA, so each unique VI is rendered exactly once — ever — and reused across every commit that contains it. If an older revision references a snapshot or report document not yet published, the VI Browser and framed report viewer show an in-page missing-document state with a Populate history link instead of framing a GitHub Pages 404.

The warm-container pattern

Both phases start one long-lived container and push every VI through it with docker exec — no per-VI or per-commit container churn:

docker run -d --name <name> ... $Image powershell -NoProfile `
  -Command "while ($true) { Start-Sleep -Seconds 3600 }"
docker exec <name> powershell -File C:\ops\render-snapshots.ps1 ...   # per worklist entry

Phase 1 — HTML snapshots and Windows 2.0 frames Windows

Orchestrated by build-snapshots.ps1, which starts a single long-lived container (docker run -d … while true sleep) and renders many commits through it via docker exec calling render-snapshots.ps1 — no per-VI or per-commit container churn. A TSV worklist (<blob>\t<rel-path>) drives rendering; render-snapshots.ps1 skips any blob already present in the deployed by-blob store (seeded from gh-pages) and writes a placeholder HTML for any VI that fails to render, so the gallery is never broken by a single bad VI. When config.viBrowser.positionAware includes windows and the bundled PrintToImagesJson operation is available, the same workflow also writes Windows 2.0 frame JSON beside the HTML as <blob>.windows.json. build-gallery.py emits the per-commit manifest.json (VI → by-blob HTML), the rolling commits.json, and the Windows JSON index used by the in-place browser.

Phase 2 — position-aware JSON (VI Snapshots 2.0) Windows Linux

Phase 2 is the separate VI Snapshots 2.0 capability, selectable on Windows and Linux (the classic gallery above is Windows-only). The vi-snapshots-json.yml (Linux) and vi-snapshots-json-windows.yml (Windows) workflows render position-aware frame JSON for the in-place browser. The render engine is baked into the Linux worker image (and runs in the stock NI image on Windows) — a Go batch runner (main.go) reads the same TSV worklist and shells out to lvctl toimages — a native Go VI-Server TCP client that launches LabVIEW under Xvfb, captures the front panel and block diagram as positioned frames, and emits a JSON array. Output is written to by-blob/<ab>/<blob>.json, deployed alongside the 1.0 HTML so any VI without JSON gracefully falls back to the classic view.

Both 2.0 renderers auto-run on every new revision via a workflow_run trigger after the snapshot pipeline finishes, each behind a gate job that reads config.viBrowser.positionAware: the Linux workflow runs when it includes linux, the Windows workflow when it includes windows (the configurator default). Because renders are content-addressed by git blob SHA, the worklist skips every VI whose blob already has a render — unchanged VIs reuse their existing frames with no work — and queues only the new/changed blobs, so a VI a revision actually changed gets a fresh render automatically.

The browser now remembers its viewer mode and 2.0 platform choice in local storage, defaults to 2.0 when a VI has a 2.0 render (preferring the Windows render when both platforms exist), and shows dismissible coaching tip strips for the block-diagram toggle and the case/event selector arrows. The 2.0 auto-default is now enforced on open whenever a 2.0 render exists (unless the URL explicitly pins viewer/plat2). Those tips are browser-local too: dismissing one with Never show this again hides it only in that browser profile, and the dismissal keys are versioned so a new coaching rollout can deliberately surface the tips once more for everyone.

16.7 SBOM Generation #

At a glance

Driver.github/labview/run-sbom-generation.ps1 Windows
Report builder.github/labview/build-sbom-report.py
Workflowgenerate-sbom-windows-container.yml windows-2022
Output & statussbom/<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:

FileProduced byRead by
masscompile/<sha>/summary.jsonMass Compile report builderDashboard cell (pass %, counts)
vi-analyzer/<sha>/summary.jsonVI Analyzer report builderDashboard cell
vi-analyzer/<sha>/linux/summary.jsonVI Analyzer report builder (Linux)Dashboard cell (Windows / Linux split)
vidiff/<segment>/<platform>/vidiff/changes.jsonVIDiffDashboard + VIDiff report
unit-tests/<sha>/results.jsonUnit-test report builderDashboard + report
builds/<sha>/summary.jsonBuilds report builderDashboard cell (succeeded/failed split) + report
sbom/<sha>/results.jsonSBOM report builderDashboard report availability + HTML report
sbom/<sha>/results/sbom.jsonVIPM CLINative CycloneDX download
vi-snapshots/<sha>/manifest.jsonSnapshot gallery builderVI Browser
dependencies/index.jsonDashboard publisherDependencies page install-time inventory
workers/<platform>/<version>/manifest.jsonWorker image workflowsDependencies page installed-package status
catalog.json (root)Installer / integrate-deployVersion 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.

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.

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:

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.

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:

  1. the labview-ci repository topic (the primary, structured signal);
  2. this repo's slug as source.repo in a client's catalog.json;
  3. 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:

  1. 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.
  2. Write the driver + report builder — e.g. .github/labview/run-unit-tests.ps1 and build-unittest-report.py, emitting a summary.json/results.json under <prefix>/<sha>/.
  3. Add one catalog entry — id, name, status, supportsOs, requires, statusContext, and the files map. The configurator, installer, and dashboard pick it up generically.
  4. Register the report type — add a single entry to DOCTYPES in lvci-header.js and emit the matching window.LVCI from 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.

Keep this page in lock-step Whenever you add a capability, change container behaviour, or "learn" how to make something work (for example a new VIPM/headless trick), update this documentation in the same change. The FAQ covers the "what"; this page is the canonical "how", and it is expected to stay accurate. This rule is enforced for contributors and AI assistants alike — see the repository's contributor and Copilot instructions.

21. Security model #

22. Constraints & gotchas #

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.

Why windows-2022, never windows-latest 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

flowchart TD A["A workflow job needs to run"] --> B{"Runs LabVIEW inside a<br/>Windows Docker container?"} B -->|yes| W["windows-2022<br/>Windows container build"] B -->|no| U["ubuntu-latest<br/>orchestration, config gating<br/>Pages deploys, releases<br/>Linux containers, discovery<br/>maintenance, image publishing"]

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.

flowchart TD T["commit / PR / workflow_dispatch"] --> C["config · ubuntu-latest<br/>reads .github/labview-ci.yml"] C --> G["gate-images · ubuntu-latest<br/>waits for this SHA's worker-image rebuild"] G --> M1["mass-compile · windows-2022<br/>if has-masscompile and os-windows"] G --> M2["mass-compile · ubuntu-latest<br/>if has-masscompile and os-linux"] G --> V["vi-analyzer · win / linux<br/>if has-vi-analyzer and os-platform"] G --> D["vidiff · win / linux<br/>if has-vidiff and os-platform"] G --> S["snapshots · windows-2022<br/>if has-snapshots and os-windows"]

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.

flowchart TD T["dispatch (dashboard 'update dependencies') / monthly cron"] --> G PUSH["push to a .vipc / .dragon"] --> G{"gate · ubuntu-latest<br/>dispatch/cron: rebuild<br/>push: only if a MONITORED file changed"} G -->|"rebuild"| CO["coordinate · ubuntu-latest<br/>drain or cancel in-flight CI"] G -->|"push, nothing monitored"| SKIP["skip — no rebuild"] CO --> B["build-and-push · windows-2022"] B --> P{"Any dependency to bake?"} P -->|"vipc/dragon (bake) · windows-2022"| BD["build dependency layer — slow"] P -->|"none (fallback) · windows-2022"| CP["crane copy / tag base — a minute or two"] CP --> PU["push tags → worker manifest → gh-pages"] BD --> PU INS["install seed (separate workflow)"] --> CW["copy-labview-image.yml · ubuntu-latest<br/>crane copy / tag base"] CW --> PU
Linux image builds mirror Windows The Linux worker uses the same two-action split as Windows, on ubuntu-latest (Linux containers build on a Linux runner). The install seed is its own workflow, copy-labview-linux-image.yml, which the installer dispatches to crane-copy the shared LCWC Linux base (…-labview:linux-2026) into the client's own package — a fast registry copy, not a rebuild, so a Linux install never scales with the number of .vipc / .dragon files. build-labview-linux-image.yml is bake-only: a project dependency change does not rebuild the worker by default (the “dependencies need to be installed” banner prompts an on-demand update), and a push rebuilds automatically only when it changes a monitored .vipc — exactly the opt-in behavior of the Windows worker. It also accepts the same update_mode dispatch input (replace rebuilds the moving linux-latest/linux-<year> tags in place; new publishes a fresh <tag>-deps-<date> tag alongside without moving them). (The Linux Beta builds directly on ubuntu-latest with no separate gate; the VI Browser 2.0 render engine is now baked into the Linux worker, so there is no separate render image.)
On-demand updates cover both workers The Dependencies page’s “Run the container update” flow lets you rebuild the Windows worker, the Linux worker, or both: a “Containers to rebuild” pair of checkboxes is pre-checked from the pending signal but you can select either or both explicitly. It then dispatches 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.
The dashboard refreshes after a build A completed worker build (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

WorkflowRunnerTriggersRuns when / skips
Mass Compile — Windows Containerwindows-2022PR / push on VI source (**.vi/.ctl/.lvproj/.lvlib/.lvclass, excl. .github/), dispatchCompiles every VI; skipped when only tooling under .github/ changed.
Mass Compile — Linux Containerubuntu-latestsame as aboveLinux counterpart; deploys under masscompile/<sha>/linux/.
Run VI Analyzer — Windows Containerwindows-2022PR / push VI source, dispatchFull .viancfg suite incl. per-path rule subsets.
Run VI Analyzer — Linux Containerubuntu-latestsame as aboveLinux counterpart; deploys under vi-analyzer/<sha>/linux/.
VIDiff Report — Windows Containerwindows-2022PR / push **.vi/.ctl, dispatchDiffs changed VIs; runs on the bare NI image (no custom worker).
VIDiff Report — Linux Containerubuntu-latestPR / push **.vi/.ctl, dispatchLinux counterpart.
Run Unit Tests — Windows Containerwindows-2022PR / push (VIs, **.lvtest), dispatchWindows-only (depends on baked VIPM/UTF tooling).
Run Antidoc — Windows Containerwindows-2022push to default branch, dispatch (no PR)Windows-only; heavier doc render, so no per-PR run.
VI Snapshots and VI Browserwindows-2022push VI source (excl. .github/), dispatch (head / backfill)Renders HTML snapshots + Windows 2.0 frames in one warm container.
VI Snapshots JSON (VI Browser 2.0, Linux)ubuntu-latestworkflow_run after snapshots, dispatchGate job decides; auto-runs only when positionAware includes linux.
VI Browser 2.0 render (Windows container)windows-2022workflow_run after snapshots, dispatchGate job decides; auto-runs only when positionAware includes windows (the configurator default). Additive Windows frames via COM in the stock NI image.

Image builds

WorkflowRunnerTriggersRuns when / skips
Build LabVIEW CI Image (Windows worker)ubuntu-latest gate + coordinate, windows-2022 builddispatch (dashboard “Review & update dependencies” or manual), monthly cron, and a push that changes a monitored .vipc/.dragonDependency 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-latestdispatch (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 — Linuxubuntu-latest (gate + build)dispatch (dashboard or manual), monthly cron, and a push that changes a monitored .vipcDependency bake (mirrors the Windows worker); also bakes the VI Browser 2.0 render engine into the worker. On a push the gate rebuilds only if a changed .vipc is monitor: true; otherwise it skips.
Copy LabVIEW CI Image — Linux (install seed)ubuntu-latestdispatch (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 Imagewindows-2022dispatch onlyProof-and-promote: verifies a VIPC installs before pushing tags.

Publishing & release

WorkflowRunnerTriggersRuns when / skips
CI Dashboard — GitHub Pagesubuntu-latestpush page assets, status event, workflow_run completions, dispatchLatest-wins rebuild; reacts to every capability's status with no enumeration.
Integrate Configurator — Pagesubuntu-latestpush configurator / catalog.json, dispatchPublishes Apply / Configure / What's New + catalog.
VIDiff Deploy — Pages + PR Commentubuntu-latestworkflow_run after VIDiff reportsDeploys the diff and comments on the PR (runs even on failure).
Catalog Source Syncubuntu-latestPR / push to catalog, docker, or build workflow; dispatchFails if installer file lists drift from the source-owned worker files.
Releaseubuntu-latestpush catalog.json on main, dispatchTags v<x.y.z> and moves the v<major>/v<major.minor> aliases.

Maintenance & diagnostics

WorkflowRunnerTriggersRuns when / skips
Discover Clientsubuntu-latestdaily cron, dispatchSource repo only (if: github.repository == source).
Apply LabVIEW CI updateubuntu-latestdispatch (the What's New "Update now")Runs install.py --update; commits to default branch or opens a PR.
Reconfigure LabVIEW CIubuntu-latestdispatch (Configure Pipeline)Rewrites labview-ci.yml and opens a reviewable PR.
VIDiff Backfill — Windows Containerwindows-2022dispatchWalks VI history rendering diffs through a warm container; resumable.
VIDiff Backfill — Linux Containerubuntu-latestdispatchLinux counterpart of the backfill.
Example project CI (composite-action demo)ubuntu-latest config + windows-2022 activitiesdispatchDogfoods the local ./actions path against the example project.
toimages COM probe (diagnostic)windows-2022dispatchConfirms COM-driven rendering in the stock NI image; touches nothing in production.
Self-hosted runners Both runner labels default to GitHub-hosted but can be redirected to self-hosted labels via the reusable workflow's 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).

← Back to the FAQ  ·  View the source on GitHub →