Skip to content
Open
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
368 changes: 368 additions & 0 deletions .github/workflows/build-size-comparison.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,368 @@
name: Build Size Comparison

on:
pull_request:
branches: [ master, develop ]

Comment on lines +1 to +6
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add explicit permissions and concurrency to harden and speed up the workflow.

  • Minimal permissions are safer, and comment posting needs issues: write.
  • Concurrency cancels stale runs for the same PR.
 name: Build Size Comparison

 on:
   pull_request:
     branches: [ master, develop ]
 
+permissions:
+  contents: read
+  issues: write
+  pull-requests: write
+
+concurrency:
+  group: build-size-${{ github.event.pull_request.number || github.ref }}
+  cancel-in-progress: true
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
name: Build Size Comparison
on:
pull_request:
branches: [ master, develop ]
name: Build Size Comparison
on:
pull_request:
branches: [ master, develop ]
permissions:
contents: read
issues: write
pull-requests: write
concurrency:
group: build-size-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
🤖 Prompt for AI Agents
.github/workflows/build-size-comparison.yml lines 1-6: add explicit
workflow-level permissions and a concurrency key to the YAML to tighten security
and cancel stale runs; set permissions to the minimal required (e.g.,
permissions: contents: read, issues: write) so the job can post comments, and
add a concurrency block (e.g., group: build-size-comparison-${{
github.event.pull_request.number }} and cancel-in-progress: true) to ensure only
the latest run for a PR proceeds.

jobs:
build-and-compare:
runs-on: ubuntu-latest
Comment on lines +8 to +9
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Skip the entire job for forked PRs to avoid secret access failures.

pull_request from forks won’t receive ${{ secrets.ACCESS_KEY }}; the job will fail at premium checkouts. Gate the job to run only for non-fork PRs.

 jobs:
   build-and-compare:
+    if: ${{ github.event.pull_request.head.repo.fork == false }}
     runs-on: ubuntu-latest
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
build-and-compare:
runs-on: ubuntu-latest
build-and-compare:
if: ${{ github.event.pull_request.head.repo.fork == false }}
runs-on: ubuntu-latest
🤖 Prompt for AI Agents
.github/workflows/build-size-comparison.yml around lines 8-9: the job currently
runs for pull_request events and will fail for forked PRs due to missing
secrets; add a job-level conditional to skip forked PRs by checking the source
repo. Modify the job to include an if condition such as: run if the event is not
a pull_request OR the pull request head repo equals the base repo (e.g. if:
github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name == github.repository) so the job
only runs for non-fork PRs (and still runs for other events).


steps:
- name: Checkout current PR
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Checkout base branch
uses: actions/checkout@v4
with:
ref: ${{ github.base_ref }}
path: base-branch

- name: Checkout Premium Repo (Current PR)
uses: actions/checkout@v4
with:
repository: 'bfintal/Stackable-Premium'
ref: 'v3'
path: 'pro__premium_only'
token: '${{ secrets.ACCESS_KEY }}'

- name: Checkout Premium Repo (Base Branch)
uses: actions/checkout@v4
with:
repository: 'bfintal/Stackable-Premium'
ref: 'v3'
path: 'base-branch/pro__premium_only'
token: '${{ secrets.ACCESS_KEY }}'

- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 14.x
cache: 'npm'

Comment on lines +39 to +44
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Update setup-node action and Node.js version (Node 14 is EOL; action v3 flagged).

Use actions/setup-node@v4 and a supported LTS (18.x or 20.x). Also cache all lockfiles via cache-dependency-path.

-    - name: Setup Node
-      uses: actions/setup-node@v3
+    - name: Setup Node
+      uses: actions/setup-node@v4
       with:
-        node-version: 14.x
-        cache: 'npm'
+        node-version: 20.x
+        cache: 'npm'
+        cache-dependency-path: |
+          package-lock.json
+          pro__premium_only/package-lock.json
+          base-branch/package-lock.json
+          base-branch/pro__premium_only/package-lock.json
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 14.x
cache: 'npm'
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'npm'
cache-dependency-path: |
package-lock.json
pro__premium_only/package-lock.json
base-branch/package-lock.json
base-branch/pro__premium_only/package-lock.json
🧰 Tools
🪛 actionlint (1.7.7)

40-40: the runner of "actions/setup-node@v3" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🤖 Prompt for AI Agents
.github/workflows/build-size-comparison.yml around lines 39-44: the workflow
uses actions/setup-node@v3 with Node 14 (EOL) and the deprecated 'cache' option;
update the action to actions/setup-node@v4 and set node-version to a supported
LTS (e.g., 18.x or 20.x), and replace the 'cache' key with
'cache-dependency-path' listing your lockfiles (e.g., package-lock.json,
yarn.lock, pnpm-lock.yaml) so all lockfiles are cached correctly.

