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
49 changes: 27 additions & 22 deletions .github/workflows/bundle-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ jobs:
bundle-analysis:
name: Bundle Size Analysis
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false

permissions:
contents: read
Expand All @@ -37,46 +36,52 @@ jobs:
- name: Build packages
run: pnpm build:packages

- name: Run bundle analysis
- name: Run bundle analysis on PR
run: pnpm bundle:analyze

- name: Save PR bundle report
- name: Create temp directory and save PR bundle report
run: |
mkdir -p ./bundle-analysis-temp
cp ./bundle-analyzer/bundle-reports/latest.json ./bundle-analysis-temp/pr-report.json
mkdir -p /tmp/bundle-analysis
cp ./bundle-analyzer/bundle-reports/latest.json /tmp/bundle-analysis/pr-current.json

- name: Checkout target branch
run: |
git fetch origin ${{ github.base_ref }}
git checkout origin/${{ github.base_ref }}

- name: Install dependencies (target branch)
run: pnpm install --frozen-lockfile

- name: Build packages (target branch)
run: pnpm build:packages
- name: Get target branch bundle report
id: check-target-bundle
run: |
if git cat-file -e HEAD:bundle-analyzer/bundle-reports/latest.json 2>/dev/null; then
echo "exists=true" >> $GITHUB_OUTPUT
git show HEAD:bundle-analyzer/bundle-reports/latest.json > /tmp/bundle-analysis/target-baseline.json
else
echo "exists=false" >> $GITHUB_OUTPUT
fi

- name: Run bundle analysis (target branch)
run: pnpm bundle:analyze
continue-on-error: true
- name: Checkout PR branch
run: git checkout ${{ github.head_ref }}

- name: Save target branch bundle report
- name: Generate target baseline if missing
if: steps.check-target-bundle.outputs.exists == 'false'
run: |
git checkout origin/${{ github.base_ref }}
pnpm install --frozen-lockfile
pnpm build:packages
pnpm bundle:analyze || echo "Bundle analysis failed"
if [ -f "./bundle-analyzer/bundle-reports/latest.json" ]; then
cp ./bundle-analyzer/bundle-reports/latest.json ./bundle-analysis-temp/target-report.json
cp ./bundle-analyzer/bundle-reports/latest.json /tmp/bundle-analysis/target-baseline.json
else
echo '{"timestamp":"","results":[]}' > ./bundle-analysis-temp/target-report.json
echo '{"timestamp":"","results":[]}' > /tmp/bundle-analysis/target-baseline.json
fi

- name: Checkout PR again
run: git checkout ${{ github.head_ref }}
git checkout ${{ github.head_ref }}

- name: Generate bundle comparison comment
id: bundle-comment
run: |
node ./bundle-analyzer/generate-pr-comment.js \
./bundle-analysis-temp/pr-report.json \
./bundle-analysis-temp/target-report.json
/tmp/bundle-analysis/pr-current.json \
/tmp/bundle-analysis/target-baseline.json

