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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abapgit-agent",
3
- "version": "1.17.4",
3
+ "version": "1.17.6",
4
4
  "description": "ABAP Git Agent - Pull and activate ABAP code via abapGit from any git repository",
5
5
  "files": [
6
6
  "bin/",
@@ -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; if no upstream, set it automatically
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 push --force-with-lease', { cwd: process.cwd(), stdio: 'pipe' });
532
- pushed = true;
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')) {
@@ -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
- // Build a set of failed method names for quick lookup
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
- testcases.push(
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
- // Emit empty <testcase> elements for passing tests (Jenkins shows total count)
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
- testcases.push(
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
- testcases.push(` <testcase name="(no tests)" classname="${escapeXml(className)}"/>`);
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
- testcases.join('\n') + '\n' +
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>,... [--coverage] [--junit-output <file>] [--json]
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,...> Comma-separated .testclasses.abap files (required).
246
- --coverage Include code coverage data in output.
247
- --junit-output <file> Write results as JUnit XML to this file.
248
- --json Output as JSON.
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
- // Check for coverage option
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));
@@ -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(`❌ Error: Version ${flags.version} not found in npm registry`);
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
  }