Commit e1191306 authored by liguangyu06's avatar liguangyu06
Browse files

新增 Jenkins 日常验证流水线 Jenkinsfile.verify

供 spd-ui-verify 手工跑 smoke/单模块,与夜间 full 分离;并补充任务创建文档。
parent 1a4c1b8b
// 日常验证专用流水线:默认 smoke,超时 2 小时,不设定时;与夜间 SPD/full 任务分离。
// Jenkins 任务 Script Path 填 Jenkinsfile.verify
def splitEmailAddrs(String raw) {
return (raw ?: '')
.split(/[,;\s]+/)
.collect { it.trim() }
.findAll { it.contains('@') }
}
def loadDefaultReportRecipients() {
def path = 'config/report-email-recipients.txt'
if (!fileExists(path)) {
return []
}
return readFile(encoding: 'UTF-8', file: path)
.split('\n')
.collect { it.trim() }
.findAll { it && !it.startsWith('#') && it.contains('@') }
}
def mergeEmailAddrs(List parts) {
def seen = []
def all = []
parts.flatten().findAll { it }.each { addr ->
def key = addr.toLowerCase()
if (!seen.contains(key)) {
seen.add(key)
all.add(addr)
}
}
return all
}
def sendReportEmail() {
def extraTo = (params.REPORT_EMAIL_TO ?: '').trim()
if (!extraTo) {
extraTo = (env.REPORT_EMAIL_TO ?: '').trim()
}
def cc = (params.REPORT_EMAIL_CC ?: '').trim()
if (!cc) {
cc = (env.REPORT_EMAIL_CC ?: '').trim()
}
def recipients = mergeEmailAddrs([
loadDefaultReportRecipients(),
splitEmailAddrs(extraTo),
splitEmailAddrs(cc),
])
if (!recipients) {
echo '未配置收件人,跳过邮件(日常验证任务通常不必发信)。'
return
}
def to = recipients.join(',')
def subject = fileExists('reports/latest/email-subject.txt')
? readFile(encoding: 'UTF-8', file: 'reports/latest/email-subject.txt').trim()
: "【SPD UI自动化·验证】${params.SUITE} ${currentBuild.currentResult} #${env.BUILD_NUMBER}"
def body = fileExists('reports/latest/email-summary.html')
? readFile(encoding: 'UTF-8', file: 'reports/latest/email-summary.html')
: """<p>构建 ${currentBuild.currentResult},未生成详细摘要。</p>
<p>测试范围:${params.SUITE}</p>
<p>Playwright 报告:<a href='${env.BUILD_URL}Playwright_20Report/'>打开 Playwright 报告</a></p>"""
if (!fileExists('reports/latest/email-summary.html')) {
writeFile encoding: 'UTF-8', file: 'reports/latest/email-summary.html', text: body
}
if (!fileExists('reports/latest/email-subject.txt')) {
writeFile encoding: 'UTF-8', file: 'reports/latest/email-subject.txt', text: subject + '\n'
}
try {
def credId = env.SMTP_CREDENTIALS_ID ?: 'sinopharm-smtp'
withCredentials([
usernamePassword(credentialsId: credId, usernameVariable: 'SMTP_USER', passwordVariable: 'SMTP_PASS')
]) {
withEnv([
"EMAIL_TO=${to}",
'SMTP_HOST=mail.sinopharm.com',
'SMTP_PORT=465',
'SMTP_FROM=liguangyu@sinopharm.com'
]) {
if (isUnix()) {
sh 'python3 scripts/send-ci-email.py'
} else {
bat 'python scripts\\send-ci-email.py'
}
}
}
echo "已发送报告邮件至 ${to}"
} catch (err) {
echo "邮件发送失败:${err}"
}
}
def runCi() {
if (isUnix()) {
sh 'node scripts/run-ci.mjs --suite="$SUITE" --shards="$SHARDS"'
} else {
bat '''
set PATH=C:\\Program Files\\nodejs;%PATH%
node scripts\\run-ci-win.mjs --suite=%SUITE% --shards=%SHARDS%
'''
}
}
pipeline {
agent any
options {
timestamps()
disableConcurrentBuilds()
timeout(time: 2, unit: 'HOURS')
buildDiscarder(logRotator(numToKeepStr: '30', artifactNumToKeepStr: '5'))
}
parameters {
choice(
name: 'SUITE',
choices: ['smoke', 'login', 'vendor', 'admit', 'basic', 'surgery', 'warehouse', 'flow', 'center', 'dept', 'nightly', 'full', 'invoice', 'trace', 'consume', 'recon', 'cost', 'warn', 'pol', 'exc', 'ana', 'ops', 'reag', 'invoice-trace', 'recon-cost', 'extended', 'quarantine'],
description: '日常验证默认 smoke(@smoke);改脚本后可单模块如 vendor / admit'
)
string(name: 'WORKERS', defaultValue: '3', description: '并行文件数,验证环境建议 2~3')
string(name: 'SHARDS', defaultValue: '1', description: '分片数,日常验证保持 1')
string(name: 'RETRIES', defaultValue: '0', description: '用例级重试,保持 0')
booleanParam(name: 'RUN_FULL', defaultValue: false, description: '含 @full 全列排序(验证任务一般不要勾)')
string(name: 'GREP', defaultValue: '', description: '只跑标题匹配的用例,例如 STAT-IES-001 或 @smoke')
booleanParam(name: 'ALLURE', defaultValue: false, description: '额外生成 Allure')
string(name: 'KEEP_ARCHIVES', defaultValue: '0', description: '验证任务建议 0,少占磁盘')
string(name: 'REPORT_EMAIL_TO', defaultValue: '', description: '追加收件人,留空则不发邮件')
string(name: 'REPORT_EMAIL_CC', defaultValue: '', description: '抄送,可选')
}
environment {
CI = '1'
SUITE = "${params.SUITE}"
PW_WORKERS = "${params.WORKERS}"
SHARDS = "${params.SHARDS}"
PW_RETRIES = "${params.RETRIES}"
RUN_FULL = "${params.RUN_FULL ? '1' : '0'}"
PW_GREP = "${params.GREP ?: ''}"
PLAYWRIGHT_BROWSERS_PATH = "${WORKSPACE}/.playwright-browsers"
ALLURE = "${params.ALLURE ? '1' : '0'}"
PW_KEEP_ARCHIVES = "${params.KEEP_ARCHIVES ?: '0'}"
SPD_CREDENTIALS_ID = "${env.SPD_CREDENTIALS_ID ?: 'spd-ui-test-account'}"
JAVA_HOME = "${env.JAVA_HOME ?: 'E:/Program Files/Java/jdk-11'}"
}
stages {
stage('Prepare') {
steps {
script {
if (isUnix()) {
sh '''
set -e
node -v
npm -v
node -e "if (parseInt(process.versions.node.split('.')[0], 10) < 20) process.exit(1)"
'''
} else {
bat '''
node -v
npm -v
node -e "if (parseInt(process.versions.node.split('.')[0], 10) < 20) process.exit(1)"
'''
}
}
}
}
stage('Install') {
steps {
script {
if (isUnix()) {
sh '''
set -e
npm ci
npx playwright install-deps chromium || true
npm run playwright:install
'''
} else {
bat '''
call npm ci
if errorlevel 1 exit /b 1
call npm run playwright:install
if errorlevel 1 exit /b 1
'''
}
}
}
}
stage('Typecheck') {
steps {
script {
if (isUnix()) {
sh 'npm run typecheck'
} else {
bat 'call npm run typecheck'
}
}
}
}
stage('Test') {
steps {
script {
if (env.TEST_USER?.trim() || fileExists('.env')) {
runCi()
} else {
withCredentials([
usernamePassword(
credentialsId: env.SPD_CREDENTIALS_ID,
usernameVariable: 'TEST_USER',
passwordVariable: 'TEST_PASS'
)
]) {
runCi()
}
}
}
}
}
}
post {
always {
script {
try {
junit allowEmptyResults: true, testResults: 'reports/latest/junit.xml'
} catch (err) {
echo "跳过 JUnit:${err}"
}
try {
archiveArtifacts artifacts: 'reports/latest/**, reports/ci-*/summary.json, reports/ci-*/meta.txt', allowEmptyArchive: true, fingerprint: true
} catch (err) {
echo "跳过归档:${err}"
}
try {
if (fileExists('reports/latest/summary.json')) {
def descScript = 'node -e "var s=require(\'./reports/latest/summary.json\'); var c=s.counts||{}; console.log(s.suite+\' fail=\'+(c.failures||0)+\'/\'+(c.tests||0))"'
def desc = isUnix()
? sh(script: descScript, returnStdout: true).trim()
: bat(script: "@echo off\n${descScript}", returnStdout: true).trim()
currentBuild.description = desc
}
} catch (err) {
echo "跳过构建描述:${err}"
}
try {
publishHTML([
allowMissing: true,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: 'reports/latest/playwright-report',
reportFiles: 'index.html',
reportName: 'Playwright Report'
])
} catch (err) {
echo "跳过 HTML 报告:${err}"
}
try {
withEnv(["BUILD_RESULT=${currentBuild.currentResult}"]) {
if (isUnix()) {
sh 'node scripts/ci-email-summary.mjs || true'
} else {
bat returnStatus: true, script: 'node scripts\\ci-email-summary.mjs'
}
}
sendReportEmail()
} catch (err) {
echo "跳过邮件:${err}"
}
}
}
}
}
......@@ -3,7 +3,7 @@
不要选「自由风格」里的 **Execute Windows batch command** 手写一长串命令。
选 **Pipeline**,它会自动读仓库里的 `Jenkinsfile`。流水线已改成 **Windows 的 cmd**,不用 Git Bash。
首页原来的 `spd3.0`、电解任务不要动。只新建 `spd-ui-smoke`。
首页原来的 `spd3.0`、电解任务不要动。日常验证新建 **`spd-ui-verify`**(见 [jenkins-job-spd-ui-verify.md](./jenkins-job-spd-ui-verify.md));定时冒烟可另建 `spd-ui-smoke`。
---
......
# Jenkins 任务:spd-ui-verify(平时验证脚本)
用于**手工触发**的冒烟/单模块验证,与夜间 `SPD`(full/nightly)分离。
**不要**给本任务配 Build periodically(定时);需要时再点 **Build with Parameters**。
## 1. 新建任务(约 3 分钟)
1. Jenkins 首页 → **新建 Item**
2. 名称:`spd-ui-verify`(勿与现有 `spd-ui-smoke`、`SPD` 重名)
3. 类型:**Pipeline** → **OK**
4. 拉到最下方 **Pipeline**:
| 项 | 值 |
|----|-----|
| Definition | Pipeline script from SCM |
| SCM | Git |
| Repository URL | `http://10.17.65.20/gitlab/test/autotest-playwright-web-spd.git` |
| Credentials | `gitlab-spd-ui`(与现有 SPD 任务相同) |
| Branch | `*/main` |
| Script Path | **`Jenkinsfile.verify`**(不是 `Jenkinsfile`) |
5. **不要**勾选 Build periodically
6. 建议勾选 **This project is parameterized**(首次构建后 Jenkins 会从流水线读参数,可不动)
7. **Save**
## 2. 第一次运行
1. 点 **Build with Parameters**
2. 默认即可:
- **SUITE** = `smoke`(`@smoke` 用例,约数十分钟级)
- **WORKERS** = `3`
- **SHARDS** = `1`
- **RUN_FULL** = 不勾
- **KEEP_ARCHIVES** = `0`(少占盘)
3. 构建完成后打开左侧 **Playwright Report**,看通过/失败与 trace
## 3. 常见用法
| 场景 | 参数 |
|------|------|
| 改完框架/公共 PO 后快速回归 | `SUITE=smoke` |
| 只验证登录 | `SUITE=login` |
| 只验证厂商模块 | `SUITE=vendor` |
| 只跑某条用例标题 | `GREP=STAT-IES-001` 或 `GREP=@smoke` |
| 本地改 spec 已 push 到 main | 直接 Build,拉最新 main |
本地等价:
```bash
npm run test:ci:smoke
# 或
SUITE=vendor PW_WORKERS=2 npm run test:ci
```
## 4. 凭据与节点
与 [Jenkins配置指南.md](./Jenkins配置指南.md) 相同:
- GitLab:`gitlab-spd-ui`
- SPD 账号:`spd-ui-test-account`(`TEST_USER` / `TEST_PASS`)
- Node **20+**,Windows 节点用 cmd(流水线已适配)
## 5. 与 temp / main 的关系
- Jenkins **只拉 `main`**(正式自动化仓库)
- 在 `temp` 开发时:先合 selective 到 `main`,或临时把 Branch 改成 `*/temp` 做试验(勿长期这样)
## 6. 和 spd-ui-smoke 的区别
| 任务 | Script Path | 默认套件 | 定时 |
|------|-------------|----------|------|
| `spd-ui-smoke` | `Jenkinsfile` | 需在参数里选 smoke | 可配工作时段 cron |
| **`spd-ui-verify`** | **`Jenkinsfile.verify`** | **默认 smoke** | **仅手工** |
两者可并存:smoke 负责定时巡检,verify 负责你 push 后随手点一次。
......@@ -8,6 +8,7 @@
| 任务 | 套件 | 建议 cron | 说明 |
|------|------|-----------|------|
| **`spd-ui-verify`** | **`smoke`(默认)** | **无(仅手工)** | **平时验证脚本**:Script Path 用 `Jenkinsfile.verify`,见 [jenkins-job-spd-ui-verify.md](./jenkins-job-spd-ui-verify.md) |
| `spd-ui-smoke` | `smoke` | `H 8-18 * * 1-5` | 工作时段冒烟:报表与基础设置中已打 `@smoke` 的用例(含厂商、准入、基础管理、手术、库房、流程) |
| `spd-ui-nightly` / `SPD` | `nightly` / `full` | `H 23 * * 1,3,5` | 周一、三、五 **23 点** 回归(Jenkins 任务 `SPD` 当前按此时段跑 full) |
| `spd-ui-full` | `full` | `H 3 * * 0` | 周末含全列排序,最慢 |
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment