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 #

Existing GitHub workflows can also call actions/worker-image and actions/unit-tests directly, without installing client configuration or vendoring scripts. Both composites use the current source scripts from their pinned action revision. They require a Windows container runner such as windows-2022; see the composite example and contracts.

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

Fork pull requests are analysis-only. Their jobs still run and upload report artifacts for review, but they do not publish to the shared gh-pages branch because GitHub gives fork code a read-only token. After a reviewed change merges, the main push reruns the pipeline with repository permissions and publishes the canonical report and dashboard update. Collaborators who need pre-merge publication should create branches in the repository, rather than use forks.

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 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. LUnit also runs on Linux.Win Linux
AntidocGenerates project documentation from the VI hierarchy using Wovalab's Antidoc CLI.Win
VI SnapshotsPosition-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.

Updates migrate the legacy snapshots-2 selection to the unified snapshots capability, including browser reinstalls that keep current settings. Snapshots remain selectable; updates do not enable them in clients that opted out.

The catalog's retiredFiles list identifies obsolete tooling workflows, with ownership signatures checked before deletion. The retired build-toimages-image.yml workflow is removed by the command-line installer; browser reinstalls offer it in the existing stale-file cleanup preview. Unrecognized content is preserved. Command-line cleanup skips symlinks and directories, and installation refuses source checkouts and forks carrying source-only release or relocation files. Test installs belong in a separate scratch client.

GitLab CI

Pass --provider gitlab to the command-line installer, or choose GitLab in the browser configurator. It copies the portable LabVIEW drivers under .github/, a native provider package under .gitlab/labview-ci/, and a generated provider pipeline. A new repository receives a small .gitlab-ci.yml root file that includes .gitlab/labview-ci/pipeline.yml. The browser flow creates a GitLab merge request using a token with the api scope.

An existing root pipeline is never text-merged. It must already include that local provider pipeline; if it declares stages, its list must contain prepare, verify, and pages. Otherwise the installer stops before writing anything and explains the manual include. The command-line --force option deliberately replaces the root pipeline only when that is the operator's intention. The browser performs the same compatibility check before it opens a merge request.

What you need first

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

After it installs

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

7. Private, self-hosted & custom setups #

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

GitLab-native provider

GitLab does not execute GitHub Actions syntax, so it uses a source-owned adapter instead of trying to translate workflows at run time. The source package at .github/labview-ci/providers/gitlab/ is copied unchanged to .gitlab/labview-ci/. Its files.json manifest names every provider file and maps each capability and operating system to a native GitLab template. The installer expands catalog requires first, verifies those mappings, and fails rather than recording a capability that has no native job. The source package is also vendored under .github/labview-ci/ so a local install can later be re-run with --provider gitlab. Its files.json ownership manifest is copied into the target package too, so an update prunes only files a prior provider version declared as its own (plus the recognized dashboard-only scaffold from the original preview).

The generated pipeline.yml owns prepare, verify, and pages stages, common job primitives, the Pages publisher, and only the selected native templates. It exports GITHUB_REPOSITORY, GITHUB_SHA, and GITHUB_REF from GitLab CI_* variables so existing portable report drivers keep their established contract. It makes no GitHub API calls, posts no GitHub commit statuses, and does not depend on workflow dispatch.

GitLab job 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 snapshot renderer requires that custom Linux worker or an explicit LVCI_LINUX_IMAGE; without either it records a diagnostic and skips rather than pulling a non-existent registry tag.

GitLab Pages is durable rather than a per-pipeline report folder. The serialized pages job restores the latest default-branch Pages artifact with its job token, merges job-local snapshot stores, stages new reports directly below public/, and publishes the resulting site. This preserves earlier report history despite GitLab artifacts normally being scoped to one pipeline.

Current GitLab parity boundary Native templates cover custom images, Mass Compile, VI Analyzer, Unit Tests, Antidoc, SBOM Generation, Builds, VIDiff, and Snapshots. 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.

The manifest writer preserves project-owned config blocks verbatim, including unitTests, container, builds, and concurrency, while refreshing the installer-owned LabVIEW version, branch, and OS fields. An existing image name is kept unless explicitly overridden. This prevents an upgrade from silently disabling a native Caraya runner or dropping its dependency selection. If an earlier upgrade already removed those blocks, restore them from the last known-good manifest; preservation cannot reconstruct settings that are no longer present.

The legacy top-level unitTests block produced by Configure Pipeline is retained too. Its JSON settings input accepts a single-line per-tool command; the workflow quotes it safely and the runner decodes escaped paths and quotes before token substitution. Restore the native wrapper command as well as enabling Caraya; enabling the framework alone selects its default scaffold.

9. The dashboard & Pages site #

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

dashboard-pages.yml rebuilds after qualifying main-branch pushes (the manifest, dashboard workflow, bundled page assets, or dependency declarations), relevant workflow_run completions, its hourly schedule, and manual dispatch. Installed repositories stage the bundled pages from their selected immutable source.ref; the source repository stages the triggering commit instead, so a page change can deploy before its new release tag exists.

dashboard.py reads each commit's statuses (keyed by the catalog's statusContext), classifies whether a commit touched real LabVIEW source vs. CI tooling, aggregates per-activity data from the deployed JSON (e.g. masscompile/<sha>/summary.json, the snapshot by-blob index), and renders one dashboard row per recent commit. To keep the project's own revisions visible even after many CI/tooling commits have piled up on main, it keeps paging back through history — past the recent window it retains only project revisions — until it has collected the project's revisions or reaches a large safety cap on how many commits it will classify. Reading that history is resilient to a transient GitHub API failure: rate-limited (HTTP 403/429), server, and network errors are retried with backoff (honoring Retry-After/X-RateLimit-Reset), and if a call still fails so the revision list would be incomplete, the build refuses to publish and exits — the previous good dashboard stays live and rebuilds automatically once the API recovers, so a burst of concurrent CI activity can never overwrite it with a blank or truncated table. It also stages the bundled tooling pages and writes the VI Browser's files.json index.

On a fresh install with project revisions but no CI results yet, dashboard.py emits a Populate the dashboard with your history card above the table. The card opens the Populate history chooser, can be dismissed per repository in local storage, and hides as soon as that dashboard has queued one of its own runnable cells or a later dashboard rebuild sees a running/finished activity from a revision or pull request. Queued-run state is matched against the current table's cells by capability and target revision (or globally for the all-history snapshot backfill) so another repository on the same Pages origin cannot suppress the fresh-install card.

Each activity in the chooser is set independently to Skip, Fill (queue only the revisions missing that result), or Re-run (queue every selected revision, replacing existing results). An activity is offered for Re-run when any selected revision has a result, an empty cell, or an in-flight queued/running cell — the latter are tagged cidash-active-cell so a re-run replaces the in-flight run via concurrency instead of greying out as “none in scope.” The optimistic “Queued” badge overlays that in-flight cell too, so re-running the latest revision's still-running activity shows its Queued spinner immediately rather than silently doing nothing. A cell whose only state is a browser-side optimistic overlay — a cidash-queued-cell that replaced an empty run glyph when you dispatched it, including the sticky red “Failed” badge for a dispatch that failed — is gathered the same way, so a failed run stays re-runnable here instead of disappearing as “none in scope.” Clicking that red “Failed” badge opens its manage dialog, which now also offers Re-run (dispatch it again for that commit) and, when the failed run still deployed a report, a View report link straight to it — a hard failure that produced no report keeps just the “View run” link to the Actions log. Queuing a large batch — more than ten workflow runs across the selected revisions and activities — first asks for confirmation, so an accidental “all revisions” selection can't fire dozens of runs without a prompt.

The chooser's revision picker (the All / range / specific selectors) mirrors the dashboard table: dependency-only revisions — those whose only change is an external dependency (.vipc/.vip) — are hidden by default and revealed with a “Show dependency-only revisions” toggle (shown only when such a revision exists), defaulting to whatever the dashboard's own dependency-only filter is currently set to. A search box narrows the picker by commit title or short SHA across all three scopes, so you can queue the latest change to LabVIEW code without scrolling past dependency-only commits. CI-only revisions never appear, because the history feeding the chooser already contains project revisions only.

The same searchable revision picker backs every other “select a revision” control (the Dependencies page, the Unit Testing project picker and the per-revision report viewer’s Revision selector). It is a shared widget in lvci-header.js (window.lvciRevPicker) that enhances a native <select> in place — the select stays the source of truth, so existing behaviour (deep links, steppers, change handlers) is unchanged — and adds a filter box that searches by commit title, build number or short SHA. By default it shows only revisions that change LabVIEW source, hiding CI/tooling and dependency-only (.vipc/.vip) revisions behind a “Show CI & dependency-only revisions” toggle, so picking “the latest revision” anywhere land On the Dependencies page this picker now lives in the shared header context bar — the same place the report viewer and VI Browser put their Revision selector (built by the header from window.LVCI.revPicker and exposed as window.lvciRevBar) — instead of a separate in-page dropdown.s on the latest LabVIEW-code change. The classification is published per revision in dependencies/index.json and vi-snapshots/files.json, so every picker filters identically to the dashboard.