- name: Install Dependencies (Current PR)
run: |
npm ci --legacy-peer-deps
cd pro__premium_only
npm ci --legacy-peer-deps

- name: Install Dependencies (Base Branch)
run: |
cd base-branch
npm ci --legacy-peer-deps
cd pro__premium_only
npm ci --legacy-peer-deps

- name: Build Current PR
run: |
npm run build:no-translate

- name: Build Base Branch
run: |
cd base-branch
npm run build:no-translate

- name: Create Zip Files
run: |
# Create zip for current PR
cd build/stackable
zip -r ../../current-build.zip . -x "*.map" "node_modules/*"

# Create zip for base branch
cd ../../base-branch/build/stackable
zip -r ../../../base-build.zip . -x "*.map" "node_modules/*"

# Move zip files to root directory for easier access
cd ../../../
ls -la *.zip

- name: Extract and Compare Files
run: |
# Extract both zip files
mkdir -p current-files base-files
unzip -q current-build.zip -d current-files/
unzip -q base-build.zip -d base-files/

# Create comparison script
cat > compare_files.py << 'EOF'
import os
import json
from pathlib import Path

def get_file_size(filepath):
if os.path.exists(filepath):
return os.path.getsize(filepath)
return 0

def format_bytes(bytes):
for unit in ['B', 'KB', 'MB', 'GB']:
if bytes < 1024.0:
return f"{bytes:.1f}{unit}"
bytes /= 1024.0
return f"{bytes:.1f}TB"

def compare_directories(current_dir, base_dir):
current_path = Path(current_dir)
base_path = Path(base_dir)

all_files = set()

# Get all files from both directories
for file_path in current_path.rglob('*'):
if file_path.is_file():
rel_path = file_path.relative_to(current_path)
all_files.add(str(rel_path))

for file_path in base_path.rglob('*'):
if file_path.is_file():
rel_path = file_path.relative_to(base_path)
all_files.add(str(rel_path))

changes = []
total_current = 0
total_base = 0

for file_path in sorted(all_files):
current_file = current_path / file_path
base_file = base_path / file_path

current_size = get_file_size(current_file)
base_size = get_file_size(base_file)

total_current += current_size
total_base += base_size

if current_size != base_size:
diff = current_size - base_size
if base_size > 0:
percent = (diff / base_size) * 100
else:
percent = 100 if current_size > 0 else 0

status = "🆕" if base_size == 0 else "❌" if current_size == 0 else "📝"

changes.append({
'file': file_path,
'current_size': current_size,
'base_size': base_size,
'diff': diff,
'percent': percent,
'status': status
})

return changes, total_current, total_base

# Compare files
changes, total_current, total_base = compare_directories('current-files', 'base-files')

# Create summary
total_diff = total_current - total_base
total_percent = (total_diff / total_base * 100) if total_base > 0 else 0

# Flag large changes (>10% or >100KB)
flagged_changes = [c for c in changes if abs(c['percent']) > 10 or abs(c['diff']) > 102400]

# Generate report
report = {
'total_current': total_current,
'total_base': total_base,
'total_diff': total_diff,
'total_percent': total_percent,
'changes': changes,
'flagged_changes': flagged_changes
}

with open('comparison_report.json', 'w') as f:
json.dump(report, f, indent=2)

# Print summary to console
print(f"Total size change: {format_bytes(total_diff)} ({total_percent:+.1f}%)")
print(f"Files changed: {len(changes)}")
print(f"Flagged changes: {len(flagged_changes)}")
EOF

python3 compare_files.py

Comment on lines +81 to +187
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Guard total percentage against base=0 and standardize to 2 decimals.

Current total_percent becomes 0 when total_base==0, which is misleading. Report N/A (or ∞) and format with two decimals.

