ci: enhance GitHub Actions workflow for performance and caching
Some checks failed
CI/CD Pipeline / Quick Checks (pull_request) Failing after 9s
CI/CD Pipeline / ESLint (pull_request) Has been skipped
CI/CD Pipeline / Test & Coverage (pull_request) Has been skipped
CI/CD Pipeline / Build Application (pull_request) Has been skipped
CI/CD Pipeline / Security Audit (pull_request) Has been skipped

This commit is contained in:
2025-05-27 13:02:05 +02:00
parent 76dd9cd838
commit 9498026e71
5 changed files with 192 additions and 37 deletions

View File

@ -6,10 +6,63 @@ on:
pull_request: pull_request:
branches: [main, develop] 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: jobs:
lint-and-test: # Job 1: Quick checks that can fail fast
name: Lint, Test & Build quick-checks:
name: Quick Checks
runs-on: ubuntu-latest 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: steps:
- name: Checkout repository - name: Checkout repository
@ -18,35 +71,114 @@ jobs:
- name: Setup Bun - name: Setup Bun
uses: oven-sh/setup-bun@v1 uses: oven-sh/setup-bun@v1
with: 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 run: bun install
- name: Run TypeScript type check
run: bun run type-check
- name: Run ESLint - name: Run ESLint
run: bun run lint run: bun run lint
- name: Check Prettier formatting # Job 3: Testing with optimizations
run: bun run format:check test:
name: Test & Coverage
runs-on: ubuntu-latest
needs: quick-checks
- name: Run tests steps:
run: bun run test:ci - 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 - name: Build application
run: bun run build run: bun run build
env:
NODE_OPTIONS: --max_old_space_size=4096
- name: Upload build artifacts - name: Upload build artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: build-files name: build-files
path: .next/ path: .next/
retention-days: 7
# Job 5: Security audit (can run in parallel)
security-audit: security-audit:
name: Security Audit name: Security Audit
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: quick-checks
steps: steps:
- name: Checkout repository - name: Checkout repository
@ -55,13 +187,19 @@ jobs:
- name: Setup Bun - name: Setup Bun
uses: oven-sh/setup-bun@v1 uses: oven-sh/setup-bun@v1
with: 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 run: bun install
- name: Run security audit - name: Run security audit
run: bun audit run: bun audit --audit-level moderate
- name: Run dependency check - name: Run dependency vulnerability check
run: bunx audit-ci --moderate run: bunx audit-ci --moderate --report-type summary

1
.gitignore vendored
View File

@ -12,6 +12,7 @@
# testing # testing
/coverage /coverage
.jest-cache
# next.js # next.js
/.next/ /.next/

View File

@ -32,6 +32,20 @@ const customJestConfig = {
'^.+\\.(js|jsx|ts|tsx)$': ['babel-jest', { presets: ['next/babel'] }], '^.+\\.(js|jsx|ts|tsx)$': ['babel-jest', { presets: ['next/babel'] }],
}, },
transformIgnorePatterns: ['/node_modules/', '^.+\\.module\\.(css|sass|scss)$'], 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: ['<rootDir>/coverage/', '<rootDir>/.next/'],
}; };
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async

View File

@ -13,7 +13,10 @@
"type-check": "tsc --noEmit", "type-check": "tsc --noEmit",
"test": "jest", "test": "jest",
"test:watch": "jest --watch", "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" "prepare": "husky"
}, },
"dependencies": { "dependencies": {

View File

@ -1,4 +1,4 @@
import { render } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import RootLayout, { metadata } from '../layout'; import RootLayout, { metadata } from '../layout';
@ -17,20 +17,19 @@ jest.mock('../globals.css', () => ({}));
describe('RootLayout', () => { describe('RootLayout', () => {
it('renders children correctly', () => { it('renders children correctly', () => {
const testContent = <div data-testid="test-content">Test content</div>; const testContent = <div data-testid='test-content'>Test content</div>;
const { getByTestId } = render(<RootLayout>{testContent}</RootLayout>); render(<RootLayout>{testContent}</RootLayout>);
expect(getByTestId('test-content')).toBeInTheDocument(); expect(screen.getByTestId('test-content')).toBeInTheDocument();
expect(getByTestId('test-content')).toHaveTextContent('Test content'); expect(screen.getByTestId('test-content')).toHaveTextContent('Test content');
}); });
it('creates proper HTML structure with lang attribute', () => { it('creates proper HTML structure', () => {
const testContent = <div>Test</div>; const testContent = <div data-testid='layout-child'>Test</div>;
const { container } = render(<RootLayout>{testContent}</RootLayout>); render(<RootLayout>{testContent}</RootLayout>);
// The component returns JSX with html and body elements // Verify the child component is rendered correctly
// We can test that the component renders without errors expect(screen.getByTestId('layout-child')).toBeInTheDocument();
expect(container.firstChild).toBeInTheDocument();
}); });
it('has correct metadata export', () => { it('has correct metadata export', () => {
@ -39,12 +38,12 @@ describe('RootLayout', () => {
expect(metadata.description).toBe('Generated by create next app'); expect(metadata.description).toBe('Generated by create next app');
}); });
it('applies font classes correctly', () => { it('renders without errors', () => {
// Test that the component can be instantiated and called const testContent = <div data-testid='render-test'>Test</div>;
const testContent = <div>Test</div>; const view = render(<RootLayout>{testContent}</RootLayout>);
const renderResult = render(<RootLayout>{testContent}</RootLayout>);
// Verify the component renders successfully
// Just ensure it renders without throwing errors expect(screen.getByTestId('render-test')).toBeInTheDocument();
expect(renderResult.container).toBeInTheDocument(); expect(view.container).toBeInTheDocument();
}); });
}); });