The Populate-history chooser offers an opt-in "keep one container warm across revisions" checkbox, shown only when at least one selected activity has a warm-container backfill workflow installed (Windows Mass Compile, VI Analyzer, Unit Tests, Antidoc). It is always off by default and carries a red warning that container state carries over between revisions (see §14). When ticked, the chooser collapses every batch-capable (activity, platform) into a single workflow_dispatch carrying the whole oldest→newest SHA list instead of one dispatch per revision; non-batch platforms still queue per revision. The chooser also shows a live, history-driven time estimate comparing fresh vs warm-batch compute: the per-activity work time is the median of the durations the dashboard already loads from each summary.json/results.json, and the startup time is the median run total (from the Actions API) minus that work time, falling back to per-platform defaults until a repository has run history. Warm-batch is serial inside one container, so the estimate notes the wall-clock vs total-cost trade-off.

The shared site-wide header is lvci-header.js: every page declares a tiny window.LVCI config and loads the script, which injects a consistent navigation chrome (Dashboard / VI Browser nav, Settings & Help menus, version badge, live activity pill, theme toggle). Reports stay immutable; the header updates the moment the shared script is redeployed.

10. Common errors & troubleshooting #

Most problems fall into a few recognisable buckets. Each entry below gives the symptom, the cause, and the fix. When you need the raw detail, open the Actions tab of your repository and click into the failing run — the job log shows the exact step and error. (Note: GitHub returns no log for a step while it is still in progress; wait for the step to end or cancel it to read the log.)

The dashboard shows a placeholder or never goes live

Cause & fix. Usually GitHub Pages is not enabled, or the repo is private on a free plan (Pages needs a paid plan — see §7). Check Settings → Pages and set the source to the gh-pages branch. On a brand-new install, many workflows publish to gh-pages at once and one deploy can briefly lose a push race; the dashboard deploy retries automatically and self-heals on the next publish, so a momentary placeholder right after install is normal.

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

LUnit runs on Windows and Linux when the worker includes LUnit CLI 1.7.1.32 or newer. Caraya, VI Tester and NI UTF have Windows runners; Linux reports list them as unsupported. Antidoc remains Windows-only. Linux unit tests support per-revision Run; history backfill remains available only on Windows.

A new install does not appear on the Clients page

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

Under the hood

11. The catalog — single source of truth #

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

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

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

The GitLab provider adds a second, explicit contract beside the catalog: its providers/gitlab/files.json file lists the copied adapter files and capability-to-template map. This prevents a new catalog capability from silently appearing installable on GitLab before a native job has been supplied for every applicable platform.

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.

The reusable workflow loads config with ni/LabVIEW-CI-CD-Toolkit/actions/load-config@v4 and checks out capability tooling from ni/LabVIEW-CI-CD-Toolkit into _lvci at the manifest's explicit source.ref. If that ref is empty or absent, or the manifest is missing, it falls back to v4, the major alias that follows releases promoted to stable, rather than v1-era tooling. An explicit immutable source.ref checks out that exact release, including a newer published release that has not reached stable. The config reader still follows @v4 independently of that capability pin. The latest published release can be newer than the deployed stable line; publication alone does not promote stable or advance v4 to the new release (see §18).

The ref resolver uses Python's standard library and Git, with no runtime YAML package dependency. Python runs with -I -S so client-root modules, PYTHONPATH, and site packages cannot supply imports to the resolver or forge its selected ref. It accepts manifests with plain top-level keys and a block source mapping whose ref is a single-line Git ref, optionally single- or double-quoted and followed by a comment. Empty refs use the default. Refs under config or source.distribution are ignored regardless of ordering. Duplicate source keys, inconsistent indentation, invalid refs, and unsupported source syntax such as flow mappings, aliases, multiline refs, or escaped quoted refs stop the job with a clear error instead of selecting another ref. Reusable Workflow Checks exercises this contract on GitHub Actions; PyYAML is installed only for the test harness.

GitLab uses the same catalog selection but emits native template includes instead of a reusable workflow. Its workflow: rules permits merge-request, default-branch, web, and scheduled pipelines; the visible pages job uses when: always so a failed verification job does not suppress publication of its report or the existing dashboard history.

Runners

Windows container jobs run on windows-2022; Linux jobs on ubuntu-latest. Both default to GitHub-hosted runners but can be pointed at self-hosted labels via workflow inputs / Actions variables. The worker container runs on the runner, so choosing a runner is how you choose where CI executes — a self-hosted runner additionally gives you a persistent Docker layer cache so the multi-gigabyte image is not re-pulled every job.

Concurrency & parallel deploys

Each report-producing workflow runs in its own concurrency group keyed by activity and revision (${{ github.workflow }}-${{ inputs.commit_sha }}, cancel-in-progress: false), so different activities — and the same activity on different revisions — run in parallel and never cancel each other; re-dispatching the same activity for the same revision simply queues behind the in-flight run. Several jobs still push to the same gh-pages branch, so races are handled at the deploy step: the shared publish-gh-pages action is retry-safe — it re-fetches gh-pages and retries the push (up to six times with back-off) so every parallel publish lands instead of one losing a non-fast-forward cannot lock ref 'refs/heads/gh-pages' push. (Earlier versions serialised all report workflows through a single report-pages-deploy group; that made unrelated activities wait on — and, because GitHub keeps only one pending run per group, cancel — each other, so it was replaced by per-activity groups once the deploy became retry-safe.) The dashboard build uses a separate group (dashboard-pages, cancel-in-progress: true) so a burst of status events collapses into a single latest-wins rebuild.

The dashboard surfaces this line live. It reads GitHub's in-progress, pending, waiting/requested, and queued runs and shows each enqueued activity as a spinning Queued cell on the revision it targets. Because a manual re-run is dispatched on the default branch ref, the run's head_sha is the workflow-file commit, not necessarily the revision being analyzed. Per-revision dispatch workflows therefore include the target SHA (inputs.commit_sha, or inputs.head_sha for VIDiff) in their run name, and dashboard.py keys active runs on that target SHA rather than the dispatch ref. Snapshot backfills are one global run, so they are treated as active for every snapshot cell still awaiting coverage. When several runs are waiting for an available runner, each queued cell also reports how many jobs are ahead of it (for example Queued · 1 ahead).

A capability that runs on two platforms (Windows and Linux) writes a single shared result for the commit, so the faster platform finishes first. The dashboard checks for an active run on the commit + capability before rendering that result, so while the slower platform is still running the cell keeps its live Running badge instead of reverting to a finished-looking result — the indicator only clears once every platform's run for that capability has completed.

13. Worker container images #

The standalone actions/worker-image composite builds only a client worker layer. Callers explicitly supply a readable, qualified Windows base-image and matching LabVIEW year; the action never builds or promotes the shared base. It resolves that reference to a digest and uses the same digest for hashing, Docker FROM, and the dependency build. Even a client without project declarations builds and verifies its selected baseline tooling. It verifies that the resolved base provides windows/amd64 before building or copying. A fresh isolated context contains the current source VIPM installer, optional source ci-tooling.vipc, and the caller's dependency files at the source worker's .github/labview/vipm/ path. Its Dockerfile adds explicit LabVIEW/public-repository build arguments, an 8 GB build memory limit, and required Dragon error handling.

Worker tags hash every staged filename and byte, the build arguments, base digest, and orchestration code. An existing exact content tag is reused; only an explicit registry MANIFEST_UNKNOWN or NAME_UNKNOWN response permits a build. Permission, authentication, rate-limit, and transport errors fail the action. Publishing verifies a manifest digest before moving the year tag and optional latest tag. These are client worker aliases, separate from toolkit release channels. Concurrent writers to the same worker package should be serialized by the caller.

The worker image is where LabVIEW and your baked-in dependencies live. The source/template repo (LCWC) owns and publishes one shared base image from .github/docker/labview-ci-base.Dockerfile — LabVIEW with VI Analyzer support, optional UTF support, VIPM, and Git already installed. Every client repo gets its worker from that base in one of two ways, and which one runs is the key distinction between installation and an upgrade.

On GitLab, native custom-image jobs build the local Dockerfile inputs and publish the finished worker to the project's own Container Registry rather than GHCR. They use GitLab's per-job registry credentials and tags keyed by CI_COMMIT_SHA, with moving windows-latest and linux-latest tags for selected jobs. Each build reads the installed catalog's qualified containers.default release and passes its pinned NI image tag to the Dockerfile; the Windows build also passes its paired NIPM feed and VIPM installer. Set the GitLab CI/CD variable LVCI_CONTAINER_RELEASE to another qualified catalog release id, such as 2026q1, when a project must remain on that release. A project can set LVCI_WINDOWS_IMAGE or LVCI_LINUX_IMAGE to consume an approved external worker instead.

Container releases — pinned, qualified, selectable

The tooling supports a set of NI LabVIEW container releases (LabVIEW 2026 Q1 and newer), declared in the catalog's containers block. Each release is a pinned triple — NI container tag, NIPM feed, and VIPM installer — that moves together and is qualified before clients can select it: the source repo builds every release's base and runs a VIPM-startup probe inside it (publish-base-images.yml), and the shared moving tags only advance after a full dependency bake succeeds on the new base. Nothing floats on NI's latest-* tags — NI repointed latest-windows from 2026 Q1 to 2026 Q3 on 2026-07-27 without notice, and a floating base pin turned that into a fleet-wide bake outage. The configurator's LabVIEW version dropdown defaults to the newest qualified release; choosing an older release pins the repository to it via the LCWC_BASE_IMAGE / LCWC_LINUX_BASE_IMAGE Actions variables, which every image workflow resolves first.

