Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions .github/labview-ci/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -4496,6 +4496,12 @@
".github/actions/publish-gh-pages/action.yml"
]
},
"retiredFiles": {
"comment": "Tooling files the source has retired. Installs and updates delete them from a target repository, so a workflow the tooling no longer ships cannot keep firing (and failing) in a client repo forever. Only ever list paths this repository no longer contains.",
"files": [
".github/workflows/build-toimages-image.yml"
]
},
"capabilities": [
{
"id": "dashboard",
Expand Down Expand Up @@ -4698,7 +4704,8 @@
"name": "VI Snapshots 2.0",
"summary": "Cross-platform, position-aware VI Browser 2.0 rendering. A vendored Go engine renders each VI's block diagram in place and publishes frames JSON on Windows and Linux, so the VI Browser can show an interactive, zoomable diagram beside the classic gallery.",
"status": "stable",
"recommended": false,
"recommended": true,
"required": true,
"supportsOs": [
"windows",
"linux"
Expand All @@ -4725,7 +4732,7 @@
".github/workflows/vi-snapshots-json.yml"
]
},
"notes": "Cross-platform position-aware renderer. The Linux path runs the render engine that is BAKED INTO the Linux worker image (no separate build -- it is copied with the worker); the Windows path runs the same Go engine in the stock NI Windows container (unless EXPERIMENTAL_WINDOWS_IMAGE is set). Renders publish <blob>.json (Linux) and <blob>.windows.json (Windows) beside the gallery."
"notes": "Always installed - every repository gets VI Browser 2.0, so the viewer's 2.0 tab always has a workflow to run. Cross-platform position-aware renderer. The Linux path runs the render engine that is BAKED INTO the Linux worker image (no separate build -- it is copied with the worker); the Windows path runs the same Go engine in the stock NI Windows container (unless EXPERIMENTAL_WINDOWS_IMAGE is set). Renders publish <blob>.json (Linux) and <blob>.windows.json (Windows) beside the gallery."
},
{
"id": "unit-tests",
Expand Down
3 changes: 3 additions & 0 deletions .github/labview-ci/config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,14 @@ config:
vidiff: base

# Installed capabilities (see .github/labview-ci/catalog.json for the full list).
# Capabilities the catalog marks `required` are installed in every repository and
# are added back on the next install or update even if you delete them here.
activities:
- dashboard
- masscompile
- vi-analyzer
- vidiff
- snapshots-2

# ----------------------------------------------------------------------------
# GitHub Actions variables the GitHub workflows honor at runtime (set with
Expand Down
60 changes: 57 additions & 3 deletions .github/labview-ci/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,18 +205,41 @@ def build_substitutions(catalog: dict, owner: str | None, name: str | None,
return subs


def default_activities(catalog: dict) -> list[str]:
def required_activities(catalog: dict) -> list[str]:
"""Capabilities every installation gets, whatever the caller asked for.

A capability flagged `required` in the catalog is not a choice: VI Snapshots
2.0 is one, because the VI Browser's 2.0 tab is always visible and needs its
render workflow to exist in the repository. Without this, a repo installed
before the capability existed never gained it -- updates reuse the manifest's
activity list, so the omission is self-perpetuating and the viewer's Run
button 404s forever on a workflow that was never vendored.
"""
return [
c["id"] for c in catalog.get("capabilities", [])
if c.get("required") and c.get("status", "stable") not in DEFAULT_EXCLUDED_STATUSES
]


def default_activities(catalog: dict) -> list[str]:
defaults = [
c["id"] for c in catalog.get("capabilities", [])
if c.get("recommended") and c.get("status", "stable") not in DEFAULT_EXCLUDED_STATUSES
]
return defaults + [a for a in required_activities(catalog) if a not in defaults]


def resolve_activities(catalog: dict, activities: list[str]) -> list[str]:
"""Expand hard capability dependencies, preserving the requested order."""
"""Expand hard capability dependencies, preserving the requested order.

Required capabilities are appended to whatever was requested, so an older
manifest (or an explicit --activities list that predates them) still heals on
the next install or update.
"""
by_id = {c["id"]: c for c in catalog.get("capabilities", [])}
selected: list[str] = []
stack = list(activities)
stack.extend(a for a in required_activities(catalog) if a not in stack)
while stack:
cid = stack.pop(0)
if cid in selected:
Expand Down Expand Up @@ -611,6 +634,35 @@ def gitlab_legacy_dashboard_builder(path: Path) -> bool:
)


def prune_retired_files(catalog: dict, target_root: Path, keep: set[str],
dry_run: bool, stats: dict) -> None:
"""Delete tooling files this repository no longer ships.

A retired workflow keeps firing in a client repo long after the source has
dropped it -- build-toimages-image.yml outlived the render engine it built
and failed on every push. The configurator's stale-file scan only recognises
files by content signature, so anything that never mentioned the tooling by
name survived every reinstall. This list is the explicit backstop.
"""
for rel in catalog.get("retiredFiles", {}).get("files", []):
if rel in keep:
warn(f"retired file {rel} is still installed by the catalog - skipping.")
continue
target = target_root / rel
if not target.is_file():
continue
if dry_run:
stats["planned"] += 1
log(f" would prune {rel} (retired)")
continue
try:
target.unlink()
stats["pruned"] += 1
log(f" prune (retired) {rel}")
except OSError as exc:
warn(f"could not prune {rel}: {exc}")


def prune_gitlab_provider_files(target_root: Path, provider: dict, dry_run: bool,
stats: dict) -> None:
"""Remove only obsolete files which a previous provider package owned."""
Expand Down Expand Up @@ -1162,6 +1214,7 @@ def main() -> int:
for entry in file_list:
copy_entry(entry, source_root, target_root, subs, force, args.dry_run, stats,
preserve, update, stamp_version)
prune_retired_files(catalog, target_root, set(file_list), args.dry_run, stats)

# The dashboard generator (actions/dashboard) is a local path that only exists
# in the tooling repo and can't be rebranded into a consumer, so the vendored
Expand Down Expand Up @@ -1200,7 +1253,8 @@ def main() -> int:
add_paths = ".github .gitlab .gitlab-ci.yml" if provider == "gitlab" else ".github"
log(f" 2. Commit the update: git add {add_paths} && git commit -m \"Update LabVIEW CI\" && git push")
return 0
log(f"Installed {stats['installed']} file(s); {stats['skipped']} skipped (already present).")
pruned = f"; {stats['pruned']} retired file(s) removed" if stats["pruned"] else ""
log(f"Installed {stats['installed']} file(s); {stats['skipped']} skipped (already present){pruned}.")
if stats["skipped"]:
log("Use --force to overwrite skipped files.")
if provider == "gitlab":
Expand Down
11 changes: 11 additions & 0 deletions .github/labview-ci/notes/snapshots-2-always-installed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
VI Snapshots 2.0 is now installed in every repository and can no longer be turned
off. The VI Browser always offers a 2.0 view, so a repository that skipped the
capability had no `vi-snapshots-json.yml` / `vi-snapshots-json-windows.yml` to run
and the viewer's "Run the 2.0 snapshot" button failed with a misleading token
error. The omission also perpetuated itself: reinstalls and updates reuse the
activity list recorded in `.github/labview-ci.yml`, so a repository set up before
the capability existed never gained it. Installs and updates now add it back
automatically, the configurator shows it as "Always installed" rather than an
option, and the retired `build-toimages-image.yml` workflow — which outlived the
render engine it built and failed on every push — is deleted from repositories
that still carry it.
34 changes: 34 additions & 0 deletions .github/labview/validate-catalog-source-sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,40 @@ def main() -> int:
if obsolete:
failures.append(f"custom-image still vendors obsolete worker files: {obsolete!r}")

# A `required` capability is installed in every repository, so it must be
# something a client can actually receive: shipping one that is planned, or
# flagged defaultOff, would install a contradiction on every consumer.
required_caps = [cap for cap in capabilities if cap.get("required")]
if not required_caps:
failures.append(
"no capability is marked required. VI Snapshots 2.0 is installed "
"everywhere so the VI Browser's 2.0 tab always has a workflow to run; "
"dropping the flag silently makes it optional again."
)
for capability in required_caps:
capability_id = capability.get("id", "<unknown>")
if capability.get("status") in ("planned", "experimental"):
failures.append(
f"capability {capability_id!r} is required but {capability.get('status')!r}"
)
if capability.get("defaultOff"):
failures.append(f"capability {capability_id!r} is both required and defaultOff")

# Retired files are deleted from consumers on install/update, so a path here
# must be gone from the source and installed by nothing.
installable = set((catalog.get("base") or {}).get("files") or [])
for capability in capabilities:
for relpaths in (capability.get("files") or {}).values():
installable.update(relpaths or [])
for relpath in (catalog.get("retiredFiles") or {}).get("files") or []:
if path_exists(relpath):
failures.append(
f"retired file still exists in the source: {relpath}. Installs delete "
"it from every consumer, so it cannot also be a file this repo ships."
)
if relpath in installable:
failures.append(f"retired file is still installed by the catalog: {relpath}")

for capability in capabilities:
capability_id = capability.get("id", "<unknown>")
files = capability.get("files") or {}
Expand Down
9 changes: 9 additions & 0 deletions .github/pages/configure.html
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,13 @@ <h2>Save and apply</h2>
// never gates config.os).
const OS_COLS = [["linux", "Linux"], ["windows", "Windows"]];
function capById(id) { return (catalog.capabilities || []).find(c => c.id === id); }
// Capabilities the catalog marks `required` are installed in every repository
// (VI Snapshots 2.0 is one), so they are added to the selection even when the
// manifest this page loaded predates them.
function requiredIds() {
return (catalog.capabilities || []).filter(c => c.required && c.status !== "planned").map(c => c.id);
}
function addRequiredActs() { requiredIds().forEach(id => actSel.add(id)); }
function defaultOsFor(cap) {
const sup = cap.supportsOs || [];
const within = sup.filter(o => osPref.has(o));
Expand Down Expand Up @@ -1375,6 +1382,7 @@ <h2>Save and apply</h2>
if (m.version) renderVersions(m.version);
osPref.clear(); (m.os.length ? m.os : ["windows"]).forEach(o => osPref.add(o));
if (m.activities.length) { actSel.clear(); m.activities.forEach(a => actSel.add(a)); }
addRequiredActs();
actionOs.clear();
actSel.forEach(id => {
const cap = capById(id); if (!cap) return;
Expand Down Expand Up @@ -1471,6 +1479,7 @@ <h2>Save and apply</h2>
// Pre-select recommended activities; seed each one's platforms from the
// default OS preference; load the default dependency manifest.
catalog.capabilities.filter(c => c.recommended && c.status !== "planned").forEach(c => actSel.add(c.id));
addRequiredActs();
seedActionOs(); recomputeOsSel();
renderVersions(); renderActivities();
pkgObj = await loadDefaultPackages();
Expand Down
4 changes: 2 additions & 2 deletions .github/pages/documentation.html
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ <h2 id="activities">5. The activities &amp; when to use each <a class="anchor" h
<tr><td><strong>Unit Tests</strong></td><td>Runs Caraya / LUnit / VI Tester / NI Unit Test Framework headlessly and merges JUnit output into one report.</td><td><span class="pill win">Win</span></td></tr>
<tr><td><strong>Antidoc</strong></td><td>Generates project documentation from the VI hierarchy using Wovalab's Antidoc CLI.</td><td><span class="pill win">Win</span></td></tr>
<tr><td><strong>VI Snapshots / Browser</strong></td><td>Renders every VI to a content-addressed HTML snapshot gallery (the classic VI Browser).</td><td><span class="pill win">Win</span></td></tr>
<tr><td><strong>VI Snapshots 2.0</strong></td><td>Position-aware, in-place VI Browser frames (JSON) rendered by the cross-platform toimages engine.</td><td><span class="pill win">Win</span> <span class="pill lin">Linux</span></td></tr>
<tr><td><strong>VI Snapshots 2.0</strong></td><td>Position-aware, in-place VI Browser frames (JSON) rendered by the cross-platform toimages engine. Installed in every repository &mdash; the VI Browser always offers a 2.0 view, so its render workflow is never optional.</td><td><span class="pill win">Win</span> <span class="pill lin">Linux</span></td></tr>
<tr><td><strong>Dashboard</strong></td><td>Aggregates everything above into a single status page on GitHub Pages.</td><td>runner host</td></tr>
</tbody>
</table>
Expand Down Expand Up @@ -1885,7 +1885,7 @@ <h4>Phase 1 — HTML snapshots and Windows 2.0 frames <span class="pill win">Win
per-commit <code>manifest.json</code> (VI → by-blob HTML), the rolling <code>commits.json</code>, and the
Windows JSON index used by the in-place browser.</p>
<h4>Phase 2 — position-aware JSON (VI Snapshots 2.0) <span class="pill win">Windows</span> <span class="pill lin">Linux</span></h4>
<p>Phase 2 is the separate <strong>VI Snapshots 2.0</strong> capability, selectable on Windows and Linux (the classic gallery above is Windows-only). The <code>vi-snapshots-json.yml</code> (Linux) and <code>vi-snapshots-json-windows.yml</code> (Windows) workflows render
<p>Phase 2 is the separate <strong>VI Snapshots 2.0</strong> capability. It is installed in every repository on both Windows and Linux and cannot be turned off (the classic gallery above is Windows-only and remains optional), because the VI Browser always shows a 2.0 tab and needs a render workflow behind it. The <code>vi-snapshots-json.yml</code> (Linux) and <code>vi-snapshots-json-windows.yml</code> (Windows) workflows render
position-aware frame JSON for the in-place browser. The render engine is <strong>baked into the Linux worker image</strong> (and runs in the stock NI image on Windows) &mdash; a Go batch runner
(<code>main.go</code>) reads the same TSV worklist and shells out to <code>lvctl toimages</code> — a native
Go VI-Server TCP client that launches LabVIEW under Xvfb, captures the front panel and block diagram as
Expand Down
22 changes: 19 additions & 3 deletions .github/pages/integrate.html
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
.badge.adv{color:#9a6700;border-color:#9a6700}
.badge.planned{color:#bf8700;border-color:#bf8700}
.badge.exp{color:#cf222e;border-color:#cf222e}
.badge.always{color:#fff;background:var(--fg-muted);border-color:transparent}
.badge.os{color:var(--fg-muted)}
.note{font-size:.78em;color:var(--fg-muted);margin-top:6px}
.exp-details{margin-top:8px;font-size:.84em;color:var(--fg-muted)}
Expand Down Expand Up @@ -588,6 +589,11 @@ <h2>4. Finish setup</h2>
}

function isPlanned(cap) { return cap && cap.status === "planned"; }
// Capabilities the catalog marks `required` are not a choice. VI Snapshots 2.0
// is one: the VI Browser always shows a 2.0 tab, so every repository needs its
// render workflow. They are checked, locked, and force-added to a kept manifest.
function isRequired(cap) { return !!(cap && cap.required && !isPlanned(cap)); }
function requiredIds() { return (catalog.capabilities || []).filter(isRequired).map(c => c.id); }

function renderCaps() {
const host = $("caps");
Expand All @@ -598,10 +604,13 @@ <h2>4. Finish setup</h2>
const wrap = document.createElement("label");
wrap.className = "cap" + (planned ? " disabled" : "");
const cb = document.createElement("input");
cb.type = "checkbox"; cb.value = cap.id; cb.disabled = planned;
const required = isRequired(cap);
cb.type = "checkbox"; cb.value = cap.id; cb.disabled = planned || required;
// Capabilities are on by default unless the catalog flags one defaultOff
// (opt-in), like Antidoc, which needs the shared image + network at run time.
cb.checked = !planned && !cap.defaultOff;
// A `required` capability cannot be turned off at all.
cb.checked = required || (!planned && !cap.defaultOff);
if (required) cb.title = "Always installed \u2014 this action cannot be turned off.";
if (cb.checked) selected.add(cap.id);
cb.addEventListener("change", () => {
markCapabilityChoicesChanged();
Expand All @@ -611,7 +620,8 @@ <h2>4. Finish setup</h2>
});
const body = document.createElement("div");
const badges = [];
if (cap.recommended) badges.push('<span class="badge rec">Recommended</span>');
if (required) badges.push('<span class="badge always">Always installed</span>');
else if (cap.recommended) badges.push('<span class="badge rec">Recommended</span>');
if (cap.status === "advanced") badges.push('<span class="badge adv">Advanced</span>');
if (planned) badges.push('<span class="badge planned">Coming soon</span>');
const osBadges = (cap.supportsOs || []).map(o => `<span class="badge os">${o}</span>`).join("");
Expand Down Expand Up @@ -679,7 +689,10 @@ <h2>4. Finish setup</h2>
// ---- file resolution (mirrors install.py) ----------------------------
function resolveActivities(activities) {
const map = {}; catalog.capabilities.forEach(c => map[c.id] = c);
// Mirrors install.py: hard requires are expanded, and required capabilities
// are appended whatever the caller asked for.
const chosen = []; const stack = [...activities];
requiredIds().forEach(id => { if (!stack.includes(id)) stack.push(id); });
while (stack.length) {
const id = stack.shift();
if (chosen.includes(id)) continue;
Expand Down Expand Up @@ -2605,6 +2618,9 @@ <h2>4. Finish setup</h2>
// locked to the supported year (see renderVersions), so reinstalling a repo
// that was pinned to an unsupported year heals it instead of re-breaking.
}
// A required capability is added even when the prior manifest predates it —
// otherwise "keep current settings" carries the omission forward forever.
requiredIds().forEach(id => { if (!activities.includes(id)) activities.push(id); });
return { activities, osList, version };
}

Expand Down
12 changes: 12 additions & 0 deletions .github/workflows/reconfigure.yml
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,18 @@ jobs:
else (['windows', 'linux'] if oss == 'both' else [oss])
acts = [a.strip() for a in acts_in.split(',') if a.strip()] \
or prev['activities'] or ['dashboard', 'masscompile', 'vi-analyzer', 'vidiff']
# Capabilities the catalog marks `required` are installed in every
# repository (VI Snapshots 2.0), so the manifest must list them even when
# it was written before the capability existed -- otherwise the next
# tooling update reads this file back and skips them again.
try:
_cat = json.loads(pathlib.Path('.github/labview-ci/catalog.json').read_text(encoding='utf-8'))
for _cap in _cat.get('capabilities', []):
if _cap.get('required') and _cap.get('status') not in ('planned', 'experimental') \
and _cap.get('id') not in acts:
acts.append(_cap['id'])
except Exception:
pass
now = datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
max_parallel = int(runners) if runners.isdigit() and int(runners) > 0 else (prev.get('maxParallel') or 5)

Expand Down
Loading