Skip to content
Merged
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
221 changes: 209 additions & 12 deletions .github/scripts/test-module-asset-landing.py

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion .github/workflows/dotnet-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2426,14 +2426,19 @@ jobs:
- name: "Execute the module publication lane against a stub registry"
run: python3 .github/scripts/test-module-publication.py

# The gate and publish-bake lanes compose every external module into /ext/<Name>/ for the
# The gate and publish-bake lanes compose every external module into /ext/modules/<Name>/ for the
# tester container, and until #3514 they copied `meshweaver/modules/` alone — leaving a module
# that LOADS and then 404s every static asset it ships (`meshweaver/moduleassets/`), announced
# in one LogDebug line. Latent here (the bake is mesh-free, the gate runs no browser) and fine
# in production, so nothing could catch it: MeshWeaver.Education, which copied the block into a
# browser-driven mesh e2e, rendered a bare Code card where the workbench belongs for three days.
# This EXTRACTS both lanes' step by `id:` and executes it against real bundle shapes, reading
# the verdict off the landed bytes — with two arms that mutate the real step and must go RED.
# Since #4126 the same harness also covers the THIRD bundle section — a module's RID-specific
# NATIVE payloads (`meshweaver/modulenatives/`), which must land at the exact four segments
# `ModuleNativeAssets` probes, under a folder whose PARENT is named `modules`. Both properties
# are asserted, and both have their own falsification arm: the drop has no symptom short of a
# DllNotFoundException at the first P/Invoke.
- name: "Execute the lanes' external-module landing against real bundle shapes"
run: python3 .github/scripts/test-module-asset-landing.py