Installation vs. upgrade — how containers are managed

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 snapshot render image also bake a set of X11 core and TrueType fonts (DejaVu, Liberation, Noto, and the 75/100 dpi xfonts-* families) — NI’s minimal Linux base ships almost none, and without them LabVIEW’s UI controls and rendered VI text (VI Browser snapshots and the Debug Run desktop) come out blank. Images are pushed to your GitHub Container Registry (GHCR) as packages attached to your repo:

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

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

Content-addressed tagging

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

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 compares the requested pins against this observed inventory. The manifest’s vipc[].packages records requirements, not proof of installation. Linux manifests also preserve dpkg_packages and raw dpkg-query output for native package status, including the installed ni-vialinux-labview-support VI Analyzer package. Because VIPM records versions with a leading v (e.g. v0.5.0.1) while VIPC files declare the bare form (0.5.0.1), the comparison normalizes that v prefix — without it, correctly-baked packages would be reported as missing and the “dependencies need to be installed” banner would never clear.

Dragon dependencies honor each file’s monitorOn platforms. Windows declarations are reconciled against the current Windows worker manifest (the worker rebuilt by build-labview-image.yml). If a manifest has not yet published a dedicated dragon.items status block, the dashboard falls back to the installed-package inventories (vipm_packages / nipkg_packages) so already-baked dependencies are marked installed instead of staying stuck at not_attempted. Linux declarations use the selected target-verified VIPM inventory, or the selected digest-pinned LVPM inventory with locally current freshness. Both require exact name/version matches. Unsupported native/NIPM dependencies and stale or unavailable Linux evidence remain unknown/pending. Files monitored on both platforms are satisfied only when both platforms are satisfied.

Complete dependency verification

The Linux VIPM worker uses the official 26.3.2 (4056) Debian installer, matching the legacy OGP compatibility fix required on Windows. Selected Dragon files with exact public VIPM pins are converted into deterministic VIPCs by the trusted staging helper. The input record preserves both the original source hash and generated VIPC hash. Unsupported NIPM requirements, custom feeds, embedded VIPC references, conflicting project pins, and mismatched LabVIEW targets fail before the build.

VIPC XML is parsed structurally, including compact single-line exports. The final Windows and Linux inventories must satisfy every effective tooling and project pin; explicit project pins take precedence over the tooling baseline. A successful install command alone is insufficient. The native validation workflow covers both example dependency files, the tooling baseline, and installed Antidoc dependencies on Windows. The captured VIPM inventories also pass through the production dashboard and Dependencies-page functions: every package cell, VIPC/Dragon status pill, and pending indicator must clear. It retains build and fresh-container evidence without publishing worker images. Its result does not establish that a published worker is active; publication and dashboard verification are separate steps.

When images rebuild

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

Dependency gate (activities wait for the rebuild)

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

Manual VIPC base-image bake

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

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 #

Windows and Linux builds keep trusted tooling at reviewed HEAD while optionally reading committed dependency inputs from dependency_sha, a full ancestor SHA. The expected_sha guard rejects a moved reviewed HEAD. Selected configuration and dependency bytes participate in worker identity; staging rejects escaping paths, symlinks and collisions. Linux uses vipm/linux-worker.py with the existing LVPM planner for selection and safe input validation. The catalog explicitly ships this helper to Linux-only clients; Windows helpers ship in the existing VIPM directory payload. These are installer/update payload changes, not machine-local repairs.

Both VIPM installers capture structured, target-bound installed inventory and verify required project pins before publishing active worker aliases. A requested file or a successful command alone is not installed-package evidence. Every selected tooling and project requirement must be observed at its effective version; only an empty dependency set can retain unavailable inventory without failing. Separately tagged new workers do not move active aliases or the default Pages pointer. Registry alias updates are not atomic; Windows attempts rollback on failure. Native Windows/Linux LabVIEW, VIPM, registry publication and activity checks still require CI qualification; local fixtures do not establish them.

The revision picker compares that revision's declarations and monitoring choices with its selected worker. Saving in a client submits a configuration proposal; it is not applied until reviewed and merged. Windows and Linux checkboxes preserve the other platform's selection. The tooling source intentionally disables client Save on both platforms and explains that monitoring changes require a source PR; forks remain ineligible for the client installer. Existing LVPM candidate verification, digest pins and explicit activation are unchanged.

native-dependency-validation.yml runs only on its dedicated validation branch or manual dispatch. Windows x64 VIPM and Linux x64 VIPM/LVPM jobs build the candidate with the real example VIPC and Dragon files, the tooling baseline, and the Windows Antidoc capability. They retain source hashes, build logs and installed-inventory evidence as artifacts. The jobs have read-only repository/package permissions and never publish images, active aliases or Pages pointers. A passed native build verifies the selected fixture and recorded scope, not every project or LabVIEW activity; failures retain diagnostic artifacts.

The fresh Windows container creates a genuine public Git checkout at the exact candidate commit before invoking VIPM Community Edition, verifies the staged VIPC hash, and deletes the baked inventory before querying again. Linux installation does not treat the original LabVIEW process exiting as proof of failure: after successful install commands, target-bound inventory must still verify every required exact package pin. Failed queries, missing packages, wrong versions and wrong LabVIEW targets remain fatal.

Windows best-effort tooling retries preserve project VIPC and Dragon version overrides, so a failed initial attempt cannot reinstall an older baseline pin over a required project dependency.

This section is the single hardest part of the whole stack, and it is documented in detail precisely because it was painful to get right. If you ever fork the worker build or debug a baking failure, read it carefully — every numbered point below is a real problem that was hit and solved.

Third-party LabVIEW packages (Antidoc, Caraya, VI Tester, LUnit, the UTF JUnit reporter, and project packages declared in staged .vipc files) are baked into the Windows worker image at build time so they are available headless during CI. Getting the modern VIPM CLI to install packages non-interactively inside a docker build required solving several non-obvious problems, documented in .github/labview/vipm/README.md and implemented by install-vipc.ps1:

Required Windows Dragon dependencies

The GitHub Windows push gate and staging share safe YAML monitoring semantics: quoted platform scalars, flow/block lists, quoted paths and inline comments are supported; monitor must be a YAML boolean. Invalid YAML or unsupported selection shapes fail explicitly. Explicit empty lists and platform opt-outs exclude declarations without parsing them; no configured list retains legacy project discovery. The isolated Python 3.13 workflow host installs PyYAML==6.0.3 and loads the trusted static TOML parser by explicit path, excluding client working-directory and PYTHONPATH import/startup overrides. Declaration files are never executed.

Every Windows producer stages selected source bytes and a normalized schema/platform/year/bitness/exact-root plan. The composite worker-image action keeps its explicit flat deps-dir contract; GitLab Windows keeps project discovery outside .github, rather than applying GitHub monitoring flags. Worker identities bind source, plan, helper and installer bytes with target inputs. This hash binding provides integrity, not signing or provenance. Missing, changed, escaping or linked sources, unsupported install-affecting settings and contradictory pins fail closed. Dragon-only and mixed VIPC/Dragon builds invoke the shared installer once, without an assumed dragon apply executable. The container validates source hashes and the requested target before bootstrapping VIPM.

Required Dragon roots cannot be bypassed with optional VIPC policy or VIPM_ALLOW_MISSING_PACKAGES. After all later project/tooling installations, final inventory must explicitly identify the requested LabVIEW year/bitness and contain every exact root/version. Later upgrades, missing packages, wrong targets, command failures and unreadable/dead-engine inventory fail the build; the installer does not choose newest versions or rewrite pins. -BootstrapOnly retains its early no-package behavior.

