Compare commits

..

10 Commits

6 changed files with 153 additions and 22 deletions
+8
View File
@@ -0,0 +1,8 @@
ARG CI_IMAGE=mcr.microsoft.com/playwright:v1.51.1-jammy
FROM ${CI_IMAGE}
ARG NPM_REGISTRY=https://registry.npmmirror.com
WORKDIR /workspace
COPY package.json package-lock.json ./
RUN npm config set registry ${NPM_REGISTRY} && npm ci --prefer-offline --no-audit --progress=false
Vendored
+141 -20
View File
@@ -1,8 +1,20 @@
pipeline {
agent any
options {
skipDefaultCheckout(true)
}
parameters {
booleanParam(name: 'RUN_FULL_E2E', defaultValue: false, description: '是否在部署后额外执行全量 E2E(失败仅标记 UNSTABLE')
booleanParam(name: 'RUN_COVERAGE', defaultValue: false, description: '是否在主链路执行覆盖率测试(关闭可提速)')
}
environment {
APP_NAME = 'no-whatever'
CI_IMAGE = 'mcr.microsoft.com/playwright:v1.51.1-jammy'
NPM_REGISTRY = 'https://registry.npmmirror.com'
BUILDX_CACHE_DIR = '.buildx-cache'
}
triggers {
@@ -23,39 +35,67 @@ pipeline {
}
stage('Prepare Test Images') {
options {
timeout(time: 15, unit: 'MINUTES')
}
steps {
sh '''
docker pull node:20-bookworm
docker pull mcr.microsoft.com/playwright:v1.51.1-jammy
'''
retry(2) {
sh '''
set -e
if docker image inspect ${CI_IMAGE} >/dev/null 2>&1; then
echo "${CI_IMAGE} already exists, skip pull"
else
docker pull ${CI_IMAGE}
fi
'''
}
}
}
stage('Quality Gate') {
steps {
sh '''
set -e
cid=$(docker create node:20-bookworm sh -lc "cd /workspace && npm ci && npm run lint && npx tsc --noEmit && npm run test:coverage")
trap 'docker rm -f "$cid" >/dev/null 2>&1 || true' EXIT
docker cp . "$cid":/workspace
docker start -a "$cid"
'''
}
}
stage('E2E Gate') {
stage('Prepare CI Deps Image') {
options {
timeout(time: 20, unit: 'MINUTES')
}
steps {
script {
env.LOCK_HASH = sh(script: "sha256sum package-lock.json | cut -d ' ' -f1", returnStdout: true).trim()
env.CI_DEPS_IMAGE = "${APP_NAME}-ci-deps:${env.LOCK_HASH}"
}
sh '''
set -e
cid=$(docker create --ipc=host mcr.microsoft.com/playwright:v1.51.1-jammy sh -lc "cd /workspace && npm ci && npm run test:e2e")
if docker image inspect "${CI_DEPS_IMAGE}" >/dev/null 2>&1; then
echo "deps image cache hit: ${CI_DEPS_IMAGE}"
else
echo "deps image cache miss, building: ${CI_DEPS_IMAGE}"
docker build \
--build-arg CI_IMAGE=${CI_IMAGE} \
--build-arg NPM_REGISTRY=${NPM_REGISTRY} \
-f Dockerfile.ci-deps \
-t "${CI_DEPS_IMAGE}" .
fi
'''
}
}
stage('CI Gate (Lint + Unit + Smoke E2E)') {
options {
timeout(time: 30, unit: 'MINUTES')
}
steps {
sh '''
set -e
rm -rf playwright-report test-results
cid=$(docker create --ipc=host \
-e HOME=/tmp \
-e RUN_COVERAGE=${RUN_COVERAGE} \
${CI_DEPS_IMAGE} \
sh -lc 'set -e; cd /workspace; npx prisma generate; npm run lint; if [ "$RUN_COVERAGE" = "true" ]; then npm run test:coverage; else npm run test; fi; npm run test:e2e:smoke')
cleanup() {
docker rm -f "$cid" >/dev/null 2>&1 || true
}
trap cleanup EXIT
docker cp . "$cid":/workspace
git archive --format=tar HEAD | docker cp - "$cid":/workspace
set +e
docker start -a "$cid"
status=$?
@@ -77,7 +117,50 @@ pipeline {
withCredentials([
string(credentialsId: 'amap-api-key', variable: 'AMAP_KEY')
]) {
sh "docker build --build-arg NEXT_PUBLIC_AMAP_API_KEY=${AMAP_KEY} -t ${APP_NAME}:${BUILD_NUMBER} -t ${APP_NAME}:latest ."
sh '''
set -e
CACHE_DIR="${BUILDX_CACHE_DIR}"
CACHE_NEW_DIR="${BUILDX_CACHE_DIR}-new"
rm -rf "$CACHE_NEW_DIR"
if docker buildx version >/dev/null 2>&1; then
echo "using docker buildx with local cache"
docker buildx create --name "${APP_NAME}-builder" --use >/dev/null 2>&1 || true
docker buildx use "${APP_NAME}-builder" >/dev/null 2>&1 || true
mkdir -p "$CACHE_DIR"
set +e
docker buildx build \
--load \
--build-arg NEXT_PUBLIC_AMAP_API_KEY="$AMAP_KEY" \
--cache-from type=local,src="$CACHE_DIR" \
--cache-to type=local,dest="$CACHE_NEW_DIR",mode=max \
-t "${APP_NAME}:${BUILD_NUMBER}" \
-t "${APP_NAME}:latest" \
.
bx_status=$?
set -e
if [ $bx_status -eq 0 ]; then
rm -rf "$CACHE_DIR"
mv "$CACHE_NEW_DIR" "$CACHE_DIR"
else
echo "buildx build failed, fallback to classic docker build"
docker build \
--build-arg NEXT_PUBLIC_AMAP_API_KEY="$AMAP_KEY" \
-t "${APP_NAME}:${BUILD_NUMBER}" \
-t "${APP_NAME}:latest" \
.
fi
else
echo "docker buildx unavailable, fallback to classic docker build"
docker build \
--build-arg NEXT_PUBLIC_AMAP_API_KEY="$AMAP_KEY" \
-t "${APP_NAME}:${BUILD_NUMBER}" \
-t "${APP_NAME}:latest" \
.
fi
'''
}
}
}
@@ -107,6 +190,44 @@ pipeline {
}
}
}
stage('Full E2E (Optional, Non-Blocking)') {
when {
expression { return params.RUN_FULL_E2E }
}
options {
timeout(time: 30, unit: 'MINUTES')
}
steps {
catchError(buildResult: 'UNSTABLE', stageResult: 'UNSTABLE') {
sh '''
set -e
rm -rf playwright-report-full test-results-full
cid=$(docker create --ipc=host \
-e HOME=/tmp \
${CI_DEPS_IMAGE} \
sh -lc 'set -e; cd /workspace; npx prisma generate; npm run test:e2e')
cleanup() {
docker rm -f "$cid" >/dev/null 2>&1 || true
}
trap cleanup EXIT
git archive --format=tar HEAD | docker cp - "$cid":/workspace
set +e
docker start -a "$cid"
status=$?
set -e
docker cp "$cid":/workspace/playwright-report ./playwright-report-full 2>/dev/null || true
docker cp "$cid":/workspace/test-results ./test-results-full 2>/dev/null || true
exit $status
'''
}
}
post {
always {
archiveArtifacts artifacts: 'playwright-report-full/**,test-results-full/**', allowEmptyArchive: true
}
}
}
}
post {
+1 -1
View File
@@ -1,6 +1,6 @@
import { test, expect } from "@playwright/test";
test("首页模式卡片可正确导航", async ({ page }) => {
test("@smoke 首页模式卡片可正确导航", async ({ page }) => {
await page.goto("/");
await expect(page.getByRole("heading", { name: "NoWhatever" })).toBeVisible();
+1 -1
View File
@@ -1,6 +1,6 @@
import { test, expect } from "@playwright/test";
test("Panic 手动加入房间会规范化房间号并发起 join 请求", async ({ page }) => {
test("@smoke Panic 手动加入房间会规范化房间号并发起 join 请求", async ({ page }) => {
const joinBodies: Array<{ userId?: string }> = [];
await page.route("**/api/room/*/join", async (route) => {
+1
View File
@@ -11,6 +11,7 @@
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:e2e:install": "playwright install chromium",
"test:e2e:smoke": "playwright test --grep @smoke",
"test:e2e": "playwright test",
"test:e2e:headed": "playwright test --headed"
},
+1
View File
@@ -8,6 +8,7 @@ const shared = {
env: {
AMAP_API_KEY: "test-amap-key",
DEEPSEEK_API_KEY: "test-deepseek-key",
JWT_SECRET: "test-jwt-secret",
},
} as const;