Skip to content

refactor(three-d-viewer): migrate iDevice to TypeScript - #40

Open
erseco wants to merge 21 commits into
mainfrom
refactor/three-d-viewer-typescript
Open

refactor(three-d-viewer): migrate iDevice to TypeScript#40
erseco wants to merge 21 commits into
mainfrom
refactor/three-d-viewer-typescript

Conversation

@erseco

@erseco erseco commented Jul 30, 2026

Copy link
Copy Markdown
Member

Summary

Migrates the 3D Viewer implementation from upstream
exelearning/exelearning#2157 to the centralized TypeScript iDevice convention
introduced by exelearning/exelearning#2147.

Source

Only the reusable parts of exelearning#2147 were taken — scripts/build-idevices.ts and its
spec, ADR-0006, doc/development/idevices-typescript.md and
slide/build.config.json verbatim, plus hand-ported edits to package.json,
vitest.config.mts, .gitignore, .github/workflows/e2e.yml,
scripts/build-resource-bundles.js, AGENTS.md, the iDevice skill and
mkdocs.yml. No Interactive Video source, tests, E2E helpers, workarea/importer
changes, dependency bumps or bun.lock changes were imported.

Architecture

  • Maintained source under three-d-viewer/src/.
  • One generated edition IIFE.
  • One generated export IIFE.
  • Shared schema, interaction controller and renderer adapters.
  • Generated bundles and source maps are not committed.
  • No separate generated runtime file.
src/
├── globals.d.ts     minimal slices of THREE, model-viewer, AssetManager, eXe
├── shared/          types, schema v2, migration, colours, urls, html, model-source, scoring
├── runtime/         registry, lifecycle, asset + path resolution, loaders, STL scene, eXe3DViewer
├── interactions/    controller, answer state, dialog, question, guided nav, fallback
├── adapters/        geometry + raycast maths, model-viewer adapter, STL adapter
├── edition/         $exeDevice: template, form, preview, marker list/editor, SCORM
├── export/          $threedviewer: renderer, bootstrap, per-wrapper controller, SCORM
└── test/            THREE / model-viewer stubs, helpers, fixtures, bundle contract

export/three-d-viewer-runtime.js is gone. Both bundles compile in the runtime
and publish window.eXe3DViewer idempotently, so a page still has exactly one
instance registry and nothing is injected at runtime any more.

config.xml is unchanged: the two generated bundles keep their existing
filenames.

Schema

  • One final schema v2, with an explicit schemaVersion field.
  • Direct migration from original unversioned 3D Viewer data — it reopens with a
    disabled, empty interaction layer and renders exactly as before.
  • Safe rejection of future schema versions: the editor shows a notice instead of
    the form and save() returns the original object untouched, so a newer
    document can never be opened and destructively overwritten.
  • No migrations for unpublished intermediate development schemas. The
    development branch's internal version: 2 field is simply ignored; its shape
    is compatible and normalizes as legacy content.
  • Persisted data is parsed as unknown, validated and normalized — never cast.
  • SCORM settings have one canonical home (the nested scorm block);
    normalizeScorm() is the single adapter to the gamification framework's
    flatter isScorm / weighted / textButtonScorm vocabulary.

Improvements

  • Strict TypeScript types (strict, noUncheckedIndexedAccess,
    noImplicitOverride, useUnknownInCatchVariables); no any anywhere in
    src/.
  • Colocated Vitest coverage: 36 spec files, 568 tests, 95% line coverage of
    src/.
  • Generated-bundle contract tests over the actual compiled IIFEs.
  • Persistent question-attempt state across dialog reopen.
  • Hardened HTML sanitization.
  • Explicit lifecycle cleanup and multiple-instance isolation.