Compatibility break / major-release candidate from 4.x Independent Dragon CLI execution and legacy install-dragon.ps1 hooks are retired. Nonempty NIPM/feed requests, nonempty project.vipc, unsupported project settings and unpinned/ranged versions fail rather than being delegated or ignored. Migrate to supported exact numeric VIPM string or inline { version = "..." } declarations and select VIPCs separately before updating. This is not a backward-compatible patch. Local or hosted mock contracts do not prove a vendor bake succeeded: a real Windows bake and final inventory remain required before claiming a published worker contains the repaired dependencies.
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. The Windows template invokes the shared trusted Dragon staging helper with isolated Python (3.11+ with tomllib, or a compatible interpreter with tomli installed in its isolated environment), validates its configured LVCI_LABVIEW_VERSION against the selected base, hashes staged inputs and target, and checks native staging/build/push exit codes. A required failure stops subsequent publication.

  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. Verify both Windows VIPM executables before required installs — the shared base/dependency bootstrap defaults to the official VIPM 2026 Q3 f2/build 4056 installer. The vendor release notes identify the legacy package-file Missing Name in Package section fix; this is not proof of container compatibility. The unmodified payload embeds CLI version 26.3.2 and Desktop file version 2026.3.2.4056. The hook logs the actual selected paths, probes CLI --version within 30 seconds, reads Desktop product/file identity, and requires Windows-trusted James Kring, Inc. Authenticode signatures on both. A missing/old executable triggers one verified upgrade and full rediscovery; stale preferred paths, unverifiable binaries, or an already-running Desktop at a different path fail closed. A supported newer pair is not downgraded. Mixed newer/old installations require an explicit paired upgrade rather than risking replacement of the newer half.
  6. Keep installer overrides authoritative and bounded — when an upgrade is needed, VIPM_INSTALLER_URL must be HTTPS without embedded credentials and supply a Windows-trusted vendor-signed VIPM installer at least 26.3.4056; both installed executable floors still apply. Custom URLs are not silently replaced; unreachable, old, untrusted, or unverifiable installers fail the build. A supported preinstalled pair leaves the URL unused. Downloads time out at 300 seconds; installation at 1200 seconds, with /exenoui /qn /norestart REBOOT=ReallySuppress. Nonzero and reboot-required (1641/3010) exits fail closed; timeout cleanup targets only the invoked process tree and temporary files. Signature trust, not an unpublished hash, checks integrity. Base Docker builds use the same hook's -BootstrapOnly mode, which exits before environment/headless changes, memory checks, Git/settings setup, engine/LabVIEW launches, or package operations. Both base-build workflows hash the bootstrap's actual file bytes into the immutable cache key. The catalog's 2026q3.vipmInstallerUrl also points to 4056 so catalog-driven overrides cannot silently retain 3954; other release records are unchanged. Old explicit overrides fail the floor when an upgrade is needed, without implying other-release qualification.
  7. Require final Windows runtime evidence — static payload inspection and AST/mock tests do not prove legacy OGP installation. A reviewed real bake must verify required Antidoc/palette and the final target inventory. Recovered-batch validation alone is insufficient: the final gate verifies every selected project, Dragon and effective tooling requirement after all installs. Project pins override baseline pins consistently during retries and final verification. Package archives, integrity verification, target identity, and exact/minimum pin semantics are preserved.
  8. Seed Settings.ini before installing — a fresh image lacks C:\ProgramData\JKI\VIPM\Settings.ini, without which the CLI aborts with an IO error.
  9. 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.
  10. 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.
  11. 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.
  12. 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).
  13. 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.
  14. 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.
  15. 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.
  16. Resolve OpenG packaging releases without losing required roots — a VIPC-normalized exact request such as palette@1.1 can select the indexed 1.1-1 packaging release; an explicitly release-pinned request remains exact, and minimum-version constraints retain their meaning. Public/local selection and installed-inventory verification use the same version predicate. A missing transitive dependency may still be assumed bundled, but is not marked resolved: if the VIPC explicitly requests it later, that required root must resolve or fail the install.
  17. Do not repair legacy packages by changing their archives — the official jki_rsc_toolkits_palette-1.1-1.ogp matches its repository checksum but uses legacy [Package Name] metadata. VIPM build 3954 rejects it with Missing Name in Package section. The vendor's 2026 Q3 f2 release notes describe the package-file compatibility fix. Resolver correction alone does not upgrade the worker's VIPM or prove legacy installation works; that requires a separately reviewed runtime upgrade and Windows bake. Required Antidoc/palette packages remain required, and archive integrity checks stay enabled.
  18. Verify local-file recovery, not earlier retry failures — VIPM can reject a legacy transitive .ogp with Missing Name in Package section, then successfully install the requested packages during per-file retries. After local-file installation, the hook queries vipm --labview-version <year> --labview-bitness <bits> list --installed and checks every requested package ID and version. A recovered set no longer fails solely because an earlier transitive file failed. Missing or wrong-version requested packages, an unreadable inventory, or an exhausted engine recovery still fail the install; required project dependencies and UTF/Caraya essentials remain fail-closed; tooling retries may continue, but the final complete-inventory gate still applies.
  19. Batch recovery is not a final-image pin guarantee — a later best-effort tooling install can change versions already verified for a project VIPC. Validate the final target inventory as well as each recovered batch before claiming that all project pins hold in the completed image.
  20. 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. Opening a revision loads its committed monitoring settings without marking them unsaved. When explicit VIPC or Dragon lists are absent, the existing default monitors discovered files on both worker platforms; explicit empty lists and opt-outs retain their meaning. Only changing a monitoring checkbox requires saving and merging a configuration PR before reviewing a worker update. Updates remain restricted to the current default-branch revision.

After a worker image is built locally, the publish workflow captures both nipkg list-installed and vipm list --installed from inside the image and writes them into the worker manifest. Fresh-install seed copies use a fast registry copy and do not pull the multi-GB image back just to inspect it; seed manifests without a captured target-verified inventory cannot establish installed package coverage. A requested dependency rebuild applies and verifies the tooling baseline as well as the selected project dependencies.

NI package coverage uses installed-package inventory only: nipkg list lists available packages and cannot establish installation. Older Windows manifests without installed-inventory provenance show unknown native coverage until rebuilt. Windows VIPM is installed through the JKI bootstrap, so a successful target-verified CLI inventory establishes its presence even without an NI package named ni-vipm. The separate JKI Dragon application is not required: the toolkit parses Dragon declarations and installs their packages using VIPM or LVPM.

When all four committed VIPCs are selected together with example/Example.dragon, experimental.vipc uses Antidoc CLI 3.4.0.126, already established by the authoritative Antidoc capability archive. The experimental configuration retains its ID, selection, flags and unrelated metadata, and embeds byte-identical CLI spec/icon metadata from that archive; the informational Antidoc JSON agrees. This explicitly reconciles a stale 3.3.0.117 declaration, not a newest-wins resolver policy. Offline planning retains all five sources and 35 exact roots, and rejects divergent pins without emitting a plan. Archive identity and source-membership tests guard the alignment. Planning success is not qualification of package installation, post-install hooks, a Linux worker bake, runtime relinking or final inventory.

Installation retries and final verification Windows installs Caraya and UTF essentials first, then project and capability VIPCs, then the tooling baseline. Tooling retries may continue after an initial failure to give remaining packages a chance to install. The final target-inventory gate still requires all effective selected packages; an incomplete toolkit cannot be published as a successful dependency build. Project and Dragon pins override baseline pins for VIPM; baseline UI rows check that effective project version rather than continuing to request the superseded tooling version. LVPM retains its explicit conflicting-pin rejection. Windows forwards the building repository URL through the Dockerfile to VIPM, so clients use their own repository context. A seed-only registry copy does not run this installation; rebuild the worker to apply dependencies.

Manage dependencies one platform at a time

The Dependencies page has separate Windows and Linux tabs. Windows keeps its VIPM workflow and never offers an LVPM installer choice. Linux offers VIPM or LVPM. Links can select a tab with ?platform=windows or ?platform=linux; switching tabs preserves the inspected revision. Keyboard users can move between platform tabs with the arrow, Home, and End keys.

Each tab shows only that platform’s default worker, package coverage, monitoring checkboxes, and update approval. The summary uses existing platform-specific inventory and declaration evidence, not aggregate missing-package counts. Missing packages and unavailable, malformed, or stale evidence require attention only on the affected platform. An empty monitored project list is neutral, not proof of installation; satisfied package inventory is not runtime verification. Legacy Windows-only Dragon status cannot establish Linux satisfaction. Explicit per-action overrides may select a different worker from the default shown here.

For VIPM, choose Review Windows VIPM update or Linux Update container image, inspect the selected platform’s package changes, then approve the build. Advanced update options are collapsed by default. Saving monitoring choices preserves the other platform’s selections and requests a configuration pull request; merge it and refresh before building committed dependencies. Tab or revision changes cancel outstanding approval/token prompts, and in-flight reads cannot retarget a dispatch. Late Linux responses do not dismiss a new token prompt or update status after the platform, revision, or monitoring selection changes. Switching away after a build is already queued does not cancel it.

Package rows, unresolved declaration details, native inventory, and candidate provenance are available on demand rather than shown as one large checklist. The aggregate dependency banner is replaced by the platform summary on Dependencies. Other dashboard pages name the affected platform(s), link to their tabs, and label combined unresolved counts as aggregate rather than attributing them to both workers. Dependency banners render text and navigation with DOM nodes, not parsed HTML; review links allow only HTTP(S) destinations.

The Linux tooling card has one primary action: Update container image. Clicking it checks authenticated current-HEAD configuration and the installed workflow, comparing complete monitoring choices (including opt-outs and the other platform), defaults, and activity overrides. A historical view with equivalent committed settings can update the current committed SHA without changing the inspected revision, URL, or choices. Unmerged choices, mismatches, unknown evidence, or contradictory capability platform classification stop with a specific inline prerequisite and no build request. An accepted Save is not proof of a proposal or merge; only demonstrably committed matching settings can proceed. Reconfigure’s omitted monitorOn with monitor: true means both platforms; false remains an opt-out. There are no Find/Open navigation stages, automatic updates on page load, or background retries. Source repositories and their forks must change monitoring through reviewed source PRs, not client Save or the installer.

Legacy implicit monitoring (omitted lists, not explicit empty lists) retains a clean-current path: authenticated HEAD must equal the fully loaded revision, with unchanged complete discovery inputs, worker settings, and no pending Save. Generated inventory requires exact repository/revision provenance; live loading requires successful manifest and file discovery. Overlapping revision loads publish inventory and provenance only from the still-current load; stale responses cannot replace newer choices. Historical or edited implicit settings cannot use this path. Unsupported or malformed configuration shapes remain unverified and do not dispatch.

The explicit update intent retains its page context across authentication, reads, VIPM approval, and submission. Changes to platform, inspected revision, monitoring, or tooling invalidate it. VIPM approval names the exact committed target and revalidates it before dispatch; LVPM builds a candidate only. The request uses the default branch and expected_sha, not an undocumented commit-SHA dispatch ref. This optional input accepts exactly 40 hexadecimal characters, normalizes their case, and leaves existing manual/push/schedule behavior unchanged when empty. Both Linux workflows reject an unexpected event SHA before checkout/setup/build, and checkout is pinned to the run SHA. Older installed workflows lacking this safeguard require a tooling update and are not dispatched. A moved HEAD is rejected without retry. Windows behavior and separately reviewed activation/rollback are unchanged.

