@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 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
- variables:
17
- AQA_PULSE_BASE_URL: "https://aqa-pulse.example.com"
18
- AQA_PULSE_WORKSPACE_SLUG: "autotests-main"
19
- AQA_PULSE_REPORT_PATH: "Playwright/test-results/dashboard/data.json"
20
-
21
- script:
22
- - npm ci
23
- - npm test
24
- - npx @aqa-pulse/cli upload-report --generate-source-facts --repo-root "$CI_PROJECT_DIR"
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
- `AQA_PULSE_WORKSPACE_API_KEY` лучше хранить в GitLab CI/CD Variables.
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@aqa-pulse/cli",
3
- "version": "0.1.0",
4
- "description": "CLI для загрузки Playwright reports в AQA Pulse",
3
+ "version": "0.2.0",
4
+ "description": "CLI для загрузки отчетов в AQA Pulse",
5
5
  "license": "UNLICENSED",
6
6
  "type": "commonjs",
7
7
  "bin": {