@aqa-pulse/cli 0.1.0 → 0.2.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/README.md +58 -18
- package/bin/aqa-pulse.js +4 -0
- package/dist/backend/upload-from-config.d.ts +1 -0
- package/dist/backend/upload-from-config.js +287 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,26 +5,74 @@ CLI для подключения Playwright-проектов к AQA Pulse бе
|
|
|
5
5
|
## Команды
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
+
aqa-pulse upload-from-config --config .aqa-pulse.yml
|
|
8
9
|
aqa-pulse upload-report --generate-source-facts --repo-root "$CI_PROJECT_DIR"
|
|
9
10
|
aqa-pulse merge-reports --project-kind ui --output test-results/dashboard/ui-merged.json test-results/dashboard/ui-*.json
|
|
10
11
|
aqa-pulse generate-source-facts --report test-results/dashboard/data.json --repo-root .
|
|
11
12
|
```
|
|
12
13
|
|
|
14
|
+
## .aqa-pulse.yml
|
|
15
|
+
|
|
16
|
+
Минимальный конфиг для одного готового отчета:
|
|
17
|
+
|
|
18
|
+
```yaml
|
|
19
|
+
projectDir: Playwright
|
|
20
|
+
reportPath: test-results/dashboard/data.json
|
|
21
|
+
repoRoot: .
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Конфиг для merge нескольких отчетов и последующего upload:
|
|
25
|
+
|
|
26
|
+
```yaml
|
|
27
|
+
projectDir: Playwright
|
|
28
|
+
repoRoot: .
|
|
29
|
+
|
|
30
|
+
merge:
|
|
31
|
+
projectKind: ui
|
|
32
|
+
output: test-results/dashboard/ui-merged.json
|
|
33
|
+
allowMissing: true
|
|
34
|
+
inputs:
|
|
35
|
+
- test-results/dashboard/ui-purchase.json
|
|
36
|
+
- test-results/dashboard/ui-cpu.json
|
|
37
|
+
- test-results/dashboard/ui-first.json
|
|
38
|
+
- test-results/dashboard/ui-second.json
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Пути считаются относительно `projectDir`. Если `projectDir` не указан, пути считаются относительно папки, где лежит `.aqa-pulse.yml`.
|
|
42
|
+
|
|
13
43
|
## GitLab CI
|
|
14
44
|
|
|
45
|
+
В GitLab CI/CD Variables храни:
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
AQA_PULSE_BASE_URL
|
|
49
|
+
AQA_PULSE_WORKSPACE_SLUG
|
|
50
|
+
AQA_PULSE_WORKSPACE_API_KEY
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
В `.gitlab-ci.yml` можно подключить общий template из репозитория AQA Pulse:
|
|
54
|
+
|
|
15
55
|
```yaml
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
56
|
+
include:
|
|
57
|
+
- project: 'your-group/AQAPulse'
|
|
58
|
+
ref: main
|
|
59
|
+
file: '/aqa-pulse-server/template/gitlab/aqa-pulse-upload.gitlab-ci.yml'
|
|
60
|
+
|
|
61
|
+
aqa pulse upload:
|
|
62
|
+
extends: .aqa_pulse_upload_from_config
|
|
63
|
+
needs:
|
|
64
|
+
- job: playwright tests
|
|
65
|
+
artifacts: true
|
|
25
66
|
```
|
|
26
67
|
|
|
27
|
-
|
|
68
|
+
Test job должна сохранить report/artifacts, например:
|
|
69
|
+
|
|
70
|
+
```yaml
|
|
71
|
+
artifacts:
|
|
72
|
+
when: always
|
|
73
|
+
paths:
|
|
74
|
+
- Playwright/test-results/dashboard
|
|
75
|
+
```
|
|
28
76
|
|
|
29
77
|
## Публикация
|
|
30
78
|
|
|
@@ -33,11 +81,3 @@ script:
|
|
|
33
81
|
```bash
|
|
34
82
|
npm publish --access public
|
|
35
83
|
```
|
|
36
|
-
|
|
37
|
-
GitLab Package Registry:
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
echo "@aqa-pulse:registry=${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/npm/" > .npmrc
|
|
41
|
-
echo "//${CI_SERVER_HOST}/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=${CI_JOB_TOKEN}" >> .npmrc
|
|
42
|
-
npm publish
|
|
43
|
-
```
|
package/bin/aqa-pulse.js
CHANGED
|
@@ -12,6 +12,8 @@ const commandMap = {
|
|
|
12
12
|
merge: '../dist/backend/merge-reports.js',
|
|
13
13
|
'generate-source-facts': '../dist/backend/generate-source-facts.js',
|
|
14
14
|
'source-facts': '../dist/backend/generate-source-facts.js',
|
|
15
|
+
'upload-from-config': '../dist/backend/upload-from-config.js',
|
|
16
|
+
config: '../dist/backend/upload-from-config.js',
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
if (command === '--help' || command === '-h') {
|
|
@@ -39,11 +41,13 @@ function printHelp() {
|
|
|
39
41
|
console.log('')
|
|
40
42
|
console.log('Использование:')
|
|
41
43
|
console.log(' aqa-pulse upload-report [--report <path>] [--generate-source-facts] [--repo-root <path>]')
|
|
44
|
+
console.log(' aqa-pulse upload-from-config [--config .aqa-pulse.yml]')
|
|
42
45
|
console.log(' aqa-pulse merge-reports --project-kind ui|api --output <path> [--allow-missing] <input...>')
|
|
43
46
|
console.log(' aqa-pulse generate-source-facts [--report <path>] [--out <path>] [--repo-root <path>]')
|
|
44
47
|
console.log('')
|
|
45
48
|
console.log('Короткие алиасы:')
|
|
46
49
|
console.log(' aqa-pulse upload')
|
|
50
|
+
console.log(' aqa-pulse config')
|
|
47
51
|
console.log(' aqa-pulse merge')
|
|
48
52
|
console.log(' aqa-pulse source-facts')
|
|
49
53
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const fs = __importStar(require("node:fs"));
|
|
37
|
+
const path = __importStar(require("node:path"));
|
|
38
|
+
const node_child_process_1 = require("node:child_process");
|
|
39
|
+
const error_utils_1 = require("../shared/error-utils");
|
|
40
|
+
if (require.main === module) {
|
|
41
|
+
void main();
|
|
42
|
+
}
|
|
43
|
+
async function main() {
|
|
44
|
+
try {
|
|
45
|
+
const cliOptions = parseCliOptions(process.argv.slice(2));
|
|
46
|
+
const configPath = resolveConfigPath(cliOptions.configPath);
|
|
47
|
+
const config = readConfig(configPath);
|
|
48
|
+
const commands = buildCommands(config, configPath);
|
|
49
|
+
if (cliOptions.json || cliOptions.dryRun) {
|
|
50
|
+
process.stdout.write(`${JSON.stringify({
|
|
51
|
+
configPath,
|
|
52
|
+
commands: commands.map((command) => ({
|
|
53
|
+
command: path.basename(command.filePath),
|
|
54
|
+
args: command.args,
|
|
55
|
+
})),
|
|
56
|
+
}, null, 2)}\n`);
|
|
57
|
+
}
|
|
58
|
+
if (cliOptions.dryRun) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
for (const command of commands) {
|
|
62
|
+
runNodeCommand(command.filePath, command.args);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
console.error(`Ошибка AQA Pulse upload-from-config: ${(0, error_utils_1.getErrorMessage)(error)}`);
|
|
67
|
+
process.exitCode = 1;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function parseCliOptions(args) {
|
|
71
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
72
|
+
printHelp();
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
const options = { json: false, dryRun: false };
|
|
76
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
77
|
+
const currentArg = args[index];
|
|
78
|
+
if (currentArg === '--json') {
|
|
79
|
+
options.json = true;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (currentArg === '--dry-run') {
|
|
83
|
+
options.dryRun = true;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (currentArg === '--config') {
|
|
87
|
+
const nextArg = args[index + 1];
|
|
88
|
+
if (!nextArg || nextArg.startsWith('--')) {
|
|
89
|
+
throw new Error('Для --config нужно передать путь к .aqa-pulse.yml.');
|
|
90
|
+
}
|
|
91
|
+
options.configPath = nextArg;
|
|
92
|
+
index += 1;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
throw new Error(`Неизвестный аргумент: ${currentArg}`);
|
|
96
|
+
}
|
|
97
|
+
return options;
|
|
98
|
+
}
|
|
99
|
+
function resolveConfigPath(configPath) {
|
|
100
|
+
if (configPath) {
|
|
101
|
+
const resolved = path.resolve(process.cwd(), configPath);
|
|
102
|
+
if (!fs.existsSync(resolved)) {
|
|
103
|
+
throw new Error(`Не найден config: ${resolved}`);
|
|
104
|
+
}
|
|
105
|
+
return resolved;
|
|
106
|
+
}
|
|
107
|
+
for (const fileName of ['.aqa-pulse.yml', '.aqa-pulse.yaml', '.aqa-pulse.json']) {
|
|
108
|
+
const candidate = path.resolve(process.cwd(), fileName);
|
|
109
|
+
if (fs.existsSync(candidate)) {
|
|
110
|
+
return candidate;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
throw new Error('Не найден .aqa-pulse.yml, .aqa-pulse.yaml или .aqa-pulse.json. Передай путь через --config.');
|
|
114
|
+
}
|
|
115
|
+
function readConfig(configPath) {
|
|
116
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
117
|
+
if (configPath.endsWith('.json')) {
|
|
118
|
+
return JSON.parse(content);
|
|
119
|
+
}
|
|
120
|
+
return parseSimpleYaml(content);
|
|
121
|
+
}
|
|
122
|
+
function buildCommands(config, configPath) {
|
|
123
|
+
const configDirectory = path.dirname(configPath);
|
|
124
|
+
const projectRoot = resolveConfigPathValue(configDirectory, config.projectDir ?? '.');
|
|
125
|
+
const repoRoot = resolveConfigPathValue(projectRoot, config.upload?.repoRoot ?? config.repoRoot ?? '.');
|
|
126
|
+
const mergeConfig = config.merge;
|
|
127
|
+
const commands = [];
|
|
128
|
+
let reportPath = config.upload?.reportPath ?? config.reportPath ?? 'test-results/dashboard/data.json';
|
|
129
|
+
if (mergeConfig) {
|
|
130
|
+
const projectKind = requireText(mergeConfig.projectKind, 'merge.projectKind');
|
|
131
|
+
const output = requireText(mergeConfig.output, 'merge.output');
|
|
132
|
+
const inputs = Array.isArray(mergeConfig.inputs) ? mergeConfig.inputs : [];
|
|
133
|
+
if (inputs.length === 0) {
|
|
134
|
+
throw new Error('merge.inputs должен содержать хотя бы один report.');
|
|
135
|
+
}
|
|
136
|
+
const outputPath = resolveConfigPathValue(projectRoot, output);
|
|
137
|
+
const mergeArgs = [
|
|
138
|
+
'--project-kind',
|
|
139
|
+
projectKind,
|
|
140
|
+
'--output',
|
|
141
|
+
outputPath,
|
|
142
|
+
];
|
|
143
|
+
if (mergeConfig.allowMissing === true) {
|
|
144
|
+
mergeArgs.push('--allow-missing');
|
|
145
|
+
}
|
|
146
|
+
mergeArgs.push(...inputs.map((inputPath) => resolveConfigPathValue(projectRoot, inputPath)));
|
|
147
|
+
commands.push({ filePath: path.resolve(__dirname, 'merge-reports.js'), args: mergeArgs });
|
|
148
|
+
reportPath = output;
|
|
149
|
+
}
|
|
150
|
+
const uploadArgs = [
|
|
151
|
+
'--report',
|
|
152
|
+
resolveConfigPathValue(projectRoot, reportPath),
|
|
153
|
+
'--repo-root',
|
|
154
|
+
repoRoot,
|
|
155
|
+
];
|
|
156
|
+
if (config.baseUrl) {
|
|
157
|
+
uploadArgs.push('--base-url', config.baseUrl);
|
|
158
|
+
}
|
|
159
|
+
if (config.workspaceSlug) {
|
|
160
|
+
uploadArgs.push('--workspace-slug', config.workspaceSlug);
|
|
161
|
+
}
|
|
162
|
+
if (config.workspaceApiKey) {
|
|
163
|
+
uploadArgs.push('--workspace-api-key', config.workspaceApiKey);
|
|
164
|
+
}
|
|
165
|
+
const sourceFactsPath = config.upload?.sourceFactsPath ?? config.sourceFactsPath;
|
|
166
|
+
if (sourceFactsPath) {
|
|
167
|
+
uploadArgs.push('--source-facts', resolveConfigPathValue(projectRoot, sourceFactsPath));
|
|
168
|
+
}
|
|
169
|
+
const generateSourceFacts = config.upload?.generateSourceFacts ?? config.generateSourceFacts ?? true;
|
|
170
|
+
if (generateSourceFacts) {
|
|
171
|
+
uploadArgs.push('--generate-source-facts');
|
|
172
|
+
}
|
|
173
|
+
commands.push({ filePath: path.resolve(__dirname, 'upload-report.js'), args: uploadArgs });
|
|
174
|
+
return commands;
|
|
175
|
+
}
|
|
176
|
+
function runNodeCommand(filePath, args) {
|
|
177
|
+
const result = (0, node_child_process_1.spawnSync)(process.execPath, [filePath, ...args], {
|
|
178
|
+
stdio: 'inherit',
|
|
179
|
+
env: process.env,
|
|
180
|
+
});
|
|
181
|
+
if (result.error) {
|
|
182
|
+
throw result.error;
|
|
183
|
+
}
|
|
184
|
+
if ((result.status ?? 1) !== 0) {
|
|
185
|
+
process.exit(result.status ?? 1);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function resolveConfigPathValue(baseDirectory, value) {
|
|
189
|
+
return path.isAbsolute(value) ? value : path.resolve(baseDirectory, value);
|
|
190
|
+
}
|
|
191
|
+
function requireText(value, label) {
|
|
192
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
193
|
+
throw new Error(`Нужно указать ${label}.`);
|
|
194
|
+
}
|
|
195
|
+
return value.trim();
|
|
196
|
+
}
|
|
197
|
+
function parseSimpleYaml(content) {
|
|
198
|
+
const root = {};
|
|
199
|
+
const stack = [{ indent: -1, value: root }];
|
|
200
|
+
const lines = content.split(/\r?\n/);
|
|
201
|
+
for (const rawLine of lines) {
|
|
202
|
+
const withoutComment = stripYamlComment(rawLine);
|
|
203
|
+
if (!withoutComment.trim()) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const indent = withoutComment.match(/^\s*/)?.[0].length ?? 0;
|
|
207
|
+
const line = withoutComment.trim();
|
|
208
|
+
while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
|
|
209
|
+
stack.pop();
|
|
210
|
+
}
|
|
211
|
+
const parent = stack[stack.length - 1].value;
|
|
212
|
+
if (line.startsWith('- ')) {
|
|
213
|
+
if (!Array.isArray(parent)) {
|
|
214
|
+
throw new Error(`YAML list item без родительского массива: ${line}`);
|
|
215
|
+
}
|
|
216
|
+
parent.push(parseYamlScalar(line.slice(2).trim()));
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const separatorIndex = line.indexOf(':');
|
|
220
|
+
if (separatorIndex === -1) {
|
|
221
|
+
throw new Error(`Не удалось разобрать строку YAML: ${line}`);
|
|
222
|
+
}
|
|
223
|
+
if (Array.isArray(parent)) {
|
|
224
|
+
throw new Error(`YAML object внутри массива пока не поддерживается: ${line}`);
|
|
225
|
+
}
|
|
226
|
+
const key = line.slice(0, separatorIndex).trim();
|
|
227
|
+
const rawValue = line.slice(separatorIndex + 1).trim();
|
|
228
|
+
if (rawValue.length > 0) {
|
|
229
|
+
parent[key] = parseYamlScalar(rawValue);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const nextMeaningfulLine = findNextMeaningfulLine(lines, rawLine);
|
|
233
|
+
const child = nextMeaningfulLine?.trim().startsWith('- ') ? [] : {};
|
|
234
|
+
parent[key] = child;
|
|
235
|
+
stack.push({ indent, value: child });
|
|
236
|
+
}
|
|
237
|
+
return root;
|
|
238
|
+
}
|
|
239
|
+
function findNextMeaningfulLine(lines, currentRawLine) {
|
|
240
|
+
const startIndex = lines.indexOf(currentRawLine) + 1;
|
|
241
|
+
for (let index = startIndex; index < lines.length; index += 1) {
|
|
242
|
+
const candidate = stripYamlComment(lines[index]);
|
|
243
|
+
if (candidate.trim()) {
|
|
244
|
+
return candidate;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
function stripYamlComment(line) {
|
|
250
|
+
const hashIndex = line.indexOf('#');
|
|
251
|
+
return hashIndex === -1 ? line : line.slice(0, hashIndex);
|
|
252
|
+
}
|
|
253
|
+
function parseYamlScalar(value) {
|
|
254
|
+
const normalized = value.trim();
|
|
255
|
+
if (normalized === 'true') {
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
if (normalized === 'false') {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
if (normalized === 'null') {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
if ((normalized.startsWith('"') && normalized.endsWith('"')) || (normalized.startsWith("'") && normalized.endsWith("'"))) {
|
|
265
|
+
return normalized.slice(1, -1);
|
|
266
|
+
}
|
|
267
|
+
return normalized;
|
|
268
|
+
}
|
|
269
|
+
function printHelp() {
|
|
270
|
+
console.log('aqa-pulse upload-from-config [--config .aqa-pulse.yml] [--dry-run] [--json]');
|
|
271
|
+
console.log('');
|
|
272
|
+
console.log('Пример .aqa-pulse.yml:');
|
|
273
|
+
console.log(' projectDir: Playwright');
|
|
274
|
+
console.log(' reportPath: test-results/dashboard/data.json');
|
|
275
|
+
console.log(' repoRoot: .');
|
|
276
|
+
console.log('');
|
|
277
|
+
console.log('Пример с merge:');
|
|
278
|
+
console.log(' projectDir: Playwright');
|
|
279
|
+
console.log(' repoRoot: .');
|
|
280
|
+
console.log(' merge:');
|
|
281
|
+
console.log(' projectKind: ui');
|
|
282
|
+
console.log(' output: test-results/dashboard/ui-merged.json');
|
|
283
|
+
console.log(' allowMissing: true');
|
|
284
|
+
console.log(' inputs:');
|
|
285
|
+
console.log(' - test-results/dashboard/ui-purchase.json');
|
|
286
|
+
console.log(' - test-results/dashboard/ui-cpu.json');
|
|
287
|
+
}
|
package/package.json
CHANGED