Dragon rows distinguish file present and parsed, file absent, parse error, and inspection unavailable from package status. Expand Declared packages to see each manager, package name, exact declared version, and the selected platform’s measured worker version/status. An unresolved package does not mean its declaration file is missing. Stale or unavailable evidence remains unverified; Windows-only evidence cannot establish Linux installation. This reporting does not install missing packages or repair runtime failures. LVPM satisfaction covers package files, not execution of CI activities.

Optional Linux LVPM environments

The Dependencies page’s Linux tab offers VIPM (the existing default) and LVPM as alternative installers for Linux. Windows behavior is unchanged. Switching tooling views in the page is exploratory, not a pipeline change: review the declarations, build a candidate, inspect its verification, and explicitly request activation through a Reconfigure pull request. While exploring LVPM, the current VIPM update controls are hidden; returning to the VIPM view does not change the active worker. If LVPM is already selected, returning to the existing default selection requires the explicit reset PR under the collapsed Current selection and rollback section. Reset removes only linuxUse, retaining container.use and per-action overrides; a base or custom image may be selected, so reset does not guarantee VIPM. The retained default ref is shown before requesting reset. Never apply the client installer to this tooling source repository or a fork to test this feature; use a separate scratch client repository.

Update container image uses the selected Linux tooling. VIPM still requires review and approval before dispatch. Candidate activation is a separate guarded action inside the collapsed candidate evidence section; rollback is available under Current selection and rollback. Building an LVPM candidate never changes the active worker. The single action clarifies submission and navigation, not package compatibility: conflicting pins or runtime installation failures can still stop the build. No successful image bake or CI activity run is established by enabling or submitting the button.

Linux requests ask GitHub for dispatch run details. An accepted request is not a successful build. When GitHub returns a run identity, the page checks the exact repository, workflow ID and path, dispatch event, branch, reviewed HEAD, authenticated actor, and first run attempt before showing queued/running/completed status with a direct log link. A Linux-only indeterminate progress bar appears while checking, submitting, awaiting run visibility, queued, or running; it does not invent a completion percentage or estimate package-install progress. Failure and cancellation stop the bar with a red label and retain the exact run link. Uncertain outcomes or stopped tracking show a warning, not success. Workflow success stops the bar with a review-required label, not a claim of candidate verification or activation. Reduced-motion preferences disable the animation. It never picks the latest run or infers ownership from timing. Delayed run visibility is retried; permission/network errors or mismatched identities stop tracking without automatically retrying the dispatch. Legacy HTTP 204 responses establish only acceptance: open the workflow runs and inspect them before refreshing or retrying. Repeated clicks are blocked while submitting, tracking, or the outcome is uncertain. Changing platform, revision, or monitoring invalidates the observer, not the remote run. Workflow success alone does not establish a published, verified candidate or activate it: refresh and review candidate evidence, then request and merge the activation PR separately.

  1. Review. The candidate uses the locked CI-tooling VIPC and project VIPC/Dragon files selected for Linux. One installer owns the environment; no per-package mixing of VIPM and LVPM occurs. Conflicting exact pins (including conflicts between tooling and project declarations), missing files, unsupported declaration settings, and Dragon NIPM requirements fail rather than being silently dropped.
  2. Build. build-labview-lvpm-image.yml is manually dispatched. The Linux custom-image capability must be installed first. The page checks workflow availability on the default branch before enabling Build and again before dispatch; unknown availability blocks dispatch rather than pretending the workflow is missing. It builds upstream Flydroid/lvpm at commit 8e8bd7e390b7d4a77558af12cfa52f79079df409 with its Cargo lockfile, as an external dependency; the toolkit does not vendor or fork its implementation. Candidates use separate linux-lvpm-* image tags and workers/linux-lvpm/ manifests. They never replace the existing Linux VIPM, Windows, seed, or default worker tags.
  3. Verify. The adapter first asks LVPM for read-only package discovery, then expands explicitly declared embedded VIP subpackages into the checked dependency closure. It preserves original archives and their SHA-256 values, and records separate hashes for normalized installation archives. Legacy non-UTF-8 specs use ZIP's CP437 filename encoding; normalized specs and archive names use UTF-8 without replacing unknown characters or changing payload bytes. File groups explicitly restricted to other LabVIEW versions are excluded with recorded evidence; package-level incompatibilities, unsupported OS restrictions, unknown conditions, conflicts, and missing transitive dependencies remain errors. Exact root pins are fetched separately with dependency traversal disabled, so upstream's transitive-first resolution cannot silently upgrade a declared version. Transitive discovery cannot replace those roots. Once every dependency constraint has been checked, both the local recheck and actual install disable further dependency resolution and use only the frozen closure. A final, local-only dry run must resolve the complete exact closure without unresolved dependencies or missing archive entries before any real installation. No initial discovery warning counts as verification. The checked closure is frozen to exact local package archives before the actual install with hooks and relinking. Installation rejects non-Linux or non-x86-64 hosts before changing packages. The exact digest-pinned image is inspected and must agree with the installed inventory on linux/amd64. Final verification requires exact installed versions, recorded files on disk, no skipped applicable groups/hooks, and completed relinking. Declared system-package paths are preserved: file verification alone does not prove a Windows-shaped CLI operation works on Linux. SHA-256 values record the acquired package bytes; they are not publisher signatures. The initial indexes are requested over HTTPS.
  4. Activate. Activation proposes a digest-pinned config.container.linuxUse selection through the existing reviewable Reconfigure PR flow. Server-side checks require the canonical candidate manifest, explicit architecture: amd64, successful build provenance, matching registry digest, and current dependency fingerprint. Older manifests lacking architecture evidence require a rebuild. Freshness includes the consumed catalog default, qualification, LabVIEW year, and Linux base tag; release-note or version bookkeeping alone does not invalidate a candidate. Explicit existing per-action container overrides continue to win, then linuxUse, then the previous fallback. Windows never reads this Linux-only selection. Unrelated reconfiguration preserves it; resetting to the existing default selection removes only linuxUse, preserving container.use and existing overrides without assuming the fallback uses VIPM. Reconfigure applies only explicitly requested settings to the parsed existing manifest: activation/reset preserves separate VIPC and Dragon lists, explicit dependency-policy values, platform settings, provenance, and unknown client fields. A no-op keeps the file byte-identical; an actual update may normalize YAML formatting.

Project declarations that intentionally duplicate CI-tooling roots (for example example/Dependencies.vipc) must keep shared roots pinned to the exact same versions; Linux planning rejects declaration drift instead of selecting a newer or transitive alternative. The current OpenG File root pin (oglib_file-6.0.2.28) targets LabVIEW 2020+ on all operating systems and supports automatic caller relinking into openg_file.lvlib. Downgrading to older OpenG file packages can require manual relinking, which planning and package-file verification do not prove at runtime.

Verification is deliberately scoped Package files verified does not mean CI activities passed. The first release records verification.scope: package-files and activity_checks: not_run. Upstream LVPM cannot yet observe a hook VI's internal error out; explicit hook failures are rejected, but a completed hook is not proof of functional correctness. Validate the candidate against the project's actual compile, test, build, and rendering activities before depending on it. Conditional file groups the adapter cannot safely validate are rejected conservatively. Private package feeds, embedded Dragon VIPC references, and NIPM installation are not supported by this initial LVPM adapter.

These are global installs inside isolated containers, not developer virtual environments. Snapshots retain their existing TOIMAGES_IMAGE or repository Linux-latest selection when linuxUse is absent. After opting in, explicit snapshot action overrides precede linuxUse; a verified LVPM pull failure never falls back to another worker. Existing LabVIEW launchers therefore see the installed dependencies without venv activation or preference-file changes. LVPM candidates are immutable selections: monitored dependency changes do not silently rebuild or switch them. Only verified immutable LVPM workers skip the VIPM image wait; existing VIPM waiting and NI-base behavior are unchanged. Linux execution checks the current requirements fingerprint and blocks stale selections until a fresh candidate is built, verified, and explicitly activated. A failed candidate leaves the previous image available but does not make stale requirements current. The selected environment drives Linux inventory; an unused candidate does not create missing-dependency warnings for the active pipeline. LVPM inventories VIP packages, not native NI packages: unmeasured native package coverage is unknown, not evidence of absence.

The dashboard separately reports local freshness for the exact inspected revision and digest-pinned selection. It uses trusted action-distributed helper code, treating the client checkout only as input data. Optional pinned Python prerequisites are installed in an isolated temporary environment only when LVPM is selected; failure leaves the rest of the dashboard running and reports freshness as unknown. Python startup is isolated so checkout modules and inherited import paths cannot replace trusted imports. Changed declaration bytes invalidate freshness even when package pins are unchanged. Stale or unknown freshness marks Linux dependencies pending and cannot produce green satisfaction. A local current result does not verify registry access, build provenance, or runtime activities; activation and execution still perform their server-side checks. Historical views cannot reuse another revision's current result.