-        total_diff = total_current - total_base
-        total_percent = (total_diff / total_base * 100) if total_base > 0 else 0
+        total_diff = total_current - total_base
+        if total_base > 0:
+            total_percent = (total_diff / total_base) * 100.0
+        elif total_current == 0:
+            total_percent = 0.0
+        else:
+            total_percent = None  # N/A: cannot compute percent vs 0 base
@@
-        print(f"Total size change: {format_bytes(total_diff)} ({total_percent:+.1f}%)")
+        pct_str = "N/A" if total_percent is None else f"{total_percent:+.2f}%"
+        print(f"Total size change: {format_bytes(total_diff)} ({pct_str})")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Extract and Compare Files
run: |
# Extract both zip files
mkdir -p current-files base-files
unzip -q current-build.zip -d current-files/
unzip -q base-build.zip -d base-files/
# Create comparison script
cat > compare_files.py << 'EOF'
import os
import json
from pathlib import Path
def get_file_size(filepath):
if os.path.exists(filepath):
return os.path.getsize(filepath)
return 0
def format_bytes(bytes):
for unit in ['B', 'KB', 'MB', 'GB']:
if bytes < 1024.0:
return f"{bytes:.1f}{unit}"
bytes /= 1024.0
return f"{bytes:.1f}TB"
def compare_directories(current_dir, base_dir):
current_path = Path(current_dir)
base_path = Path(base_dir)
all_files = set()
# Get all files from both directories
for file_path in current_path.rglob('*'):
if file_path.is_file():
rel_path = file_path.relative_to(current_path)
all_files.add(str(rel_path))
for file_path in base_path.rglob('*'):
if file_path.is_file():
rel_path = file_path.relative_to(base_path)
all_files.add(str(rel_path))
changes = []
total_current = 0
total_base = 0
for file_path in sorted(all_files):
current_file = current_path / file_path
base_file = base_path / file_path
current_size = get_file_size(current_file)
base_size = get_file_size(base_file)
total_current += current_size
total_base += base_size
if current_size != base_size:
diff = current_size - base_size
if base_size > 0:
percent = (diff / base_size) * 100
else:
percent = 100 if current_size > 0 else 0
status = "🆕" if base_size == 0 else "❌" if current_size == 0 else "📝"
changes.append({
'file': file_path,
'current_size': current_size,
'base_size': base_size,
'diff': diff,
'percent': percent,
'status': status
})
return changes, total_current, total_base
# Compare files
changes, total_current, total_base = compare_directories('current-files', 'base-files')
# Create summary
total_diff = total_current - total_base
total_percent = (total_diff / total_base * 100) if total_base > 0 else 0
# Flag large changes (>10% or >100KB)
flagged_changes = [c for c in changes if abs(c['percent']) > 10 or abs(c['diff']) > 102400]
# Generate report
report = {
'total_current': total_current,
'total_base': total_base,
'total_diff': total_diff,
'total_percent': total_percent,
'changes': changes,
'flagged_changes': flagged_changes
}
with open('comparison_report.json', 'w') as f:
json.dump(report, f, indent=2)
# Print summary to console
print(f"Total size change: {format_bytes(total_diff)} ({total_percent:+.1f}%)")
print(f"Files changed: {len(changes)}")
print(f"Flagged changes: {len(flagged_changes)}")
EOF
python3 compare_files.py
# Create summary
total_diff = total_current - total_base
if total_base > 0:
total_percent = (total_diff / total_base) * 100.0
elif total_current == 0:
total_percent = 0.0
else:
total_percent = None # N/A: cannot compute percent vs 0 base
# Flag large changes (>10% or >100KB)
flagged_changes = [c for c in changes if abs(c['percent']) > 10 or abs(c['diff']) > 102400]
# Generate report
report = {
'total_current': total_current,
'total_base': total_base,
'total_diff': total_diff,
'total_percent': total_percent,
'changes': changes,
'flagged_changes': flagged_changes
}
with open('comparison_report.json', 'w') as f:
json.dump(report, f, indent=2)
# Print summary to console
pct_str = "N/A" if total_percent is None else f"{total_percent:+.2f}%"
print(f"Total size change: {format_bytes(total_diff)} ({pct_str})")
print(f"Files changed: {len(changes)}")
print(f"Flagged changes: {len(flagged_changes)}")
🧰 Tools
🪛 YAMLlint (1.37.1)

[error] 87-87: trailing spaces

(trailing-spaces)


[error] 109-109: trailing spaces

(trailing-spaces)


[error] 111-111: trailing spaces

(trailing-spaces)


[error] 117-117: trailing spaces

(trailing-spaces)


[error] 122-122: trailing spaces

(trailing-spaces)


[error] 126-126: trailing spaces

(trailing-spaces)


[error] 130-130: trailing spaces

(trailing-spaces)


[error] 133-133: trailing spaces

(trailing-spaces)


[error] 136-136: trailing spaces

(trailing-spaces)


[error] 143-143: trailing spaces

(trailing-spaces)


[error] 145-145: trailing spaces

(trailing-spaces)


[error] 154-154: trailing spaces

(trailing-spaces)


[error] 159-159: trailing spaces

(trailing-spaces)


[error] 163-163: trailing spaces

(trailing-spaces)


[error] 166-166: trailing spaces

(trailing-spaces)


[error] 176-176: trailing spaces

(trailing-spaces)


[error] 179-179: trailing spaces

(trailing-spaces)


[error] 185-185: trailing spaces

(trailing-spaces)

- name: Generate PR Comment
id: pr_comment
run: |
# Read the comparison report
python3 -c "
import json
with open('comparison_report.json', 'r') as f:
report = json.load(f)

