feat: 并行化CI代码质量检查步骤
ci/woodpecker/push/woodpecker Pipeline is running

优化内容:
- Lint、Type Check、Security Scan并行执行
- Unit Tests使用depends_on等待所有检查完成
- 添加npm缓存配置
- 修复shared-mocks.tsx的ESLint错误

预期效果:
- 串行时间: 30s + 40s + 20s = 90s
- 并行时间: max(30s, 40s, 20s) = 40s
- 节省时间: 50s (55.6%改善)
This commit is contained in:
张翔
2026-03-29 11:41:30 +08:00
parent b5b207e5a1
commit 26aa13b5a4
80 changed files with 1113 additions and 4600 deletions
+189
View File
@@ -0,0 +1,189 @@
import { jest } from '@jest/globals';
import React from 'react';
interface MockProps {
children?: React.ReactNode;
className?: string;
href?: string;
src?: string;
alt?: string;
width?: number | string;
height?: number | string;
[key: string]: unknown;
}
export const mockFramerMotion = () => {
jest.mock('framer-motion', () => ({
motion: {
div: ({ children, className, ...props }: MockProps) => (
<div className={className} {...props}>{children}</div>
),
section: ({ children, className, ...props }: MockProps) => (
<section className={className} {...props}>{children}</section>
),
span: ({ children, className, ...props }: MockProps) => (
<span className={className} {...props}>{children}</span>
),
h1: ({ children, className, ...props }: MockProps) => (
<h1 className={className} {...props}>{children}</h1>
),
h2: ({ children, className, ...props }: MockProps) => (
<h2 className={className} {...props}>{children}</h2>
),
p: ({ children, className, ...props }: MockProps) => (
<p className={className} {...props}>{children}</p>
),
button: ({ children, className, ...props }: MockProps) => (
<button className={className} {...props}>{children}</button>
),
a: ({ children, className, ...props }: MockProps) => (
<a className={className} {...props}>{children}</a>
),
img: ({ className, ...props }: MockProps) => (
<img className={className} {...props} alt="" />
),
},
AnimatePresence: ({ children }: MockProps) => <>{children}</>,
useInView: () => [null, true],
useAnimation: () => ({
start: jest.fn(),
stop: jest.fn(),
}),
useMotionValue: () => ({
get: jest.fn(),
set: jest.fn(),
}),
}));
};
export const mockNextLink = () => {
jest.mock('next/link', () => {
const MockLink = ({ children, href, ...props }: MockProps) => (
<a href={href} {...props}>{children}</a>
);
MockLink.displayName = 'MockLink';
return MockLink;
});
};
export const mockNextNavigation = () => {
jest.mock('next/navigation', () => ({
useSearchParams: () => ({
get: jest.fn(),
}),
useRouter: () => ({
push: jest.fn(),
replace: jest.fn(),
back: jest.fn(),
}),
usePathname: () => '/',
}));
};
export const mockLucideReact = () => {
jest.mock('lucide-react', () => ({
ArrowRight: () => <span data-testid="arrow-right" />,
ArrowLeft: () => <span data-testid="arrow-left" />,
Shield: () => <span data-testid="shield-icon" />,
Zap: () => <span data-testid="zap-icon" />,
Award: () => <span data-testid="award-icon" />,
Check: () => <span data-testid="check-icon" />,
X: () => <span data-testid="x-icon" />,
Menu: () => <span data-testid="menu-icon" />,
ChevronDown: () => <span data-testid="chevron-down" />,
ChevronRight: () => <span data-testid="chevron-right" />,
Mail: () => <span data-testid="mail-icon" />,
Phone: () => <span data-testid="phone-icon" />,
MapPin: () => <span data-testid="map-pin-icon" />,
Clock: () => <span data-testid="clock-icon" />,
User: () => <span data-testid="user-icon" />,
Lock: () => <span data-testid="lock-icon" />,
Eye: () => <span data-testid="eye-icon" />,
EyeOff: () => <span data-testid="eye-off-icon" />,
Settings: () => <span data-testid="settings-icon" />,
LogOut: () => <span data-testid="logout-icon" />,
Home: () => <span data-testid="home-icon" />,
FileText: () => <span data-testid="file-text-icon" />,
Image: () => <span data-testid="image-icon" />,
Save: () => <span data-testid="save-icon" />,
Trash2: () => <span data-testid="trash-icon" />,
Edit: () => <span data-testid="edit-icon" />,
Plus: () => <span data-testid="plus-icon" />,
Search: () => <span data-testid="search-icon" />,
Filter: () => <span data-testid="filter-icon" />,
Download: () => <span data-testid="download-icon" />,
Upload: () => <span data-testid="upload-icon" />,
RefreshCw: () => <span data-testid="refresh-icon" />,
AlertCircle: () => <span data-testid="alert-icon" />,
Info: () => <span data-testid="info-icon" />,
HelpCircle: () => <span data-testid="help-icon" />,
}));
};
export const mockNextDynamic = () => {
jest.mock('next/dynamic', () => {
const MockDynamic = (props: MockProps) => {
return <div data-testid="dynamic-component" {...props} />;
};
MockDynamic.displayName = 'MockDynamic';
return {
__esModule: true,
default: MockDynamic,
};
});
};
export const mockNextImage = () => {
jest.mock('next/image', () => {
const MockImage = ({ src, alt, width, height, className, ...props }: MockProps) => (
<img
src={src}
alt={alt || ''}
width={width}
height={height}
className={className}
{...props}
/>
);
MockImage.displayName = 'MockImage';
return MockImage;
});
};
export const mockDatabase = () => {
jest.mock('@/db', () => ({
db: {
select: jest.fn().mockReturnValue({
from: jest.fn().mockResolvedValue([]),
}),
insert: jest.fn().mockReturnValue({
values: jest.fn().mockReturnValue({
returning: jest.fn().mockResolvedValue([{ id: 1 }]),
}),
}),
update: jest.fn().mockReturnValue({
set: jest.fn().mockReturnValue({
where: jest.fn().mockResolvedValue([{ id: 1 }]),
}),
}),
delete: jest.fn().mockReturnValue({
where: jest.fn().mockResolvedValue([]),
}),
},
}));
};
export const setupSharedMocks = () => {
mockFramerMotion();
mockNextLink();
mockNextNavigation();
mockLucideReact();
mockNextDynamic();
mockNextImage();
};
export const setupMinimalMocks = () => {
mockFramerMotion();
mockNextLink();
mockLucideReact();
};