abapgit-agent 1.13.5 → 1.13.7

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/abap/CLAUDE.md CHANGED
@@ -211,6 +211,21 @@ abapgit-agent pull --files src/<intf_name>.intf.abap,src/<class_name>.clas.abap
211
211
 
212
212
  → See `guidelines/object-creation.md` — run: `abapgit-agent ref --topic object-creation`
213
213
 
214
+ **XML metadata when adding test classes:**
215
+
216
+ ```
217
+ Adding .clas.testclasses.abap to an existing class?
218
+ └── Update the .clas.xml → set WITH_UNIT_TESTS flag:
219
+ <clas:abapClassProperties ... abpUnitTestable="true" ... />
220
+ WITHOUT this flag, abapGit will not push/activate the test include.
221
+
222
+ Adding .clas.locals_def.abap (local type definitions)?
223
+ └── Update the .clas.xml → set CLSCCINCL flag:
224
+ <CLSCCINCL>X</CLSCCINCL>
225
+ ```
226
+
227
+ → For exact XML flag placement: `abapgit-agent ref --topic abapgit` (search "WITH_UNIT_TESTS")
228
+
214
229
  ---
215
230
 
216
231
  ### 6. Use `guide`, `ref`, `view` and `where` Commands to Learn About Unknown Classes/Methods
@@ -299,7 +314,47 @@ Use `CL_CDS_TEST_ENVIRONMENT` for unit tests that read CDS views.
299
314
 
300
315
  ---
301
316
 
302
- ### 8. Use `unit` Command for Unit Tests
317
+ ### 8. Writing and Running Unit Tests
318
+
319
+ #### Writing tests — use ABAP Test Double Framework by default
320
+
321
+ ```
322
+ ❌ WRONG: Write a manual test double class (ltd_mock_xxx) when the framework can do it
323
+ ✅ CORRECT: Use cl_abap_testdouble=>create / configure_call for all interface mocking
324
+ ```
325
+
326
+ **Decision — which double pattern to use:**
327
+
328
+ ```
329
+ Does the mock need stateful behaviour (e.g. count calls, vary results per call, complex logic)?
330
+ └── YES → manual test double class (ltd_mock_xxx DEFINITION FOR TESTING)
331
+ └── NO → ABAP Test Double Framework (cl_abap_testdouble=>create / configure_call)
332
+ This covers 90 %+ of cases — simple return value / exception mocking
333
+ ```
334
+
335
+ **ABAP Test Double Framework — quick pattern:**
336
+
337
+ ```abap
338
+ " 1. Create double (declare with interface type)
339
+ DATA lo_agent TYPE REF TO zif_abgagt_agent.
340
+ lo_agent ?= cl_abap_testdouble=>create( 'ZIF_ABGAGT_AGENT' ).
341
+
342
+ " 2. Configure return value
343
+ cl_abap_testdouble=>configure_call( lo_agent )->returning( ls_result ).
344
+ lo_agent->pull( iv_url = 'https://...' ). " registers config for these params
345
+
346
+ " 3. Inject and call
347
+ DATA(lo_cut) = NEW zcl_my_class( io_agent = lo_agent ).
348
+ DATA(ls_actual) = lo_cut->execute( ).
349
+ ```
350
+
351
+ → Full API reference (EXPORT params, exceptions, inherited methods, common mistakes):
352
+ `abapgit-agent ref --topic unit-testable-code`
353
+
354
+ → For class design rules (constructor injection, interfaces for dependencies):
355
+ `abapgit-agent ref --topic unit-testable-code`
356
+
357
+ #### Running tests — use `unit` command
303
358
 
304
359
  **Use `abapgit-agent unit` to run ABAP unit tests (AUnit).**
305
360
 
@@ -399,6 +454,56 @@ abapgit-agent debug step --type continue --json # 4. release
399
454
 
400
455
  ---
401
456
 