Dragon declaration inspection loads only the parser from the trusted action repository, resolved relative to the dashboard generator itself; Python helpers in the inspected client checkout are never imported. The parser requires Python 3.11+ with tomllib, or an available tomli backend. Missing tooling/backend, missing monitored files, and malformed declarations produce per-file inspection.status: unknown evidence instead of an empty installed inventory. Monitored unknowns keep the dependency banner pending for their monitored platforms; unmonitored files do not create pending work. The Dependencies page requires matching-revision declaration evidence and measured installed package statuses before displaying installed. Update the trusted tooling or repair the declaration and regenerate the dashboard; rebuilding a worker alone does not repair parser evidence. Per-dependency platform_status records honor the file’s monitorOn list; the existing status, installed_version, and message fields aggregate only applicable monitored platforms. Installed requires all applicable platforms installed; conflict, missing, wrong version, unknown, pending, and not attempted take precedence in that order. Windows-only files retain measured Windows inference. Linux-only VIPM declarations can be satisfied by exact name/version in the selected canonical LVPM inventory, only with locally current revision/selection-bound freshness. Linux scope: package-files is not runtime verification; native/NIPM declarations remain unknown. Pending package records include per-platform evidence and target only unresolved monitored platforms.

Public-index fallback (existing VIPM path) 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 #

Standalone Windows worker and unit-test actions

For an existing workflow, grant contents: read and packages: write, check out the project, and call these actions on windows-2022. Pin both actions to the same reviewed immutable toolkit tag or commit. Replace REVIEWED_REF below after publication; this example does not imply the actions exist in an earlier release.

permissions:
  contents: read
  packages: write
jobs:
  tests:
    runs-on: windows-2022
    steps:
      - uses: actions/checkout@v5
      - uses: ni/LabVIEW-CI-CD-Toolkit/actions/worker-image@REVIEWED_REF
        id: worker
        with:
          base-image: ${{ vars.LCWC_BASE_IMAGE }}
          labview-version: '2026'
          # Optional flat directory containing project VIPCs/Dragon files and referenced VIPs:
          # deps-dir: dependencies
      - uses: ni/LabVIEW-CI-CD-Toolkit/actions/unit-tests@REVIEWED_REF
        id: tests
        with:
          image: ${{ steps.worker.outputs.image }}@${{ steps.worker.outputs.digest }}
          framework: lunit
          test-dir: tests
      - uses: actions/upload-artifact@v6
        if: always()
        with:
          name: unit-tests-report
          path: ci-out/unit-tests/
          if-no-files-found: warn

The worker's image input is an unqualified GHCR package name; the default is <repo>-labview. Outputs are image, tag (a win- content hash), verified digest, and built (also true for a dependency-free registry copy). deps-dir must exist inside the workspace and contain files directly: nested dependency files, links, duplicate names and reserved ci-tooling* names fail rather than being silently skipped or overwritten. Exact .vip extensions are hashed separately from .vipc. Loose VIPs are used only when a VIPC references them; a VIP-only directory without a VIPC fails. Project VIPCs and the effective included tooling baseline are required in the final target inventory. include-ci-tooling: 'false' omits the tooling VIPC and disables default UTF/JUnit/early framework packages. Supported exact Dragon VIPM declarations use the shared host-generated target plan and verified VIPM installer, including Dragon-only builds; no separate Dragon CLI is needed. The isolated action entry and content fingerprint include the trusted parser, normalized plan, source bytes, installer and target inputs. Required Dragon versions are verified after later tooling. NIPM/feed requests, unsupported settings and custom Dragon hooks fail explicitly; migrate before adopting this incompatible change. A successful mock contract is not proof of a real Windows dependency bake.

The unit-test composite requires image and framework (Caraya, vi-tester, utf, or lunit). Its optional command is an intentionally executable, trusted cmd.exe template with {ver}, {dir}, {out}, {lv}, and {cli} tokens ({proj} replaces {dir} for UTF projects); custom framework identifiers require it. A command overrides even the native UTF/LUnit runner. test-dir accepts a workspace-relative directory, glob, or UTF project; empty means the whole project. Inputs reach the container through a UTF-8 environment file, preserving empty values and quotes without inserting input text into a shell script. Newlines are rejected.

The optional source-runner parameters -Framework, -TestDir, and -Command (or LVCI_FRAMEWORK, LVCI_TEST_DIR, and LVCI_COMMAND) select exactly one framework. Without a framework, existing config and autodetection behavior remain intact. Installed LabVIEW/CLI path detection is preserved.

report-dir defaults to ci-out/unit-tests and must be a dedicated directory inside the workspace. The action owns its results directory and removes old results and summaries before running. Outputs are report-exists, tests, passed, and failed (including errors). Docker pull/run errors, report-generation errors, absent XML, empty tests, all-skipped tests, and runner errors fail the action. Reports are rendered after a nonzero runner exit when Docker returns normally, so diagnostics remain available for artifact upload. fail-on-test-failure: 'false' permits assertion failures only. The optional pages-url controls navigation links; the caller owns artifact upload, Pages deployment, and commit statuses. Python and the worker action's Go/crane runtime are installed explicitly. Composite Action Checks runs portable regressions on Linux and Windows plus real Windows PowerShell runner tests without LabVIEW; an actual Windows container bake/test remains a separate acceptance check.

VIPM bootstrap contract checks read the catalog, workflows, Dockerfiles, and installer source as UTF-8 rather than the Windows host's legacy code page. A portable regression exercises the affected catalog/bootstrap assertion with simulated Windows-1252 defaults (None and locale), confirming implicit reads fail and explicit UTF-8 preserves Unicode notes. This validates source contracts, not a native Windows VIPM install or container bake.

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

The shared anatomy of a capability:

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

For modified VIs, the Linux runner requests LabVIEW’s XML comparison output and renders it as static HTML with build-vidiff-report.py. This avoids native HTML report generation crashes reproduced in LabVIEW 26.3. Functional, cosmetic, and VI attribute changes retain their descriptions and all available before/after screenshots; when LabVIEW supplies no image for a change, the report explicitly says so. The source comparison.xml is retained for inspection. These static reports do not include the native HTML renderer’s interactive sub-VI hotspots. Added/deleted VIs still use PrintToSingleFileHtml.

The Linux runner preserves per-VI cli-attempt-1.log (and, when retried, cli-attempt-2.log) in the report artifact. CLI error 66 (connection lost during an operation) or −350000 (VI Server connection failure) resets the container’s LabVIEW process using pkill and retries the same VI once. An exhausted retry also resets the session before processing the next VI, but still fails the job; other operation errors are not retried. A successful CLI exit without a nonempty report, or malformed comparison XML, is also treated as failure.

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 (Windows) and .github/labview/run-unit-tests.sh (Linux) — run each enabled framework headless, write one JUnit XML per tool.
Host report builder.github/labview/build-unittest-report.py — merges the JUnit XML into results.json + the navigable HTML report.
Per-commit workflowunit-tests-windows-container.yml on windows-2022; unit-tests-linux-container.yml on ubuntu-latest.
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 with commit-status context CI / Unit Tests; the Linux run deploys to unit-tests/<sha>/linux/ with CI / Unit Tests (Linux).
PlatformWindows; LUnit also runs on Linux. Every framework depends on baked VIPM/UTF tooling (§15).

The four frameworks & their headless commands

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

FrameworkTest unit it discoversRuns viaDefault command template
Caraya (JKI)A project-owned runner with an explicit test inventorynative LabVIEWCLI RunVI through command:Use a wrapper that disables Caraya's interactive dialog, writes JUnit to {out}, checks its assertion inventory, and propagates CLI failures (example below). The legacy g-cli ... caraya default is an unverified extension scaffold, not provided by the Caraya package itself.
LUnit (Astemes)Test Case classes (.lvclass), under a 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

On Linux, LUnit runs with the same default command and tokens ({lv} is labviewprofull, {cli} is LabVIEWCLI), so one command: line can work on both platforms when the executable and flags are portable. The Linux runner requires Python 3 (already installed in the worker). It splits a command into arguments before substituting tokens, so spaces, quotes and shell metacharacters in paths remain literal; shell operators in overrides are not evaluated. Invoke a project-owned script explicitly for more complex commands. Keep -Headless as the last argument of any override: on Linux, LabVIEWCLI swallows whatever follows it, and the Linux driver warns when an override doesn’t end with it.

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

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

Configuring which frameworks run

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

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

Only tools with enabled: true run. A location may be a directory (recursed), a glob / extension pattern (each match’s parent directory becomes a test root), or — for UTF — a .lvproj path. The driver reads indentation relative to unitTests:, including the canonical config.unitTests.tools nesting, preserves quoted command arguments, and stops at sibling blocks.

Unconfigured fallback. When .github/labview-ci.yml has no unitTests: block at all (the unit-tests activity is installed but the Unit Testing settings page was never saved), the driver auto-detects every framework present and runs them all: NI UTF (empty locations) when any .lvproj references a .lvtest / TestItem; Caraya, LUnit and VI Tester when a .vi’s linker strings name Caraya.lvlib, LUnit.lvlib or TestCase.lvclass respectively, using the outermost directories holding those VIs as locations (.github, ci-out, build and .git are skipped). The Caraya signature was checked against real Caraya test VIs; the LUnit and VI Tester signatures have not yet been checked against real projects. Any explicit unitTests: block — even one with every tool disabled — is respected as-is and produces the “No enabled config.unitTests.tools” runner error instead.

A native Caraya integration can use this override under config.unitTests.tools:

caraya:
  enabled: true
  locations:
    - "tests"
  command: powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "{dir}\Invoke-CoreTests.ps1" -ResultsPath "{out}" -LabVIEWPath "{lv}" -Headless