- name: Find existing comment
uses: peter-evans/find-comment@a723a15ad60cf9419c01a6b8c9548ddd15e79531
Expand All @@ -91,5 +96,5 @@ jobs:
with:
comment-id: ${{ steps.existing-comment.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body-path: ./bundle-analysis-temp/comment.md
body-path: /tmp/bundle-analysis/comment.md
edit-mode: replace
21 changes: 19 additions & 2 deletions .github/workflows/update-bundle-baseline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ on:
push:
branches:
- main
paths:
- bundle-analyzer/**
- packages/bits-ui/**

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
Expand Down Expand Up @@ -55,8 +59,21 @@ jobs:
echo "No changes to latest.json - skipping PR"
echo "changed=false" >> $GITHUB_OUTPUT
else
echo "latest.json changed - creating/updating PR"
echo "changed=true" >> $GITHUB_OUTPUT
# Check if only timestamp changed
OLD_FILE=$(git show HEAD:bundle-analyzer/bundle-reports/latest.json)
NEW_FILE=$(cat ./bundle-analyzer/bundle-reports/latest.json)

# Remove timestamp fields and compare
OLD_NO_TIMESTAMP=$(echo "$OLD_FILE" | jq 'del(.timestamp)')
NEW_NO_TIMESTAMP=$(echo "$NEW_FILE" | jq 'del(.timestamp)')

if [ "$OLD_NO_TIMESTAMP" = "$NEW_NO_TIMESTAMP" ]; then
echo "Only timestamp changed - skipping PR"
echo "changed=false" >> $GITHUB_OUTPUT
else
echo "Bundle data changed - creating/updating PR"
echo "changed=true" >> $GITHUB_OUTPUT
fi
fi
else
# File doesn't exist in git yet (first time) - create PR
Expand Down
93 changes: 11 additions & 82 deletions bundle-analyzer/generate-pr-comment.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,13 @@ function formatBytes(bytes) {
return (bytes / 1024).toFixed(2);
}

function formatPercent(percent) {
if (!isFinite(percent)) return "0.0";
return Math.abs(percent).toFixed(1);
}

function formatDiff(diff, showSign = true) {
const sign = showSign && diff > 0 ? "+" : "";
return `${sign}${formatBytes(diff)}`;
}

function getStatusIcon(status, sizeDiff) {
switch (status) {
case "added":
return "✨";
case "removed":
return "❌";
case "changed":
return sizeDiff > 0 ? "📈" : sizeDiff < 0 ? "📉" : "➡️";
case "unchanged":
Expand All @@ -71,33 +62,7 @@ function analyzeBundleChanges(prReport, targetReport) {
const prResult = prMap.get(component);
const targetResult = targetMap.get(component);

if (!prResult && targetResult) {
changes.push({
component,
status: "removed",
sizeDiff: -targetResult.size,
gzipSizeDiff: -targetResult.gzipSize,
sizePercent: -100,
gzipSizePercent: -100,
currentSize: 0,
currentGzipSize: 0,
targetSize: targetResult.size,
targetGzipSize: targetResult.gzipSize,
});
} else if (prResult && !targetResult) {
changes.push({
component,
status: "added",
sizeDiff: prResult.size,
gzipSizeDiff: prResult.gzipSize,
sizePercent: Infinity,
gzipSizePercent: Infinity,
currentSize: prResult.size,
currentGzipSize: prResult.gzipSize,
targetSize: 0,
targetGzipSize: 0,
});
} else if (prResult && targetResult) {
if (prResult && targetResult) {
const sizeDiff = prResult.size - targetResult.size;
const gzipSizeDiff = prResult.gzipSize - targetResult.gzipSize;
const sizePercent = targetResult.size > 0 ? (sizeDiff / targetResult.size) * 100 : 0;
Expand Down Expand Up @@ -136,12 +101,12 @@ function generateComment(changes, hasBaseline = true) {

if (changes.length > 0) {
comment += "### 📊 Current Component Sizes\n\n";
comment += "| Component | Size | Gzipped |\n";
comment += "|-----------|------|----------|\n";
comment += "| Component | Size |\n";
comment += "|-----------|------|\n";

const sortedComponents = changes.sort((a, b) => a.component.localeCompare(b.component));
for (const comp of sortedComponents) {
comment += `| \`${comp.component}\` | ${formatBytes(comp.currentSize)} KB | ${formatBytes(comp.currentGzipSize)} KB |\n`;
comment += `| \`${comp.component}\` | ${formatBytes(comp.currentSize)} KB <sub>gzipped: (${formatBytes(comp.currentGzipSize)} KB)</sub> |\n`;
}
comment += "\n";
}
Expand All @@ -154,59 +119,23 @@ function generateComment(changes, hasBaseline = true) {
return comment;
}

// Summary stats
const totalSizeDiff = changedComponents.reduce((sum, c) => sum + c.sizeDiff, 0);
const totalGzipDiff = changedComponents.reduce((sum, c) => sum + c.gzipSizeDiff, 0);

const summaryIcon = totalSizeDiff > 0 ? "📈" : totalSizeDiff < 0 ? "📉" : "➡️";
comment += `### ${summaryIcon} Summary\n\n`;
comment += `**Total bundle size change**: ${formatDiff(totalSizeDiff)} KB (${formatDiff(totalGzipDiff)} KB gzipped)\n\n`;

// Group changes by status
const addedComponents = changedComponents.filter((c) => c.status === "added");
const removedComponents = changedComponents.filter((c) => c.status === "removed");
const modifiedComponents = changedComponents.filter((c) => c.status === "changed");

if (addedComponents.length > 0) {
comment += "### ✨ New Components\n\n";
comment += "| Component | Size | Gzipped |\n";
comment += "|-----------|------|----------|\n";
for (const comp of addedComponents) {
comment += `| \`${comp.component}\` | +${formatBytes(comp.currentSize)} KB | +${formatBytes(comp.currentGzipSize)} KB |\n`;
}
comment += "\n";
}

if (removedComponents.length > 0) {
comment += "### ❌ Removed Components\n\n";
comment += "| Component | Size | Gzipped |\n";
comment += "|-----------|------|----------|\n";
for (const comp of removedComponents) {
comment += `| \`${comp.component}\` | -${formatBytes(comp.targetSize)} KB | -${formatBytes(comp.targetGzipSize)} KB |\n`;
}
comment += "\n";
}

if (modifiedComponents.length > 0) {
comment += "### 📊 Modified Components\n\n";
comment += "| Component | Size Change | Gzipped Change | % Change |\n";
comment += "|-----------|-------------|----------------|----------|\n";
comment += "| Component | Current | New | Change |\n";
comment += "|-----------|---------|-----|--------|\n";

for (const comp of modifiedComponents) {
const icon = getStatusIcon(comp.status, comp.sizeDiff);
const sizeChange = `${formatDiff(comp.sizeDiff)} KB`;
const gzipChange = `${formatDiff(comp.gzipSizeDiff)} KB`;
const percentChange =
comp.sizeDiff !== 0
? `${comp.sizeDiff > 0 ? "+" : ""}${formatPercent(comp.sizePercent)}%`
: "0.0%";

comment += `| ${icon} \`${comp.component}\` | ${sizeChange} | ${gzipChange} | ${percentChange} |\n`;
const currentSize = `${formatBytes(comp.targetSize)} KB <sub>gzipped: (${formatBytes(comp.targetGzipSize)} KB)</sub>`;
const newSize = `${formatBytes(comp.currentSize)} KB <sub>gzipped: (${formatBytes(comp.currentGzipSize)} KB)</sub>`;
const sizeChange = `${formatDiff(comp.sizeDiff)} KB <sub>gzipped: (${formatDiff(comp.gzipSizeDiff)} KB)</sub>`;
comment += `| ${icon} \`${comp.component}\` | ${currentSize} | ${newSize} | ${sizeChange} |\n`;
}
comment += "\n";
}

// Add helpful context
comment += "---\n\n";
comment += "<details>\n";
comment += "<summary>📋 Understanding Bundle Analysis</summary>\n\n";
Expand Down Expand Up @@ -260,7 +189,7 @@ function main() {

const comment = generateComment(changes, hasBaseline);

writeFileSync("./bundle-analysis-temp/comment.md", comment);
writeFileSync("/tmp/bundle-analysis/comment.md", comment);

console.log("✅ Bundle analysis comment generated successfully");
if (hasBaseline) {
Expand Down