457
+ ### 12. abaplint — Static Analysis (Optional, Project-Controlled)
458
+
459
+ abaplint is **optional**. Only run it if `.abaplint.json` exists in the project root.
460
+ Each project defines its own rules — never assume which rules are active.
461
+
462
+ **Detection:**
463
+ ```bash
464
+ # Check whether this project uses abaplint
465
+ ls .abaplint.json 2>/dev/null && echo "abaplint enabled" || echo "no abaplint"
466
+ ```
467
+
468
+ **When to run:**
469
+
470
+ Run abaplint as step 4b — after `syntax`, before `git commit`:
471
+
472
+ ```bash
473
+ # Only if .abaplint.json exists
474
+ npx @abaplint/cli .abaplint.json
475
+ ```
476
+
477
+ Fix any reported issues, then commit.
478
+
479
+ **Before applying any quickfix:**
480
+
481
+ ```
482
+ ❌ WRONG: Accept abaplint quickfixes without checking
483
+ ✅ CORRECT: Run abapgit-agent ref --topic abaplint FIRST, then decide
484
+ ```
485
+
486
+ The `prefer_inline` quickfix is known to introduce a **silent type truncation bug**
487
+ when applied to variables that are later extended with `&&`. Read the guidelines
488
+ before applying it.
489
+
490
+ **When abaplint flags an issue you don't understand:**
491
+ ```bash
492
+ abapgit-agent ref --topic abaplint # bundled rule guidance
493
+ abapgit-agent ref "prefer_inline" # search for specific rule
494
+ abapgit-agent ref "no_inline" # search by keyword
495
+ ```
496
+
497
+ **Project-specific rule guidance:**
498
+
499
+ Projects can add their own abaplint notes to `guidelines/abaplint-local.md` in the
500
+ project repository. After running `abapgit-agent ref export`, the `ref` command
501
+ surfaces both bundled and project-specific guidance together.
502
+
503
+ → See `guidelines/abaplint.md` — run: `abapgit-agent ref --topic abaplint`
504
+
505
+ ---
506
+
402
507
  ## Development Workflow
403
508
 
404
509
  This project's workflow mode is configured in `.abapGitAgent` under `workflow.mode`.
@@ -535,14 +640,17 @@ abapgit-agent pull --files src/<name>.clas.abap --sync-xml
535
640
  Modified ABAP files?
536
641
  ├─ CLAS/INTF/PROG/DDLS files?
537
642
  │ ├─ Independent files (no cross-dependencies)?
538
- │ │ └─ ✅ Use: syntax → commit → push → pull --sync-xml
643
+ │ │ └─ ✅ Use: syntax → [abaplint] → commit → push → pull --sync-xml
539
644
  │ └─ Dependent files (interface + class, class uses class)?
540
- │ └─ ✅ Use: skip syntax → commit → push → pull --sync-xml
645
+ │ └─ ✅ Use: skip syntax → [abaplint] → commit → push → pull --sync-xml
541
646
  └─ Other types (FUGR, TABL, STRU, DTEL, TTYP, etc.)?
542
647
  ├─ XML-only objects (TABL, STRU, DTEL, TTYP)?
543
- │ └─ ✅ Use: skip syntax → commit → push → pull --files abap/ztable.tabl.xml --sync-xml
648
+ │ └─ ✅ Use: skip syntax → [abaplint] → commit → push → pull --files abap/ztable.tabl.xml --sync-xml
544
649
  └─ FUGR and other complex objects?