Invoke-CoreTests.ps1 is supplied by the project, not generated by the toolkit. Its Run Tests.vi must implement the installed RunVI connector contract and explicitly select test VIs; fixtures and the runner are not independent tests. Local headless validation against the Measurement Utility Reference working tree passed eight suites / 44 assertions on 2026-10-23 without g-cli. This does not certify a clean container. The historical 6d69ccd worker failed with LabVIEW error 1003; its mass-compile report identified missing Caraya runner VIs and Linked Network Actor dependencies. Bake both the test framework and the project's full dependency closure, including authorized private/offline dependencies that are not in the public VIPM indexes.

How the workflow composes the runner (per commit)

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

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

unit-tests-linux-container.yml follows the same step order on ubuntu-latest, with the Linux image selection used by the other Linux activities (config.container.actions.unit-tests → config.container.use → the shared Linux worker :linux-latest; base/none selects nationalinstruments/labview:latest-linux). It runs the driver in a fresh container:

docker run --rm -v "$GITHUB_WORKSPACE:/workspace" "$LABVIEW_CONTAINER_IMAGE" \
  bash /workspace/.github/labview/run-unit-tests.sh /workspace /workspace/ci-out/unit-tests/results

The GitLab provider installs templates/unit-tests-linux.yml for Linux clients. The unit-test catalog capability now advertises Linux support; the installer selects the OS-specific runner and workflow without changing existing client configuration. Windows-only installations omit the Linux runner. The dashboard Run button dispatches the Linux workflow with commit_sha; historical dispatches restore current driver/report/publishing scripts from the default branch before execution.

The bundled VIPC upgrades only LUnit CLI from 1.6.1.23 to 1.7.1.32. Rebuild the shared Linux worker before clients use it, then rebuild client workers that copy that base. Existing consumer package pins remain owned by the consumer and must be updated explicitly if older than this minimum. Linux VIPM installation preserves package revision suffixes, for example jki_rsc_toolkits_palette-1.1-1 becomes jki_rsc_toolkits_palette@1.1-1; dropping the revision makes VIPM reject the pinned package.

The output contract

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

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

Malformed, empty, non-JUnit, or count-inconsistent XML is represented as an explicit report-validation error. An existing empty report renders its zero counts instead of claiming the Windows report is absent. Runner/report errors contribute to summary.errored; assertion results remain separate cases. The workflow fails the job as well as the commit status, and test-script/inventory edits under tests/ trigger a run. Regression checks run without LabVIEW in the Windows Unit Test Runner Checks workflow. Linux offline checks run in its Ubuntu job and cover autodetection, disabled configs, missing tooling, stale results, safe argument substitution, installation selection and VIPM revision suffixes. Locally run python .github/labview/test-unit-tests.py and python .github/labview/test-linux-unit-tests.py. Real LabVIEW and container execution still require worker validation.

The dashboard colours the Unit Tests badge from the passedPercent thresholds above (green at or above 100%, yellow at or above 80%, red below 80%). When Linux results are present the cell shows the same two-number Windows/Linux pill as Mass Compile.

Warm-container history backfill

Backfilling a long history one revision at a time would pay the multi-GB pull and the slow Windows LabVIEW cold-start once per revision. unit-tests-backfill.ps1 instead:

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.” On Linux the same banner appears when LUnit CLI is missing (-350053 or -350006). The UTF JUnit reporter in particular is mandatory — see §15.

16.5 Antidoc #

At a glance

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 #

Populate history respects the selected revisions. The snapshot workflow receives an explicit space-separated shas list, oldest first, and renders only those commits in one run. Choosing only the newest revision does not walk older history; Fill excludes completed cells, while Re-run forces rendering only within the selection. A single cell’s Run action also passes that revision’s SHA. The workflow’s run name carries the selected SHAs so queued, running, and failed badges belong only to those rows. Direct workflow dispatches without shas retain the manual head and unrestricted backfill modes, with max_commits applying only to the latter. Update installed tooling to receive both the dashboard and the workflow’s new input together; an older workflow rejects the input rather than silently expanding the requested scope.

The Populate history chooser offers an advisory banner when older, unselected revisions are missing snapshots for an enabled snapshot activity and selected platform. It counts revisions once, excludes hidden dependency-only revisions and browser-tracked queued work, and explains that Fill reuses snapshots for unchanged file content. The banner recommends All revisions plus Fill while noting that more revisions may start more workflows and that other enabled activities follow the same revision scope. It never changes the selection or queues work. Keep selected revisions dismisses the tip for that dialog session; opening the chooser again enables it again.

Snapshots are interactive views of a VI's front panel and block diagram. Pan and zoom the code, switch between the front panel and block diagram, and navigate cases, events, and sequences directly on the diagram without opening LabVIEW. The same viewer is used in the VI Browser, standalone links, and report previews. Snapshots and Diff are the only viewer choices.

Rendering and reuse

Snapshot Checks: Windows contract tests read repository text and scratch-client fixtures as UTF-8, including Unicode release notes, rather than relying on the host's legacy code page. A regression exercises catalog loading with a simulated Windows-1252 default. These checks validate installer and viewer contracts; they do not establish successful LabVIEW rendering or a container bake.

The VI Snapshots capability installs the browser, revision metadata, and frame renderers. Configure selects the Windows and/or Linux render platforms under config.viBrowser.platforms. Windows uses vi-snapshots-json-windows.yml; Linux uses vi-snapshots-json.yml and the render engine baked into the Linux worker. The gallery workflow is vi-snapshots.yml. The gallery workflow renders Windows frames directly; platform render workflows can fill missing frames for a selected revision.

Frames are content-addressed by git blob SHA: by-blob/<ab>/<blob>.windows.json on Windows and by-blob/<ab>/<blob>.json on Linux. Unchanged VIs reuse frames across revisions. Each job keeps a warm container to avoid per-VI startup costs. The dashboard counts frame coverage, not exported HTML files.

Browsing and missing snapshots

VI file rows show a brown, keyboard-accessible Diff pill when a side-by-side report exists for that revision. Clicking it opens the report in the viewer, independently of snapshot availability, with the existing Linux/Windows report fallback. Snapshot and New pills remain omitted. Once the frame indexes confirm that neither platform has a snapshot, a small red X beside the filename indicates No snapshot available, with the same accessible label and hover text. Unknown or still-loading availability has no marker. The X disappears when polling finds frames from either platform. Folder change indicators and the toolbar’s Diff control remain available.

When both platforms have frames, the browser initially prefers Windows; a platform-specific shared link preserves its selected render. Front Panel and Block Diagram controls choose the pane, and share links preserve the revision, VI, pane, and platform. Coaching tips can be dismissed locally.

A VI without frames shows a render-needed, running, or failed state with a link to the render workflow. There is no static-image fallback. Use Populate history to generate snapshots for older revisions. Older snapshot links and saved configuration are accepted and open the interactive viewer. Installing the capability makes the renderer workflows available for the selected operating systems; it does not queue historical renders or expand a selected revision scope.

16.7 SBOM Generation #

At a glance

Driver.github/labview/run-sbom-generation.ps1 Windows
Report builder.github/labview/build-sbom-report.py
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
unit-tests/<sha>/linux/results.jsonUnit-test report builder (Linux)Dashboard cell (Windows / Linux split) + 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
columns[].freshness in dependencies/index.jsonTrusted dashboard LVPM helperLocal status, reason, inspected revision, and exact selection; stale/unknown block satisfaction
dependencies/index.json: dragon.files[].inspectionTrusted Dragon parser in the action repositoryParsed or unknown declaration evidence with a reason; monitored parse/tooling failures cannot report installed
dragon.files[].dependencies[].platform_status / dragon.install_set[].platform_statusMonitored Windows inventory and selected, locally current Linux LVPM inventoryPer-platform status, installed_version and message; Linux scope is package-files. Aggregate status cannot be installed when an applicable platform is missing or unknown.
deps-pending.jsonDashboard publisherPersistent pending banner, including local LVPM freshness under linux_lvpm and monitored Dragon inspection failures as dragon[].status: unknown
workers/<platform>/<version>/manifest.jsonWorker image workflowsDependencies page installed-package status
manifest.json schema 3: vipm_inventory, vipm_packages, dependency_sha, labview_bitnessShared worker manifest producerTarget-verified observed packages, explicit unavailable/unverified evidence, and separate tooling/input revisions. Raw lists and vipc[].packages remain diagnostic/declaration data, never installed inventory.
workers/<platform>/<version>/observed-inventory.jsonVIPM worker buildsSchema 1 evidence: platform, LabVIEW year/bitness, boolean target verification and required-project flags, source, status, raw output, and exact package names/versions. Malformed or wrong-target evidence blocks manifest generation.
workers/linux-lvpm/<tag>/manifest.jsonLVPM candidate buildDigest-bound candidate inventory, activation and Linux runtime freshness checks
workers/linux-lvpm/latest.jsonLVPM candidate buildAlternative candidate discovery only; never selects an active worker
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.

If an earlier preparation already occupies lvci-release/<kind>-<version> and rejects the new catalog with a non-fast-forward error, the helper retries on a branch suffixed with the full prepared commit SHA. It never force-pushes or rewrites the older review branch. It creates or reuses the PR for that exact retry branch, and reports other push failures with their Git output. Close superseded catalog PRs after checking that the new proposal includes the current release notes; merge only the current proposal. Release-routing regressions run in the Catalog Source Sync PR gate.

Generated catalog PR titles describe release-note content first, with the version as secondary context, and bodies identify the update type, baseline, unpublished status, and exact client update guidance. Existing bot PR metadata is refreshed when reused; meaningful missing notes stop generation rather than producing a version-only placeholder. Catalog Source Sync rejects known generic titles on creation and edits. Reconfigure titles name actual changed settings or explicit LVPM activation/reset. Metadata-only release PRs describe already-merged capabilities without claiming to implement them again.