Behaviour fixes

  1. Question attempts persist across a dialog reopen. The attempt counter was
    recreated every time a marker dialog was built, so the configured allowance
    reset on each reopen. Answer and attempt state now live per marker id in the
    interaction controller, so the limit applies to the marker for the whole
    activity session. A correct answer also stays resolved, and the chosen option
    is restored. Covered by unit tests and by the E2E spec.
  2. HTML sanitization handles foreign-content casing. The banned-element
    check compared tagName against an upper-case table, which lowercase
    SVG/MathML elements bypass (<script> inside <svg>, <foreignObject>,
    <annotation-xml>). Tag names are now upper-cased before the lookup, those
    elements are banned explicitly, and srcset, srcdoc and xlink:href are
    URL-checked alongside href/src. Still pure DOM traversal — no regex
    scrubbing.
  3. A model-viewer library that never registers cannot block the STL path.
    customElements.whenDefined() never settles for an element that fails to
    load; the wait is now bounded.
  4. Source maps no longer ship inside server-generated export packages. The
    browser export path builds from the resource ZIP, which now excludes .map,
    but the server path (CLI + external API) walked the iDevice export/
    directory and only filtered .test.js / .spec.js.
  5. .gitignore no longer swallows iDevice source directories. The
    repository's runtime/ rule was unanchored, so it matched a directory named
    runtime at any depth — including this iDevice's src/runtime/. The working
    tree built fine while the commit was missing 17 files. The rule is now
    anchored to the repository root, which is what it was always for (it sits
    beside symfony_legacy / nestjs_legacy), and a regression test asserts that
    no maintained iDevice source file is matched by an ignore rule. It uses
    git check-ignore --no-index, so it tests the rules rather than the index and
    still catches a rule that would drop a newly added file.
  6. The CI unit-test target now builds assets first. test-unit-ci was the
    only test target without a bundle dependency — test-unit,
    test-integration and test-frontend all have one. That was invisible while
    every iDevice bundle was committed. Now that export/three-d-viewer.js is
    generated, IdeviceRenderer.spec.ts found no file on a fresh checkout and
    failed in CI while passing locally, where the bundle happened to exist on
    disk. The spec guards on the iDevice export/ directory, which is still
    committed, so it could not detect the missing artifact either. The assertion
    is left intact and the CI target gets the build precondition the other test
    targets already had.

Compatibility

  • Preserves GLB/GLTF through <model-viewer>.
  • Preserves STL through Three.js.
  • Preserves marker authoring, guided navigation, questions and SCORM
    (score is still correct question markers / total question markers on the 0..10
    convention, with de-duplication by marker id, off by default, auto-save and
    save-button modes, weighting, translated button text, and safe degradation
    when the gamification helpers are absent).
  • Preserves legacy interaction-free 3D Viewer content.
  • Preserves the legacy base64 data-config upgrade path for persisted HTML.
  • $exeDevice, $threedviewer, ThreeDViewerExportObject and eXe3DViewer
    are all still published, assigned explicitly from the entry points rather than
    relying on the bundler's globalName.

Generated files

The following files are generated during build and intentionally ignored:

  • edition/three-d-viewer.js
  • edition/three-d-viewer.js.map
  • export/three-d-viewer.js
  • export/three-d-viewer.js.map

build:all runs typecheck:idevices + bundle:idevices before
bundle:resources, so make bundle, the static build and every test target
regenerate them first. scripts/build-resource-bundles.js keeps .map files
out of the resource ZIPs, and the E2E workflow uploads both bundles as artifacts
because the runners get a fresh checkout.

Documentation

Testing

Every command below was run on this branch.

Command Result
bun install 781 packages, bun.lock unchanged
bun scripts/build-idevices.ts --typecheck-only --only three-d-viewer pass
bun scripts/build-idevices.ts --only three-d-viewer 4 files emitted
bun x vitest run --config vitest.config.mts public/files/perm/idevices/base/three-d-viewer/src 36 files, 568 tests, all pass
bun x vitest run --config vitest.config.mts (whole frontend suite) 280 files, 14086 tests, all pass
coverage of three-d-viewer/src 95.5% lines
bun test scripts/build-idevices.spec.ts 9 pass
clean-checkout build of the pushed commit (git archive HEAD) typecheck:idevices, bundle:idevices, build:static and the full Vitest suite all pass
bun test ./test/integration 721 pass, 0 fail
make fix / make lint clean (pre-existing warnings elsewhere, unchanged)
bun x biome check public/files/perm/idevices/base/three-d-viewer/src clean
make bundle success; idevices.zip contains three-d-viewer/three-d-viewer.js and zero .map entries
clean-regeneration check all four generated files recreated; git status --short empty
bun x playwright test --project=chromium .../three-d-viewer-interactions.spec.ts 3 passed

