chore: release packages #5966
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: Bundle Analysis | |
| on: | |
| push: | |
| branches: [main, develop] | |
| paths: | |
| - 'packages/**' | |
| - 'apps/console/**' | |
| - 'pnpm-lock.yaml' | |
| pull_request: | |
| branches: [main, develop] | |
| paths: | |
| - 'packages/**' | |
| - 'apps/console/**' | |
| - 'pnpm-lock.yaml' | |
| concurrency: | |
| group: bundle-analysis-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| jobs: | |
| bundle-analysis: | |
| name: Bundle Analysis | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| with: | |
| submodules: true | |
| - name: Enable Corepack | |
| run: corepack enable | |
| - name: Verify pnpm version | |
| run: pnpm --version | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v7 | |
| with: | |
| node-version: '22.x' | |
| cache: 'pnpm' | |
| - name: Turbo Cache | |
| uses: actions/cache@v6 | |
| with: | |
| path: .turbo/cache | |
| key: turbo-${{ runner.os }}-${{ github.sha }} | |
| restore-keys: | | |
| turbo-${{ runner.os }}- | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Build packages | |
| id: build_packages | |
| run: pnpm turbo run build --filter='./packages/*' | |
| - name: Build Console | |
| run: pnpm --filter @object-ui/console build | |
| - name: Check console performance budget | |
| id: budget | |
| run: | | |
| # Performance budget: main entry must be < 350 KB gzip | |
| # This is a realistic threshold for a full-featured enterprise app | |
| # with React, routing, UI components, and core business logic. | |
| MAX_ENTRY_GZIP_KB=350 | |
| DIST_DIR="apps/console/dist/assets" | |
| if [ ! -d "$DIST_DIR" ]; then | |
| echo "❌ Build output not found at $DIST_DIR" | |
| # Declare the outcome instead of leaving every output empty. An | |
| # empty `budget_status` used to render as a ❌ FAIL verdict | |
| # downstream (objectui#3152); the comment renderer now needs each | |
| # path to say what happened rather than infer it from silence. | |
| echo "budget_status=error" >> "$GITHUB_OUTPUT" | |
| echo "budget_message=Build output not found at $DIST_DIR" >> "$GITHUB_OUTPUT" | |
| exit 1 | |
| fi | |
| # Find the main entry chunk (index-*.js) | |
| ENTRY_FILE=$(find "$DIST_DIR" -name 'index-*.js' -not -name '*.gz' -not -name '*.br' | head -1) | |
| if [ -z "$ENTRY_FILE" ]; then | |
| echo "⚠️ Could not find main entry chunk, checking all JS files..." | |
| ENTRY_FILE=$(find "$DIST_DIR" -name '*.js' -not -name '*.gz' -not -name '*.br' | sort | head -1) | |
| fi | |
| if [ -z "$ENTRY_FILE" ]; then | |
| echo "❌ No JS files found in $DIST_DIR" | |
| echo "budget_status=error" >> "$GITHUB_OUTPUT" | |
| echo "budget_message=No JS files found in $DIST_DIR" >> "$GITHUB_OUTPUT" | |
| exit 1 | |
| fi | |
| echo "📦 Main entry file: $(basename $ENTRY_FILE)" | |
| # Calculate gzip size | |
| GZIP_BYTES=$(gzip -c "$ENTRY_FILE" | wc -c) | |
| GZIP_KB=$(awk "BEGIN {printf \"%.1f\", $GZIP_BYTES / 1024}") | |
| echo " Raw size: $(awk "BEGIN {printf \"%.1f\", $(wc -c < "$ENTRY_FILE") / 1024}") KB" | |
| echo " Gzip size: ${GZIP_KB} KB" | |
| echo " Budget: ${MAX_ENTRY_GZIP_KB} KB" | |
| echo "gzip_kb=$GZIP_KB" >> "$GITHUB_OUTPUT" | |
| echo "budget_kb=$MAX_ENTRY_GZIP_KB" >> "$GITHUB_OUTPUT" | |
| echo "entry_file=$(basename $ENTRY_FILE)" >> "$GITHUB_OUTPUT" | |
| # Check budget | |
| OVER=$(awk "BEGIN {print ($GZIP_KB > $MAX_ENTRY_GZIP_KB) ? 1 : 0}") | |
| if [ "$OVER" -eq 1 ]; then | |
| echo "" | |
| echo "❌ BUDGET EXCEEDED: Main entry is ${GZIP_KB} KB gzip (limit: ${MAX_ENTRY_GZIP_KB} KB)" | |
| echo "budget_status=fail" >> "$GITHUB_OUTPUT" | |
| exit 1 | |
| else | |
| echo "" | |
| echo "✅ Budget OK: Main entry is ${GZIP_KB} KB gzip (limit: ${MAX_ENTRY_GZIP_KB} KB)" | |
| echo "budget_status=pass" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Generate package size report | |
| id: size-report | |
| # NOT `always()`. `always()` also fires on a cancelled run, where | |
| # `packages/*/dist` is only partly populated — the report was then | |
| # emitted from that partial tree and looked complete while silently | |
| # missing 7 packages (objectui#3152). The report is only meaningful | |
| # once every package has finished building, so gate it on that. | |
| if: ${{ !cancelled() && steps.build_packages.outcome == 'success' }} | |
| run: | | |
| echo "## 📦 Bundle Size Report" > size-report.md | |
| echo "" >> size-report.md | |
| echo "| Package | Size | Gzipped |" >> size-report.md | |
| echo "|---------|------|---------|" >> size-report.md | |
| for pkg in packages/*/dist; do | |
| if [ -d "$pkg" ]; then | |
| pkg_name=$(basename $(dirname $pkg)) | |
| # Calculate sizes for main bundle files | |
| for file in "$pkg"/*.js; do | |
| if [ -f "$file" ] && [ ! -f "${file}.map" ]; then | |
| # Use portable method to get file size | |
| size=$(wc -c < "$file") | |
| size_kb=$(awk "BEGIN {printf \"%.2f\", $size/1024}") | |
| # Estimate gzipped size | |
| gzip_size=$(gzip -c "$file" | wc -c) | |
| gzip_kb=$(awk "BEGIN {printf \"%.2f\", $gzip_size/1024}") | |
| echo "| $pkg_name ($(basename $file)) | ${size_kb}KB | ${gzip_kb}KB |" >> size-report.md | |
| fi | |
| done | |
| fi | |
| done | |
| echo "" >> size-report.md | |
| echo "### Size Limits" >> size-report.md | |
| echo "- ✅ Core packages should be < 50KB gzipped" >> size-report.md | |
| echo "- ✅ Component packages should be < 100KB gzipped" >> size-report.md | |
| echo "- ⚠️ Plugin packages should be < 150KB gzipped" >> size-report.md | |
| # Rendering lives in `scripts/render-budget-comment.mjs` so it can be unit | |
| # tested (`scripts/__tests__/render-budget-comment.test.ts`) — the bug this | |
| # replaces was purely a rendering bug, and logic inlined in YAML is | |
| # untestable. Step outputs are passed through `env` rather than | |
| # interpolated into a JS string literal, so an absent output stays an | |
| # empty *variable* instead of vanishing into source text. | |
| - name: Render performance budget comment | |
| id: render_comment | |
| # `!cancelled()`, NOT `always()`: a cancelled run measured nothing, and | |
| # the superseding run posts the real verdict moments later. Commenting | |
| # on cancellation is what produced a ❌ FAIL on every PR that got a | |
| # second push (objectui#3152). | |
| if: ${{ github.event_name == 'pull_request' && !cancelled() }} | |
| env: | |
| BUDGET_STATUS: ${{ steps.budget.outputs.budget_status }} | |
| BUDGET_MESSAGE: ${{ steps.budget.outputs.budget_message }} | |
| BUDGET_GZIP_KB: ${{ steps.budget.outputs.gzip_kb }} | |
| BUDGET_LIMIT_KB: ${{ steps.budget.outputs.budget_kb }} | |
| BUDGET_ENTRY_FILE: ${{ steps.budget.outputs.entry_file }} | |
| BUDGET_STEP_OUTCOME: ${{ steps.budget.outcome }} | |
| BUILD_PACKAGES_OUTCOME: ${{ steps.build_packages.outcome }} | |
| run: node scripts/render-budget-comment.mjs > budget-comment.md | |
| - name: Comment PR with results | |
| if: ${{ github.event_name == 'pull_request' && !cancelled() }} | |
| continue-on-error: true | |
| uses: actions/github-script@v9 | |
| with: | |
| # GitHub's API intermittently rejects valid run tokens with 401 | |
| # (seen on PR #1627), so 401/403 must stay retryable here. | |
| retries: 3 | |
| retry-exempt-status-codes: 400,404,422 | |
| script: | | |
| const fs = require('fs'); | |
| if (!fs.existsSync('budget-comment.md')) { | |
| core.warning('budget-comment.md was not rendered; skipping PR comment.'); | |
| return; | |
| } | |
| const body = fs.readFileSync('budget-comment.md', 'utf8'); | |
| try { | |
| await github.rest.issues.createComment({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body | |
| }); | |
| } catch (error) { | |
| core.warning(`Could not comment on PR: ${error.message}`); | |
| await core.summary.addRaw(body).write(); | |
| } |