After merging a prepared catalog PR, Release verifies that it was created by github-actions[bot] in this repository and changes only the catalog and release-note fragments. It publishes the prepared version without generating another bump. If the merge does not trigger Release, run it manually on the default branch immediately after the merge, before other changes land. Review one pending catalog PR at a time; regenerate a stale or conflicting proposal rather than changing historical tags. Both release workflows must run on the default branch.

Knowing which release contains a fix

Every pull request includes a Release impact section after its feature summary: an explicit target vMAJOR.MINOR.PATCH, update type, published baseline, availability, and client update guidance. Major identifies incompatible changes; minor identifies backward-compatible new capabilities; and incremental (patch) identifies backward-compatible fixes or documentation/maintenance updates. The corresponding release:major, release:minor, or release:patch label records the intended bump; no label still defaults to patch.

A projected version can change with merge order. A prepared catalog PR is not a published release. After publication, the feature PR must link the verified first containing immutable release. Client owners should select that exact tooling version once available and confirm the installed version after updating. A rolling channel or alias is not proof that a fix is included: check its resolved release separately. Publication does not promote beta or stable. Explicitly skipped releases have no assigned containing version until a subsequent release is verified.

Canonical source and GitLab distribution

GitHub is the canonical source of implementation and releases. When the optional GitLab mirror is enabled, the release workflow runs sync-gitlab-mirror.sh after GitHub has published an immutable tag. It first pushes immutable vMAJOR.MINOR.PATCH tags without force, then force-updates the mirror's main to that released commit and updates rolling aliases such as vMAJOR, beta, stable, and dev. The GitLab project elijah286/ci-for-labview is therefore a one-way distribution mirror, not a contribution branch or a second codebase.

The mirror does not publish arbitrary GitHub main work: it advances only from a canonical release tag. Set the GitHub Actions variable GITLAB_MIRROR_ENABLED=true and the write-capable secret GITLAB_MIRROR_TOKEN (optionally GITLAB_MIRROR_USERNAME) to enable the post-release push. The token needs GitLab write_repository access, and a protected mirror main branch must permit that automation identity to force-update the release snapshot. An enabled mirror failure produces a warning without invalidating the published release. If it needs recovery, the source-only Sync GitLab Distribution workflow accepts an immutable tag; it never manufactures a release or moves an immutable tag.

A GitLab-only command-line install starts from the mirror's raw bootstrapper with --source-host gitlab; the bootstrapper follows its GitLab source.json distribution entry and downloads its archive through the GitLab API. It writes a source.distribution block in the target manifest. The browser configurator remains served from GitHub Pages and records GitHub as its origin even when it creates a GitLab target, leaving the command-line installer as the GitHub-free route without maintaining two implementations.

To update a GitLab-distribution target, run its install.sh --update or install.ps1 --update bootstrapper. It reads source.distribution, fetches fresh tooling from GitLab, and then runs the archive-sourced install.py --update to refresh .gitlab/labview-ci/. Direct install.py --update operates only on the source checkout already on disk. The GitHub-only one-click Update now workflow and Dependabot flow are not GitLab update mechanisms.

GitHub clients update in one of two ways:

Tooling updates retain a selected legacy snapshots-2 activity as unified snapshots and clean recognized retired image-build tooling without enabling an opted-out capability. Reconfigure migrates the activity alias when activities are explicitly submitted; a blank request preserves the existing manifest unchanged.

Reconfigure applies only explicitly requested settings to the parsed existing manifest rather than regenerating the entire file. Separate VIPC and Dragon declarations, explicit false dependency policies, operating systems, provenance, and unknown client fields are preserved. Blank or unchanged requests keep the file byte-identical, including comments; actual updates may normalize YAML formatting. Invalid YAML or a non-mapping configuration path is rejected before the manifest is written.

Changed manifests use the layout expected by existing tooling readers: indented dependency lists, path-first dependency records, config-first VI Analyzer rules, double-quoted numeric strings, and inline OS lists. Dependencies also recognizes adjacent VIPC, Dragon, and action sections without dropping a header. Scalar values are not line-wrapped, and explicit none dependency selections remain configured empty lists rather than falling back to automatic discovery. Scoped updates detach shared YAML mappings before editing so aliases cannot change unrelated client settings. A no-op remains byte-identical. If a prior update left an older dashboard unable to read the formatting, the saved values are still present: update tooling, then make an intentional settings change through the GitHub Reconfigure workflow to regenerate compatible formatting. Do not rely on a misread UI to overwrite monitored lists.

Upgrade preservation covers the complete existing project configuration, not just runner concurrency. Unit Test Runner Checks exercises manifest upgrades alongside execution and report regressions, and is triggered by installer changes as well as unit-test runtime changes.

The major alias @v<major> tracks the newest release promoted to stable, not the newest release published. Every merge to the source repo is tagged and released, but a release only reaches @v4 once the maintainer blesses it — so consumers on the major alias receive reviewed work, not the development tip. Pin @beta for release candidates, @dev for every build, or @v<major>.<minor> to follow one line. The major alias never moves backwards, so an automatic update can never downgrade a consumer.

Published release notes and workflow summaries distinguish publication from promotion. A successful release does not itself move the stable or beta channel. To test a newly published version before promotion, select its immutable version tag explicitly rather than assuming @v4 has advanced.

The browser Apply to New Repo flow (re-applied on top of an existing install) mirrors the same prune: when it re-vendors a tooling directory it deletes any target file the source no longer ships, except the consumer's own preserved config — so re-applying after a file is removed upstream stays in sync with Update now and never leaves an orphan (e.g. an obsolete Go source) that breaks a build.

reconfigure.yml backs the Configure Pipeline dialog: it rewrites .github/labview-ci.yml from a workflow_dispatch form and opens a reviewable pull request rather than committing directly.

Release channels (Dev / Beta / Stable)

Every published build is a Dev build by default (the tooling ships many a day). The maintainer can then mark a build up two tiers: Beta (a release candidate — “might be good enough”) and Stable (a release, ready for clients). The catalog carries this as two optional top-level pointers — betaVersion and stableVersion — plus a "beta": true / "stable": true flag on each marked release entry. Marking never builds anything: it blesses an already-published, immutable v<version> release, and release.yml force-moves a rolling beta and stable git tag to the marked commits.

The install and What's New dialogs offer a cumulative channel picker — Release only (stable), Release + beta (beta), or Release + beta + dev (dev) — stored per browser in lvci_install_channel / lvci_update_channel. The header's update indicator and the What's New compare resolve the channel target the same way: dev = latest published; beta = newest of betaVersion/stableVersion; stable = stableVersion. Everything gracefully falls back to the latest published version while a tier hasn't been marked yet. The install dialog fetches the target version's own catalog so its version, capabilities, and vendored files are all consistent at the tag being installed.

The maintainer marks builds from the per-release Mark as beta / Mark as stable buttons on their own source repo's What's New page. They dispatch promote-release.yml (owner-only — GitHub requires Actions: write on the source repo) with a tier input, which verifies the v<version> tag exists, runs the catalog/source-sync gate, edits the catalog via promote-release.py --tier (set the pointer, flag the entry, bump the version), and commits to the default branch or opens a reviewable catalog PR when it is protected. Promotion remains pending until that PR merges and Release publishes it; the same buttons un-mark to roll a channel back to the previous build in that tier.

Relocation pointer

.github/labview-ci/source.json names the canonical NI repository, ni/LabVIEW-CI-CD-Toolkit, without breaking installs: if it names a different repo than the current one, the bootstrappers and the update workflow follow it automatically, and a redirected update re-pins the consumer's manifest to the new home.

The source dashboard, documentation, FAQ, and installer are published at ni.github.io/LabVIEW-CI-CD-Toolkit. GitHub repository redirects do not redirect the old GitHub Pages site, so published links must use this new address. The pointer's GitHub distribution, the catalog's source.repo, and this repository's install manifest all name the same NI source. That matching manifest makes the source dashboard publish its current commit's page bundle rather than an older installed release; consumer dashboards still use their opted-in tooling ref.

Immutable releases made before the transfer retain their original source metadata. Publish a release containing the updated metadata and promote it to the desired install channel so new default installs use the NI source. Keep legacy GitHub repository redirects reachable for existing clients following the relocation pointer; do not rewrite historical tags or assume container packages moved with the repo.

19. Client discovery #

The Clients page is populated by discover-clients.yml / discover-clients.py, which run only in the source repository on a daily schedule (or on demand). Discovery is deliberately pull-based — your repo is never modified and never phones home. It queries GitHub's public search index for three signals an install necessarily leaves behind:

  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 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 frameworks depend on baked VIPM/UTF tooling.
Run Unit Tests — Linux Containerubuntu-latestPR / push (LabVIEW source, test scripts), dispatchLUnit CLI 1.7.1.32+; report under unit-tests/<sha>/linux/.
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)Builds revision metadata and queues interactive snapshot rendering.
VI Snapshots JSON (VI Browser, Linux)ubuntu-latestworkflow_run after snapshots, dispatchGate job decides; auto-runs only when positionAware includes linux.
VI Browser 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 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 reportsPublishes trusted in-repo/default-history reports to Pages; fork PR and untrusted dispatch targets stay on Actions artifact/run links in PR comments/statuses.
Catalog Source 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 →