GitHub Actions on this branch is green on every check: build, build_and_test,
merge-report, codecov/patch, codecov/bundles, and all twelve e2e shards
(chromium 1–4, firefox 1–4, static 1–4).

Remaining limitations

The notes below describe the local development sandbox only; CI is green on
every check for this branch.

  • The full Playwright suite has pre-existing failures in that sandbox
    (home-is-where-art-is, latex-rendering, page-properties,
    file-manager*, block-reorder-stability, component-export-import,
    project-shared-delete, idevices/interactive-video, and others). A
    representative subset was run on this branch and on the untouched
    reference/pr-2157 worktree: 13 failed / 24 passed on both, with an
    identical set of failing test ids
    , so they are environmental. None of them
    touch the 3D Viewer, and all twelve CI E2E shards pass.
  • bun test ./src ./test/helpers reports two failures in that sandbox
    (convert.spec.ts internal-error handling and the elp:convert
    unwritable-output-directory check). They reproduce identically on the
    untouched main of this repository, which is green in CI, and they do not
    appear in this branch's CI logs — the sandbox runs as root, so a
    "directory is not writable" assertion cannot hold. Not caused by this change.
  • make test-e2e-static was not run locally; the four e2e (static, …) CI
    shards cover it and are green.
  • make fix still does not cover public/files/perm/idevices/base/*/src/.
    Extending it would pull pre-existing Biome findings in the Slide iDevice into
    this PR, so it is left for a separate change; the 3D Viewer sources are
    Biome-clean and can be checked with
    bun x biome check public/files/perm/idevices/base/three-d-viewer/src.

erseco and others added 16 commits July 10, 2026 14:26
Specify hotspots, guided navigation and single-choice questions for the
3D Viewer iDevice (issue exelearning#2153): versioned state, renderer-adapter
architecture over the shared runtime, migration, accessibility, export
and test strategy. No code changes yet.
Introduce state version 2 with an optional interaction block (markers,
guided mode, questions). Add pure, idempotent normalization helpers
(interaction/marker/anchor/camera/action/question) and single-choice
grading, mirrored byte-for-byte between edition and export. Legacy
state migrates transparently to a disabled, empty interaction; blob:/
data: URLs are never persisted. Colocated unit tests (TDD).
Add createInteractionLayer() to the shared eXe3DViewer runtime: a
renderer-agnostic controller (markers, accessible content dialog with
focus trap, single-choice questions with ARIA-live feedback, guided
navigation) driven through two adapters — native model-viewer hotspots
for GLB/GLTF and a DOM overlay projected per-frame for STL. Adds an
onFrame hook and raycastFromPointer helper to the STL instance, and
tears the layer down in destroy(). Colocated unit tests with a THREE
stub cover projection, occlusion, placement, questions and cleanup.
renderView embeds enabled interaction state as an escaped JSON <script>
block plus a static, escaped fallback marker list and (in guided mode)
baked prev/next controls. renderBehaviour loads the shared runtime on
demand and attaches the interaction layer per render path (native
model-viewer hotspots or the STL instance, resolved once its mesh is
ready). asset:// media survives into the block for the export rewriter;
blob:/data: is never emitted. Colocated tests cover markup, escaping and
guided controls.
Add a collapsed Interactions section to the editor: enable toggle,
guided/labels/wrap flags, click-to-place marker creation, an editable
marker list (reorder/edit/delete) and an accessible marker editor with
per-action-type fields (information, image, video, link, single-choice
question with options/feedback/attempts) and camera capture. The live
preview attaches the shared interaction layer per render path. Adds the
shared marker/dialog/question/guided-nav/fallback styles (mirrored in
edition + export CSS) and editor-only authoring styles. Colocated
editor unit tests.
…hange

Add a Playwright spec covering the full flow: author an informational
marker and a single-choice question, save, reload, open the preview,
verify markers render with accessible labels + guided nav, the content
dialog opens, and the question shows accessible feedback.

Fix a data-loss bug the E2E surfaced: the generic display-option change
listener ran readFormState(), which rebuilt state without the
interaction block — wiping authored markers. readFormState now preserves
interaction and the interaction toggles are excluded from the generic
listener. Regression test added.
- Guided nav: bind prev/next once (re-render no longer stacks handlers so
  one click advances one step); re-enable buttons in wrap mode.
- Fallback: keep the accessible text list visible when WebGL is
  unavailable and reveal it if STL boot never yields a mesh or the runtime
  fails to load, so assistive-tech users always reach marker content.
- Marker button listeners bind on the (rebuilt/removed) button instead of
  the controller's lifetime array, removing a listener-ref leak.
- Strip javascript:/vbscript:/blob:/data: from link URLs at normalize time
  (parity with image/video); sanitizer now also scrubs form/action/formaction
  and related URL attributes and drops <form>.
- Add regression tests: wrap navigation, no-double-bind, setState active
  invalidation, link/image activation, model-viewer camera capture +
  placement, no-WebGL fallback, sanitizer form/action.
CI (headless Chromium) cannot create a WebGL context, so the Three.js
STL scene and model-viewer projection do not render there. Adjust the
E2E accordingly:
- Activate markers via dispatchEvent instead of a real click — whether a
  model-viewer hotspot is visually clickable depends on WebGL projection,
  which is env-dependent and not what we assert.
- Replace the STL 'renders in preview' test with a WebGL-independent one:
  author + persist an STL marker and assert the interaction data reaches
  the STL export path and the accessible fallback carries the content
  (the STL projection/placement math is unit-tested with a THREE stub).
- Wait for the STL runtime instance with a generous, time-based budget
  (cold Three.js module load) before attaching / revealing the fallback.
Add optional SCORM scoring for question markers, reusing the shared
gamification.scorm framework (no new scoring engine):
- State gains isScorm (0/1/2) + weighted + textButtonScorm, normalized
  and round-tripped, mirrored between edition and export.
- Editor shows an 'Assessment (SCORM)' section (the standard SCORM tab)
  when interactions are enabled and at least one question marker exists;
  everything is guarded so the iDevice never breaks without the framework.
- The shared runtime reports each graded answer via a new
  onQuestionAnswered hook; in a SCORM export (body.exe-scorm) the export
  layer registers the activity and reports the fraction of question
  markers answered correctly through registerActivity + sendScoreNew.
Colocated unit tests cover normalization, the data block, the scoring
wiring and the runtime hook.
Bring in the convention-based TypeScript iDevice build from upstream
PR exelearning#2147 so the 3D Viewer can be migrated to TypeScript without adding
another bespoke build script.

Imported verbatim from that PR:

- scripts/build-idevices.ts + its spec (retargeted at three-d-viewer)
- ADR-0006 (TypeScript iDevices build convention)
- doc/development/idevices-typescript.md
- slide/build.config.json, which replaces scripts/build-slide-editor.ts

Ported by hand (no Interactive Video code, no dependency bumps):

- package.json: typecheck:idevices / bundle:idevices / bundle:idevices:watch,
  wired into build:all before bundle:resources
- vitest.config.mts: discover public/files/perm/idevices/**/src/**/*.spec.ts
  and measure coverage on base/*/src/**/*.ts instead of generated bundles
