abapgit-agent 1.13.4 → 1.13.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/bin/abapgit-agent CHANGED
@@ -36,6 +36,12 @@ async function main() {
36
36
 
37
37
  const commandModules = require('../src/commands/index');
38
38
 
39
+ // Handle --version / -v
40
+ if (command === '--version' || command === '-v') {
41
+ console.log(versionCheck.getCliVersion());
42
+ return;
43
+ }
44
+
39
45
  // Check if this is a modular command
40
46
  if (commandModules[command] || command === '--help' || command === '-h') {
41
47
  const cmd = command === '--help' || command === '-h' ? commandModules.help : commandModules[command];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abapgit-agent",
3
- "version": "1.13.4",
3
+ "version": "1.13.6",
4
4
  "description": "ABAP Git Agent - Pull and activate ABAP code via abapGit from any git repository",
5
5
  "files": [
6
6
  "bin/",
@@ -29,6 +29,7 @@
29
29
  "test:cmd:pull": "node tests/run-all.js --cmd --command=pull",
30
30
  "test:sync-xml": "node tests/run-all.js --sync-xml",
31
31
  "test:xml-only": "node tests/run-all.js --xml-only",
32
+ "test:junit": "node tests/run-all.js --junit",
32
33
  "test:cmd:inspect": "node tests/run-all.js --cmd --command=inspect",
33
34
  "test:cmd:unit": "node tests/run-all.js --cmd --command=unit",
34
35
  "test:cmd:view": "node tests/run-all.js --cmd --command=view",
@@ -9,8 +9,10 @@ module.exports = {
9
9
  requiresVersionCheck: false,
10
10
 
11
11
  async execute(args, context) {
12
+ const { versionCheck } = context;
13
+ const version = versionCheck.getCliVersion();
12
14
  console.log(`
13
- ABAP Git Agent
15
+ ABAP Git Agent v${version}
14
16
 
15
17
  Usage:
16
18
  abapgit-agent <command> [options]
@@ -3,8 +3,109 @@
3
3
  */
4
4
 
5
5
  const pathModule = require('path');
6
+ const fs = require('fs');
6
7
  const { printHttpError } = require('../utils/format-error');
7
8
 
9
+ /**
10
+ * Escape a string for safe embedding in XML text/attribute content
11
+ */
12
+ function escapeXml(str) {
13
+ return String(str)
14
+ .replace(/&/g, '&amp;')
15
+ .replace(/</g, '&lt;')
16
+ .replace(/>/g, '&gt;')
17
+ .replace(/"/g, '&quot;')
18
+ .replace(/'/g, '&apos;');
19
+ }
20
+
21
+ /**
22
+ * Build JUnit XML from inspect results array.
23
+ *
24
+ * Maps to JUnit schema:
25
+ * <testsuites>
26
+ * <testsuite name="CLAS ZCL_MY_CLASS" tests="N" failures="F" errors="0">
27
+ * <testcase name="Syntax check" classname="ZCL_MY_CLASS">
28
+ * <failure type="SyntaxError" message="...">line/col/method detail</failure>
29
+ * </testcase>
30
+ * </testsuite>
31
+ * </testsuites>
32
+ *
33
+ * One testsuite per object. Each error becomes a <failure>. Warnings become
34
+ * a single <failure type="Warning"> so they are visible but don't fail the build
35
+ * unless there are also hard errors (Jenkins distinguishes failure vs unstable).
36
+ */
37
+ function buildInspectJUnit(results) {
38
+ const suites = results.map(res => {
39
+ const objectType = res.OBJECT_TYPE !== undefined ? res.OBJECT_TYPE : (res.object_type || 'UNKNOWN');
40
+ const objectName = res.OBJECT_NAME !== undefined ? res.OBJECT_NAME : (res.object_name || 'UNKNOWN');
41
+ const errors = res.ERRORS !== undefined ? res.ERRORS : (res.errors || []);
42
+ const warnings = res.WARNINGS !== undefined ? res.WARNINGS : (res.warnings || []);
43
+ const errorCount = errors.length;
44
+ const warnCount = warnings.length;
45
+ // One testcase per error/warning; at least one testcase for a clean object
46
+ const testCount = Math.max(1, errorCount + warnCount);
47
+
48
+ const testcases = [];
49
+
50
+ if (errorCount === 0 && warnCount === 0) {
51
+ testcases.push(` <testcase name="Syntax check" classname="${escapeXml(objectName)}"/>`);
52
+ }
53
+
54
+ for (const err of errors) {
55
+ const line = err.LINE || err.line || '?';
56
+ const column = err.COLUMN || err.column || '?';
57
+ const text = err.TEXT || err.text || 'Unknown error';
58
+ const methodName = err.METHOD_NAME || err.method_name;
59
+ const sobjname = err.SOBJNAME || err.sobjname || '';
60
+ const detail = [
61
+ methodName ? `Method: ${methodName}` : null,
62
+ `Line ${line}, Column ${column}`,
63
+ sobjname ? `Include: ${sobjname}` : null,
64
+ text
65
+ ].filter(Boolean).join('\n');
66
+ const caseName = methodName ? `${methodName} line ${line}` : `Line ${line}`;
67
+ testcases.push(
68
+ ` <testcase name="${escapeXml(caseName)}" classname="${escapeXml(objectName)}">\n` +
69
+ ` <failure type="SyntaxError" message="${escapeXml(text)}">${escapeXml(detail)}</failure>\n` +
70
+ ` </testcase>`
71
+ );
72
+ }
73
+
74
+ for (const warn of warnings) {
75
+ const line = warn.LINE || warn.line || '?';
76
+ const text = warn.MESSAGE || warn.message || warn.TEXT || warn.text || 'Warning';
77
+ const methodName = warn.METHOD_NAME || warn.method_name;
78
+ const sobjname = warn.SOBJNAME || warn.sobjname || '';
79
+ const detail = [
80
+ methodName ? `Method: ${methodName}` : null,
81
+ `Line ${line}`,
82
+ sobjname ? `Include: ${sobjname}` : null,
83
+ text
84
+ ].filter(Boolean).join('\n');
85
+ const caseName = methodName ? `${methodName} line ${line} (warning)` : `Line ${line} (warning)`;
86
+ testcases.push(
87
+ ` <testcase name="${escapeXml(caseName)}" classname="${escapeXml(objectName)}">\n` +
88
+ ` <failure type="Warning" message="${escapeXml(text)}">${escapeXml(detail)}</failure>\n` +
89
+ ` </testcase>`
90
+ );
91
+ }
92
+
93
+ return (
94
+ ` <testsuite name="${escapeXml(objectType + ' ' + objectName)}" ` +
95
+ `tests="${testCount}" failures="${errorCount + warnCount}" errors="0">\n` +
96
+ testcases.join('\n') + '\n' +
97
+ ` </testsuite>`
98
+ );
99
+ });
100
+
101
+ return (
102
+ '<?xml version="1.0" encoding="UTF-8"?>\n' +
103
+ '<testsuites>\n' +
104
+ suites.join('\n') + '\n' +
105
+ '</testsuites>\n'
106
+ );
107
+ }
108
+
8
109
  /**
9
110
  * Inspect all files in one request
10
111
  */
@@ -154,10 +255,10 @@ module.exports = {
154
255
  const filesArgIndex = args.indexOf('--files');
155
256
  if (filesArgIndex === -1 || filesArgIndex + 1 >= args.length) {
156
257
  console.error('Error: --files parameter required');
157
- console.error('Usage: abapgit-agent inspect --files <file1>,<file2>,... [--variant <check-variant>] [--json]');
258
+ console.error('Usage: abapgit-agent inspect --files <file1>,<file2>,... [--variant <check-variant>] [--junit-output <file>] [--json]');
158
259
  console.error('Example: abapgit-agent inspect --files src/zcl_my_class.clas.abap');
159
260
  console.error('Example: abapgit-agent inspect --files src/zcl_my_class.clas.abap --variant ALL_CHECKS');
160
- console.error('Example: abapgit-agent inspect --files src/zcl_my_class.clas.abap --json');
261
+ console.error('Example: abapgit-agent inspect --files src/zcl_my_class.clas.abap --junit-output reports/inspect.xml');
161
262
  process.exit(1);
162
263
  }
163
264
 
@@ -167,11 +268,18 @@ module.exports = {
167
268
  const variantArgIndex = args.indexOf('--variant');
168
269
  const variant = variantArgIndex !== -1 ? args[variantArgIndex + 1] : null;
169
270
 
271
+ // Parse optional --junit-output parameter
272
+ const junitArgIndex = args.indexOf('--junit-output');
273
+ const junitOutput = junitArgIndex !== -1 ? args[junitArgIndex + 1] : null;
274
+
170
275
  if (!jsonOutput) {
171
276
  console.log(`\n Inspect for ${filesSyntaxCheck.length} file(s)`);
172
277
  if (variant) {
173
278
  console.log(` Using variant: ${variant}`);
174
279
  }
280
+ if (junitOutput) {
281
+ console.log(` JUnit output: ${junitOutput}`);
282
+ }
175
283
  console.log('');
176
284
  }
177
285
 
@@ -182,6 +290,22 @@ module.exports = {
182
290
  // Send all files in one request
183
291
  const results = await inspectAllFiles(filesSyntaxCheck, csrfToken, config, variant, http, verbose);
184
292
 
293
+ // JUnit output mode — write XML file, then continue to normal output
294
+ if (junitOutput) {
295
+ const xml = buildInspectJUnit(results);
296
+ const outputPath = pathModule.isAbsolute(junitOutput)
297
+ ? junitOutput
298
+ : pathModule.join(process.cwd(), junitOutput);
299
+ const dir = pathModule.dirname(outputPath);
300
+ if (!fs.existsSync(dir)) {
301
+ fs.mkdirSync(dir, { recursive: true });
302
+ }
303
+ fs.writeFileSync(outputPath, xml, 'utf8');
304
+ if (!jsonOutput) {
305
+ console.log(` JUnit report written to: ${outputPath}`);
306
+ }
307
+ }
308
+
185
309
  // JSON output mode
186
310
  if (jsonOutput) {
187
311
  console.log(JSON.stringify(results, null, 2));
@@ -189,8 +313,15 @@ module.exports = {
189
313
  }
190
314
 
191
315
  // Process results
316
+ let hasErrors = false;
192
317
  for (const result of results) {
193
318
  await processInspectResult(result);
319
+ const errorCount = result.ERROR_COUNT !== undefined ? result.ERROR_COUNT : (result.error_count || 0);
320
+ if (errorCount > 0) hasErrors = true;
321
+ }
322
+
323
+ if (hasErrors) {
324
+ process.exit(1);
194
325
  }
195
326
  }
196
327
  };
@@ -168,8 +168,13 @@ module.exports = {
168
168
  const csrfToken = await http.fetchCsrfToken();
169
169
  statusResult = await http.post('/sap/bc/z_abapgit_agent/status', { url: gitUrl }, { csrfToken });
170
170
  } catch (e) {
171
+ const isNetworkError = e.code && ['ENOTFOUND', 'ECONNREFUSED', 'ETIMEDOUT', 'ECONNRESET'].includes(e.code);
171
172
  console.error(`❌ Repository status check failed: ${e.message}`);
172
- console.error(' Make sure the repository is registered with abapgit-agent (run "abapgit-agent create").');
173
+ if (isNetworkError) {
174
+ console.error(' Cannot reach the ABAP system. Check your network connection and the host in .abapGitAgent.');
175
+ } else {
176
+ console.error(' Make sure the repository is registered with abapgit-agent (run "abapgit-agent create").');
177
+ }
173
178
  process.exit(1);
174
179
  }
175
180
 
@@ -6,6 +6,92 @@ const pathModule = require('path');
6
6
  const fs = require('fs');
7
7
  const { formatHttpError } = require('../utils/format-error');
8
8
 
9
+ /**
10
+ * Escape a string for safe embedding in XML text/attribute content
11
+ */
12
+ function escapeXml(str) {
13
+ return String(str)
14
+ .replace(/&/g, '&amp;')
15
+ .replace(/</g, '&lt;')
16
+ .replace(/>/g, '&gt;')
17
+ .replace(/"/g, '&quot;')
18
+ .replace(/'/g, '&apos;');
19
+ }
20
+
21
+ /**
22
+ * Build JUnit XML from unit test results array.
23
+ *
24
+ * Maps to JUnit schema:
25
+ * <testsuites>
26
+ * <testsuite name="ZCL_MY_TEST" tests="10" failures="2" errors="0">
27
+ * <testcase name="TEST_METHOD_1" classname="ZCL_MY_TEST"/>
28
+ * <testcase name="TEST_METHOD_2" classname="ZCL_MY_TEST">
29
+ * <failure type="FAILURE" message="...">detail</failure>
30
+ * </testcase>
31
+ * </testsuite>
32
+ * </testsuites>
33
+ *
34
+ * One testsuite per test class file. Each failed test method becomes a <failure>.
35
+ * Passing methods are listed as empty <testcase> elements (Jenkins counts them).
36
+ */
37
+ function buildUnitJUnit(results) {
38
+ const suites = results.map(res => {
39
+ const success = res.SUCCESS || res.success;
40
+ const testCount = res.TEST_COUNT || res.test_count || 0;
41
+ const passedCount = res.PASSED_COUNT || res.passed_count || 0;
42
+ const failedCount = res.FAILED_COUNT || res.failed_count || 0;
43
+ const errors = res.ERRORS || res.errors || [];
44
+ const className = res._className || 'UNKNOWN'; // injected by caller
45
+
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
54
+ for (const err of errors) {
55
+ const errClassName = err.CLASS_NAME || err.class_name || className;
56
+ const methodName = err.METHOD_NAME || err.method_name || '?';
57
+ const errorKind = err.ERROR_KIND || err.error_kind || 'FAILURE';
58
+ const errorText = err.ERROR_TEXT || err.error_text || 'Test failed';
59
+ testcases.push(
60
+ ` <testcase name="${escapeXml(methodName)}" classname="${escapeXml(errClassName)}">\n` +
61
+ ` <failure type="${escapeXml(errorKind)}" message="${escapeXml(errorText)}">${escapeXml(errorText)}</failure>\n` +
62
+ ` </testcase>`
63
+ );
64
+ }
65
+
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
69
+ if (passedCount > 0) {
70
+ testcases.push(
71
+ ` <testcase name="(${passedCount} passing test(s))" classname="${escapeXml(className)}"/>`
72
+ );
73
+ }
74
+
75
+ if (testCount === 0) {
76
+ testcases.push(` <testcase name="(no tests)" classname="${escapeXml(className)}"/>`);
77
+ }
78
+
79
+ return (
80
+ ` <testsuite name="${escapeXml(className)}" ` +
81
+ `tests="${Math.max(testCount, 1)}" failures="${failedCount}" errors="0">\n` +
82
+ testcases.join('\n') + '\n' +
83
+ ` </testsuite>`
84
+ );
85
+ });
86
+
87
+ return (
88
+ '<?xml version="1.0" encoding="UTF-8"?>\n' +
89
+ '<testsuites>\n' +
90
+ suites.join('\n') + '\n' +
91
+ '</testsuites>\n'
92
+ );
93
+ }
94
+
9
95
  /**
10
96
  * Run unit test for a single file
11
97
  */
@@ -151,10 +237,10 @@ module.exports = {
151
237
  const filesArgIndex = args.indexOf('--files');
152
238
  if (filesArgIndex === -1 || filesArgIndex + 1 >= args.length) {
153
239
  console.error('Error: --files parameter required');
154
- console.error('Usage: abapgit-agent unit --files <file1>,<file2>,... [--coverage] [--json]');
155
- console.error('Example: abapgit-agent unit --files src/zcl_my_test.clas.abap');
156
- console.error('Example: abapgit-agent unit --files src/zcl_my_test.clas.abap --coverage');
157
- console.error('Example: abapgit-agent unit --files src/zcl_my_test.clas.abap --json');
240
+ console.error('Usage: abapgit-agent unit --files <file1>,<file2>,... [--coverage] [--junit-output <file>] [--json]');
241
+ console.error('Example: abapgit-agent unit --files src/zcl_my_test.clas.testclasses.abap');
242
+ console.error('Example: abapgit-agent unit --files src/zcl_my_test.clas.testclasses.abap --coverage');
243
+ console.error('Example: abapgit-agent unit --files src/zcl_my_test.clas.testclasses.abap --junit-output reports/unit.xml');
158
244
  process.exit(1);
159
245
  }
160
246
 
@@ -163,8 +249,15 @@ module.exports = {
163
249
  // Check for coverage option
164
250
  const coverage = args.includes('--coverage');
165
251
 
252
+ // Parse optional --junit-output parameter
253
+ const junitArgIndex = args.indexOf('--junit-output');
254
+ const junitOutput = junitArgIndex !== -1 ? args[junitArgIndex + 1] : null;
255
+
166
256
  if (!jsonOutput) {
167
257
  console.log(`\n Running unit tests for ${files.length} file(s)${coverage ? ' (with coverage)' : ''}`);
258
+ if (junitOutput) {
259
+ console.log(` JUnit output: ${junitOutput}`);
260
+ }
168
261
  console.log('');
169
262
  }
170
263
 
@@ -172,19 +265,42 @@ module.exports = {
172
265
  const http = new AbapHttp(config);
173
266
  const csrfToken = await http.fetchCsrfToken();
174
267
 
175
- // Collect results for JSON output
268
+ // Collect results for JSON / JUnit output
176
269
  const results = [];
177
270
  let hasErrors = false;
178
271
 
179
272
  for (const sourceFile of files) {
180
273
  const result = await runUnitTestForFile(sourceFile, csrfToken, config, coverage, http, jsonOutput, verbose);
181
274
  if (result) {
275
+ // Inject class name derived from file path for JUnit builder
276
+ const fileName = pathModule.basename(sourceFile).toUpperCase();
277
+ result._className = fileName.split('.')[0];
182
278
  results.push(result);
183
279
 
184
- // Check if this result contains an error
185
280
  if (result.error || result.statusCode >= 400) {
186
281
  hasErrors = true;
187
282
  }
283
+ // Also treat failed tests as an error for exit code
284
+ const failedCount = result.FAILED_COUNT || result.failed_count || 0;
285
+ if (failedCount > 0) {
286
+ hasErrors = true;
287
+ }
288
+ }
289
+ }
290
+
291
+ // JUnit output mode — write XML, then continue to normal output
292
+ if (junitOutput) {
293
+ const xml = buildUnitJUnit(results);
294
+ const outputPath = pathModule.isAbsolute(junitOutput)
295
+ ? junitOutput
296
+ : pathModule.join(process.cwd(), junitOutput);
297
+ const dir = pathModule.dirname(outputPath);
298
+ if (!fs.existsSync(dir)) {
299
+ fs.mkdirSync(dir, { recursive: true });
300
+ }
301
+ fs.writeFileSync(outputPath, xml, 'utf8');
302
+ if (!jsonOutput) {
303
+ console.log(` JUnit report written to: ${outputPath}`);
188
304
  }
189
305
  }
190
306
 
@@ -135,7 +135,8 @@ async function getLatestNpmVersion() {
135
135
  const baseUrl = registry.endsWith('/') ? registry : registry + '/';
136
136
  const url = `${baseUrl}abapgit-agent/latest`;
137
137
 
138
- https.get(url, (res) => {
138
+ const client = url.startsWith('http://') ? http : https;
139
+ client.get(url, (res) => {
139
140
  let body = '';
140
141
  res.on('data', chunk => body += chunk);
141
142
  res.on('end', () => {