perf: Reduce GC allocations in DOM diffing #110
Workflow file for this run
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: 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 |