- .gitignore + .github/workflows/e2e.yml: generated three-d-viewer bundles
- scripts/build-resource-bundles.js: keep .map files out of resource ZIPs
- AGENTS.md, .agents/skills/idevice/SKILL.md, mkdocs.yml

The 3D Viewer ADR/SDD are renumbered to ADR-0007 / SDD-0002 so they do
not collide with the IDs reserved by PR exelearning#2147.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TDMjPiVVapwAo6DtpZZGfb
Move every maintained line of the 3D Viewer into `src/`, compiled by the
centralized TypeScript iDevice build into one edition IIFE and one export
IIFE. The generated bundles and their source maps are gitignored.

What was three hand-maintained JavaScript files with a duplicated schema
layer is now one source tree:

  shared/       document model, schema v2, migration, colours, URLs, HTML
                sanitizer, model-source detection, pure scoring
  runtime/      instance registry, explicit lifecycle/teardown, asset
                resolution, path resolution, lazy library loaders, the STL
                Three.js scene, the `eXe3DViewer` facade
  interactions/ the renderer-agnostic controller: marker state, accessible
                dialog, questions, learner answer state, guided navigation,
                text fallback
  adapters/     pure projection/raycast maths plus the `<model-viewer>` and
                STL implementations of one `MarkerAdapter` contract
  edition/      `$exeDevice`: template, form, live preview, marker list,
                marker editor, SCORM section
  export/       `$threedviewer`: markup builders, boot, per-wrapper
                controller, SCORM transport