545
- └─ ✅ Use: skip syntax → commit → push → pull --sync-xml → (if errors: inspect)
650
+ └─ ✅ Use: skip syntax → [abaplint] → commit → push → pull --sync-xml → (if errors: inspect)
651
+
652
+ [abaplint] = run npx @abaplint/cli .abaplint.json only if .abaplint.json exists in repo root
653
+ before applying any quickfix: run abapgit-agent ref --topic abaplint
546
654
  ```
547
655
 
548
656
  → For creating new objects (what files to write): `abapgit-agent ref --topic object-creation`
@@ -583,6 +691,7 @@ Detailed guidelines are available in the `guidelines/` folder:
583
691
  | `guidelines/workflow-detailed.md` | Development Workflow (Detailed) |
584
692
  | `guidelines/object-creation.md` | Object Creation (XML metadata, local classes) |
585
693
  | `guidelines/cds-testing.md` | CDS Testing (Test Double Framework) |
694
+ | `guidelines/abaplint.md` | abaplint Rule Guidelines (prefer_inline trap, safe patterns) |
586
695
 
587
696
  These guidelines are automatically searched by the `ref` command.
588
697
 
@@ -0,0 +1,111 @@
1
+ ---
2
+ layout: default
3
+ title: abaplint Local Rules
4
+ nav_order: 18
5
+ parent: ABAP Coding Guidelines
6
+ grand_parent: ABAP Development
7
+ ---
8
+
9
+ # abaplint Local Rules — abapgit-agent project
10
+
11
+ **Searchable keywords**: naming, local_variable_names, method_parameter_names,
12
+ prefix, lv, lt, ls, lo, li, lx, iv, it, is, io, rv, rs, rt, ro
13
+
14
+ This project enforces **type-specific Hungarian notation** via `local_variable_names`
15
+ and `method_parameter_names` in `.abaplint.json`.
16
+
17
+ ---
18
+
19
+ ## Variable Naming — Required Prefixes
20
+
21
+ ### Local Variables (inside methods)
22
+
23
+ | Prefix | Type | Example |
24
+ |--------|------|---------|
25
+ | `lv_` | Scalar / value (i, string, char, …) | `lv_count TYPE i` |
26
+ | `lt_` | Internal table | `lt_files TYPE ty_files` |
27
+ | `ls_` | Structure | `ls_result TYPE ty_result` |
28
+ | `lo_` | Object reference | `lo_agent TYPE REF TO zcl_abgagt_agent` |
29
+ | `li_` | Interface reference | `li_repo TYPE REF TO zif_abapgit_repo` |
30
+ | `lx_` | Exception reference | `lx_error TYPE REF TO cx_static_check` |
31
+ | `lr_` | Data reference | `lr_data TYPE REF TO data` |
32
+ | `lc_` | Constant | `lc_max TYPE i VALUE 100` |
33
+
34
+ ### Field-Symbols (inside methods)
35
+
36
+ | Prefix | Example |
37
+ |--------|---------|
38
+ | `<lv_>`, `<lt_>`, `<ls_>`, `<lo_>`, `<li_>` | `FIELD-SYMBOLS <ls_item> TYPE ty_item` |
39
+ | `<comp>` | Allowed for generic component iteration |
40
+
41
+ ### Method Parameters
42
+
43
+ | Direction | Prefix | Type |
44
+ |-----------|--------|------|
45
+ | IMPORTING | `iv_` | scalar |
46
+ | IMPORTING | `it_` | table |
47
+ | IMPORTING | `is_` | structure |
48
+ | IMPORTING | `io_` | object ref |
49
+ | IMPORTING | `ii_` | interface ref |
50
+ | IMPORTING | `ix_` | exception ref |
51
+ | RETURNING | `rv_` | scalar |
52
+ | RETURNING | `rs_` | structure |
53
+ | RETURNING | `rt_` | table |
54
+ | RETURNING | `ro_` | object ref |
55
+ | RETURNING | `ri_` | interface ref |
56
+ | RETURNING | `rr_` | data ref |
57
+ | EXPORTING | `ev_`, `es_`, `et_` | scalar / structure / table |
58
+ | CHANGING | `cv_`, `cs_`, `ct_` | scalar / structure / table |
59
+
60
+ ---
61
+
62
+ ## Quick Reference
63
+
64
+ ```abap
65
+ " Local variables
66
+ DATA lv_name TYPE string.
67
+ DATA lt_files TYPE ty_files.
68
+ DATA ls_result TYPE ty_result.
69
+ DATA lo_agent TYPE REF TO zcl_abgagt_agent.
70
+ DATA li_repo TYPE REF TO zif_abapgit_repo.
71
+ DATA lx_error TYPE REF TO cx_static_check.
72
+ FIELD-SYMBOLS <ls_item> TYPE ty_item.
73
+
74
+ " Method signature
75
+ METHODS process
76
+ IMPORTING
77
+ iv_url TYPE string
78
+ it_files TYPE ty_files
79
+ is_config TYPE ty_config
80
+ io_agent TYPE REF TO zcl_abgagt_agent
81
+ ii_repo TYPE REF TO zif_abapgit_repo
82
+ RETURNING
83
+ VALUE(rv_result) TYPE string.
84
+
85
+ METHODS get_repo
86
+ RETURNING
87
+ VALUE(ro_repo) TYPE REF TO zcl_abgagt_agent.
88
+ ```
89
+
90
+ ---
91
+
92
+ ## Rule: Never Use Generic `lv_` for Objects, Tables, or Structures
93
+
94
+ ```abap
95
+ " WRONG — abaplint will flag these
96
+ DATA lv_repo TYPE REF TO zcl_abgagt_agent. " use lo_
97
+ DATA lv_files TYPE ty_files. " use lt_
98
+ DATA lv_result TYPE ty_result. " use ls_
99
+
100
+ " CORRECT
101
+ DATA lo_repo TYPE REF TO zcl_abgagt_agent.
102
+ DATA lt_files TYPE ty_files.
103
+ DATA ls_result TYPE ty_result.
104
+ ```
105
+
106
+ ---
107
+
108
+ ## See Also
109
+
110
+ - `.abaplint.json` — rule definitions (`local_variable_names`, `method_parameter_names`)
111
+ - `guidelines/abaplint.md` — bundled guidance on `prefer_inline` trap
@@ -0,0 +1,173 @@
1
+ ---
2
+ layout: default
3
+ title: abaplint Rule Guidelines
4
+ nav_order: 17
5
+ parent: ABAP Coding Guidelines
6
+ grand_parent: ABAP Development
7
+ ---
8
+
9
+ # abaplint Rule Guidelines
10
+
11
+ **Searchable keywords**: abaplint, prefer_inline, inline declaration, char literal, string truncation,
12
+ no_inline_in_optional_branches, fully_type_constants, linting, static analysis
13
+
14
+ This file covers rules that have **non-obvious or dangerous implications** — cases where applying
15
+ a rule mechanically (or accepting its quickfix) can introduce subtle bugs.
16
+
17
+ For project-specific rule guidance, add a `guidelines/abaplint-local.md` file to the project
18
+ repository. The `ref` command searches both bundled and project guidelines automatically.
19
+
20
+ ---
21
+
22
+ ## prefer_inline — Inline Declarations
23
+
24
+ ### What the rule does
25
+
26
+ Flags up-front `DATA` declarations and suggests replacing them with inline `DATA(var) = expr`:
27
+
28
+ ```abap
29
+ * Bad (flagged by rule)
30
+ DATA lv_count TYPE i.
31
+ lv_count = lines( lt_table ).
32
+
33
+ * Good (preferred by rule)
34
+ DATA(lv_count) = lines( lt_table ).
35
+ ```
36
+
37
+ This is safe when the RHS expression is a **function call, method call, or constructor
38
+ operator** — because the return type is fully defined.
39
+
40
+ ### The char-literal trap — NEVER apply the quickfix here
41
+
42
+ ```
43
+ ❌ DANGEROUS: DATA(var) = 'literal'.
44
+ ```
45
+
46
+ When the RHS is a quoted string literal, ABAP infers type `C LENGTH N` where N equals
47
+ the exact character count of the literal. Any subsequent `&&` concatenation computes the
48
+ correct longer string but **silently truncates it back to N characters**. The variable
49
+ never grows beyond the length of its initial value.
50
+
51
+ **This is the most dangerous quickfix the rule offers — it changes the runtime type.**
52
+
53
+ ```abap
54
+ * WRONG — produced by prefer_inline quickfix, causes silent truncation
55
+ DATA(lv_response) = '{"success":"X",'. " → TYPE C LENGTH 16
56
+ lv_response = lv_response && '"key":"val"'. " computed correctly, truncated to 16 chars
57
+ " lv_response is STILL '{"success":"X",' — the && had no effect
58
+
59
+ * CORRECT — use a string template to build the full value in one step
60
+ DATA(lv_key) = condense( val = CONV string( li_repo->get_key( ) ) ).
61
+ rv_result = |\{"success":"X","key":"{ lv_key }"\}|.
62
+
63
+ * ALSO CORRECT — explicit TYPE string, safe to concatenate
64
+ DATA lv_response TYPE string.
65
+ lv_response = '{"success":"X",'.
66
+ lv_response = lv_response && '"key":"val"}'.
67
+ ```
68
+
69
+ ### Safe vs unsafe patterns
70
+
71
+ | Pattern | Safe? | Why |
72
+ |---|---|---|
73
+ | `DATA(n) = lines( lt_tab ).` | ✅ | Return type `I` — no truncation risk |
74
+ | `DATA(lo) = NEW zcl_foo( ).` | ✅ | Object reference — fully typed |
75
+ | `DATA(ls) = CORRESPONDING #( ls_src ).` | ✅ | Inherits structure type |
76
+ | `DATA(lv) = lv_other.` | ✅ | Inherits type from source variable |
77
+ | `SELECT ... INTO TABLE @DATA(lt).` | ✅ | Type from DB dictionary |
78
+ | `DATA(lv) = 'literal'.` followed by `&&` | ❌ | Infers `C LENGTH N`, truncates |
79
+ | `DATA(lv) = 'literal'.` used only in `\|{ lv }\|` | ⚠️ | Technically works but misleading — prefer explicit type |
80
+ | `DATA(lv) = 'X'.` used as abap_bool flag | ✅ | `C LENGTH 1` is correct for flags |
81
+
82
+ ### Rule of thumb
83
+
84
+ > If the inline-declared variable will ever appear on the left side of `&&`,
85
+ > or be passed to a parameter typed `TYPE string`, declare it explicitly:
86
+ > `DATA lv_foo TYPE string.`
87
+
88
+ ---
89
+
90
+ ## no_inline_in_optional_branches
91
+
92
+ ### What the rule does
93
+
94
+ Flags inline `DATA(var)` declarations inside `IF`, `CASE/WHEN`, `LOOP`, `WHILE`, `DO`,
95
+ and `SELECT` loops — branches that may not execute, leaving the variable uninitialized
96
+ when code after the branch reads it.
97
+
98
+ ```abap
99
+ * Bad (flagged)
100
+ LOOP AT lt_items INTO DATA(ls_item).
101
+ DATA(lv_key) = ls_item-key. " declared inside LOOP — only set when loop runs
102
+ ENDLOOP.
103
+ WRITE lv_key. " undefined if lt_items was empty
104
+
105
+ * Good
106
+ DATA lv_key TYPE string.
107
+ LOOP AT lt_items INTO DATA(ls_item).
108
+ lv_key = ls_item-key.
109
+ ENDLOOP.
110
+ WRITE lv_key.
111
+ ```
112
+
113
+ **Exception**: `TRY/CATCH/CLEANUP` is explicitly NOT considered an optional branch by
114
+ the rule — inline declarations inside `TRY` are allowed.
115
+
116
+ ### When you see this rule triggered
117
+
118
+ Move the `DATA(var)` declaration out of the branch to the top of the method, giving it
119
+ an explicit type:
120
+
121
+ ```abap
122
+ * Before (flagged)
123
+ IF condition.
124
+ DATA(lv_result) = compute( ).
125
+ ENDIF.
126
+
127
+ * After (clean)
128
+ DATA lv_result TYPE string.
129
+ IF condition.
130
+ lv_result = compute( ).
131
+ ENDIF.
132
+ ```
133
+
134
+ ---
135
+
136
+ ## Project-Specific Rule Overrides
137
+
138
+ Each project can add its own abaplint guidance by creating a file in the `guidelines/`
139
+ folder of the project repository:
140
+
141
+ ```
142
+ guidelines/
143
+ abaplint-local.md ← project-specific rule notes
144
+ ```
145
+
146
+ After creating the file, export it so the `ref` command can find it:
147
+
148
+ ```bash
149
+ abapgit-agent ref export
150
+ ```
151
+
152
+ Then `abapgit-agent ref "prefer_inline"` will surface both this bundled guidance
153
+ and the project-specific notes together.
154
+
155
+ **Example `guidelines/abaplint-local.md`:**
156
+
157
+ ```markdown
158
+ ## prefer_inline — project rules
159
+
160
+ This project's .abaplint.json enables prefer_inline.
161
+
162
+ Additional constraint: never inline-declare response-building variables.
163
+ All JSON response strings must use string templates (| ... |) directly
164
+ on rv_result — no intermediate lv_response variable at all.
165
+ ```
166
+
167
+ ---
168
+
169
+ ## See Also
170
+
171
+ - **common-errors.md** — char-literal truncation listed as a known error pattern
172
+ - **json.md** — safe patterns for building JSON strings in ABAP
173
+ - **workflow-detailed.md** — where abaplint fits in the development workflow
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  layout: default
3
3
  title: CDS Testing
4
- nav_order: 17
4
+ nav_order: 19
5
5
  parent: ABAP Coding Guidelines
6
6
  grand_parent: ABAP Development
7
7
  ---
@@ -89,7 +89,38 @@ Find data elements: `abapgit-agent view --objects <TABLE> --type TABL`
89
89
 
90
90
  ---
91
91
 
92
+ ## Inline Declaration from String Literal — Silent Truncation
93
+
94
+ **Symptom**: A `&&` concatenation has no effect; the variable retains its initial value.
95
+
96
+ **Root cause**: `DATA(var) = 'literal'` infers type `C LENGTH N` (N = literal length).
97
+ Any subsequent `&&` computes the correct longer string but truncates it back to N chars.
98
+ The abaplint `prefer_inline` quickfix can introduce this bug automatically.
99
+
100
+ ```abap
101
+ * WRONG — lv_response stays '{"success":"X",' after && (16 chars, always truncated)
102
+ DATA(lv_response) = '{"success":"X",'.
103
+ lv_response = lv_response && '"key":"value"}'. " no effect!
104
+
105
+ * CORRECT — use string template
106
+ DATA(lv_key) = condense( val = CONV string( li_repo->get_key( ) ) ).
107
+ rv_result = |\{"success":"X","key":"{ lv_key }"\}|.
108
+
109
+ * ALSO CORRECT — explicit TYPE string
110
+ DATA lv_response TYPE string.
111
+ lv_response = '{"success":"X",'.
112
+ lv_response = lv_response && '"key":"value"}'. " works correctly
113
+ ```
114
+
115
+ **Fix**: Replace the inline declaration + `&&` chain with a string template,
116
+ or declare the variable explicitly with `TYPE string`.
117
+
118
+ → See `abaplint.md` for full guidance on the `prefer_inline` rule.
119
+
120
+ ---
121
+
92
122
  ## See Also
