Skip to content

perf: Reduce GC allocations in DOM diffing #110

perf: Reduce GC allocations in DOM diffing

perf: Reduce GC allocations in DOM diffing #110

Workflow file for this run

name: PR Validation
on:
pull_request:
branches: [ "main" ]
types: [opened, synchronize, reopened, ready_for_review]
# Ensure only one workflow runs per PR
concurrency:
group: pr-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
# Validate PR title follows Conventional Commits
validate-pr-title:
name: Validate PR Title
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false && github.event.pull_request.user.login != 'dependabot[bot]'
steps:
- name: Validate PR title
uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
types: |
feat
fix
docs
style
refactor
perf
test
build
ci
chore
revert
requireScope: false
subjectPattern: ^[A-Z].+$
subjectPatternError: |
The subject "{subject}" found in the pull request title "{title}"
didn't match the configured pattern. Please ensure that the subject
starts with an uppercase character.
# Validate PR has description
validate-pr-description:
name: Validate PR Description
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false && github.event.pull_request.user.login != 'dependabot[bot]'
steps:
- name: Check PR description
uses: actions/github-script@v8
with:
script: |
const prBody = context.payload.pull_request.body || '';
const minLength = 50;
if (prBody.trim().length < minLength) {
core.setFailed(
`PR description is too short (${prBody.trim().length} chars). ` +
`Please provide a meaningful description (minimum ${minLength} chars).`
);
return;
}
// Check for required sections (flexible check)
const hasWhat = /###?\s*What/i.test(prBody);
const hasWhy = /###?\s*Why/i.test(prBody);
const hasTesting = /###?\s*Testing/i.test(prBody) || /\[x\].*test/i.test(prBody);
if (!hasWhat || !hasWhy) {
core.setFailed(
'PR description is missing required sections. ' +
'Please use the PR template and fill in: What, Why, and Testing sections.'
);
return;
}
core.info('✅ PR description looks good!');
# Check PR size
check-pr-size:
name: Check PR Size
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- name: Check PR size
uses: actions/github-script@v8
with:
script: |
const pr = context.payload.pull_request;
const additions = pr.additions || 0;
const deletions = pr.deletions || 0;
const totalChanges = additions + deletions;
// Soft limit (warning) and hard limit (failure)
const hardLimit = 1500;
const softLimit = 400;
if (totalChanges > hardLimit) {
core.setFailed(
`⚠️ PR is too large (${totalChanges} lines changed). ` +
`Please consider breaking it into smaller PRs (< ${hardLimit} lines).`
);
return;
}
if (totalChanges > softLimit) {
core.warning(
`⚠️ PR is getting large (${totalChanges} lines changed). ` +
`Consider breaking it into smaller PRs for easier review.`
);
} else {
core.info(`✅ PR size is good (${totalChanges} lines changed)`);
}
# Lint PR changes
lint-check:
name: Lint Check
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup .NET 10
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Restore
run: dotnet restore
- name: Get changed C# files
id: changed-files
run: |
# Get list of changed .cs files in the PR
CHANGED_FILES=$(git diff --name-only --diff-filter=ACMRT origin/${{ github.event.pull_request.base.ref }}...HEAD | grep '\.cs$' || true)
if [ -z "$CHANGED_FILES" ]; then
echo "has_cs_files=false" >> $GITHUB_OUTPUT
echo "ℹ️ No C# files changed in this PR"
else
echo "has_cs_files=true" >> $GITHUB_OUTPUT
# Convert newlines to spaces and store
FILES_SPACE_SEPARATED=$(echo "$CHANGED_FILES" | tr '\n' ' ')
echo "files=$FILES_SPACE_SEPARATED" >> $GITHUB_OUTPUT
echo "📝 Changed C# files:"
echo "$CHANGED_FILES"
fi
- name: Format check changed files
if: steps.changed-files.outputs.has_cs_files == 'true'
run: |
# Check formatting only for changed files
FILES="${{ steps.changed-files.outputs.files }}"
echo "🔍 Checking formatting for changed files..."
echo "Files: $FILES"
# Run format check with --include for each file
dotnet format --verify-no-changes --include $FILES --verbosity diagnostic
if [ $? -ne 0 ]; then
echo ""
echo "❌ Code formatting issues detected in your changes."
echo "Please run the following command locally:"
echo " dotnet format --include $FILES"
echo ""
exit 1
fi
echo "✅ Code formatting is correct for all changed files"
- name: Skip format check
if: steps.changed-files.outputs.has_cs_files == 'false'
run: |
echo "✅ No C# files to check - skipping format validation"
# Security scan
security-scan:
name: Security Scan
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup .NET 10
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Restore
run: dotnet restore
- name: Check for vulnerable packages
run: |
echo "🔍 Scanning for vulnerable packages..."
dotnet list package --vulnerable --include-transitive 2>&1 | tee vulnerability-report.txt
if grep -qi "critical\|high" vulnerability-report.txt; then
echo "❌ Critical or High severity vulnerabilities detected!"
echo "Please review and update dependencies before merging."
exit 1
else
echo "✅ No critical or high severity vulnerabilities found"
fi
# Bundle size quality gate
bundle-size-check:
name: Bundle Size Check
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup .NET 10
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Install WASM workloads
run: dotnet workload install wasm-experimental wasm-tools
- name: Restore dependencies
run: dotnet restore
- name: Build and Publish Release (Trimmed)
run: |
dotnet publish Abies.Conduit/Abies.Conduit.csproj -c Release -o ./publish-trimmed
- name: Measure Bundle Size
id: bundle-size
run: |
# Measure the _framework directory size (WASM bundle)
FRAMEWORK_DIR="./publish-trimmed/wwwroot/_framework"
if [ -d "$FRAMEWORK_DIR" ]; then
# Get total size in bytes
TOTAL_BYTES=$(du -sb "$FRAMEWORK_DIR" | cut -f1)
TOTAL_MB=$((TOTAL_BYTES / 1024 / 1024))
FILE_COUNT=$(find "$FRAMEWORK_DIR" -type f | wc -l)
echo "bundle_size_mb=$TOTAL_MB" >> $GITHUB_OUTPUT
echo "bundle_size_bytes=$TOTAL_BYTES" >> $GITHUB_OUTPUT
echo "file_count=$FILE_COUNT" >> $GITHUB_OUTPUT
echo "📦 WASM Bundle Size Report"
echo "=========================="
echo "Total Size: ${TOTAL_MB}MB ($TOTAL_BYTES bytes)"
echo "File Count: $FILE_COUNT"
echo ""
echo "Largest files:"
find "$FRAMEWORK_DIR" -type f -exec du -h {} + | sort -rh | head -10
else
echo "❌ Framework directory not found at $FRAMEWORK_DIR"
exit 1
fi
- name: Check Bundle Size Limits
run: |
BUNDLE_SIZE=${{ steps.bundle-size.outputs.bundle_size_mb }}
# Validate BUNDLE_SIZE is a valid integer
if ! echo "$BUNDLE_SIZE" | grep -qE '^[0-9]+$'; then
echo "❌ FAILED: Could not determine bundle size (got '$BUNDLE_SIZE')"
exit 1
fi
# Hard limit: 15MB for trimmed Release build
HARD_LIMIT=15
# Soft limit (warning): 10MB
SOFT_LIMIT=10
echo "📊 Bundle Size: ${BUNDLE_SIZE}MB"
echo "🔴 Hard Limit: ${HARD_LIMIT}MB"
echo "🟡 Soft Limit: ${SOFT_LIMIT}MB"
if [ "$BUNDLE_SIZE" -gt "$HARD_LIMIT" ]; then
echo ""
echo "❌ FAILED: Bundle size (${BUNDLE_SIZE}MB) exceeds hard limit (${HARD_LIMIT}MB)"
echo ""
echo "The WASM bundle is too large. Please:"
echo "1. Ensure PublishTrimmed=true is set for Release configuration"
echo "2. Review and remove unnecessary dependencies"
echo "3. Enable InvariantGlobalization if not already done"
echo "4. Consider code splitting if applicable"
exit 1
elif [ "$BUNDLE_SIZE" -gt "$SOFT_LIMIT" ]; then
echo ""
echo "⚠️ WARNING: Bundle size (${BUNDLE_SIZE}MB) exceeds soft limit (${SOFT_LIMIT}MB)"
echo "Consider optimizing bundle size for faster startup times."
else
echo ""
echo "✅ Bundle size is within acceptable limits"
fi
# Check for TODO/FIXME without issues
check-todos:
name: Check TODOs
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Check for untracked TODOs
run: |
# Find TODO/FIXME comments without issue references
untracked=$(grep -rn "TODO\|FIXME" --include="*.cs" --include="*.fs" --exclude-dir=obj --exclude-dir=bin . | grep -v "#[0-9]" || true)
if [ ! -z "$untracked" ]; then
echo "⚠️ Found TODO/FIXME comments without issue references:"
echo "$untracked"
echo ""
echo "Please either:"
echo "1. Create an issue and reference it (e.g., // TODO #123: description)"
echo "2. Fix the item in this PR"
echo "3. Remove the comment if not needed"
# For now, just warn
# exit 1
else
echo "✅ No untracked TODOs found"
fi
# Summary
pr-validation-summary:
name: PR Validation Summary
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
needs: [validate-pr-title, validate-pr-description, check-pr-size, lint-check, security-scan, bundle-size-check, check-todos]
steps:
- name: Check if automated PR
id: check-automated
run: |
if [ "${{ github.event.pull_request.user.login }}" = "dependabot[bot]" ]; then
echo "automated=true" >> $GITHUB_OUTPUT
else
echo "automated=false" >> $GITHUB_OUTPUT
fi
- name: All checks passed
run: |
if [ "${{ steps.check-automated.outputs.automated }}" = "true" ]; then
echo "🤖 Automated PR validation summary"
echo "✅ PR size is reasonable"
echo "✅ Code formatting is correct"
echo "✅ No security vulnerabilities"
echo "✅ Bundle size within limits"
echo "✅ No untracked TODOs"
echo ""
echo "Note: Title and description checks skipped for automated PRs"
else
echo "🎉 All PR validation checks passed!"
echo "✅ PR title follows Conventional Commits"
echo "✅ PR has adequate description"
echo "✅ PR size is reasonable"
echo "✅ Code formatting is correct"
echo "✅ No security vulnerabilities"
echo "✅ Bundle size within limits"
echo "✅ No untracked TODOs"
echo ""
echo "Next steps:"
echo "1. Wait for CD and E2E workflows to complete"
echo "2. Request review from team members"
echo "3. Address any feedback"
echo "4. Merge when approved!"
fi