def format_bytes(bytes):
for unit in ['B', 'KB', 'MB', 'GB']:
if bytes < 1024.0:
return f'{bytes:.1f}{unit}'
bytes /= 1024.0
return f'{bytes:.1f}TB'

# Generate comment
comment = '## 📦 Build Size Comparison\n\n'

# Overall summary
total_current = report['total_current']
total_base = report['total_base']
total_diff = report['total_diff']
total_percent = report['total_percent']

comment += '| Build | Size |\n'
comment += '|-------|------|\n'
comment += f'| **Current PR** | **{format_bytes(total_current)}** |\n'
comment += f'| Base Branch | {format_bytes(total_base)} |\n\n'

if total_diff > 0:
comment += f'📈 **Total size increased by** {format_bytes(total_diff)} (+{total_percent:.1f}%)\n\n'
elif total_diff < 0:
comment += f'📉 **Total size decreased by** {format_bytes(abs(total_diff))} ({total_percent:.1f}%)\n\n'
else:
comment += '✅ **No total size change**\n\n'

# File changes summary
changes = report['changes']
flagged = report['flagged_changes']

comment += f'**Files changed:** {len(changes)}\n'
comment += f'**Large changes flagged:** {len(flagged)}\n\n'

if flagged:
comment += '## ⚠️ Large Changes (Requires Review)\n\n'
comment += '| File | Current | Base | Change | % | Status |\n'
comment += '|------|---------|------|--------|---|--------|\n'

for change in flagged:
status_icon = change['status']
file_name = change['file'].replace('`', '\\`') # Escape backticks
current_size = format_bytes(change['current_size'])
base_size = format_bytes(change['base_size'])
diff_size = format_bytes(abs(change['diff']))
percent = change['percent']

if change['diff'] > 0:
change_str = f'+{diff_size}'
elif change['diff'] < 0:
change_str = f'-{diff_size}'
else:
change_str = '0B'

# Truncate long file names and escape special characters
display_name = file_name
if len(display_name) > 50:
display_name = '...' + display_name[-47:]

comment += f'| `{display_name}` | {current_size} | {base_size} | {change_str} | {percent:+.1f}% | {status_icon} |\n'

comment += '\n'

# All changes table (if not too many)
if len(changes) <= 50:
comment += '## 📋 All File Changes\n\n'
comment += '| File | Current | Base | Change | % | Status |\n'
comment += '|------|---------|------|--------|---|--------|\n'

for change in changes:
status_icon = change['status']
file_name = change['file'].replace('`', '\\`') # Escape backticks
current_size = format_bytes(change['current_size'])
base_size = format_bytes(change['base_size'])
diff_size = format_bytes(abs(change['diff']))
percent = change['percent']

if change['diff'] > 0:
change_str = f'+{diff_size}'
elif change['diff'] < 0:
change_str = f'-{diff_size}'
else:
change_str = '0B'

# Truncate long file names and escape special characters
display_name = file_name
if len(display_name) > 50:
display_name = '...' + display_name[-47:]

comment += f'| `{display_name}` | {current_size} | {base_size} | {change_str} | {percent:+.1f}% | {status_icon} |\n'
else:
comment += f'## 📋 File Changes (showing first 50 of {len(changes)})\n\n'
comment += '| File | Current | Base | Change | % | Status |\n'
comment += '|------|---------|------|--------|---|--------|\n'

for change in changes[:50]:
status_icon = change['status']
file_name = change['file'].replace('`', '\\`') # Escape backticks
current_size = format_bytes(change['current_size'])
base_size = format_bytes(change['base_size'])
diff_size = format_bytes(abs(change['diff']))
percent = change['percent']

if change['diff'] > 0:
change_str = f'+{diff_size}'
elif change['diff'] < 0:
change_str = f'-{diff_size}'
else:
change_str = '0B'

# Truncate long file names and escape special characters
display_name = file_name
if len(display_name) > 50:
display_name = '...' + display_name[-47:]

comment += f'| `{display_name}` | {current_size} | {base_size} | {change_str} | {percent:+.1f}% | {status_icon} |\n'

# Save comment to file
with open('pr_comment.md', 'w') as f:
f.write(comment)

print('PR comment generated')
"

- name: Post/Update PR Comment
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const comment = fs.readFileSync('pr_comment.md', 'utf8');

// Find existing comment
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});

const existingComment = comments.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('📦 Build Size Comparison')
);

if (existingComment) {
// Update existing comment
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existingComment.id,
body: comment
});
console.log('Updated existing PR comment');
} else {
// Create new comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment
});
console.log('Created new PR comment');
}

- name: Upload Build Artifacts
uses: actions/upload-artifact@v4
with:
name: build-comparison
path: |
current-build.zip
base-build.zip
retention-days: 7
Loading
Loading