qa-deck-backend 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +6 -0
- package/README.md +126 -0
- package/bin/cli.js +36 -0
- package/dashboard/index.html +1222 -0
- package/dashboard/recorder.html +1359 -0
- package/package.json +23 -0
- package/recorder/cicd.js +760 -0
- package/recorder/converter.js +539 -0
- package/recorder/recorder.js +294 -0
- package/server.js +4616 -0
package/recorder/cicd.js
ADDED
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QA Deck — CI/CD Config Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates production-ready CI/CD pipeline configs:
|
|
5
|
+
* - GitHub Actions (.github/workflows/qa-tests.yml)
|
|
6
|
+
* - Jenkins (Jenkinsfile)
|
|
7
|
+
*
|
|
8
|
+
* Supports all 4 frameworks × multiple options:
|
|
9
|
+
* - Browser: chromium / firefox / webkit (Playwright) | chrome / firefox (Selenium)
|
|
10
|
+
* - Parallelism: matrix / sharding
|
|
11
|
+
* - Reporting: JUnit XML, HTML, Allure
|
|
12
|
+
* - Environments: staging / production, PR triggers
|
|
13
|
+
* - Notifications: Slack, email
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// ─── Main entry ───────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
function generateCICD(options = {}) {
|
|
19
|
+
const {
|
|
20
|
+
framework = "selenium-python",
|
|
21
|
+
projectName = "qa-deck",
|
|
22
|
+
pageType = "page",
|
|
23
|
+
baseUrl = "https://staging.example.com",
|
|
24
|
+
browsers = ["chromium"],
|
|
25
|
+
parallel = false,
|
|
26
|
+
reporters = ["html", "junit"],
|
|
27
|
+
slackWebhook = false,
|
|
28
|
+
emailNotify = false,
|
|
29
|
+
branches = ["main", "develop"],
|
|
30
|
+
prTrigger = true,
|
|
31
|
+
testCaseCount = 10,
|
|
32
|
+
useAllure = false,
|
|
33
|
+
nodeVersion = "20",
|
|
34
|
+
pythonVersion = "3.11",
|
|
35
|
+
javaVersion = "17",
|
|
36
|
+
} = options;
|
|
37
|
+
|
|
38
|
+
const ctx = {
|
|
39
|
+
framework,
|
|
40
|
+
projectName: slugify(projectName),
|
|
41
|
+
pageType,
|
|
42
|
+
baseUrl,
|
|
43
|
+
browsers,
|
|
44
|
+
parallel,
|
|
45
|
+
reporters,
|
|
46
|
+
slackWebhook,
|
|
47
|
+
emailNotify,
|
|
48
|
+
branches,
|
|
49
|
+
prTrigger,
|
|
50
|
+
testCaseCount,
|
|
51
|
+
useAllure,
|
|
52
|
+
nodeVersion,
|
|
53
|
+
pythonVersion,
|
|
54
|
+
javaVersion,
|
|
55
|
+
isPlaywright: framework.startsWith("playwright"),
|
|
56
|
+
isPython: framework.includes("python"),
|
|
57
|
+
isJava: framework.includes("java"),
|
|
58
|
+
isTypeScript: framework.includes("typescript"),
|
|
59
|
+
ext: framework.includes("java") ? "java" : framework.includes("typescript") ? "ts" : "py",
|
|
60
|
+
runner: getRunner(framework),
|
|
61
|
+
installCmd: getInstallCmd(framework),
|
|
62
|
+
testCmd: getTestCmd(framework, parallel, reporters, useAllure),
|
|
63
|
+
cacheKey: getCacheKey(framework),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
githubActions: {
|
|
68
|
+
filename: ".github/workflows/qa-tests.yml",
|
|
69
|
+
content: generateGitHubActions(ctx),
|
|
70
|
+
},
|
|
71
|
+
jenkins: {
|
|
72
|
+
filename: "Jenkinsfile",
|
|
73
|
+
content: generateJenkinsfile(ctx),
|
|
74
|
+
},
|
|
75
|
+
dockerCompose: {
|
|
76
|
+
filename: "docker-compose.ci.yml",
|
|
77
|
+
content: generateDockerCompose(ctx),
|
|
78
|
+
},
|
|
79
|
+
makefileTargets: {
|
|
80
|
+
filename: "Makefile",
|
|
81
|
+
content: generateMakefile(ctx),
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─── GitHub Actions ───────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
function generateGitHubActions(ctx) {
|
|
89
|
+
const { framework, projectName, baseUrl, browsers, parallel, branches, prTrigger,
|
|
90
|
+
slackWebhook, emailNotify, useAllure, nodeVersion, pythonVersion, javaVersion,
|
|
91
|
+
isPlaywright, isPython, isJava, isTypeScript, testCmd, reporters } = ctx;
|
|
92
|
+
|
|
93
|
+
const branchList = branches.map(b => ` - ${b}`).join("\n");
|
|
94
|
+
const browserMatrix = parallel && browsers.length > 1
|
|
95
|
+
? ` strategy:
|
|
96
|
+
fail-fast: false
|
|
97
|
+
matrix:
|
|
98
|
+
browser: [${browsers.map(b => `"${b}"`).join(", ")}]`
|
|
99
|
+
: "";
|
|
100
|
+
|
|
101
|
+
const browserEnv = parallel && browsers.length > 1
|
|
102
|
+
? ` BROWSER: \${{ matrix.browser }}`
|
|
103
|
+
: ` BROWSER: ${browsers[0]}`;
|
|
104
|
+
|
|
105
|
+
const setupSteps = isJava ? `
|
|
106
|
+
- name: Set up JDK ${javaVersion}
|
|
107
|
+
uses: actions/setup-java@v4
|
|
108
|
+
with:
|
|
109
|
+
java-version: '${javaVersion}'
|
|
110
|
+
distribution: 'temurin'
|
|
111
|
+
cache: maven
|
|
112
|
+
|
|
113
|
+
- name: Cache Maven packages
|
|
114
|
+
uses: actions/cache@v4
|
|
115
|
+
with:
|
|
116
|
+
path: ~/.m2
|
|
117
|
+
key: \${{ runner.os }}-m2-\${{ hashFiles('**/pom.xml') }}
|
|
118
|
+
restore-keys: \${{ runner.os }}-m2` :
|
|
119
|
+
isTypeScript ? `
|
|
120
|
+
- name: Set up Node.js ${nodeVersion}
|
|
121
|
+
uses: actions/setup-node@v4
|
|
122
|
+
with:
|
|
123
|
+
node-version: '${nodeVersion}'
|
|
124
|
+
cache: 'npm'
|
|
125
|
+
|
|
126
|
+
- name: Install dependencies
|
|
127
|
+
run: npm ci
|
|
128
|
+
|
|
129
|
+
- name: Install Playwright browsers
|
|
130
|
+
run: npx playwright install --with-deps ${browsers.join(" ")}` :
|
|
131
|
+
/* Python */ `
|
|
132
|
+
- name: Set up Python ${pythonVersion}
|
|
133
|
+
uses: actions/setup-python@v5
|
|
134
|
+
with:
|
|
135
|
+
python-version: '${pythonVersion}'
|
|
136
|
+
|
|
137
|
+
- name: Cache pip packages
|
|
138
|
+
uses: actions/cache@v4
|
|
139
|
+
with:
|
|
140
|
+
path: ~/.cache/pip
|
|
141
|
+
key: \${{ runner.os }}-pip-\${{ hashFiles('**/requirements.txt') }}
|
|
142
|
+
restore-keys: \${{ runner.os }}-pip-
|
|
143
|
+
|
|
144
|
+
- name: Install dependencies
|
|
145
|
+
run: pip install -r requirements.txt${isPlaywright ? "\n\n - name: Install Playwright browsers\n run: playwright install --with-deps " + browsers.join(" ") : ""}`;
|
|
146
|
+
|
|
147
|
+
const testStep = isJava ? `
|
|
148
|
+
- name: Run tests
|
|
149
|
+
env:
|
|
150
|
+
BASE_URL: \${{ env.BASE_URL }}
|
|
151
|
+
${browserEnv}
|
|
152
|
+
run: mvn test -Dsurefire.failIfNoSpecifiedTests=false
|
|
153
|
+
|
|
154
|
+
- name: Publish Test Results
|
|
155
|
+
uses: EnricoMi/publish-unit-test-result-action@v2
|
|
156
|
+
if: always()
|
|
157
|
+
with:
|
|
158
|
+
files: target/surefire-reports/*.xml` :
|
|
159
|
+
isTypeScript ? `
|
|
160
|
+
- name: Run Playwright tests
|
|
161
|
+
env:
|
|
162
|
+
BASE_URL: \${{ env.BASE_URL }}
|
|
163
|
+
${browserEnv}
|
|
164
|
+
run: ${testCmd}
|
|
165
|
+
|
|
166
|
+
- name: Upload Playwright report
|
|
167
|
+
uses: actions/upload-artifact@v4
|
|
168
|
+
if: always()
|
|
169
|
+
with:
|
|
170
|
+
name: playwright-report-\${{ matrix.browser || '${browsers[0]}' }}
|
|
171
|
+
path: playwright-report/
|
|
172
|
+
retention-days: 30` :
|
|
173
|
+
/* Python */ `
|
|
174
|
+
- name: Run tests
|
|
175
|
+
env:
|
|
176
|
+
BASE_URL: \${{ env.BASE_URL }}
|
|
177
|
+
${browserEnv}
|
|
178
|
+
run: ${testCmd}
|
|
179
|
+
|
|
180
|
+
- name: Upload test results
|
|
181
|
+
uses: actions/upload-artifact@v4
|
|
182
|
+
if: always()
|
|
183
|
+
with:
|
|
184
|
+
name: test-results-\${{ matrix.browser || '${browsers[0]}' }}
|
|
185
|
+
path: |
|
|
186
|
+
reports/
|
|
187
|
+
test-results.xml
|
|
188
|
+
retention-days: 30`;
|
|
189
|
+
|
|
190
|
+
const allureSteps = useAllure ? `
|
|
191
|
+
- name: Generate Allure report
|
|
192
|
+
uses: simple-elf/allure-report-action@master
|
|
193
|
+
if: always()
|
|
194
|
+
with:
|
|
195
|
+
allure_results: allure-results
|
|
196
|
+
allure_history: allure-history
|
|
197
|
+
|
|
198
|
+
- name: Deploy Allure report to GitHub Pages
|
|
199
|
+
uses: peaceiris/actions-gh-pages@v3
|
|
200
|
+
if: always()
|
|
201
|
+
with:
|
|
202
|
+
github_token: \${{ secrets.GITHUB_TOKEN }}
|
|
203
|
+
publish_branch: gh-pages
|
|
204
|
+
publish_dir: allure-history` : "";
|
|
205
|
+
|
|
206
|
+
const slackStep = slackWebhook ? `
|
|
207
|
+
- name: Notify Slack on failure
|
|
208
|
+
uses: 8398a7/action-slack@v3
|
|
209
|
+
if: failure()
|
|
210
|
+
with:
|
|
211
|
+
status: \${{ job.status }}
|
|
212
|
+
fields: repo,message,commit,author,action,eventName,ref,workflow
|
|
213
|
+
text: ':x: QA tests failed on \`\${{ github.ref_name }}\`'
|
|
214
|
+
env:
|
|
215
|
+
SLACK_WEBHOOK_URL: \${{ secrets.SLACK_WEBHOOK_URL }}` : "";
|
|
216
|
+
|
|
217
|
+
const emailStep = emailNotify ? `
|
|
218
|
+
- name: Send email on failure
|
|
219
|
+
uses: dawidd6/action-send-mail@v3
|
|
220
|
+
if: failure()
|
|
221
|
+
with:
|
|
222
|
+
server_address: smtp.gmail.com
|
|
223
|
+
server_port: 465
|
|
224
|
+
username: \${{ secrets.MAIL_USERNAME }}
|
|
225
|
+
password: \${{ secrets.MAIL_PASSWORD }}
|
|
226
|
+
subject: "QA Tests Failed — \${{ github.repository }} (\${{ github.ref_name }})"
|
|
227
|
+
body: "Test suite failed on commit \${{ github.sha }}. View run: \${{ github.server_url }}/\${{ github.repository }}/actions/runs/\${{ github.run_id }}"
|
|
228
|
+
to: \${{ secrets.NOTIFY_EMAIL }}
|
|
229
|
+
from: QA Deck` : "";
|
|
230
|
+
|
|
231
|
+
const prSection = prTrigger ? `
|
|
232
|
+
pull_request:
|
|
233
|
+
branches:
|
|
234
|
+
${branchList}
|
|
235
|
+
types: [opened, synchronize, reopened]` : "";
|
|
236
|
+
|
|
237
|
+
return `# QA Deck — Generated GitHub Actions Workflow
|
|
238
|
+
# Framework: ${framework}
|
|
239
|
+
# Generated: ${new Date().toISOString()}
|
|
240
|
+
#
|
|
241
|
+
# Required secrets:
|
|
242
|
+
# BASE_URL_STAGING — staging environment URL${slackWebhook ? "\n# SLACK_WEBHOOK_URL — Slack incoming webhook" : ""}${emailNotify ? "\n# MAIL_USERNAME, MAIL_PASSWORD, NOTIFY_EMAIL — email alerts" : ""}
|
|
243
|
+
|
|
244
|
+
name: QA Tests — ${projectName}
|
|
245
|
+
|
|
246
|
+
on:
|
|
247
|
+
push:
|
|
248
|
+
branches:
|
|
249
|
+
${branchList}
|
|
250
|
+
${prSection}
|
|
251
|
+
workflow_dispatch:
|
|
252
|
+
inputs:
|
|
253
|
+
environment:
|
|
254
|
+
description: 'Target environment'
|
|
255
|
+
required: true
|
|
256
|
+
default: 'staging'
|
|
257
|
+
type: choice
|
|
258
|
+
options: [staging, production]
|
|
259
|
+
browser:
|
|
260
|
+
description: 'Browser to test'
|
|
261
|
+
required: false
|
|
262
|
+
default: '${browsers[0]}'
|
|
263
|
+
type: choice
|
|
264
|
+
options: [${isPlaywright ? "chromium, firefox, webkit" : "chrome, firefox"}]
|
|
265
|
+
|
|
266
|
+
env:
|
|
267
|
+
BASE_URL: \${{ secrets.BASE_URL_STAGING || '${baseUrl}' }}
|
|
268
|
+
CI: true
|
|
269
|
+
|
|
270
|
+
jobs:
|
|
271
|
+
qa-tests:
|
|
272
|
+
name: QA Tests${parallel && browsers.length > 1 ? " (${{ matrix.browser }})" : ""}
|
|
273
|
+
runs-on: ubuntu-latest
|
|
274
|
+
timeout-minutes: 30
|
|
275
|
+
${browserMatrix}
|
|
276
|
+
|
|
277
|
+
steps:
|
|
278
|
+
- name: Checkout repository
|
|
279
|
+
uses: actions/checkout@v4
|
|
280
|
+
|
|
281
|
+
- name: Set up environment variables
|
|
282
|
+
run: |
|
|
283
|
+
echo "RUN_ID=\${{ github.run_id }}" >> \$GITHUB_ENV
|
|
284
|
+
echo "COMMIT_SHA=\${{ github.sha }}" >> \$GITHUB_ENV
|
|
285
|
+
echo "BRANCH_NAME=\${{ github.ref_name }}" >> \$GITHUB_ENV
|
|
286
|
+
${setupSteps}
|
|
287
|
+
${testStep}
|
|
288
|
+
${allureSteps}
|
|
289
|
+
${slackStep}
|
|
290
|
+
${emailStep}
|
|
291
|
+
|
|
292
|
+
# ── Summary job — runs after all matrix jobs complete ──────────────────────
|
|
293
|
+
summary:
|
|
294
|
+
name: Test Summary
|
|
295
|
+
runs-on: ubuntu-latest
|
|
296
|
+
needs: qa-tests
|
|
297
|
+
if: always()
|
|
298
|
+
steps:
|
|
299
|
+
- name: Check result
|
|
300
|
+
run: |
|
|
301
|
+
if [ "\${{ needs.qa-tests.result }}" = "success" ]; then
|
|
302
|
+
echo "✅ All QA tests passed"
|
|
303
|
+
else
|
|
304
|
+
echo "❌ QA tests failed — result: \${{ needs.qa-tests.result }}"
|
|
305
|
+
exit 1
|
|
306
|
+
fi
|
|
307
|
+
`.trimStart();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ─── Jenkinsfile ──────────────────────────────────────────────────────────────
|
|
311
|
+
|
|
312
|
+
function generateJenkinsfile(ctx) {
|
|
313
|
+
const { framework, projectName, baseUrl, browsers, parallel, branches, slackWebhook,
|
|
314
|
+
emailNotify, useAllure, nodeVersion, pythonVersion, javaVersion,
|
|
315
|
+
isPlaywright, isPython, isJava, isTypeScript, testCmd } = ctx;
|
|
316
|
+
|
|
317
|
+
const agentLabel = isJava ? "java-agent" : "python-agent";
|
|
318
|
+
|
|
319
|
+
const toolBlock = isJava ? `
|
|
320
|
+
jdk 'JDK-${javaVersion}'
|
|
321
|
+
maven 'Maven-3.9'` :
|
|
322
|
+
isTypeScript ? `
|
|
323
|
+
nodejs 'Node-${nodeVersion}'` :
|
|
324
|
+
`
|
|
325
|
+
// Python available via system or pyenv`;
|
|
326
|
+
|
|
327
|
+
const installStage = isJava ? `
|
|
328
|
+
stage('Install Dependencies') {
|
|
329
|
+
steps {
|
|
330
|
+
sh 'mvn dependency:resolve -q'
|
|
331
|
+
}
|
|
332
|
+
}` :
|
|
333
|
+
isTypeScript ? `
|
|
334
|
+
stage('Install Dependencies') {
|
|
335
|
+
steps {
|
|
336
|
+
sh 'npm ci'
|
|
337
|
+
sh 'npx playwright install --with-deps ${browsers.join(" ")}'
|
|
338
|
+
}
|
|
339
|
+
}` :
|
|
340
|
+
`
|
|
341
|
+
stage('Install Dependencies') {
|
|
342
|
+
steps {
|
|
343
|
+
sh '''
|
|
344
|
+
python${pythonVersion.split('.')[0]} -m venv .venv
|
|
345
|
+
. .venv/bin/activate
|
|
346
|
+
pip install --upgrade pip -q
|
|
347
|
+
pip install -r requirements.txt -q${isPlaywright ? "\n playwright install --with-deps " + browsers.join(" ") : ""}
|
|
348
|
+
'''
|
|
349
|
+
}
|
|
350
|
+
}`;
|
|
351
|
+
|
|
352
|
+
const testStage = isJava ? `
|
|
353
|
+
stage('Run Tests') {
|
|
354
|
+
steps {
|
|
355
|
+
script {
|
|
356
|
+
try {
|
|
357
|
+
sh """
|
|
358
|
+
mvn test \\
|
|
359
|
+
-DBASE_URL=\${BASE_URL} \\
|
|
360
|
+
-DBROWSER=\${BROWSER}
|
|
361
|
+
"""
|
|
362
|
+
} catch (err) {
|
|
363
|
+
currentBuild.result = 'UNSTABLE'
|
|
364
|
+
throw err
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
post {
|
|
369
|
+
always {
|
|
370
|
+
junit 'target/surefire-reports/*.xml'
|
|
371
|
+
archiveArtifacts artifacts: 'target/surefire-reports/**', allowEmptyArchive: true
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}` :
|
|
375
|
+
isTypeScript ? `
|
|
376
|
+
stage('Run Tests') {
|
|
377
|
+
steps {
|
|
378
|
+
script {
|
|
379
|
+
try {
|
|
380
|
+
sh """
|
|
381
|
+
${testCmd} \\
|
|
382
|
+
--reporter=junit \\
|
|
383
|
+
--output-dir=test-results
|
|
384
|
+
"""
|
|
385
|
+
} catch (err) {
|
|
386
|
+
currentBuild.result = 'UNSTABLE'
|
|
387
|
+
throw err
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
post {
|
|
392
|
+
always {
|
|
393
|
+
junit 'test-results/*.xml'
|
|
394
|
+
publishHTML([
|
|
395
|
+
allowMissing: true,
|
|
396
|
+
alwaysLinkToLastBuild: true,
|
|
397
|
+
keepAll: true,
|
|
398
|
+
reportDir: 'playwright-report',
|
|
399
|
+
reportFiles: 'index.html',
|
|
400
|
+
reportName: 'Playwright Report'
|
|
401
|
+
])
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}` :
|
|
405
|
+
`
|
|
406
|
+
stage('Run Tests') {
|
|
407
|
+
steps {
|
|
408
|
+
script {
|
|
409
|
+
try {
|
|
410
|
+
sh """
|
|
411
|
+
. .venv/bin/activate
|
|
412
|
+
${testCmd}
|
|
413
|
+
"""
|
|
414
|
+
} catch (err) {
|
|
415
|
+
currentBuild.result = 'UNSTABLE'
|
|
416
|
+
throw err
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
post {
|
|
421
|
+
always {
|
|
422
|
+
junit 'reports/junit.xml'
|
|
423
|
+
publishHTML([
|
|
424
|
+
allowMissing: true,
|
|
425
|
+
alwaysLinkToLastBuild: true,
|
|
426
|
+
keepAll: true,
|
|
427
|
+
reportDir: 'reports/html',
|
|
428
|
+
reportFiles: 'index.html',
|
|
429
|
+
reportName: 'Test Report'
|
|
430
|
+
])
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}`;
|
|
434
|
+
|
|
435
|
+
const allureStage = useAllure ? `
|
|
436
|
+
stage('Allure Report') {
|
|
437
|
+
steps {
|
|
438
|
+
allure([
|
|
439
|
+
includeProperties: false,
|
|
440
|
+
jdk: '',
|
|
441
|
+
properties: [],
|
|
442
|
+
reportBuildPolicy: 'ALWAYS',
|
|
443
|
+
results: [[path: 'allure-results']]
|
|
444
|
+
])
|
|
445
|
+
}
|
|
446
|
+
}` : "";
|
|
447
|
+
|
|
448
|
+
const notifyBlock = slackWebhook ? `
|
|
449
|
+
failure {
|
|
450
|
+
slackSend(
|
|
451
|
+
channel: '#qa-alerts',
|
|
452
|
+
color: 'danger',
|
|
453
|
+
message: """❌ *QA Tests FAILED*
|
|
454
|
+
Pipeline: \${env.JOB_NAME} #\${env.BUILD_NUMBER}
|
|
455
|
+
Branch: \${env.BRANCH_NAME}
|
|
456
|
+
URL: \${env.BUILD_URL}"""
|
|
457
|
+
)
|
|
458
|
+
}
|
|
459
|
+
success {
|
|
460
|
+
slackSend(
|
|
461
|
+
channel: '#qa-alerts',
|
|
462
|
+
color: 'good',
|
|
463
|
+
message: "✅ QA Tests passed — \${env.JOB_NAME} #\${env.BUILD_NUMBER}"
|
|
464
|
+
)
|
|
465
|
+
}` :
|
|
466
|
+
emailNotify ? `
|
|
467
|
+
failure {
|
|
468
|
+
emailext(
|
|
469
|
+
subject: "QA Tests FAILED — \${env.JOB_NAME} #\${env.BUILD_NUMBER}",
|
|
470
|
+
body: """<h3>QA Test Failure</h3>
|
|
471
|
+
<p>Pipeline: <a href="\${env.BUILD_URL}">\${env.JOB_NAME} #\${env.BUILD_NUMBER}</a></p>
|
|
472
|
+
<p>Branch: \${env.BRANCH_NAME}</p>""",
|
|
473
|
+
mimeType: 'text/html',
|
|
474
|
+
recipientProviders: [[$class: 'DevelopersRecipientProvider'], [$class: 'RequesterRecipientProvider']]
|
|
475
|
+
)
|
|
476
|
+
}` : `
|
|
477
|
+
failure {
|
|
478
|
+
echo 'Tests failed — check console output for details'
|
|
479
|
+
}`;
|
|
480
|
+
|
|
481
|
+
const parallelBrowsers = parallel && browsers.length > 1 ? `
|
|
482
|
+
stage('Cross-Browser Tests') {
|
|
483
|
+
parallel {
|
|
484
|
+
${browsers.map(b => `stage('${b}') {
|
|
485
|
+
steps {
|
|
486
|
+
script {
|
|
487
|
+
env.BROWSER = '${b}'
|
|
488
|
+
}
|
|
489
|
+
sh 'echo "Running on browser: ${b}"'
|
|
490
|
+
}
|
|
491
|
+
}`).join("\n ")}
|
|
492
|
+
}
|
|
493
|
+
}` : "";
|
|
494
|
+
|
|
495
|
+
return `// QA Deck — Generated Jenkinsfile
|
|
496
|
+
// Framework: ${framework}
|
|
497
|
+
// Generated: ${new Date().toISOString()}
|
|
498
|
+
//
|
|
499
|
+
// Required Jenkins plugins:
|
|
500
|
+
// - Pipeline
|
|
501
|
+
// - HTML Publisher
|
|
502
|
+
// - JUnit
|
|
503
|
+
${useAllure ? "// - Allure Jenkins Plugin" : ""}
|
|
504
|
+
${slackWebhook ? "// - Slack Notification" : ""}
|
|
505
|
+
${isTypeScript || isJava ? `// - NodeJS Plugin (Node ${nodeVersion})` : ""}
|
|
506
|
+
//
|
|
507
|
+
// Required Jenkins credentials:
|
|
508
|
+
// - BASE_URL — environment variable with target URL
|
|
509
|
+
${slackWebhook ? "// - SLACK_TOKEN — Slack bot token" : ""}
|
|
510
|
+
|
|
511
|
+
pipeline {
|
|
512
|
+
agent {
|
|
513
|
+
label '${agentLabel}'
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
tools {${toolBlock}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
options {
|
|
520
|
+
timeout(time: 30, unit: 'MINUTES')
|
|
521
|
+
buildDiscarder(logRotator(numToKeepStr: '20'))
|
|
522
|
+
disableConcurrentBuilds()
|
|
523
|
+
timestamps()
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
triggers {
|
|
527
|
+
// Poll SCM every 15 minutes (or use webhooks)
|
|
528
|
+
pollSCM('H/15 * * * *')
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
parameters {
|
|
532
|
+
choice(
|
|
533
|
+
name: 'ENVIRONMENT',
|
|
534
|
+
choices: ['staging', 'production'],
|
|
535
|
+
description: 'Target environment'
|
|
536
|
+
)
|
|
537
|
+
choice(
|
|
538
|
+
name: 'BROWSER',
|
|
539
|
+
choices: [${isPlaywright ?
|
|
540
|
+
"'chromium', 'firefox', 'webkit'" :
|
|
541
|
+
"'chrome', 'firefox'"}],
|
|
542
|
+
description: 'Browser to run tests in'
|
|
543
|
+
)
|
|
544
|
+
booleanParam(
|
|
545
|
+
name: 'RUN_SMOKE_ONLY',
|
|
546
|
+
defaultValue: false,
|
|
547
|
+
description: 'Run only smoke tests (faster)'
|
|
548
|
+
)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
environment {
|
|
552
|
+
BASE_URL = credentials('BASE_URL') ?: '${baseUrl}'
|
|
553
|
+
BROWSER = "\${params.BROWSER}"
|
|
554
|
+
CI = 'true'
|
|
555
|
+
REPORT_DIR = "reports/\${BUILD_NUMBER}"
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
stages {
|
|
559
|
+
stage('Checkout') {
|
|
560
|
+
steps {
|
|
561
|
+
checkout scm
|
|
562
|
+
sh 'git log --oneline -5'
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
${installStage}
|
|
566
|
+
${parallelBrowsers}
|
|
567
|
+
${testStage}
|
|
568
|
+
${allureStage}
|
|
569
|
+
|
|
570
|
+
stage('Archive Results') {
|
|
571
|
+
steps {
|
|
572
|
+
archiveArtifacts(
|
|
573
|
+
artifacts: 'reports/**,${isTypeScript ? "playwright-report/**" : ""}',
|
|
574
|
+
allowEmptyArchive: true,
|
|
575
|
+
fingerprint: true
|
|
576
|
+
)
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
post {
|
|
582
|
+
always {
|
|
583
|
+
cleanWs(cleanWhenNotBuilt: false, cleanWhenAborted: true, cleanWhenFailure: false)
|
|
584
|
+
}
|
|
585
|
+
${notifyBlock}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
`;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// ─── Docker Compose for local CI simulation ───────────────────────────────────
|
|
592
|
+
|
|
593
|
+
function generateDockerCompose(ctx) {
|
|
594
|
+
const { framework, baseUrl, browsers, isPlaywright, isJava, isTypeScript, pythonVersion, nodeVersion, javaVersion } = ctx;
|
|
595
|
+
|
|
596
|
+
const serviceImage = isJava
|
|
597
|
+
? `maven:3.9-eclipse-temurin-${javaVersion}`
|
|
598
|
+
: isTypeScript
|
|
599
|
+
? `mcr.microsoft.com/playwright:v1.40.0-jammy`
|
|
600
|
+
: isPlaywright
|
|
601
|
+
? `mcr.microsoft.com/playwright/python:v1.40.0-jammy`
|
|
602
|
+
: `python:${pythonVersion}-slim`;
|
|
603
|
+
|
|
604
|
+
const installCmd = isJava
|
|
605
|
+
? "mvn dependency:resolve -q"
|
|
606
|
+
: isTypeScript
|
|
607
|
+
? "npm ci && npx playwright install"
|
|
608
|
+
: `pip install -r requirements.txt -q${isPlaywright ? " && playwright install" : ""}`;
|
|
609
|
+
|
|
610
|
+
const testCmd = isJava
|
|
611
|
+
? "mvn test"
|
|
612
|
+
: isTypeScript
|
|
613
|
+
? "npx playwright test --reporter=html"
|
|
614
|
+
: isPlaywright
|
|
615
|
+
? "pytest tests/ -v --html=reports/report.html --self-contained-html"
|
|
616
|
+
: "pytest tests/ -v --html=reports/report.html --self-contained-html";
|
|
617
|
+
|
|
618
|
+
return `# QA Deck — Docker Compose for local CI simulation
|
|
619
|
+
# Usage: docker-compose -f docker-compose.ci.yml up --abort-on-container-exit
|
|
620
|
+
|
|
621
|
+
version: '3.8'
|
|
622
|
+
|
|
623
|
+
services:
|
|
624
|
+
qa-tests:
|
|
625
|
+
image: ${serviceImage}
|
|
626
|
+
working_dir: /app
|
|
627
|
+
volumes:
|
|
628
|
+
- .:/app
|
|
629
|
+
- /app/${isJava ? ".m2" : isTypeScript ? "node_modules" : ".venv"}
|
|
630
|
+
environment:
|
|
631
|
+
- BASE_URL=${baseUrl}
|
|
632
|
+
- BROWSER=${browsers[0]}
|
|
633
|
+
- CI=true
|
|
634
|
+
- PYTHONDONTWRITEBYTECODE=1
|
|
635
|
+
command: >
|
|
636
|
+
sh -c "${installCmd} &&
|
|
637
|
+
${testCmd}"
|
|
638
|
+
${!isJava && !isTypeScript && !isPlaywright ? `# For Selenium, you need a browser service:
|
|
639
|
+
depends_on:
|
|
640
|
+
- selenium-hub` : ""}
|
|
641
|
+
|
|
642
|
+
${!isJava && !isTypeScript && !isPlaywright ? ` selenium-hub:
|
|
643
|
+
image: selenium/standalone-chrome:latest
|
|
644
|
+
ports:
|
|
645
|
+
- "4444:4444"
|
|
646
|
+
environment:
|
|
647
|
+
- SE_NODE_MAX_SESSIONS=3
|
|
648
|
+
shm_size: '2g'` : ""}
|
|
649
|
+
|
|
650
|
+
volumes:
|
|
651
|
+
${isJava ? "maven-cache" : isTypeScript ? "node-cache" : "pip-cache"}:
|
|
652
|
+
`;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// ─── Makefile ─────────────────────────────────────────────────────────────────
|
|
656
|
+
|
|
657
|
+
function generateMakefile(ctx) {
|
|
658
|
+
const { framework, isJava, isTypeScript, isPlaywright, isPython, browsers, useAllure } = ctx;
|
|
659
|
+
|
|
660
|
+
const installTarget = isJava
|
|
661
|
+
? `install:\n\tmvn dependency:resolve -q`
|
|
662
|
+
: isTypeScript
|
|
663
|
+
? `install:\n\tnpm ci\n\tnpx playwright install --with-deps`
|
|
664
|
+
: `install:\n\tpip install -r requirements.txt${isPlaywright ? "\n\tplaywright install --with-deps" : ""}`;
|
|
665
|
+
|
|
666
|
+
const testTarget = isJava
|
|
667
|
+
? `test:\n\tmvn test\n\ntest-smoke:\n\tmvn test -Dgroups=smoke\n\ntest-regression:\n\tmvn test -Dgroups=regression`
|
|
668
|
+
: isTypeScript
|
|
669
|
+
? `test:\n\tnpx playwright test\n\ntest-smoke:\n\tnpx playwright test --grep @smoke\n\ntest-headed:\n\tnpx playwright test --headed\n\ntest-debug:\n\tnpx playwright test --debug`
|
|
670
|
+
: `test:\n\tpytest tests/ -v\n\ntest-smoke:\n\tpytest tests/ -v -m smoke\n\ntest-parallel:\n\tpytest tests/ -v -n auto\n\ntest-html:\n\tpytest tests/ -v --html=reports/report.html --self-contained-html`;
|
|
671
|
+
|
|
672
|
+
const browserTargets = browsers.map(b =>
|
|
673
|
+
`test-${b}:\n\tBROWSER=${b} ${isTypeScript ? `npx playwright test --project=${b}` : isJava ? `mvn test -DBROWSER=${b}` : `pytest tests/ -v`}`
|
|
674
|
+
).join("\n\n");
|
|
675
|
+
|
|
676
|
+
const allureTargets = useAllure
|
|
677
|
+
? `\nallure-open:\n\tallure serve allure-results\n\nallure-report:\n\tallure generate allure-results --clean -o allure-report`
|
|
678
|
+
: "";
|
|
679
|
+
|
|
680
|
+
return `.PHONY: install test clean help ${browsers.map(b => `test-${b}`).join(" ")}
|
|
681
|
+
|
|
682
|
+
# QA Deck — Makefile
|
|
683
|
+
# Generated: ${new Date().toISOString()}
|
|
684
|
+
|
|
685
|
+
${installTarget}
|
|
686
|
+
|
|
687
|
+
${testTarget}
|
|
688
|
+
|
|
689
|
+
${browserTargets}
|
|
690
|
+
|
|
691
|
+
${allureTargets}
|
|
692
|
+
|
|
693
|
+
clean:
|
|
694
|
+
${isJava ? "\tmvn clean" : isTypeScript ? "\trm -rf test-results/ playwright-report/" : "\trm -rf reports/ .pytest_cache/ __pycache__/"}
|
|
695
|
+
|
|
696
|
+
ci: install test
|
|
697
|
+
|
|
698
|
+
docker-test:
|
|
699
|
+
\tdocker-compose -f docker-compose.ci.yml up --abort-on-container-exit --exit-code-from qa-tests
|
|
700
|
+
|
|
701
|
+
help:
|
|
702
|
+
\t@echo "Available targets:"
|
|
703
|
+
\t@grep -E '^[a-zA-Z_-]+:' Makefile | awk -F: '{print " " $$1}'
|
|
704
|
+
`;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
708
|
+
|
|
709
|
+
function getRunner(framework) {
|
|
710
|
+
const map = {
|
|
711
|
+
"selenium-python": "pytest", "selenium-java": "TestNG",
|
|
712
|
+
"playwright-python": "pytest-playwright", "playwright-typescript": "Playwright Test",
|
|
713
|
+
};
|
|
714
|
+
return map[framework] || "pytest";
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function getInstallCmd(framework) {
|
|
718
|
+
if (framework.includes("java")) return "mvn dependency:resolve";
|
|
719
|
+
if (framework.includes("typescript")) return "npm ci && npx playwright install";
|
|
720
|
+
if (framework.includes("playwright")) return "pip install -r requirements.txt && playwright install";
|
|
721
|
+
return "pip install -r requirements.txt";
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function getTestCmd(framework, parallel, reporters, useAllure) {
|
|
725
|
+
const junitFlag = reporters.includes("junit");
|
|
726
|
+
const htmlFlag = reporters.includes("html");
|
|
727
|
+
const allureFlag = useAllure;
|
|
728
|
+
|
|
729
|
+
if (framework === "playwright-typescript") {
|
|
730
|
+
const flags = ["--reporter=list"];
|
|
731
|
+
if (junitFlag) flags.push("--reporter=junit");
|
|
732
|
+
if (allureFlag) flags.push("'allure-playwright'");
|
|
733
|
+
return `npx playwright test ${flags.join(" ")}`;
|
|
734
|
+
}
|
|
735
|
+
if (framework === "selenium-java" || framework === "playwright-java") {
|
|
736
|
+
return `mvn test${junitFlag ? " -Dsurefire.reportFormat=xml" : ""}`;
|
|
737
|
+
}
|
|
738
|
+
// Python (pytest)
|
|
739
|
+
const args = ["pytest tests/ -v"];
|
|
740
|
+
if (parallel) args.push("-n auto");
|
|
741
|
+
if (junitFlag) args.push("--junit-xml=reports/junit.xml");
|
|
742
|
+
if (htmlFlag) args.push("--html=reports/html/report.html --self-contained-html");
|
|
743
|
+
if (allureFlag) args.push("--alluredir=allure-results");
|
|
744
|
+
return args.join(" ");
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function getCacheKey(framework) {
|
|
748
|
+
if (framework.includes("java")) return "~/.m2/repository";
|
|
749
|
+
if (framework.includes("typescript")) return "~/.npm";
|
|
750
|
+
return "~/.cache/pip";
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function slugify(str) {
|
|
754
|
+
return (str || "project")
|
|
755
|
+
.toLowerCase()
|
|
756
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
757
|
+
.replace(/^-|-$/g, "");
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
module.exports = { generateCICD };
|