93
123
  - **ABAP SQL** (sql.md) - for SQL syntax rules
94
124
  - **CDS Views** (cds.md) - for CDS selection patterns
95
125
  - **abapGit** (abapgit.md) - for XML metadata templates
126
+ - **abaplint** (abaplint.md) - for abaplint rule guidance and known quickfix traps
@@ -30,6 +30,7 @@ This folder contains detailed ABAP coding guidelines that can be searched using
30
30
  | `workflow-detailed.md` | Development Workflow (Detailed) |
31
31
  | `object-creation.md` | Object Creation (XML metadata, local classes) |
32
32
  | `cds-testing.md` | CDS Testing (Test Double Framework) |
33
+ | `abaplint.md` | abaplint Rule Guidelines (prefer_inline trap, safe patterns) |
33
34
 
34
35
  ## Usage
35
36
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  layout: default
3
3
  title: Probe and PoC Guide
4
- nav_order: 19
4
+ nav_order: 21
5
5
  parent: ABAP Coding Guidelines
6
6
  grand_parent: ABAP Development
7
7
  ---
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  layout: default
3
3
  title: run Command Guide
4
- nav_order: 18
4
+ nav_order: 20
5
5
  parent: ABAP Coding Guidelines
6
6
  grand_parent: ABAP Development
