From 9498026e71753d71eea1a4727c555fae13515ab8 Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Tue, 27 May 2025 13:02:05 +0200 Subject: [PATCH] ci: enhance GitHub Actions workflow for performance and caching --- .gitea/workflows/ci.yml | 170 +++++++++++++++++++++++++++--- .gitignore | 1 + jest.config.js | 14 +++ package.json | 5 +- src/app/__tests__/layout.test.tsx | 39 ++++--- 5 files changed, 192 insertions(+), 37 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 7394d66..9a18312 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -6,10 +6,63 @@ on: pull_request: branches: [main, develop] +# Concurrency control to cancel previous runs +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + BUN_VERSION: '1.2.5' # Pin version for consistency + jobs: - lint-and-test: - name: Lint, Test & Build + # Job 1: Quick checks that can fail fast + quick-checks: + name: Quick Checks runs-on: ubuntu-latest + outputs: + cache-key: ${{ steps.cache-key.outputs.key }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Generate cache key + id: cache-key + run: echo "key=bun-${{ hashFiles('bun.lock') }}" >> $GITHUB_OUTPUT + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: ${{ env.BUN_VERSION }} + + - name: Cache dependencies + uses: actions/cache@v4 + id: cache-deps + with: + path: ~/.bun/install/cache + key: ${{ steps.cache-key.outputs.key }} + restore-keys: bun- + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Cache node_modules + uses: actions/cache@v4 + with: + path: node_modules + key: node-modules-${{ steps.cache-key.outputs.key }} + + - name: Run TypeScript type check + run: bun run type-check + + - name: Check Prettier formatting + run: bun run format:check + + # Job 2: Linting (can run in parallel with type checking) + lint: + name: ESLint + runs-on: ubuntu-latest + needs: quick-checks steps: - name: Checkout repository @@ -18,35 +71,114 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@v1 with: - bun-version: latest + bun-version: ${{ env.BUN_VERSION }} - - name: Install dependencies + - name: Restore node_modules cache + uses: actions/cache@v4 + with: + path: node_modules + key: node-modules-${{ needs.quick-checks.outputs.cache-key }} + + - name: Install dependencies (if cache miss) run: bun install - - name: Run TypeScript type check - run: bun run type-check - - name: Run ESLint run: bun run lint - - name: Check Prettier formatting - run: bun run format:check + # Job 3: Testing with optimizations + test: + name: Test & Coverage + runs-on: ubuntu-latest + needs: quick-checks - - name: Run tests - run: bun run test:ci + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: ${{ env.BUN_VERSION }} + + - name: Restore node_modules cache + uses: actions/cache@v4 + with: + path: node_modules + key: node-modules-${{ needs.quick-checks.outputs.cache-key }} + + - name: Install dependencies (if cache miss) + run: bun install + + - name: Cache Jest cache + uses: actions/cache@v4 + with: + path: .jest-cache + key: jest-cache-${{ hashFiles('jest.config.js', 'src/**/*.{ts,tsx}') }} + restore-keys: jest-cache- + + - name: Run tests with optimizations + run: bun run test:ci --maxWorkers=2 --cacheDirectory=.jest-cache + env: + NODE_OPTIONS: --max_old_space_size=4096 + + - name: Upload coverage reports + uses: actions/upload-artifact@v4 + if: always() + with: + name: coverage-report + path: coverage/ + retention-days: 7 + + # Job 4: Build (depends on tests passing) + build: + name: Build Application + runs-on: ubuntu-latest + needs: [quick-checks, lint, test] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: ${{ env.BUN_VERSION }} + + - name: Restore node_modules cache + uses: actions/cache@v4 + with: + path: node_modules + key: node-modules-${{ needs.quick-checks.outputs.cache-key }} + + - name: Install dependencies (if cache miss) + run: bun install + + - name: Cache Next.js build + uses: actions/cache@v4 + with: + path: | + .next/cache + .next/static + key: nextjs-${{ hashFiles('next.config.ts', 'src/**/*.{ts,tsx}', 'public/**/*') }} + restore-keys: nextjs- - name: Build application run: bun run build + env: + NODE_OPTIONS: --max_old_space_size=4096 - name: Upload build artifacts uses: actions/upload-artifact@v4 with: name: build-files path: .next/ + retention-days: 7 + # Job 5: Security audit (can run in parallel) security-audit: name: Security Audit runs-on: ubuntu-latest + needs: quick-checks steps: - name: Checkout repository @@ -55,13 +187,19 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@v1 with: - bun-version: latest + bun-version: ${{ env.BUN_VERSION }} - - name: Install dependencies + - name: Restore node_modules cache + uses: actions/cache@v4 + with: + path: node_modules + key: node-modules-${{ needs.quick-checks.outputs.cache-key }} + + - name: Install dependencies (if cache miss) run: bun install - name: Run security audit - run: bun audit + run: bun audit --audit-level moderate - - name: Run dependency check - run: bunx audit-ci --moderate + - name: Run dependency vulnerability check + run: bunx audit-ci --moderate --report-type summary diff --git a/.gitignore b/.gitignore index db0384d..21b1ea7 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ # testing /coverage +.jest-cache # next.js /.next/ diff --git a/jest.config.js b/jest.config.js index fa081d4..2484b30 100644 --- a/jest.config.js +++ b/jest.config.js @@ -32,6 +32,20 @@ const customJestConfig = { '^.+\\.(js|jsx|ts|tsx)$': ['babel-jest', { presets: ['next/babel'] }], }, transformIgnorePatterns: ['/node_modules/', '^.+\\.module\\.(css|sass|scss)$'], + // Performance optimizations + maxWorkers: '50%', + cache: true, + cacheDirectory: '.jest-cache', + clearMocks: true, + collectCoverage: false, // Only collect coverage when explicitly requested + coverageReporters: ['text', 'lcov'], + errorOnDeprecated: true, + // Reduce memory usage + logHeapUsage: true, + // Faster test discovery + testLocationInResults: true, + // Skip coverage for faster runs in watch mode + watchPathIgnorePatterns: ['/coverage/', '/.next/'], }; // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async diff --git a/package.json b/package.json index 287bc09..0b18be0 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,10 @@ "type-check": "tsc --noEmit", "test": "jest", "test:watch": "jest --watch", - "test:ci": "jest --ci --coverage --watchAll=false", + "test:ci": "jest --ci --coverage --watchAll=false --maxWorkers=2", + "test:fast": "jest --watchAll=false --maxWorkers=50%", + "test:coverage": "jest --coverage --watchAll=false", + "ci:all": "bun run type-check && bun run lint && bun run test:ci && bun run build", "prepare": "husky" }, "dependencies": { diff --git a/src/app/__tests__/layout.test.tsx b/src/app/__tests__/layout.test.tsx index 3e44687..1c7f294 100644 --- a/src/app/__tests__/layout.test.tsx +++ b/src/app/__tests__/layout.test.tsx @@ -1,4 +1,4 @@ -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import RootLayout, { metadata } from '../layout'; @@ -17,20 +17,19 @@ jest.mock('../globals.css', () => ({})); describe('RootLayout', () => { it('renders children correctly', () => { - const testContent =
Test content
; - const { getByTestId } = render({testContent}); + const testContent =
Test content
; + render({testContent}); - expect(getByTestId('test-content')).toBeInTheDocument(); - expect(getByTestId('test-content')).toHaveTextContent('Test content'); + expect(screen.getByTestId('test-content')).toBeInTheDocument(); + expect(screen.getByTestId('test-content')).toHaveTextContent('Test content'); }); - it('creates proper HTML structure with lang attribute', () => { - const testContent =
Test
; - const { container } = render({testContent}); - - // The component returns JSX with html and body elements - // We can test that the component renders without errors - expect(container.firstChild).toBeInTheDocument(); + it('creates proper HTML structure', () => { + const testContent =
Test
; + render({testContent}); + + // Verify the child component is rendered correctly + expect(screen.getByTestId('layout-child')).toBeInTheDocument(); }); it('has correct metadata export', () => { @@ -39,12 +38,12 @@ describe('RootLayout', () => { expect(metadata.description).toBe('Generated by create next app'); }); - it('applies font classes correctly', () => { - // Test that the component can be instantiated and called - const testContent =
Test
; - const renderResult = render({testContent}); - - // Just ensure it renders without throwing errors - expect(renderResult.container).toBeInTheDocument(); + it('renders without errors', () => { + const testContent =
Test
; + const view = render({testContent}); + + // Verify the component renders successfully + expect(screen.getByTestId('render-test')).toBeInTheDocument(); + expect(view.container).toBeInTheDocument(); }); -}); \ No newline at end of file +});