π§© WASM Assets #72
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: π§± Build WASM Assets | |
| on: | |
| workflow_call: | |
| inputs: | |
| force: | |
| description: 'Force rebuild (ignore cache)' | |
| required: false | |
| type: boolean | |
| default: false | |
| build-yoga: | |
| description: 'Build Yoga Layout WASM' | |
| required: false | |
| type: boolean | |
| default: true | |
| build-models: | |
| description: 'Build AI Models' | |
| required: false | |
| type: boolean | |
| default: true | |
| models-quant-level: | |
| description: 'AI Models quantization level' | |
| required: false | |
| type: string | |
| default: 'INT4' | |
| build-onnx: | |
| description: 'Build ONNX Runtime WASM' | |
| required: false | |
| type: boolean | |
| default: true | |
| workflow_dispatch: | |
| inputs: | |
| force: | |
| description: 'Force rebuild (ignore cache)' | |
| required: false | |
| type: boolean | |
| default: false | |
| build-yoga: | |
| description: 'Build Yoga Layout WASM' | |
| required: false | |
| type: boolean | |
| default: true | |
| build-models: | |
| description: 'Build AI Models' | |
| required: false | |
| type: boolean | |
| default: true | |
| models-quant-level: | |
| description: 'AI Models quantization level' | |
| required: false | |
| type: choice | |
| options: | |
| - INT4 | |
| - INT8 | |
| default: INT4 | |
| build-onnx: | |
| description: 'Build ONNX Runtime WASM' | |
| required: false | |
| type: boolean | |
| default: true | |
| # Removed push/pull_request triggers to avoid runner contention. | |
| # WASM assets are cached and used by socketbin builds. | |
| # Run manually via workflow_dispatch when WASM sources change. | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: build-wasm-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| build-yoga-layout: | |
| name: π§ Build Yoga Layout WASM | |
| if: ${{ inputs.build-yoga != false }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 | |
| with: | |
| node-version: 22 | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@9fd676a19091d4595eefd76e4bd31c97133911f1 # v4.2.0 | |
| with: | |
| version: ^10.16.0 | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Generate yoga build cache key | |
| id: yoga-cache-key | |
| run: | | |
| # Extract Yoga version from package.json (package version matches Yoga Layout release). | |
| YOGA_VERSION=$(node -p "require('./packages/yoga-layout/package.json').version") | |
| # Hash includes source files and package.json. | |
| HASH=$(find packages/yoga-layout -type f \( -name "*.cpp" -o -name "*.h" -o -name "*.mjs" -o -name "CMakeLists.txt" -o -name "package.json" \) | sort | xargs sha256sum | sha256sum | cut -d' ' -f1) | |
| FULL_HASH="${HASH}-${YOGA_VERSION}" | |
| echo "hash=$FULL_HASH" >> $GITHUB_OUTPUT | |
| echo "Yoga Layout version: v$YOGA_VERSION" | |
| - name: Restore yoga output cache | |
| id: yoga-cache | |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | |
| with: | |
| path: packages/yoga-layout/build/wasm | |
| key: yoga-wasm-${{ steps.yoga-cache-key.outputs.hash }} | |
| restore-keys: yoga-wasm- | |
| enableCrossOsArchive: true | |
| - name: Restore yoga build cache | |
| id: yoga-build-cache | |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | |
| with: | |
| path: packages/yoga-layout/build | |
| key: yoga-build-${{ steps.yoga-cache-key.outputs.hash }} | |
| restore-keys: | | |
| yoga-build- | |
| - name: Verify cached artifacts | |
| id: yoga-cache-valid | |
| run: | | |
| if [ -f "packages/yoga-layout/build/wasm/yoga.wasm" ] && [ -f "packages/yoga-layout/build/wasm/yoga.js" ]; then | |
| echo "valid=true" >> $GITHUB_OUTPUT | |
| echo "Cache hit: artifacts found" | |
| ls -lh packages/yoga-layout/build/wasm/ | |
| else | |
| echo "valid=false" >> $GITHUB_OUTPUT | |
| echo "Cache miss or incomplete: forcing rebuild" | |
| ls -lh packages/yoga-layout/build/wasm/ 2>/dev/null || echo "Directory does not exist" | |
| fi | |
| - name: Install Emscripten | |
| if: steps.yoga-cache-valid.outputs.valid != 'true' || inputs.force | |
| run: | | |
| echo "::group::Installing Emscripten" | |
| git clone https://github.com/emscripten-core/emsdk.git | |
| cd emsdk | |
| ./emsdk install latest | |
| ./emsdk activate latest | |
| echo "::endgroup::" | |
| - name: Build Yoga Layout WASM | |
| if: steps.yoga-cache-valid.outputs.valid != 'true' || inputs.force | |
| run: | | |
| echo "::group::Building Yoga Layout WASM" | |
| source emsdk/emsdk_env.sh | |
| if [ "${{ inputs.force }}" = "true" ]; then | |
| pnpm --filter @socketsecurity/yoga-layout run build -- --force | |
| else | |
| pnpm --filter @socketsecurity/yoga-layout run build | |
| fi | |
| echo "Build exit code: $?" | |
| echo "Checking for build artifacts..." | |
| ls -lh packages/yoga-layout/build/wasm/ || echo "wasm directory not found" | |
| ls -lh packages/yoga-layout/build/cmake/ || echo "cmake directory not found" | |
| echo "::endgroup::" | |
| - name: Verify build artifacts | |
| run: | | |
| echo "=== Yoga Layout Build Artifacts ===" | |
| if [ ! -f "packages/yoga-layout/build/wasm/yoga.wasm" ] || [ ! -f "packages/yoga-layout/build/wasm/yoga.js" ]; then | |
| echo "ERROR: Required WASM artifacts not found!" | |
| ls -lh packages/yoga-layout/build/wasm/ || echo "Directory does not exist" | |
| exit 1 | |
| fi | |
| ls -lh packages/yoga-layout/build/wasm/ | |
| echo "" | |
| echo "yoga.wasm size: $(du -h packages/yoga-layout/build/wasm/yoga.wasm | cut -f1)" | |
| echo "yoga.js size: $(du -h packages/yoga-layout/build/wasm/yoga.js | cut -f1)" | |
| - name: Upload yoga artifacts | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | |
| with: | |
| name: yoga-wasm | |
| path: packages/yoga-layout/build/wasm/ | |
| retention-days: 7 | |
| build-models: | |
| name: π€ Build AI Models | |
| if: ${{ inputs.build-models != false }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 | |
| with: | |
| node-version: 22 | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@9fd676a19091d4595eefd76e4bd31c97133911f1 # v4.2.0 | |
| with: | |
| version: ^10.16.0 | |
| - name: Setup Python | |
| uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 | |
| with: | |
| python-version: '3.11' | |
| - name: Cache pip packages | |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | |
| with: | |
| path: ~/.cache/pip | |
| key: pip-${{ runner.os }}-py3.11-${{ hashFiles('**/requirements*.txt') }}-onnx-torch | |
| restore-keys: | | |
| pip-${{ runner.os }}-py3.11- | |
| pip-${{ runner.os }}- | |
| - name: Install Python dependencies | |
| run: | | |
| echo "::group::Installing Python ML dependencies" | |
| pip install torch transformers | |
| pip install "onnx>=1.15.0" "onnxruntime>=1.20.0" | |
| pip install "optimum[onnxruntime]" | |
| echo "Installed packages:" | |
| pip list | grep -E "(onnx|optimum|torch)" | |
| echo "" | |
| python3 -c "import onnxruntime; print(f'ONNX Runtime version: {onnxruntime.__version__}')" | |
| python3 -c "from onnxruntime.quantization.matmul_nbits_quantizer import MatMulNBitsQuantizer, RTNWeightOnlyQuantConfig; print('β INT4 quantization available')" | |
| echo "::endgroup::" | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Generate models cache key | |
| id: models-cache-key | |
| run: | | |
| # Extract models version from package.json. | |
| MODELS_VERSION=$(node -p "require('./packages/models/package.json').version") | |
| # Hash includes script files and package.json. | |
| HASH=$(find packages/models -type f \( -name "*.mjs" -o -name "package.json" \) | sort | xargs sha256sum | sha256sum | cut -d' ' -f1) | |
| # Include quantization level in cache key. | |
| QUANT_LEVEL="${{ inputs.models-quant-level || 'INT4' }}" | |
| FULL_HASH="${HASH}-${MODELS_VERSION}-${QUANT_LEVEL}" | |
| echo "hash=$FULL_HASH" >> $GITHUB_OUTPUT | |
| echo "quant-level=${QUANT_LEVEL}" >> $GITHUB_OUTPUT | |
| echo "suffix=$(echo ${QUANT_LEVEL} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT | |
| echo "Models version: v$MODELS_VERSION" | |
| echo "Quantization level: $QUANT_LEVEL" | |
| - name: Restore models cache | |
| id: models-cache | |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | |
| with: | |
| path: packages/models/dist | |
| key: models-${{ steps.models-cache-key.outputs.hash }} | |
| restore-keys: models- | |
| enableCrossOsArchive: true | |
| - name: Verify cached artifacts | |
| id: models-cache-valid | |
| run: | | |
| SUFFIX="${{ steps.models-cache-key.outputs.suffix }}" | |
| # Check for both MiniLM and CodeT5 models. | |
| if [ -f "packages/models/dist/minilm-l6-${SUFFIX}.onnx" ] && \ | |
| [ -f "packages/models/dist/codet5-encoder-${SUFFIX}.onnx" ] && \ | |
| [ -f "packages/models/dist/codet5-decoder-${SUFFIX}.onnx" ]; then | |
| echo "valid=true" >> $GITHUB_OUTPUT | |
| echo "Cache hit: all artifacts found" | |
| ls -lh packages/models/dist/ | |
| else | |
| echo "valid=false" >> $GITHUB_OUTPUT | |
| echo "Cache miss or incomplete: forcing rebuild" | |
| echo "Expected files:" | |
| echo " - minilm-l6-${SUFFIX}.onnx" | |
| echo " - codet5-encoder-${SUFFIX}.onnx" | |
| echo " - codet5-decoder-${SUFFIX}.onnx" | |
| ls -lh packages/models/dist/ 2>/dev/null || echo "Directory does not exist" | |
| fi | |
| - name: Build AI models | |
| if: steps.models-cache-valid.outputs.valid != 'true' || inputs.force | |
| run: | | |
| QUANT_LEVEL="${{ steps.models-cache-key.outputs.quant-level }}" | |
| echo "::group::Building ${QUANT_LEVEL}-quantized AI models" | |
| # Build command with quantization flag and --all for both models. | |
| BUILD_CMD="pnpm --filter @socketsecurity/models run build -- --all" | |
| if [ "$QUANT_LEVEL" = "INT8" ]; then | |
| BUILD_CMD="$BUILD_CMD --int8" | |
| fi | |
| if [ "${{ inputs.force }}" = "true" ]; then | |
| BUILD_CMD="$BUILD_CMD --force" | |
| fi | |
| echo "Running: $BUILD_CMD" | |
| eval $BUILD_CMD | |
| echo "Build exit code: $?" | |
| echo "Checking for build artifacts..." | |
| ls -lh packages/models/dist/ || echo "dist directory not found" | |
| echo "::endgroup::" | |
| - name: Verify build artifacts | |
| run: | | |
| SUFFIX="${{ steps.models-cache-key.outputs.suffix }}" | |
| echo "=== AI Models Build Artifacts ===" | |
| if [ ! -f "packages/models/dist/minilm-l6-${SUFFIX}.onnx" ]; then | |
| echo "ERROR: minilm-l6-${SUFFIX}.onnx not found!" | |
| ls -lh packages/models/dist/ || echo "Directory does not exist" | |
| exit 1 | |
| fi | |
| if [ ! -f "packages/models/dist/codet5-encoder-${SUFFIX}.onnx" ]; then | |
| echo "ERROR: codet5-encoder-${SUFFIX}.onnx not found!" | |
| ls -lh packages/models/dist/ || echo "Directory does not exist" | |
| exit 1 | |
| fi | |
| if [ ! -f "packages/models/dist/codet5-decoder-${SUFFIX}.onnx" ]; then | |
| echo "ERROR: codet5-decoder-${SUFFIX}.onnx not found!" | |
| ls -lh packages/models/dist/ || echo "Directory does not exist" | |
| exit 1 | |
| fi | |
| ls -lh packages/models/dist/ | |
| echo "" | |
| echo "minilm-l6-${SUFFIX}.onnx size: $(du -h packages/models/dist/minilm-l6-${SUFFIX}.onnx | cut -f1)" | |
| echo "codet5-encoder-${SUFFIX}.onnx size: $(du -h packages/models/dist/codet5-encoder-${SUFFIX}.onnx | cut -f1)" | |
| echo "codet5-decoder-${SUFFIX}.onnx size: $(du -h packages/models/dist/codet5-decoder-${SUFFIX}.onnx | cut -f1)" | |
| - name: Upload models artifacts | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | |
| with: | |
| name: ai-models | |
| path: packages/models/dist/ | |
| retention-days: 7 | |
| build-onnx-runtime: | |
| name: π Build ONNX Runtime WASM | |
| if: ${{ inputs.build-onnx != false }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 90 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 | |
| with: | |
| node-version: 22 | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@9fd676a19091d4595eefd76e4bd31c97133911f1 # v4.2.0 | |
| with: | |
| version: ^10.16.0 | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Generate ONNX Runtime cache key | |
| id: onnx-cache-key | |
| run: | | |
| # Extract ONNX Runtime version from package.json (package version matches ONNX Runtime release). | |
| ONNX_VERSION=$(node -p "require('./packages/onnxruntime/package.json').version") | |
| # Hash includes script files and package.json. | |
| HASH=$(find packages/onnxruntime -type f \( -name "*.mjs" -o -name "package.json" \) | sort | xargs sha256sum | sha256sum | cut -d' ' -f1) | |
| FULL_HASH="${HASH}-${ONNX_VERSION}" | |
| echo "hash=$FULL_HASH" >> $GITHUB_OUTPUT | |
| echo "ONNX Runtime version: v$ONNX_VERSION" | |
| - name: Restore ONNX Runtime output cache | |
| id: onnx-cache | |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | |
| with: | |
| path: packages/onnxruntime/build/wasm | |
| key: onnx-runtime-${{ steps.onnx-cache-key.outputs.hash }} | |
| restore-keys: onnx-runtime- | |
| enableCrossOsArchive: true | |
| - name: Restore ONNX Runtime build cache | |
| id: onnx-build-cache | |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | |
| with: | |
| path: packages/onnxruntime/build | |
| key: onnx-runtime-build-${{ steps.onnx-cache-key.outputs.hash }} | |
| restore-keys: | | |
| onnx-runtime-build- | |
| - name: Verify cached artifacts | |
| id: onnx-cache-valid | |
| run: | | |
| if [ -f "packages/onnxruntime/build/wasm/ort-wasm-simd-threaded.wasm" ] && [ -f "packages/onnxruntime/build/wasm/ort-wasm-simd-threaded.js" ]; then | |
| echo "valid=true" >> $GITHUB_OUTPUT | |
| echo "Cache hit: artifacts found" | |
| ls -lh packages/onnxruntime/build/wasm/ | |
| else | |
| echo "valid=false" >> $GITHUB_OUTPUT | |
| echo "Cache miss or incomplete: forcing rebuild" | |
| ls -lh packages/onnxruntime/build/wasm/ 2>/dev/null || echo "Directory does not exist" | |
| fi | |
| - name: Cache Emscripten SDK | |
| id: emsdk-cache | |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | |
| with: | |
| path: emsdk | |
| key: emsdk-${{ runner.os }}-3.1.69 | |
| restore-keys: emsdk-${{ runner.os }}- | |
| - name: Install Emscripten | |
| if: (steps.onnx-cache-valid.outputs.valid != 'true' || inputs.force) && steps.emsdk-cache.outputs.cache-hit != 'true' | |
| run: | | |
| echo "::group::Installing Emscripten" | |
| git clone https://github.com/emscripten-core/emsdk.git | |
| cd emsdk | |
| ./emsdk install latest | |
| ./emsdk activate latest | |
| echo "::endgroup::" | |
| - name: Activate Emscripten (from cache) | |
| if: (steps.onnx-cache-valid.outputs.valid != 'true' || inputs.force) && steps.emsdk-cache.outputs.cache-hit == 'true' | |
| run: | | |
| cd emsdk | |
| ./emsdk activate latest | |
| - name: Build ONNX Runtime WASM | |
| if: steps.onnx-cache-valid.outputs.valid != 'true' || inputs.force | |
| run: | | |
| echo "::group::Building ONNX Runtime WASM (this will take 30-60 minutes)" | |
| source emsdk/emsdk_env.sh | |
| if [ "${{ inputs.force }}" = "true" ]; then | |
| pnpm --filter @socketsecurity/onnxruntime run build -- --force | |
| else | |
| pnpm --filter @socketsecurity/onnxruntime run build | |
| fi | |
| echo "Build exit code: $?" | |
| echo "Checking for build artifacts..." | |
| ls -lh packages/onnxruntime/build/wasm/ || echo "wasm directory not found" | |
| echo "::endgroup::" | |
| - name: Save ONNX Runtime build cache | |
| if: always() && (steps.onnx-cache-valid.outputs.valid != 'true' || inputs.force) | |
| uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | |
| with: | |
| path: packages/onnxruntime/build | |
| key: onnx-runtime-build-${{ steps.onnx-cache-key.outputs.hash }} | |
| - name: Verify build artifacts | |
| run: | | |
| echo "=== ONNX Runtime Build Artifacts ===" | |
| if [ ! -f "packages/onnxruntime/build/wasm/ort-wasm-simd-threaded.wasm" ] || [ ! -f "packages/onnxruntime/build/wasm/ort-wasm-simd-threaded.js" ]; then | |
| echo "ERROR: Required ONNX Runtime WASM artifacts not found!" | |
| ls -lh packages/onnxruntime/build/wasm/ || echo "Directory does not exist" | |
| exit 1 | |
| fi | |
| ls -lh packages/onnxruntime/build/wasm/ | |
| echo "" | |
| echo "ort-wasm-simd-threaded.wasm size: $(du -h packages/onnxruntime/build/wasm/ort-wasm-simd-threaded.wasm | cut -f1)" | |
| echo "ort-wasm-simd-threaded.js size: $(du -h packages/onnxruntime/build/wasm/ort-wasm-simd-threaded.js | cut -f1)" | |
| - name: Upload ONNX Runtime artifacts | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | |
| with: | |
| name: onnx-runtime | |
| path: packages/onnxruntime/build/wasm/ | |
| retention-days: 7 | |
| summary: | |
| name: π π§± WASM Build Summary | |
| if: always() | |
| needs: [build-yoga-layout, build-models, build-onnx-runtime] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 | |
| continue-on-error: true | |
| with: | |
| path: artifacts | |
| - name: Generate summary | |
| run: | | |
| echo "# π§± WASM Build Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "## β Build Complete" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "Selected WASM assets built successfully and cached." >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### π¦ Artifacts" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Asset | Files |" >> $GITHUB_STEP_SUMMARY | |
| echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY | |
| if [ -d "artifacts/yoga-wasm" ]; then | |
| echo "| π§ Yoga Layout | \`yoga.wasm\`, \`yoga.js\` |" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| if [ -d "artifacts/ai-models" ]; then | |
| # Detect quantization level from file names. | |
| if [ -f "artifacts/ai-models/minilm-l6-int8.onnx" ]; then | |
| echo "| π€ AI Models | \`minilm-l6-int8.onnx\` (INT8), \`codet5-encoder-int8.onnx\` (INT8), \`codet5-decoder-int8.onnx\` (INT8) |" >> $GITHUB_STEP_SUMMARY | |
| elif [ -f "artifacts/ai-models/minilm-l6-int4.onnx" ]; then | |
| echo "| π€ AI Models | \`minilm-l6-int4.onnx\` (INT4), \`codet5-encoder-int4.onnx\` (INT4), \`codet5-decoder-int4.onnx\` (INT4) |" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "| π€ AI Models | $(ls artifacts/ai-models/*.onnx 2>/dev/null | xargs -n1 basename | sed 's/^/`/;s/$/`/' | tr '\n' ',' | sed 's/,$//' || echo "No ONNX files found") |" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| fi | |
| if [ -d "artifacts/onnx-runtime" ]; then | |
| echo "| π ONNX Runtime | \`ort-wasm-simd-threaded.wasm\`, \`ort-wasm-simd-threaded.mjs\` |" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### π― Next Steps" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "- These artifacts are now cached for CI builds" >> $GITHUB_STEP_SUMMARY | |
| echo "- CLI builds will use these cached WASM assets" >> $GITHUB_STEP_SUMMARY | |
| echo "- Cache is invalidated when source files change" >> $GITHUB_STEP_SUMMARY |