7
7
  ---
@@ -257,31 +257,11 @@ ENDMETHOD.
257
257
 
258
258
  ## Test Double Patterns
259
259
 
260
- ### Manual Test Double (Local Class)
260
+ **Prefer the ABAP Test Double Framework** (`cl_abap_testdouble`) over manual doubles.
261
+ Use manual doubles only when stateful logic is required (e.g. call-count tracking, results that
262
+ vary per call, or complex setup that configure_call cannot express).
261
263
 
262
- ```abap
263
- " Create test double class
264
- CLASS ltd_mock_reader DEFINITION FOR TESTING.
265
- PUBLIC SECTION.
266
- INTERFACES zif_data_reader PARTIALLY IMPLEMENTED.
267
- METHODS set_result_data
268
- IMPORTING it_data TYPE ANY TABLE.
269
- PRIVATE SECTION.
270
- DATA mt_data TYPE ANY TABLE.
271
- ENDCLASS.
272
-
273
- CLASS ltd_mock_reader IMPLEMENTATION.
274
- METHOD set_result_data.
275
- mt_data = it_data.
276
- ENDMETHOD.
277
-
278
- METHOD zif_data_reader~read_all.
279
- rt_data = mt_data.
280
- ENDMETHOD.
281
- ENDCLASS.
282
- ```
283
-
284
- ### Using ABAP Test Double Framework
264
+ ### Using ABAP Test Double Framework (Recommended)
285
265
 
