feat(emception): shrink npm tarball below 200 MB #948
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: Main CI/CD Workflow | |
| on: | |
| push: | |
| branches: | |
| - main | |
| pull_request: | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: main-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| jobs: | |
| detect-changes: | |
| name: Detect changed folders | |
| runs-on: ubuntu-latest | |
| outputs: | |
| api: ${{ steps.filter.outputs.api }} | |
| web: ${{ steps.filter.outputs.web }} | |
| emception: ${{ steps.filter.outputs.emception }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Detect path changes | |
| id: filter | |
| uses: dorny/paths-filter@v3 | |
| with: | |
| filters: | | |
| api: | |
| - 'apps/api/**' | |
| web: | |
| - 'apps/web/**' | |
| emception: | |
| - 'tools/emception/**' | |
| # ── Validation: API build (independent, no npm deps) ─────────────────────── | |
| build-api: | |
| name: Build API | |
| needs: detect-changes | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| fetch-tags: true | |
| lfs: false | |
| submodules: true | |
| - name: Setup .NET 9 | |
| if: needs.detect-changes.outputs.api == 'true' | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: '9.0.x' | |
| - name: Build apps/api | |
| if: needs.detect-changes.outputs.api == 'true' | |
| run: cd apps/api && dotnet build "GameGuild.Production.sln" -c Release | |
| - name: Skip apps/api build (no changes) | |
| if: needs.detect-changes.outputs.api != 'true' | |
| run: echo "No changes in apps/api/**; skipping API build." | |
| # ── Validation: Web build (dotnet-wasm + Next.js) ────────────────────────── | |
| build-web: | |
| name: Build Web | |
| needs: detect-changes | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| fetch-tags: true | |
| lfs: false | |
| submodules: true | |
| - uses: actions/setup-node@v6 | |
| if: needs.detect-changes.outputs.web == 'true' | |
| with: | |
| node-version: '22' | |
| package-manager-cache: false | |
| - name: Setup .NET 9 | |
| if: needs.detect-changes.outputs.web == 'true' | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: '9.0.x' | |
| - name: Install monorepo dependencies | |
| if: needs.detect-changes.outputs.web == 'true' | |
| run: npm install --no-package-lock --include=optional --ignore-scripts | |
| - name: Build packages/dotnet-wasm | |
| if: needs.detect-changes.outputs.web == 'true' | |
| run: npm run setup -w @game-guild/dotnet-wasm | |
| - name: Build apps/web | |
| if: needs.detect-changes.outputs.web == 'true' | |
| run: npm run build -w apps/web | |
| - name: Skip apps/web build (no changes) | |
| if: needs.detect-changes.outputs.web != 'true' | |
| run: echo "No changes in apps/web/**; skipping Web build." | |
| # ── Gource visualization (independent, uploads artifact) ─────────────────── | |
| gource: | |
| name: Gource Visualization | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| fetch-tags: true | |
| lfs: false | |
| submodules: true | |
| - name: Install Linux native dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y --fix-missing ffmpeg gource xvfb | |
| - name: Make gource script executable | |
| run: chmod +x ./contributors/gource.sh | |
| - name: Run headless gource | |
| run: xvfb-run -a ./contributors/gource.sh | |
| - name: Upload gource artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: gource | |
| path: | | |
| contributors/gource.mp4 | |
| contributors/gource.gif | |
| if-no-files-found: warn | |
| # ── Emception build (critical path, ~30 min on cold cache) ───────────────── | |
| build-emception: | |
| name: Build Emception | |
| needs: detect-changes | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 360 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| fetch-tags: true | |
| lfs: false | |
| submodules: true | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: '22' | |
| package-manager-cache: false | |
| - name: Install Linux native dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y --fix-missing cmake ninja-build build-essential pkg-config brotli | |
| - name: Free disk space and add swap | |
| shell: bash | |
| run: | | |
| sudo rm -rf /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL || true | |
| sudo docker image prune --all --force || true | |
| df -h / | |
| if [[ -z "${ACT:-}" ]]; then | |
| sudo swapoff -a || true | |
| sudo rm -f /mnt/swapfile /swapfile || true | |
| sudo fallocate -l 24G /mnt/swapfile | |
| sudo chmod 600 /mnt/swapfile | |
| sudo mkswap /mnt/swapfile | |
| sudo swapon /mnt/swapfile | |
| echo 80 | sudo tee /proc/sys/vm/swappiness | |
| echo 1 | sudo tee /proc/sys/vm/overcommit_memory | |
| else | |
| echo "ℹ️ Skipping swap configuration (act/container environment lacks CAP_SYS_ADMIN)" | |
| fi | |
| free -h | |
| - name: Install monorepo dependencies | |
| run: npm install --no-package-lock --include=optional --ignore-scripts | |
| # Tier 1: Caching ─────────────────────────────────────────────────────── | |
| - name: Cache emsdk | |
| uses: actions/cache@v4 | |
| id: cache-emsdk | |
| with: | |
| path: tools/emception/tools/emsdk | |
| key: emsdk-${{ runner.os }}-${{ hashFiles('tools/emception/scripts/setup-emsdk.ts') }} | |
| - name: Cache userland (toolchain sources + compiled outputs) | |
| uses: actions/cache@v4 | |
| id: cache-userland | |
| with: | |
| path: tools/emception/userland | |
| key: userland-${{ runner.os }}-${{ hashFiles('tools/emception/scripts/build-*.ts', 'tools/emception/scripts/setup-emsdk.ts') }} | |
| restore-keys: | | |
| userland-${{ runner.os }}- | |
| - name: Cache sysroot and CDN bundles | |
| uses: actions/cache@v4 | |
| id: cache-cdn | |
| with: | |
| path: | | |
| tools/emception/sysroot | |
| tools/emception/build | |
| tools/emception/public/cdn | |
| key: cdn-${{ runner.os }}-${{ hashFiles('tools/emception/scripts/**') }} | |
| restore-keys: | | |
| cdn-${{ runner.os }}- | |
| - name: Cache emception deploy artifacts | |
| uses: actions/cache@v4 | |
| id: cache-emception-artifacts | |
| with: | |
| path: | | |
| tools/emception/apps/ide-react/dist | |
| tools/emception/public/cdn | |
| tools/emception/dist | |
| tools/emception/packages/core/dist | |
| tools/emception/packages/core/cdn | |
| tools/emception/packages/browser/dist | |
| tools/emception/packages/xterm/dist | |
| tools/emception/packages/react/dist | |
| tools/emception/packages/webcomponent/dist | |
| tools/emception/packages/ide/dist | |
| key: emception-artifacts-${{ runner.os }}-${{ hashFiles('tools/emception/package.json', 'tools/emception/tsconfig*.json', 'tools/emception/scripts/**/*.ts', 'tools/emception/packages/**/package.json', 'tools/emception/packages/**/tsconfig*.json', 'tools/emception/apps/ide-react/package.json', 'tools/emception/apps/ide-react/src/**', 'tools/emception/apps/ide-react/vite.config.*') }} | |
| restore-keys: | | |
| emception-artifacts-${{ runner.os }}- | |
| # Build (skipped entirely on full CDN cache hit) ───────────────────────── | |
| - name: Build tools/emception (full) | |
| if: (needs.detect-changes.outputs.emception == 'true' || steps.cache-emception-artifacts.outputs.cache-hit != 'true') && steps.cache-cdn.outputs.cache-hit != 'true' | |
| run: cd tools/emception && npm run build:all | |
| # Always rebuild IDE package (fast TypeScript-only, no emsdk needed) ──── | |
| - name: Build IDE package dependencies (core/xterm/browser) | |
| if: (needs.detect-changes.outputs.emception == 'true' || steps.cache-emception-artifacts.outputs.cache-hit != 'true') && steps.cache-cdn.outputs.cache-hit == 'true' | |
| run: | | |
| cd tools/emception | |
| npm run build:packages:core | |
| npm run build:packages:xterm | |
| npm run build:packages:browser | |
| - name: Build @gameguild/emception-ide package | |
| if: needs.detect-changes.outputs.emception == 'true' || steps.cache-emception-artifacts.outputs.cache-hit != 'true' | |
| run: cd tools/emception && npm run build:packages:ide | |
| - name: Sync CDN to demo app | |
| if: needs.detect-changes.outputs.emception == 'true' || steps.cache-emception-artifacts.outputs.cache-hit != 'true' | |
| run: node scripts/sync-emception-cdn.mjs tools/emception/apps/ide-react | |
| - name: Stage CDN into emception package | |
| if: needs.detect-changes.outputs.emception == 'true' || steps.cache-cdn.outputs.cache-hit == 'true' || steps.cache-emception-artifacts.outputs.cache-hit != 'true' | |
| run: cd tools/emception && npm run stage:core:cdn | |
| - name: Build tools/emception/apps/ide-react | |
| if: needs.detect-changes.outputs.emception == 'true' || steps.cache-emception-artifacts.outputs.cache-hit != 'true' | |
| run: cd tools/emception/apps/ide-react && npm run build --ignore-scripts | |
| env: | |
| VITE_BASE: /gameguild/ | |
| - name: Upload emception artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: emception | |
| path: | | |
| tools/emception/apps/ide-react/dist | |
| tools/emception/public/cdn | |
| tools/emception/dist | |
| tools/emception/packages/core/dist | |
| tools/emception/packages/core/cdn | |
| tools/emception/packages/browser/dist | |
| tools/emception/packages/xterm/dist | |
| tools/emception/packages/react/dist | |
| tools/emception/packages/webcomponent/dist | |
| tools/emception/packages/ide/dist | |
| # ── Deploy: assemble pages + release (needs all jobs above) ──────────────── | |
| deploy: | |
| name: Deploy | |
| needs: [build-api, build-web, build-emception, gource] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: write | |
| pages: write | |
| id-token: write | |
| issues: write | |
| pull-requests: write | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| PAGES_ARTIFACT_NAME: github-pages-${{ github.run_id }}-${{ github.run_attempt }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| fetch-tags: true | |
| lfs: false | |
| submodules: true | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: '22' | |
| package-manager-cache: false | |
| - name: Install monorepo dependencies | |
| run: npm install --no-package-lock --include=optional --ignore-scripts | |
| - name: Download emception artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: emception | |
| path: _artifacts/emception | |
| - name: Restore emception build outputs to workspace | |
| run: | | |
| if [[ -d "_artifacts/emception/tools/emception" ]]; then | |
| echo "Using nested artifact layout" | |
| rsync -a _artifacts/emception/tools/emception/ tools/emception/ | |
| else | |
| echo "Using flattened artifact layout" | |
| rsync -a _artifacts/emception/ tools/emception/ | |
| fi | |
| - name: Download gource artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: gource | |
| path: contributors | |
| continue-on-error: true | |
| - name: Assemble GitHub Pages directory | |
| run: | | |
| rm -rf .pages | |
| mkdir -p .pages/contributors | |
| cp -R tools/emception/apps/ide-react/dist/* .pages/ | |
| rm -rf .pages/cdn | |
| cp -R tools/emception/public/cdn .pages/cdn | |
| cp -R contributors/* .pages/contributors/ | |
| cp contributors/gource.mp4 .pages/contributors/gource.mp4 || echo "::warning::gource.mp4 not found, skipping" | |
| cp contributors/gource.gif .pages/contributors/gource.gif || echo "::warning::gource.gif not found, skipping" | |
| find .pages -name '.gitignore' -delete | |
| echo "Pages directory summary:" | |
| echo " Total size: $(du -sh .pages | cut -f1)" | |
| echo " CDN size: $(du -sh .pages/cdn 2>/dev/null | cut -f1 || echo 'MISSING')" | |
| ls -la .pages/ | |
| - name: Upload pages artifact | |
| if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/main' }} | |
| uses: actions/upload-pages-artifact@v3 | |
| with: | |
| name: ${{ env.PAGES_ARTIFACT_NAME }} | |
| path: .pages | |
| - name: Deploy to GitHub Pages | |
| if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/main' }} | |
| id: deployment | |
| uses: actions/deploy-pages@v4 | |
| with: | |
| artifact_name: ${{ env.PAGES_ARTIFACT_NAME }} | |
| - name: Ensure git safe directory | |
| if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/main' }} | |
| run: git config --global --add safe.directory '*' | |
| - name: Fetch all tags | |
| if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/main' }} | |
| run: | | |
| git fetch --tags --force | |
| echo "Latest tags:" | |
| git tag --sort=-v:refname | head -5 | |
| - name: Semantic Release (root) | |
| if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/main' }} | |
| id: semantic | |
| uses: cycjimmy/semantic-release-action@v4 | |
| with: | |
| semantic_version: 23 | |
| extra_plugins: | | |
| @semantic-release/changelog@6 | |
| @semantic-release/git@10 | |
| @semantic-release/exec@6 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Verify npm is available | |
| if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/main' }} | |
| run: npm --version | |
| - name: Configure npm registry for npm token publishing | |
| if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/main' }} | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '22' | |
| registry-url: 'https://registry.npmjs.org' | |
| - name: Publish emception packages to npm | |
| if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/main' && steps.semantic.outputs.new_release_published == 'true' }} | |
| working-directory: tools/emception | |
| run: | | |
| PACKAGE_DIRS=( | |
| "packages/core" | |
| "packages/xterm" | |
| "packages/browser" | |
| "packages/react" | |
| "packages/webcomponent" | |
| "packages/ide" | |
| ) | |
| for DIR in "${PACKAGE_DIRS[@]}"; do | |
| NAME="$(node -p "require('./${DIR}/package.json').name")" | |
| VERSION="$(node -p "require('./${DIR}/package.json').version")" | |
| if npm view "${NAME}@${VERSION}" version >/dev/null 2>&1; then | |
| echo "✅ ${NAME}@${VERSION} already published — skipping" | |
| continue | |
| fi | |
| echo "📦 Publishing ${NAME}@${VERSION}" | |
| npm publish --workspace="${NAME}" --access public | |
| done | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} |