Removed, because their source now lives in `src/`:

- `export/three-d-viewer-runtime.js` (no separate runtime file, no script
  injection — both bundles carry a compiled copy and publish
  `window.eXe3DViewer` idempotently, so one registry per page)
- the committed `edition/three-d-viewer.js` and `export/three-d-viewer.js`

Schema: one final v2 with an explicit `schemaVersion`. Original
unversioned content migrates straight to it and reopens with a disabled,
empty interaction layer; a future version is refused instead of being
opened and destructively overwritten. Persisted data is parsed as
`unknown`, validated and normalized — never cast.

Behaviour fixes:

- Question attempts and the resolved state now live per marker id in the
  interaction controller, so the configured attempt limit applies for the
  whole activity session instead of resetting each time a marker dialog is
  reopened.
- The HTML sanitizer upper-cases tag names before the banned-element
  check, so lowercase SVG/MathML foreign content (`<script>` inside
  `<svg>`, `<foreignObject>`, `<annotation-xml>`) can no longer slip
  through; `srcset`, `srcdoc` and `xlink:href` are URL-checked too.
- `ensureModelViewerLoaded` bounds its wait on `customElements.whenDefined`,
  so a library that never registers cannot block the STL path.

`config.xml` is unchanged: the two bundles keep their existing filenames.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TDMjPiVVapwAo6DtpZZGfb
…erage

The JavaScript tests that lived beside the old `edition/` and `export/`
sources are replaced by colocated `*.spec.ts` files next to the modules
they cover: 36 files, 567 tests, 95% line coverage of `src/`.

New ground the old suite did not cover:

- generated-bundle contract tests that evaluate the ACTUAL compiled IIFEs
  and assert `$exeDevice`, `$threedviewer`, `ThreeDViewerExportObject` and
  `eXe3DViewer`, that neither bundle carries module syntax or chunk
  imports, that source maps are linked rather than inlined, and that a
  schema-v2 document renders end to end through the export bundle
- schema-v2 migration: legacy → v2, future-version rejection, round-trip
  and idempotency, and that no blob:/data: URL is ever persisted
- lifecycle and registry teardown, including multiple-instance isolation
- the STL adapter's projection, occlusion, raycast and camera maths
  against a small deterministic Three.js stub
- HTML sanitization of lowercase SVG/MathML foreign content

Small Three.js and `<model-viewer>` stubs replace WebGL, so the suite is
deterministic and needs no browser.

E2E (`three-d-viewer-interactions.spec.ts`) gains a third marker with a
one-attempt question that proves the allowance survives closing and
reopening the dialog, an assertion that a resolved question stays
resolved, and a new spec asserting interaction-free content still exports
with no interaction payload, markers, guided controls or fallback list.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TDMjPiVVapwAo6DtpZZGfb
Amend SDD-0002 and ADR-0007 — both still unpublished — so they describe
what was built rather than the classic-script plan.

The main correction: the SDD and the ADR both said the pure schema layer
would be MIRRORED byte-for-byte between `edition/` and `export/`, with a
`// mirror edition` marker and identical tests to police the drift. That
is no longer true and no longer needed. The schema, the migration and the
whole behavioural layer have a single TypeScript source under `src/`; the
compiler puts a copy into each generated bundle. Duplicated bytes are
accepted, duplicated maintained source is not — so the "duplication drift"
risk is replaced by a "stale generated bundle" one, and the mitigation
moves accordingly.