286
266
  ```abap
287
267
  " Step 1: Declare with correct interface type, then assign
@@ -315,6 +295,30 @@ lo_mock->my_method( ... ).
315
295
  - Use `returning(value = ...)` not `IMPORTING`
316
296
  - Call method after configure_call to register the configuration
317
297
 
298
+ ### Manual Test Double (Local Class — use only when stateful logic is needed)
299
+
300
+ ```abap
301
+ " Create test double class
302
+ CLASS ltd_mock_reader DEFINITION FOR TESTING.
303
+ PUBLIC SECTION.
304
+ INTERFACES zif_data_reader PARTIALLY IMPLEMENTED.
305
+ METHODS set_result_data
306
+ IMPORTING it_data TYPE ANY TABLE.
307
+ PRIVATE SECTION.
308
+ DATA mt_data TYPE ANY TABLE.
309
+ ENDCLASS.
310
+
311
+ CLASS ltd_mock_reader IMPLEMENTATION.
312
+ METHOD set_result_data.
313
+ mt_data = it_data.
314
+ ENDMETHOD.
315
+
316
+ METHOD zif_data_reader~read_all.
317
+ rt_data = mt_data.
318
+ ENDMETHOD.
319
+ ENDCLASS.
320
+ ```
321
+
318
322
  ### Mocking EXPORT Parameters
319
323
 
320
324
  Some methods use EXPORT parameters instead of returning values. Use `set_parameter`:
@@ -24,9 +24,22 @@ grand_parent: ABAP Development
24
24
  │ │
25
25
  │ ├─► Errors? → Fix locally (no commit needed), re-run syntax
26
26
  │ │
27
+ │ └─► Clean ✅ → Proceed to 4b
28
+
29
+ └─► Other types (FUGR, TABL, etc.) → Skip syntax, go to 4b
30
+
31
+
32
+ 4b. abaplint (OPTIONAL — only if .abaplint.json exists in repo root)
33
+
34
+ ├─► .abaplint.json exists → npx @abaplint/cli .abaplint.json
35
+ │ │
36
+ │ ├─► Issues? → Fix locally, re-run abaplint
37
+ │ │ ⚠️ Before applying any quickfix: run abapgit-agent ref --topic abaplint
38
+ │ │ Quickfixes for prefer_inline can introduce silent type truncation bugs.
39
+ │ │
27
40
  │ └─► Clean ✅ → Proceed to commit
28
41
 
29
- └─► Other types (FUGR, TABL, etc.) → Skip syntax, go to commit
42
+ └─► No .abaplint.json → Skip, go to commit
30
43
 
31
44
 
32
45
  5. Commit and push → git add . && git commit && git push
@@ -140,8 +153,9 @@ git push
140
153
  abapgit-agent pull --files src/zcl_my_class.clas.abap,src/zc_my_view.ddls.asddls
141
154
  ```
142
155
 