Expand Down
46 changes: 38 additions & 8 deletions .github/workflows/node-repo-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1722,6 +1722,8 @@ jobs:
MODULE_COUNT=0
ASSET_MODULES=0
ASSET_FILES=0
NATIVE_MODULES=0
NATIVE_FILES=0
# ───── ONE BUILD PER ASSEMBLY NAME, ACROSS THE WHOLE COMPOSED SET (MeshWeaver#3732) ─────
# 🚨 A module-owned `MeshWeaver.*` sibling RIDES every bundle that references it, and some
# of those siblings are ANOTHER package's declared module (Doc/Architecture/
Expand Down Expand Up @@ -1779,8 +1781,14 @@ jobs:
printf '%s %s %s %s\n' "$simple" "$(digest_of "$dll")" "$(basename "$b")" \
"$(if [ "$simple" = "$name" ]; then echo declared; else echo riding; fi)" >> "$DIGESTS"
done < <(find "$u/meshweaver/modules" -maxdepth 1 -type f \( -name 'MeshWeaver.*.dll' -o -name 'MeshWeaver.dll' \) | sort)
mkdir -p "$EXT/$name"
cp -R "$u"/meshweaver/modules/. "$EXT/$name/"
# 🚨 ONE LEVEL DEEPER THAN IT LOOKS (MeshWeaver#4126): the module lands under
# `<ext>/modules/<Name>/`, never `<ext>/<Name>/`. `ModuleNativeAssets` derives the
# module folder from the REQUESTING assembly's location and accepts it only when its
# PARENT is named `modules` — the same shape ModuleLandingService writes and the image
# publish lays out. Composed at the shallower path, a module's natives ride the bundle,
# land on disk, and are never probed.
mkdir -p "$EXT/modules/$name"
cp -R "$u"/meshweaver/modules/. "$EXT/modules/$name/"
# 🚨 A module bundle carries its STATIC WEB ASSETS separately from the flat closure,
# under `meshweaver/moduleassets/` (NuGetPackageWriter.ModuleAssetFolder), and they keep
# their MODULE-RELATIVE path — `wwwroot/x.js`, `wwwroot/_content/<Dep>/y.css`. That is
Expand All @@ -1793,7 +1801,7 @@ jobs:
# the collaboration views into MeshWeaver.Markdown.Collaboration (MeshWeaver#3514).
assets=0
if [ -d "$u/meshweaver/moduleassets" ]; then
cp -R "$u"/meshweaver/moduleassets/. "$EXT/$name/"
cp -R "$u"/meshweaver/moduleassets/. "$EXT/modules/$name/"
# Fail CLOSED, per file, against a DENOMINATOR: what the bundle ships is the
# expectation, what the module folder holds is the measurement. A landing that copied
# nothing must go RED here rather than report a green zero — an uncounted zero is
Expand All @@ -1802,15 +1810,37 @@ jobs:
assets=$(wc -l < "${u}.shipped-assets" | tr -d ' ')
missing=0
while IFS= read -r rel; do
[ -f "$EXT/$name/$rel" ] || { echo "::error::$name: shipped static asset '$rel' did not land at /ext/$name/$rel"; missing=$((missing + 1)); }
[ -f "$EXT/modules/$name/$rel" ] || { echo "::error::$name: shipped static asset '$rel' did not land at /ext/modules/$name/$rel"; missing=$((missing + 1)); }
done < "${u}.shipped-assets"
[ "$missing" -eq 0 ] || { echo "::error::$(basename "$b"): $missing of $assets shipped static asset(s) are missing from /ext/$name — a browser rendering against this mesh would 404 them"; exit 1; }
[ "$missing" -eq 0 ] || { echo "::error::$(basename "$b"): $missing of $assets shipped static asset(s) are missing from /ext/modules/$name — a browser rendering against this mesh would 404 them"; exit 1; }
[ "$assets" -eq 0 ] || ASSET_MODULES=$((ASSET_MODULES + 1))
fi
ASSET_FILES=$((ASSET_FILES + assets))
# 🚨 …AND ITS RID-SPECIFIC NATIVE PAYLOADS, under `meshweaver/modulenatives/`
# (NuGetPackageWriter.ModuleNativeFolder, MeshWeaver#4126), keeping the module-relative
# path `runtimes/<rid>/native/<file>`. That path is not decoration: `ModuleNativeAssets`
# composes its probe from exactly those four segments under the module folder and has no
# recursive walk, so a payload landed anywhere else reads as shipped and behaves as
# absent — a module that loads, renders, and throws DllNotFoundException at its first
# P/Invoke with nothing in any log connecting the two.
natives=0
if [ -d "$u/meshweaver/modulenatives" ]; then
cp -R "$u"/meshweaver/modulenatives/. "$EXT/modules/$name/"
# Fail CLOSED per file against a DENOMINATOR, exactly as the assets above: what the
# bundle ships is the expectation, what the module folder holds is the measurement.
( cd "$u/meshweaver/modulenatives" && find . -type f ) | sed 's|^\./||' > "${u}.shipped-natives"
natives=$(wc -l < "${u}.shipped-natives" | tr -d ' ')
missing=0
while IFS= read -r rel; do
[ -f "$EXT/modules/$name/$rel" ] || { echo "::error::$name: shipped native payload '$rel' did not land at /ext/modules/$name/$rel"; missing=$((missing + 1)); }
done < "${u}.shipped-natives"
[ "$missing" -eq 0 ] || { echo "::error::$(basename "$b"): $missing of $natives shipped native payload(s) are missing from /ext/modules/$name — this module throws DllNotFoundException at its first P/Invoke"; exit 1; }
[ "$natives" -eq 0 ] || NATIVE_MODULES=$((NATIVE_MODULES + 1))
fi
NATIVE_FILES=$((NATIVE_FILES + natives))
MODULE_COUNT=$((MODULE_COUNT + 1))
MODULE_ARGS="$MODULE_ARGS --module /ext/$name/$name.dll"
echo "external module composed: $name ($(basename "$b")) — $assets static asset file(s)"
MODULE_ARGS="$MODULE_ARGS --module /ext/modules/$name/$name.dll"
echo "external module composed: $name ($(basename "$b")) — $assets static asset file(s), $natives native payload(s)"
done
[ -n "$MODULE_ARGS" ] || { echo "::error::external modules were requested but none were assembled — the artifact pattern matched nothing and no registry bundle was fetched."; exit 1; }
# ───── THE VERDICT ON THE SET (MeshWeaver#3732; the reading is collected above) ─────
Expand All @@ -1832,7 +1862,7 @@ jobs:
echo "::error::the composed module set carries $DIVERGED assembly name(s) at more than one build. The bake would stamp every NodeType against whichever copy it loaded first, and every consumer that loaded another would decline them. Build the whole set in ONE compilation, or stop the divergent copy riding — Doc/Architecture/ModuleOwnedSiblingsRide."
exit 1
fi
echo "external modules: $MODULE_COUNT composed, $ASSET_MODULES of them carrying static web assets, $ASSET_FILES asset file(s) landed module-relative under /ext/<module>/"
echo "external modules: $MODULE_COUNT composed, $ASSET_MODULES of them carrying static web assets, $ASSET_FILES asset file(s), $NATIVE_MODULES carrying native payloads, $NATIVE_FILES native file(s) landed module-relative under /ext/modules/<module>/"
echo "EXT_MODULES_DIR=$EXT" >> "$GITHUB_ENV"
echo "EXT_MODULE_ARGS=$MODULE_ARGS" >> "$GITHUB_ENV"
- name: Import + compile + render + run tests for each node repo
Expand Down
30 changes: 30 additions & 0 deletions .github/workflows/node-repo-module-pack.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2636,6 +2636,21 @@ jobs:
echo "closure: $ride RIDES — the module's private closure (deps.json-derived, from the image or the shelf)"
done < "$packdir/module-libs.txt"
fi
# 🚨 The NATIVE half of the same closure (MeshWeaver#4126). A SECOND manifest, not
# more lines in module-libs.txt, because the two are declared by OPPOSITE rules:
# `--with` refuses a path component (the flat closure has no place for one) while
# `--with-native` requires the path, since runtimes/<rid>/native/<file> is exactly
# what the module loader probes. The builder derived these from the image's and the
# shelf's own deps.json and laid them out under the pack input; a payload it did
# not derive cannot reach a bundle, which is what makes this provenance and not a
# glob. Its ABSENCE is a statement — this module ships no engine.
if [ -f "$packdir/module-natives.txt" ]; then
while IFS= read -r native; do
[ -n "$native" ] || continue
printf '%s\n%s\n' --with-native "$native" >> "$packargs"
echo "closure: $native RIDES — a native payload of the module's private closure (deps.json-derived, from the image or the shelf)"
done < "$packdir/module-natives.txt"
fi
echo "closure: NO --deps-closure on the container path — there is no SDK deps.json to derive one from."
echo "closure: complete BY RECORD instead — the builder walked the module's own package references over the image's and the shelf's deps.json and copied everything the shared framework does not supply; it refuses BY NAME anything neither supplies, and this lane passes no --extra-refs."
# 🚨 THE BINDING IDENTITY, CHECKED (MeshWeaver#143): the expected value is the one
Expand Down Expand Up @@ -2971,6 +2986,21 @@ jobs:
"$manifest" > /dev/null \
|| { echo "::error::the container-built bundle carries a non-MeshWeaver assembly with NO builder provenance (not in the builder's module-libs.txt) — its closure cannot be accounted for, so the pack fails rather than landing a module that faults at first use"; exit 1; }
echo "container path: closure is entry + module-owned MeshWeaver siblings + the manifested private closure, as recorded"
# 🚨 The same accountability for the NATIVE section (MeshWeaver#4126). A loadable
# binary in a bundle is the one payload a consumer EXECUTES, so it may be there only
# with builder provenance: module-natives.txt names every payload the builder derived
# from the image's and the shelf's deps.json. Anything else has no accountable origin
# — the 2026-08-19/20 shape, one step more dangerous.
native_manifest="$PACKDIR/module-natives.txt"
allowed_natives="$( [ -f "$native_manifest" ] && tr '\n' ';' < "$native_manifest" || true )"
jq -e --arg allowed "$allowed_natives" \
'($allowed | split(";") | map(select(length > 0))) as $declared
| [(.module.nativeAssets // [])[]
| select(. as $n | $declared | index($n) | not)]
| length == 0' \
"$manifest" > /dev/null \
|| { echo "::error::the container-built bundle carries a NATIVE payload with NO builder provenance (not in the builder's module-natives.txt) — a loadable binary whose origin this lane cannot account for, so the pack fails rather than shipping it"; exit 1; }
echo "container path: $(jq '(.module.nativeAssets // []) | length' "$manifest") native payload(s) declared, every one with builder provenance"
# 🚨 The STATIC-ASSET half, asserted — because its absence is the one failure with no
# symptom at any later gate: the publish 200s, the module lands and loads, every page
# renders, only UNSTYLED (#2221's exact signature; PluginBundlePublishAssetsTest names
Expand Down
Loading
Loading