abapgit-agent 1.17.4 → 1.17.6
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/package.json +1 -1
- package/src/commands/pull.js +22 -3
- package/src/commands/unit.js +66 -27
- package/src/commands/upgrade.js +1 -1
package/package.json
CHANGED
package/src/commands/pull.js
CHANGED
|
@@ -525,11 +525,30 @@ Examples:
|
|
|
525
525
|
// 3. Amend last commit
|
|
526
526
|
execSync('git commit --amend --no-edit', { cwd: process.cwd() });
|
|
527
527
|
|
|
528
|
-
// 4. Push with force-with-lease;
|
|
528
|
+
// 4. Push with force-with-lease; fetch first so tracking ref is current
|
|
529
|
+
// (the remote may have been force-pushed by another process between our last
|
|
530
|
+
// fetch and this push — a fetch makes --force-with-lease reliable)
|
|
529
531
|
let pushed = false;
|
|
530
532
|
try {
|
|
531
|
-
execSync('git
|
|
532
|
-
|
|
533
|
+
try { execSync('git fetch origin', { cwd: process.cwd(), stdio: 'pipe' }); } catch (_) { /* no remote is fine */ }
|
|
534
|
+
// Retry the push up to 3 times on transient server errors (e.g. GitHub Enterprise 500)
|
|
535
|
+
let pushErr;
|
|
536
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
537
|
+
try {
|
|
538
|
+
execSync('git push --force-with-lease', { cwd: process.cwd(), stdio: 'pipe' });
|
|
539
|
+
pushed = true;
|
|
540
|
+
break;
|
|
541
|
+
} catch (err) {
|
|
542
|
+
pushErr = err;
|
|
543
|
+
const msg = (err.stderr || err.stdout || err.message || '').toString();
|
|
544
|
+
const transient = /internal server error|remote rejected|\b5\d\d\b|connection reset|timed? ?out/i.test(msg);
|
|
545
|
+
if (attempt < 3 && transient) {
|
|
546
|
+
execSync('sleep 2', { stdio: 'pipe' });
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
throw err;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
533
552
|
} catch (pushErr) {
|
|
534
553
|
const msg = (pushErr.stderr || pushErr.stdout || pushErr.message || '').toString();
|
|
535
554
|
if (msg.includes('no upstream branch') || msg.includes('has no upstream')) {
|
package/src/commands/unit.js
CHANGED
|
@@ -24,6 +24,11 @@ function escapeXml(str) {
|
|
|
24
24
|
* Maps to JUnit schema:
|
|
25
25
|
* <testsuites>
|
|
26
26
|
* <testsuite name="ZCL_MY_TEST" tests="10" failures="2" errors="0">
|
|
27
|
+
* <properties>
|
|
28
|
+
* <property name="coverage.rate" value="67"/>
|
|
29
|
+
* <property name="coverage.lines.total" value="120"/>
|
|
30
|
+
* <property name="coverage.lines.covered" value="80"/>
|
|
31
|
+
* </properties>
|
|
27
32
|
* <testcase name="TEST_METHOD_1" classname="ZCL_MY_TEST"/>
|
|
28
33
|
* <testcase name="TEST_METHOD_2" classname="ZCL_MY_TEST">
|
|
29
34
|
* <failure type="FAILURE" message="...">detail</failure>
|
|
@@ -33,53 +38,59 @@ function escapeXml(str) {
|
|
|
33
38
|
*
|
|
34
39
|
* One testsuite per test class file. Each failed test method becomes a <failure>.
|
|
35
40
|
* Passing methods are listed as empty <testcase> elements (Jenkins counts them).
|
|
41
|
+
* Coverage stats (if present) are emitted as <properties> on the testsuite.
|
|
36
42
|
*/
|
|
37
43
|
function buildUnitJUnit(results) {
|
|
38
44
|
const suites = results.map(res => {
|
|
39
|
-
const success = res.SUCCESS || res.success;
|
|
40
45
|
const testCount = res.TEST_COUNT || res.test_count || 0;
|
|
41
46
|
const passedCount = res.PASSED_COUNT || res.passed_count || 0;
|
|
42
47
|
const failedCount = res.FAILED_COUNT || res.failed_count || 0;
|
|
43
48
|
const errors = res.ERRORS || res.errors || [];
|
|
44
49
|
const className = res._className || 'UNKNOWN'; // injected by caller
|
|
50
|
+
const coverageStats = res.COVERAGE_STATS || res.coverage_stats;
|
|
51
|
+
|
|
52
|
+
const lines = [];
|
|
53
|
+
|
|
54
|
+
// Coverage <properties> block — only emitted when coverage data is present
|
|
55
|
+
if (coverageStats) {
|
|
56
|
+
const rate = coverageStats.COVERAGE_RATE || coverageStats.coverage_rate || 0;
|
|
57
|
+
const total = coverageStats.TOTAL_LINES || coverageStats.total_lines || 0;
|
|
58
|
+
const covered = coverageStats.COVERED_LINES || coverageStats.covered_lines || 0;
|
|
59
|
+
lines.push(' <properties>');
|
|
60
|
+
lines.push(` <property name="coverage.rate" value="${rate}"/>`);
|
|
61
|
+
lines.push(` <property name="coverage.lines.total" value="${total}"/>`);
|
|
62
|
+
lines.push(` <property name="coverage.lines.covered" value="${covered}"/>`);
|
|
63
|
+
lines.push(' </properties>');
|
|
64
|
+
}
|
|
45
65
|
|
|
46
|
-
//
|
|
47
|
-
const failedMethods = new Set(
|
|
48
|
-
errors.map(e => (e.CLASS_NAME || e.class_name || '') + '=>' + (e.METHOD_NAME || e.method_name || ''))
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
const testcases = [];
|
|
52
|
-
|
|
53
|
-
// Emit one <testcase> per failed test
|
|
66
|
+
// One <testcase> per failed test
|
|
54
67
|
for (const err of errors) {
|
|
55
68
|
const errClassName = err.CLASS_NAME || err.class_name || className;
|
|
56
69
|
const methodName = err.METHOD_NAME || err.method_name || '?';
|
|
57
70
|
const errorKind = err.ERROR_KIND || err.error_kind || 'FAILURE';
|
|
58
71
|
const errorText = err.ERROR_TEXT || err.error_text || 'Test failed';
|
|
59
|
-
|
|
72
|
+
lines.push(
|
|
60
73
|
` <testcase name="${escapeXml(methodName)}" classname="${escapeXml(errClassName)}">\n` +
|
|
61
74
|
` <failure type="${escapeXml(errorKind)}" message="${escapeXml(errorText)}">${escapeXml(errorText)}</failure>\n` +
|
|
62
75
|
` </testcase>`
|
|
63
76
|
);
|
|
64
77
|
}
|
|
65
78
|
|
|
66
|
-
//
|
|
67
|
-
// We can't enumerate them individually (ABAP doesn't return passing method names),
|
|
68
|
-
// so emit one aggregate passing testcase when passedCount > 0
|
|
79
|
+
// Aggregate passing testcase (ABAP doesn't return individual passing method names)
|
|
69
80
|
if (passedCount > 0) {
|
|
70
|
-
|
|
81
|
+
lines.push(
|
|
71
82
|
` <testcase name="(${passedCount} passing test(s))" classname="${escapeXml(className)}"/>`
|
|
72
83
|
);
|
|
73
84
|
}
|
|
74
85
|
|
|
75
86
|
if (testCount === 0) {
|
|
76
|
-
|
|
87
|
+
lines.push(` <testcase name="(no tests)" classname="${escapeXml(className)}"/>`);
|
|
77
88
|
}
|
|
78
89
|
|
|
79
90
|
return (
|
|
80
91
|
` <testsuite name="${escapeXml(className)}" ` +
|
|
81
92
|
`tests="${Math.max(testCount, 1)}" failures="${failedCount}" errors="0">\n` +
|
|
82
|
-
|
|
93
|
+
lines.join('\n') + '\n' +
|
|
83
94
|
` </testsuite>`
|
|
84
95
|
);
|
|
85
96
|
});
|
|
@@ -235,21 +246,23 @@ module.exports = {
|
|
|
235
246
|
if (args.includes('--help') || args.includes('-h')) {
|
|
236
247
|
console.log(`
|
|
237
248
|
Usage:
|
|
238
|
-
abapgit-agent unit --files <file1>,<file2>,... [
|
|
249
|
+
abapgit-agent unit --files <file1>,<file2>,... [options]
|
|
239
250
|
|
|
240
251
|
Description:
|
|
241
252
|
Run AUnit tests for ABAP test class files (.testclasses.abap).
|
|
242
253
|
Objects must be already active in the ABAP system (run pull first).
|
|
243
254
|
|
|
244
255
|
Parameters:
|
|
245
|
-
--files <file1,...>
|
|
246
|
-
--coverage
|
|
247
|
-
--
|
|
248
|
-
--
|
|
256
|
+
--files <file1,...> Comma-separated .testclasses.abap files (required).
|
|
257
|
+
--coverage Include code coverage data in output.
|
|
258
|
+
--coverage-threshold <N> Fail/warn when coverage is below N percent (0–100). Default: 0 (off).
|
|
259
|
+
--coverage-mode <warn|fail> Action when below threshold: warn = UNSTABLE, fail = error. Default: fail.
|
|
260
|
+
--junit-output <file> Write results as JUnit XML to this file.
|
|
261
|
+
--json Output as JSON.
|
|
249
262
|
|
|
250
263
|
Examples:
|
|
251
264
|
abapgit-agent unit --files src/zcl_my_test.clas.testclasses.abap
|
|
252
|
-
abapgit-agent unit --files src/zcl_my_test.clas.testclasses.abap --coverage
|
|
265
|
+
abapgit-agent unit --files src/zcl_my_test.clas.testclasses.abap --coverage --coverage-threshold 80
|
|
253
266
|
abapgit-agent unit --files src/zcl_my_test.clas.testclasses.abap --junit-output reports/unit.xml
|
|
254
267
|
`);
|
|
255
268
|
return;
|
|
@@ -260,18 +273,21 @@ Examples:
|
|
|
260
273
|
const filesArgIndex = args.indexOf('--files');
|
|
261
274
|
if (filesArgIndex === -1 || filesArgIndex + 1 >= args.length) {
|
|
262
275
|
console.error('Error: --files parameter required');
|
|
263
|
-
console.error('Usage: abapgit-agent unit --files <file1>,<file2>,... [--coverage] [--junit-output <file>] [--json]');
|
|
264
|
-
console.error('Example: abapgit-agent unit --files src/zcl_my_test.clas.testclasses.abap');
|
|
265
|
-
console.error('Example: abapgit-agent unit --files src/zcl_my_test.clas.testclasses.abap --coverage');
|
|
266
|
-
console.error('Example: abapgit-agent unit --files src/zcl_my_test.clas.testclasses.abap --junit-output reports/unit.xml');
|
|
276
|
+
console.error('Usage: abapgit-agent unit --files <file1>,<file2>,... [--coverage] [--coverage-threshold <N>] [--junit-output <file>] [--json]');
|
|
267
277
|
process.exit(1);
|
|
268
278
|
}
|
|
269
279
|
|
|
270
280
|
const files = args[filesArgIndex + 1].split(',').map(f => f.trim());
|
|
271
281
|
|
|
272
|
-
//
|
|
282
|
+
// Coverage options
|
|
273
283
|
const coverage = args.includes('--coverage');
|
|
274
284
|
|
|
285
|
+
const coverageThresholdIdx = args.indexOf('--coverage-threshold');
|
|
286
|
+
const coverageThreshold = coverageThresholdIdx !== -1 ? parseInt(args[coverageThresholdIdx + 1], 10) : 0;
|
|
287
|
+
|
|
288
|
+
const coverageModeIdx = args.indexOf('--coverage-mode');
|
|
289
|
+
const coverageMode = coverageModeIdx !== -1 ? args[coverageModeIdx + 1] : 'fail';
|
|
290
|
+
|
|
275
291
|
// Parse optional --junit-output parameter
|
|
276
292
|
const junitArgIndex = args.indexOf('--junit-output');
|
|
277
293
|
const junitOutput = junitArgIndex !== -1 ? args[junitArgIndex + 1] : null;
|
|
@@ -327,6 +343,29 @@ Examples:
|
|
|
327
343
|
}
|
|
328
344
|
}
|
|
329
345
|
|
|
346
|
+
// Coverage threshold enforcement — aggregated across all files
|
|
347
|
+
if (coverage && coverageThreshold > 0) {
|
|
348
|
+
const totalLines = results.reduce((s, r) => s + ((r.COVERAGE_STATS || r.coverage_stats)?.TOTAL_LINES || (r.COVERAGE_STATS || r.coverage_stats)?.total_lines || 0), 0);
|
|
349
|
+
const coveredLines = results.reduce((s, r) => s + ((r.COVERAGE_STATS || r.coverage_stats)?.COVERED_LINES || (r.COVERAGE_STATS || r.coverage_stats)?.covered_lines || 0), 0);
|
|
350
|
+
|
|
351
|
+
if (totalLines === 0) {
|
|
352
|
+
if (!jsonOutput) console.warn('⚠️ Coverage data unavailable — threshold not enforced');
|
|
353
|
+
} else {
|
|
354
|
+
const rate = Math.round((coveredLines / totalLines) * 100);
|
|
355
|
+
if (rate < coverageThreshold) {
|
|
356
|
+
const msg = `Coverage ${rate}% is below threshold ${coverageThreshold}%`;
|
|
357
|
+
if (coverageMode === 'warn') {
|
|
358
|
+
if (!jsonOutput) console.warn(`⚠️ ${msg}`);
|
|
359
|
+
} else {
|
|
360
|
+
if (!jsonOutput) console.error(`❌ ${msg}`);
|
|
361
|
+
hasErrors = true;
|
|
362
|
+
}
|
|
363
|
+
} else {
|
|
364
|
+
if (!jsonOutput) console.log(`✅ Coverage ${rate}% meets threshold ${coverageThreshold}%`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
330
369
|
// JSON output mode
|
|
331
370
|
if (jsonOutput) {
|
|
332
371
|
console.log(JSON.stringify(results, null, 2));
|
package/src/commands/upgrade.js
CHANGED
|
@@ -109,7 +109,7 @@ Examples:
|
|
|
109
109
|
if (flags.version && !flags.abapOnly) {
|
|
110
110
|
const versionExists = await this.validateVersionExists(flags.version);
|
|
111
111
|
if (!versionExists) {
|
|
112
|
-
console.error(
|
|
112
|
+
console.error(`Error: Version ${flags.version} not found in npm registry`);
|
|
113
113
|
console.error(' Please check available versions at: https://www.npmjs.com/package/abapgit-agent?activeTab=versions');
|
|
114
114
|
process.exit(1);
|
|
115
115
|
}
|