143
- **When to use syntax vs inspect vs view**:
144
- - **syntax**: Check LOCAL code BEFORE commit (CLAS, INTF, PROG, DDLS)
156
+ **When to use syntax vs abaplint vs inspect vs view**:
157
+ - **syntax**: Check LOCAL ABAP syntax BEFORE commit (CLAS, INTF, PROG, DDLS)
158
+ - **abaplint**: Check LOCAL code style/quality BEFORE commit (only if .abaplint.json present)
145
159
  - **inspect**: Check ACTIVATED code AFTER pull (all types, runs Code Inspector)
146
160
  - **view**: Understand object STRUCTURE (not for debugging errors)
147
161
 
@@ -164,22 +178,25 @@ abapgit-agent pull --files src/zcl_my_class.clas.abap,src/zc_my_view.ddls.asddls
164
178
  └─ Unrelated bug fixes across files? → INDEPENDENT
165
179
 
166
180
  3. For SUPPORTED types (CLAS/INTF/PROG/DDLS):
167
- ├─ INDEPENDENT files → Run syntax → Fix errors → Commit → Push → Pull
181
+ ├─ INDEPENDENT files → Run syntax → [abaplint if enabled] → Fix errors → Commit → Push → Pull
168
182
 
169
183
  └─ DEPENDENT files (NEW objects):
170
184
  ├─ RECOMMENDED: Create underlying object first (interface, base class, etc.)
171
- │ 1. Create underlying object → Syntax → Commit → Push → Pull
172
- │ 2. Create dependent object → Syntax (works!) → Commit → Push → Pull
185
+ │ 1. Create underlying object → Syntax → [abaplint] → Commit → Push → Pull
186
+ │ 2. Create dependent object → Syntax (works!) → [abaplint] → Commit → Push → Pull
173
187
  │ ✅ Benefits: Both syntax checks work, cleaner workflow
174
188
 
175
189
  └─ ALTERNATIVE: If interface design uncertain, commit both together
176
- → Skip syntax → Commit both → Push → Pull → (if errors: inspect)
190
+ → Skip syntax → [abaplint] → Commit both → Push → Pull → (if errors: inspect)
177
191
 
178
192
  4. For UNSUPPORTED types (FUGR, TABL, etc.):
179
- Write code → Skip syntax → Commit → Push → Pull → (if errors: inspect)
193
+ Write code → Skip syntax → [abaplint] → Commit → Push → Pull → (if errors: inspect)
180
194
 
181
195
  5. For MIXED types (some supported + some unsupported):
182
- Write all code → Run syntax on independent supported files ONLY → Commit ALL → Push → Pull ALL
196
+ Write all code → Run syntax on independent supported files ONLY → [abaplint] → Commit ALL → Push → Pull ALL
197
+
198
+ [abaplint] = run npx @abaplint/cli .abaplint.json only if .abaplint.json exists in repo root
199
+ before applying any quickfix: run abapgit-agent ref --topic abaplint
183
200
  ```
184
201
 
185
202
  **Example workflows:**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abapgit-agent",
3
- "version": "1.13.5",
3
+ "version": "1.13.7",
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",
@@ -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
 
@@ -40,7 +40,18 @@ function getBranch() {
40
40
 
41
41
  const content = fs.readFileSync(headPath, 'utf8').trim();
42
42
  const match = content.match(/ref: refs\/heads\/(.+)/);
43
- return match ? match[1] : 'main';
43
+ if (match) return match[1];
44
+
45
+ // Detached HEAD (e.g. Jenkins checkout) — use Jenkins env vars:
46
+ // CHANGE_BRANCH is set in PR builds (actual head branch name),
47
+ // BRANCH_NAME is set in all builds (branch name or "PR-N" for PRs).
48
+ // For PR builds BRANCH_NAME is "PR-N" so prefer CHANGE_BRANCH first.
49
+ if (process.env.CHANGE_BRANCH) return process.env.CHANGE_BRANCH;
50
+ if (process.env.BRANCH_NAME && !process.env.BRANCH_NAME.startsWith('PR-')) {
51
+ return process.env.BRANCH_NAME;
52
+ }
53
+
54
+ return 'main';
44
55
  }
45
56
 
46
57
  /**