Compare commits

..

18 Commits

Author SHA1 Message Date
kurihada 47f92ac7e1 CI: simplified workflow with pre-built CI image
CI and Deploy / ci-gate (push) Successful in 34s
CI and Deploy / build-and-deploy (push) Failing after 14m26s
CI and Deploy / full-e2e (push) Failing after 14m58s
2026-06-02 10:21:42 +08:00
kurihada 48a20c800d Optimize workflow with proper tool installation
CI and Deploy / build-and-deploy (push) Blocked by required conditions
CI and Deploy / full-e2e (push) Blocked by required conditions
CI and Deploy / ci-gate (push) Has started running
2026-06-01 17:57:58 +08:00
kurihada 3b7545dd52 Use direct git clone with Docker gateway
CI and Deploy / ci-gate (push) Failing after 12m59s
CI and Deploy / build-and-deploy (push) Has been skipped
CI and Deploy / full-e2e (push) Failing after 14m59s
2026-06-01 17:28:46 +08:00
kurihada 120b06a50c CI: test with runner config
CI and Deploy / build-and-deploy (push) Blocked by required conditions
CI and Deploy / full-e2e (push) Blocked by required conditions
CI and Deploy / ci-gate (push) Has started running
2026-06-01 17:06:37 +08:00
kurihada 989ea68f7e Retrigger CI
CI and Deploy / build-and-deploy (push) Blocked by required conditions
CI and Deploy / full-e2e (push) Blocked by required conditions
CI and Deploy / ci-gate (push) Has started running
2026-06-01 16:59:15 +08:00
kurihada 59c947f2e0 Simplify Gitea Actions workflow
CI and Deploy / ci-gate (push) Waiting to run
CI and Deploy / build-and-deploy (push) Blocked by required conditions
CI and Deploy / full-e2e (push) Blocked by required conditions
2026-06-01 16:58:03 +08:00
kurihada 9f27e4db51 Trigger Gitea Actions workflow
CI and Deploy / build-and-deploy (push) Blocked by required conditions
CI and Deploy / full-e2e (push) Blocked by required conditions
CI and Deploy / ci-gate (push) Has started running
2026-06-01 16:47:19 +08:00
kurihada f2eb3f5cb1 Add Gitea Actions workflow (migrated from Jenkins)
CI and Deploy / build-and-deploy (push) Blocked by required conditions
CI and Deploy / full-e2e (push) Blocked by required conditions
CI and Deploy / ci-gate (push) Has started running
2026-06-01 16:35:25 +08:00
kurihada 00683e51c4 修复无 buildx 环境下构建回退逻辑 2026-03-03 18:15:23 +08:00
kurihada 5b5ecd1b29 CI 提速:构建阶段启用 buildx 缓存并移除重复类型检查 2026-03-03 18:07:26 +08:00
kurihada 8ab8557ef1 CI 提速:主链路覆盖率改为可选参数 2026-03-03 17:54:40 +08:00
kurihada 91dcb735cd CI 依赖镜像流程补充 Prisma generate 修复类型检查失败 2026-03-03 17:06:40 +08:00
kurihada 57dd743fc1 CI 提速:引入按 lock hash 的依赖镜像缓存 2026-03-03 16:54:43 +08:00
kurihada a61081fca9 修复 CI 缓存脚本变量展开错误并恢复 node_modules 命中 2026-03-03 16:38:31 +08:00
kurihada 6490cd068e CI 提速:缓存 node_modules 并改用 git archive 传输工作区 2026-03-03 16:28:10 +08:00
kurihada 303e32f599 测试环境补充 JWT_SECRET 避免登录注册用例在 CI 失败 2026-03-03 16:19:51 +08:00
kurihada b117a3694d CI 提速:合并门禁容器执行并拆分 Smoke/全量 E2E 2026-03-03 16:09:24 +08:00
kurihada 0b28c94762 Jenkins 镜像预拉取增加超时重试与缓存检测 2026-03-03 15:30:16 +08:00
7 changed files with 237 additions and 22 deletions
+84
View File
@@ -0,0 +1,84 @@
name: CI and Deploy
on:
push:
branches: [main]
workflow_dispatch:
inputs:
run_full_e2e:
description: 'Run full E2E tests after deploy'
type: boolean
default: false
jobs:
ci-gate:
runs-on: ubuntu-latest
steps:
- name: Checkout
run: |
git clone http://172.17.0.1:3000/kurihada/no-whatever.git .
git checkout ${{ github.sha }}
- name: Install dependencies
run: npm ci
- name: Generate Prisma
run: npx prisma generate
- name: Lint
run: npm run lint
- name: Unit tests
run: npm run test
build-and-deploy:
needs: ci-gate
runs-on: ubuntu-latest
steps:
- name: Checkout
run: |
git clone http://172.17.0.1:3000/kurihada/no-whatever.git .
git checkout ${{ github.sha }}
- name: Build Docker image
run: |
docker build \
--build-arg NEXT_PUBLIC_AMAP_API_KEY="${{ secrets.AMAP_API_KEY }}" \
-t no-whatever:latest .
- name: Deploy
run: |
docker stop no-whatever || true
docker rm no-whatever || true
mkdir -p /data/no-whatever && chown 1001:1001 /data/no-whatever
docker run -d \
--name no-whatever \
--network nginx \
-p 3721:3721 \
-v /data/no-whatever:/app/data \
-e DATABASE_URL=file:/app/data/prod.db \
-e AMAP_API_KEY="${{ secrets.AMAP_API_KEY }}" \
-e DEEPSEEK_API_KEY="${{ secrets.DEEPSEEK_API_KEY }}" \
--restart unless-stopped \
no-whatever:latest
full-e2e:
needs: build-and-deploy
if: ${{ inputs.run_full_e2e }}
runs-on: ubuntu-latest
steps:
- name: Checkout
run: |
git clone http://172.17.0.1:3000/kurihada/no-whatever.git .
git checkout ${{ github.sha }}
- name: Install dependencies
run: npm ci
- name: Full E2E
run: |
npx prisma generate
npm run test:e2e
+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
+137 -16
View File
@@ -1,8 +1,20 @@
pipeline { pipeline {
agent any agent any
options {
skipDefaultCheckout(true)
}
parameters {
booleanParam(name: 'RUN_FULL_E2E', defaultValue: false, description: '是否在部署后额外执行全量 E2E(失败仅标记 UNSTABLE')
booleanParam(name: 'RUN_COVERAGE', defaultValue: false, description: '是否在主链路执行覆盖率测试(关闭可提速)')
}
environment { environment {
APP_NAME = 'no-whatever' 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 { triggers {
@@ -23,39 +35,67 @@ pipeline {
} }
stage('Prepare Test Images') { stage('Prepare Test Images') {
steps { options {
sh ''' timeout(time: 15, unit: 'MINUTES')
docker pull node:20-bookworm
docker pull mcr.microsoft.com/playwright:v1.51.1-jammy
'''
} }
}
stage('Quality Gate') {
steps { steps {
retry(2) {
sh ''' sh '''
set -e 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 if docker image inspect ${CI_IMAGE} >/dev/null 2>&1; then
docker cp . "$cid":/workspace echo "${CI_IMAGE} already exists, skip pull"
docker start -a "$cid" else
docker pull ${CI_IMAGE}
fi
''' '''
} }
} }
}
stage('E2E Gate') { stage('Prepare CI Deps Image') {
options { options {
timeout(time: 20, unit: 'MINUTES') timeout(time: 20, unit: 'MINUTES')
} }
steps { 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 ''' sh '''
set -e 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() { cleanup() {
docker rm -f "$cid" >/dev/null 2>&1 || true docker rm -f "$cid" >/dev/null 2>&1 || true
} }
trap cleanup EXIT trap cleanup EXIT
docker cp . "$cid":/workspace git archive --format=tar HEAD | docker cp - "$cid":/workspace
set +e set +e
docker start -a "$cid" docker start -a "$cid"
status=$? status=$?
@@ -77,7 +117,50 @@ pipeline {
withCredentials([ withCredentials([
string(credentialsId: 'amap-api-key', variable: 'AMAP_KEY') 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 { post {
+1 -1
View File
@@ -1,6 +1,6 @@
import { test, expect } from "@playwright/test"; import { test, expect } from "@playwright/test";
test("首页模式卡片可正确导航", async ({ page }) => { test("@smoke 首页模式卡片可正确导航", async ({ page }) => {
await page.goto("/"); await page.goto("/");
await expect(page.getByRole("heading", { name: "NoWhatever" })).toBeVisible(); await expect(page.getByRole("heading", { name: "NoWhatever" })).toBeVisible();
+1 -1
View File
@@ -1,6 +1,6 @@
import { test, expect } from "@playwright/test"; import { test, expect } from "@playwright/test";
test("Panic 手动加入房间会规范化房间号并发起 join 请求", async ({ page }) => { test("@smoke Panic 手动加入房间会规范化房间号并发起 join 请求", async ({ page }) => {
const joinBodies: Array<{ userId?: string }> = []; const joinBodies: Array<{ userId?: string }> = [];
await page.route("**/api/room/*/join", async (route) => { await page.route("**/api/room/*/join", async (route) => {
+1
View File
@@ -11,6 +11,7 @@
"test:watch": "vitest", "test:watch": "vitest",
"test:coverage": "vitest run --coverage", "test:coverage": "vitest run --coverage",
"test:e2e:install": "playwright install chromium", "test:e2e:install": "playwright install chromium",
"test:e2e:smoke": "playwright test --grep @smoke",
"test:e2e": "playwright test", "test:e2e": "playwright test",
"test:e2e:headed": "playwright test --headed" "test:e2e:headed": "playwright test --headed"
}, },
+1
View File
@@ -8,6 +8,7 @@ const shared = {
env: { env: {
AMAP_API_KEY: "test-amap-key", AMAP_API_KEY: "test-amap-key",
DEEPSEEK_API_KEY: "test-deepseek-key", DEEPSEEK_API_KEY: "test-deepseek-key",
JWT_SECRET: "test-jwt-secret",
}, },
} as const; } as const;