Also documented: the `src/` tree and what each directory owns; one edition
IIFE and one export IIFE, gitignored; no separate runtime JavaScript file;
schema v2 with an explicit `schemaVersion`, the direct legacy → v2
migration and the safe rejection of future versions; the single canonical
home for SCORM settings; the build, watch and typecheck commands; source
maps and how they are kept out of resource ZIPs; the test layout including
the generated-bundle contract; and the four browser globals the bundles
publish.

`doc/development/idevices-typescript.md` points at this iDevice as the
reference for the convention now that it follows it.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TDMjPiVVapwAo6DtpZZGfb
…kages

The browser export path builds from `public/bundles/idevices.zip`, which
`scripts/build-resource-bundles.js` now assembles without `.map` files, so
a learner package produced in the browser never carries a source map.

The server path (CLI commands and the external API) walks
`public/files/perm/idevices/base/<name>/export/` directly and only filtered
`.test.js` / `.spec.js`, so it would have shipped the ~230 KB
`three-d-viewer.js.map` inside every HTML5, SCORM, EPUB and IMS package it
generated. Filter `.map` alongside the tests so both paths agree.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TDMjPiVVapwAo6DtpZZGfb
…rdown

Two things the classic-script version did that the first pass of the
migration dropped:

- **Animation playback in the editor preview.** The old `$exeDevice`
  called `applyAnimationState()` from `updatePreview()` and from the
  model-viewer `load` handler, so ticking "Play animation" started the
  clip immediately. The preview now owns an `applyAnimation()` that does
  the same — pick the stored clip when the model still offers it, fall
  back to the first one, set the speed, play on repeat and announce it —
  and the device calls it after refreshing the animation picker.
- **Canvas teardown.** `hideThreeJSCanvas()` used to hide the Three.js
  canvas when the author swapped an STL model for a GLB one. The runtime
  created that canvas, so `disposeInstance()` now removes it and un-hides
  the sibling `<model-viewer>`; otherwise a dead canvas stayed on top of
  the GLB preview.

Also untrack `edition/three-d-viewer.js` and `export/three-d-viewer.js`.
They were correctly `git rm --cached`-ed during the migration, but an
intervening `git reset` put them back in the index, and .gitignore does
not apply to already-tracked files. `git status` is clean after a build
again.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TDMjPiVVapwAo6DtpZZGfb
claude added 2 commits July 30, 2026 11:35
The `runtime/` entry in .gitignore was unanchored, so it matched a
directory named `runtime` at any depth — including the TypeScript
iDevice source directory `three-d-viewer/src/runtime/`. All 17 files
there were silently excluded from the commit, so CI failed to type-check
with 33 TS2307 "Cannot find module '../runtime/...'" errors while the
working tree built fine.

Anchor the rule to the repository root, which is what it was always for
(it sits alongside `symfony_legacy` and `nestjs_legacy`), and restore the
17 lost source files.

Add a regression test asserting no maintained iDevice source file is
matched by an ignore rule. It uses `git check-ignore --no-index` so it
tests the rules rather than the index, and therefore still catches a rule
that would drop a newly added file.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TDMjPiVVapwAo6DtpZZGfb
`test-unit-ci` was the only test target that did not depend on `bundle`
— `test-unit`, `test-integration` and `test-frontend` all do. That went
unnoticed while every iDevice bundle was committed.

Now that `three-d-viewer/export/three-d-viewer.js` is generated, the
backend spec that asserts the renderer does not mark it as a module
found no file on a fresh checkout and failed in CI while passing
locally, where the bundle happened to exist on disk.

The test guards on the existence of the iDevice `export/` directory,
which is still committed, so it could not detect the missing artifact
either. Rather than weaken that assertion, give the CI target the same
build precondition the other test targets already have.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TDMjPiVVapwAo6DtpZZGfb
@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Bundle Report

Bundle size has no change ✅

erseco added 3 commits August 4, 2026 22:26
Resolve conflicts with upstream main:
- doc/architecture/adr/records.md: keep ADR-0001/ADR-0042 from main and ADR-0006/ADR-0007 from this branch
- package.json: auto-merged (iDevice build scripts + main dep/test updates)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants