mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Valery Kharseko
yesterday b6db4d342bdd1845c7ff5740ef69e6bf5afaf091
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
# The contents of this file are subject to the terms of the Common Development and
# Distribution License (the License). You may not use this file except in compliance with the
# License.
#
# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
# specific language governing permission and limitations under the License.
#
# When distributing Covered Software, include this CDDL Header Notice in each file and include
# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
# Header, with the fields enclosed by brackets [] replaced by your own identifying
# information: "Portions copyright [year] [name of copyright owner]".
#
# Copyright 2021-2026 3A Systems, LLC.
 
name: Build
 
on:
  push:
    branches: [ 'sustaining/4.10.x','master', 'issues/**', 'features/**' ]
  pull_request:
 
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
 
# Nothing in this workflow writes back to the repository: the docker jobs push to
# the local registry service, not to a remote one, and the other jobs only
# publish artifacts through the actions API. The docker jobs additionally get
# security-events: write to upload Trivy scan results to code scanning.
permissions:
  contents: read
 
jobs:
  build-maven:
    runs-on: ${{ matrix.os }}
    strategy:
      # The full Java sweep runs on ubuntu only; macOS and Windows build with the
      # minimum supported (11) and the latest (26) Java.
      matrix:
        os: [ 'ubuntu-latest' ]
        java: [ '11','17','21','25','26']
        include:
          - { os: 'macos-latest',   java: '11' }
          - { os: 'macos-latest',   java: '26' }
          - { os: 'windows-latest', java: '11' }
          - { os: 'windows-latest', java: '26' }
      fail-fast: false
    steps:
    - name:  Install wine+rpm for distribution
      if: runner.os == 'Linux'
      shell: bash
      run:   |
        sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list
        sudo dpkg --add-architecture i386
        sudo mkdir -pm755 /etc/apt/keyrings && sudo wget -O /etc/apt/keyrings/winehq-archive.key https://dl.winehq.org/wine-builds/winehq.key
        sudo wget -NP /etc/apt/sources.list.d/ https://dl.winehq.org/wine-builds/ubuntu/dists/$(lsb_release -c -s)/winehq-$(lsb_release -c -s).sources
        sudo apt-get update
        sudo apt install --install-recommends winehq-stable || sudo apt install --install-recommends winehq-staging
        wine --version
        version="9.4.0"; sudo wget "https://dl.winehq.org/wine/wine-mono/$version/wine-mono-$version-x86.msi" -O /tmp/wine-mono.msi
        wine msiexec /i /tmp/wine-mono.msi
    - uses: actions/checkout@v6
      with:
        fetch-depth: 0
        submodules: recursive
    - name: Java ${{ matrix.Java }} (${{ matrix.os }})
      uses: actions/setup-java@v5
      with:
        java-version: ${{ matrix.java }}
        distribution: 'zulu'
    - name: Cache Maven packages
      uses: actions/cache@v5
      with:
         path: ~/.m2/repository
         key: ${{ runner.os }}-m2-repository-${{ hashFiles('**/pom.xml') }}
         restore-keys: ${{ runner.os }}-m2-repository
    - name: Setup MSVC Developer Command Prompt (x86)
      if: runner.os == 'Windows'
      uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1.13.0
      env:
        # Opt in to Node.js 24 for this action, which still ships on Node.js 20.
        # See https://github.com/ilammy/msvc-dev-cmd/issues/99
        FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
      with:
        arch: x86
    - name: Build Windows native executables
      if: runner.os == 'Windows'
      shell: cmd
      run: |
        cd opendj-server-legacy\src\build-tools\windows
        nmake all
        xcopy /Y *.exe ..\..\..\lib\
        git status
    - name: Set Integration Test Environment
      id: failsafe
      if: runner.os == 'Linux'
      run:   |
        echo "MAVEN_PROFILE_FLAG=-P precommit" >> $GITHUB_OUTPUT
 
    # The per-module javadoc:jar that runs during verify only ever sees one
    # module's sources, so it cannot report a package declared by two modules
    # at once. Only the aggregate on the root reactor can, and it used to run
    # nowhere but deploy.yml, after the merge and under continue-on-error, so a
    # broken aggregate blocked nothing and went unnoticed. This is the gate for
    # it. Appending the goal to the same Maven invocation keeps it in the
    # reactor that just built; one cell is enough, and this is the platform and
    # Java version deploy.yml aggregates with.
    - name: Set Aggregate Javadoc Goal
      id: javadoc
      if: runner.os == 'Linux' && matrix.java == '11'
      run:   |
        echo "MAVEN_JAVADOC_GOAL=javadoc:aggregate" >> $GITHUB_OUTPUT
 
    - name: Build with Maven
      timeout-minutes: 180
      env:
        MAVEN_OPTS: -Dhttps.protocols=TLSv1.2 -Dmaven.wagon.httpconnectionManager.ttlSeconds=120 -Dmaven.wagon.http.retryHandler.requestSentEnabled=true -Dmaven.wagon.http.retryHandler.count=10
      run: mvn --batch-mode --errors --update-snapshots verify ${{ steps.javadoc.outputs.MAVEN_JAVADOC_GOAL }} --file pom.xml ${{ steps.failsafe.outputs.MAVEN_PROFILE_FLAG }}
    - name: Test on Unix
      if: runner.os == 'Linux'
      run:   |
        export OPENDJ_JAVA_ARGS="-server -Xmx512m"
        opendj-server-legacy/target/package/opendj/setup -h localhost -p 1389 --ldapsPort 1636 --adminConnectorPort 4444 --enableStartTLS --generateSelfSignedCertificate --rootUserDN "cn=Directory Manager" --rootUserPassword password --baseDN dc=example,dc=com --sampleData 5000 --cli --acceptLicense --no-prompt
        opendj-server-legacy/target/package/opendj/bin/status --hostname localhost --bindDN "cn=Directory Manager" --bindPassword password --trustAll
        opendj-server-legacy/target/package/opendj/bin/ldapsearch --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "dc=example,dc=com" --searchScope base "(objectClass=*)" 1.1
        opendj-server-legacy/target/package/opendj/bin/ldapsearch --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "ou=people,dc=example,dc=com" --searchScope sub "(uid=user.*)" dn | grep ^dn: | wc -l | grep -q 5000
        # --- Load-testing tools smoke tests (server online, 5000 sample users) ---
        echo "===== searchrate ====="
        opendj-server-legacy/target/package/opendj/bin/searchrate --hostname localhost --port 1636 --useSSL --trustAll --bindDN "cn=Directory Manager" --bindPassword password --baseDN "ou=people,dc=example,dc=com" --searchScope sub -c 4 -t 1 -m 2000 -i 1 -g "rand(0,4999)" "(uid=user.%d)" 1.1
        echo "===== authrate ====="
        opendj-server-legacy/target/package/opendj/bin/authrate --hostname localhost --port 1636 --useSSL --trustAll -D "uid=user.%d,ou=people,dc=example,dc=com" -w password -c 4 -m 2000 -i 1 -g "rand(0,4999)"
        echo "===== modrate ====="
        opendj-server-legacy/target/package/opendj/bin/modrate --hostname localhost --port 1636 --useSSL --trustAll --bindDN "cn=Directory Manager" --bindPassword password -b "uid=user.%d,ou=people,dc=example,dc=com" -c 4 -t 1 -m 2000 -i 1 -g "rand(0,4999)" "description:modrate-load-test"
        echo "===== addrate ====="
        cat > /tmp/addrate.template <<'EOF'
        define suffix=dc=example,dc=com
 
        branch: ou=People,[suffix]
        subordinateTemplate: person
 
        template: person
        rdnAttr: uid
        objectClass: top
        objectClass: person
        objectClass: organizationalPerson
        objectClass: inetOrgPerson
        uid: addrate.<sequential:0>
        cn: AddRate {uid}
        sn: AddRate
        userPassword: password
        EOF
        opendj-server-legacy/target/package/opendj/bin/addrate --hostname localhost --port 1636 --useSSL --trustAll --bindDN "cn=Directory Manager" --bindPassword password -c 4 -t 1 -m 2000 -i 1 -C fifo -s 100 /tmp/addrate.template
        opendj-server-legacy/target/package/opendj/bin/dsconfig create-backend --hostname localhost --port 4444 --bindDN "cn=Directory Manager" --bindPassword password --backend-name=example2 --type je --set=base-dn:dc=example2,dc=com --set=enabled:true --no-prompt --trustAll
        opendj-server-legacy/target/package/opendj/bin/makeldif -o /tmp/test.ldif -c suffix=dc=example2,dc=com opendj-server-legacy/target/package/opendj/config/MakeLDIF/example.template
        opendj-server-legacy/target/package/opendj/bin/stop-ds
        echo "4.9.9.0" > opendj-server-legacy/target/package/opendj/config/buildinfo
        opendj-server-legacy/target/package/opendj/upgrade -n
        opendj-server-legacy/target/package/opendj/bin/import-ldif --offline --ldifFile /tmp/test.ldif --backendID=example2
        opendj-server-legacy/target/package/opendj/bin/rebuild-index --offline --bindDN "cn=Directory Manager" --bindPassword password  --baseDN "dc=example2,dc=com" --rebuildAll
        opendj-server-legacy/target/package/opendj/bin/start-ds
        opendj-server-legacy/target/package/opendj/bin/rebuild-index --bindDN "cn=Directory Manager" --bindPassword password --baseDN "dc=example2,dc=com" --rebuildAll --trustAll
        opendj-server-legacy/target/package/opendj/bin/ldapsearch --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "ou=people,dc=example2,dc=com" --searchScope sub "(uid=user.*)" dn | grep ^dn: | wc -l | grep -q 10000
        opendj-server-legacy/target/package/opendj/bin/stop-ds
        rm -rf opendj-server-legacy/target/package/opendj/{config,db,changelogDb,logs,tmp}
 
    - name: Test on Unix FIPS
      if: runner.os == 'Linux'
      run: |
        export OPENDJ_JAVA_ARGS="-server -Xmx512m" 
        echo password > /tmp/opendj.keystore.pin
        
        keytool -genkey -alias server-cert -keyalg rsa \
          -dname "CN=example.com,O=OpenDJ RSA Self-Signed Certificate" \
          -keystore /tmp/opendj.bcfks -storetype BCFKS -validity 3650 -providername BCFIPS \
          -provider org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider \
          -providerpath ./opendj-server-legacy/target/package/opendj/lib/org.bouncycastle.bc-fips.jar:./opendj-server-legacy/target/package/opendj/lib/org.bouncycastle.bcpkix-fips.jar \
          -keypass:file /tmp/opendj.keystore.pin -storepass:file /tmp/opendj.keystore.pin -keysize 2048 -sigalg SHA256WITHRSA
  
        keytool -selfcert -alias server-cert -keystore /tmp/opendj.bcfks \
          -storetype BCFKS -validity 3650 -providername BCFIPS \
          -provider org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider \
          -providerpath ./opendj-server-legacy/target/package/opendj/lib/org.bouncycastle.bc-fips.jar:./opendj-server-legacy/target/package/opendj/lib/org.bouncycastle.bcpkix-fips.jar \
          -storepass:file /tmp/opendj.keystore.pin
        
        keytool -genkey -alias admin-cert -keyalg rsa \
          -dname "CN=example.com,O=Administration Connector RSA Self-Signed Certificate" \
          -keystore /tmp/opendj.bcfks -storetype BCFKS -validity 3650 -providername BCFIPS \
          -provider org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider \
          -providerpath ./opendj-server-legacy/target/package/opendj/lib/org.bouncycastle.bc-fips.jar:./opendj-server-legacy/target/package/opendj/lib/org.bouncycastle.bcpkix-fips.jar \
          -keypass:file /tmp/opendj.keystore.pin -storepass:file /tmp/opendj.keystore.pin -keysize 2048 -sigalg SHA256WITHRSA
        
        keytool -selfcert -alias admin-cert -keystore /tmp/opendj.bcfks \
          -storetype BCFKS -validity 3650 -providername BCFIPS \
          -provider org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider \
          -providerpath ./opendj-server-legacy/target/package/opendj/lib/org.bouncycastle.bc-fips.jar:./opendj-server-legacy/target/package/opendj/lib/org.bouncycastle.bcpkix-fips.jar \
          -storepass:file /tmp/opendj.keystore.pin
        
        echo "useBcfksKeystore=/tmp/opendj.bcfks
        keyStorePasswordFile=/tmp/opendj.keystore.pin" > /tmp/opendj-setup.properties.bcfks
        
        opendj-server-legacy/target/package/opendj/setup -h localhost -p 1389 --ldapsPort 1636 --adminConnectorPort 4444 \
          --enableStartTLS --certNickname admin-cert --rootUserDN "cn=Directory Manager" --rootUserPassword password \
          --baseDN dc=example,dc=com --sampleData 5000 --cli --acceptLicense --no-prompt \
          --propertiesFilePath /tmp/opendj-setup.properties.bcfks --doNotStart
        
        opendj-server-legacy/target/package/opendj/bin/start-ds
        
        opendj-server-legacy/target/package/opendj/bin/status --hostname localhost --bindDN "cn=Directory Manager" --bindPassword password --trustAll
        opendj-server-legacy/target/package/opendj/bin/ldapsearch --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "dc=example,dc=com" --searchScope base "(objectClass=*)" 1.1
        opendj-server-legacy/target/package/opendj/bin/ldapsearch --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "ou=people,dc=example,dc=com" --searchScope sub "(uid=user.*)" dn | grep ^dn: | wc -l | grep -q 5000
        opendj-server-legacy/target/package/opendj/bin/dsconfig create-backend --hostname localhost --port 4444 --bindDN "cn=Directory Manager" --bindPassword password --backend-name=example2 --type je --set=base-dn:dc=example2,dc=com --set=enabled:true --no-prompt --trustAll
        opendj-server-legacy/target/package/opendj/bin/makeldif -o /tmp/test.ldif -c suffix=dc=example2,dc=com opendj-server-legacy/target/package/opendj/config/MakeLDIF/example.template
        opendj-server-legacy/target/package/opendj/bin/stop-ds
        echo "4.9.9.0" > opendj-server-legacy/target/package/opendj/config/buildinfo
        opendj-server-legacy/target/package/opendj/upgrade -n
        opendj-server-legacy/target/package/opendj/bin/import-ldif --offline --ldifFile /tmp/test.ldif --backendID=example2
        opendj-server-legacy/target/package/opendj/bin/rebuild-index --offline --bindDN "cn=Directory Manager" --bindPassword password  --baseDN "dc=example2,dc=com" --rebuildAll
        opendj-server-legacy/target/package/opendj/bin/start-ds
        opendj-server-legacy/target/package/opendj/bin/rebuild-index --bindDN "cn=Directory Manager" --bindPassword password --baseDN "dc=example2,dc=com" --rebuildAll --trustAll
        opendj-server-legacy/target/package/opendj/bin/ldapsearch --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "ou=people,dc=example2,dc=com" --searchScope sub "(uid=user.*)" dn | grep ^dn: | wc -l | grep -q 10000
        opendj-server-legacy/target/package/opendj/bin/stop-ds
        rm -rf opendj-server-legacy/target/package/opendj/{config,db,changelogDb,logs,tmp}
    - name: Test LDAP in Cassandra
      if: runner.os == 'Linux'
      run:   |
        docker run --rm -it -d -p 9042:9042 --name cassandra cassandra
        timeout 5m bash -c 'until docker logs cassandra | grep -q "Created default superuser role"; do sleep 5; done'
        export OPENDJ_JAVA_ARGS="-server -Xmx512m -Ddatastax-java-driver.basic.contact-points.0=localhost:9042 -Ddatastax-java-driver.basic.load-balancing-policy.local-datacenter=datacenter1"
        opendj-server-legacy/target/package/opendj/setup -h localhost -p 1389 --ldapsPort 1636 --adminConnectorPort 4444 --enableStartTLS --generateSelfSignedCertificate --rootUserDN "cn=Directory Manager" --rootUserPassword password --cli --acceptLicense --no-prompt
        opendj-server-legacy/target/package/opendj/bin/dsconfig create-backend -h localhost -p 4444 --bindDN "cn=Directory Manager" --bindPassword password --backend-name=userRoot --type cas --set base-dn:dc=example,dc=com --set db-directory:keyspace_name --set enabled:true --no-prompt --trustAll
        opendj-server-legacy/target/package/opendj/bin/makeldif -o /tmp/test.ldif -c suffix=dc=example,dc=com opendj-server-legacy/target/package/opendj/config/MakeLDIF/example.template
        opendj-server-legacy/target/package/opendj/bin/import-ldif --ldifFile /tmp/test.ldif --backendID=userRoot -h localhost -p 4444 --bindDN "cn=Directory Manager" --bindPassword password --trustAll
        opendj-server-legacy/target/package/opendj/bin/status --hostname localhost --bindDN "cn=Directory Manager" --bindPassword password --trustAll
        opendj-server-legacy/target/package/opendj/bin/ldapsearch --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "dc=example,dc=com" --searchScope base "(objectClass=*)" 1.1
        opendj-server-legacy/target/package/opendj/bin/ldapsearch --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "ou=people,dc=example,dc=com" --searchScope sub "(uid=user.*)" dn | grep ^dn: | wc -l | grep -q 10000
        opendj-server-legacy/target/package/opendj/bin/stop-ds
        rm -rf opendj-server-legacy/target/package/opendj/{config,db,changelogDb,logs,tmp}
    - name: Test LDAP in Postgres
      if: runner.os == 'Linux'
      run:   |
        docker run --rm -it -d -p 5432:5432 -e POSTGRES_DB=database_name -e POSTGRES_PASSWORD=password --name postgres postgres
        timeout 5m bash -c 'until docker logs postgres | grep -q "database system is ready to accept connections"; do sleep 5; done'
        export OPENDJ_JAVA_ARGS="-server -Xmx512m"
        opendj-server-legacy/target/package/opendj/setup -h localhost -p 1389 --ldapsPort 1636 --adminConnectorPort 4444 --enableStartTLS --generateSelfSignedCertificate --rootUserDN "cn=Directory Manager" --rootUserPassword password --cli --acceptLicense --no-prompt
        opendj-server-legacy/target/package/opendj/bin/dsconfig create-backend -h localhost -p 4444 --bindDN "cn=Directory Manager" --bindPassword password --backend-name=userRoot --type jdbc --set base-dn:dc=example,dc=com --set db-directory:jdbc:postgresql://localhost:5432/database_name?user=postgres\&password=password --set enabled:true --no-prompt --trustAll
        opendj-server-legacy/target/package/opendj/bin/makeldif -o /tmp/test.ldif -c suffix=dc=example,dc=com opendj-server-legacy/target/package/opendj/config/MakeLDIF/example.template
        opendj-server-legacy/target/package/opendj/bin/ldapmodify --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll -f /tmp/test.ldif -a 1> /dev/null
        opendj-server-legacy/target/package/opendj/bin/status --hostname localhost --bindDN "cn=Directory Manager" --bindPassword password --trustAll
        opendj-server-legacy/target/package/opendj/bin/ldapsearch --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "dc=example,dc=com" --searchScope base "(objectClass=*)" 1.1
        opendj-server-legacy/target/package/opendj/bin/ldapsearch --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "ou=people,dc=example,dc=com" --searchScope sub "(uid=user.*)" dn | grep ^dn: | wc -l | grep -q 10000
        opendj-server-legacy/target/package/opendj/bin/stop-ds
        rm -rf opendj-server-legacy/target/package/opendj/{config,db,changelogDb,logs,tmp}
    - name: Test replication
      if: runner.os == 'Linux'
      # dsreplication enable can hang indefinitely; without a timeout a hang
      # holds the runner until the 6-hour job limit. Normal duration is ~4 min.
      timeout-minutes: 30
      run: |
        cp -r ./opendj-server-legacy/target/package/opendj ./opendj-server-legacy/target/package/opendj1
        cp -r ./opendj-server-legacy/target/package/opendj ./opendj-server-legacy/target/package/opendj2
        cp -r ./opendj-server-legacy/target/package/opendj ./opendj-server-legacy/target/package/opendj3
        
        echo "Setup OpenDJ-1"
        
        opendj-server-legacy/target/package/opendj1/setup -h localhost -p 1389 --ldapsPort 1636 --adminConnectorPort 4444 --enableStartTLS \
        --generateSelfSignedCertificate --rootUserDN "cn=Directory Manager" --rootUserPassword password --baseDN dc=example,dc=com \
        --sampleData 100000 --cli --acceptLicense --no-prompt
        
        echo "Setup OpenDJ-2 with replication"
        
        opendj-server-legacy/target/package/opendj2/setup -h localhost -p 2389 --ldapsPort 2636 --adminConnectorPort 24444 --enableStartTLS \
        --generateSelfSignedCertificate --rootUserDN "cn=Directory Manager" --rootUserPassword password --baseDN dc=example,dc=com \
        --addBaseEntry --cli --acceptLicense --no-prompt
        
        opendj-server-legacy/target/package/opendj2/bin/dsreplication enable --no-prompt --host1 localhost --port1 4444 --bindDN1 "cn=Directory Manager"  --bindPassword1 password --replicationPort1 8989 \
        --host2 localhost --port2 24444 --bindDN2 "cn=Directory Manager" --bindPassword2 password --replicationPort2 28989 \
        --adminUID admin --adminPassword password --baseDN dc=example,dc=com --trustAll --noPropertiesFile
        
        opendj-server-legacy/target/package/opendj2/bin/dsreplication initialize --baseDN dc=example,dc=com --adminUID admin --adminPassword password --hostSource localhost \
        --portSource 4444 --hostDestination localhost --portDestination 24444 -X -n
        
        opendj-server-legacy/target/package/opendj2/bin/ldapsearch --port 2636 --hostname localhost --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll \
        --baseDN "ou=people,dc=example,dc=com" --searchScope sub "(uid=user.*)" dn  | grep ^dn: | wc -l | grep -q 100000
        
        echo "Setup OpenDJ-3 with replication"
        
        opendj-server-legacy/target/package/opendj3/setup -h localhost -p 3389 --ldapsPort 3636 --adminConnectorPort 34444 --enableStartTLS \
        --generateSelfSignedCertificate --rootUserDN "cn=Directory Manager" --rootUserPassword password --baseDN dc=example,dc=com \
        --addBaseEntry --cli --acceptLicense --no-prompt
        
        opendj-server-legacy/target/package/opendj3/bin/dsreplication enable --no-prompt --host1 localhost --port1 24444 --bindDN1 "cn=Directory Manager"  --bindPassword1 password --replicationPort1 28989 \
        --host2 localhost --port2 34444 --bindDN2 "cn=Directory Manager" --bindPassword2 password --replicationPort2 38989 \
        --adminUID admin --adminPassword password --baseDN dc=example,dc=com --trustAll --noPropertiesFile
        
        opendj-server-legacy/target/package/opendj3/bin/dsreplication initialize --baseDN dc=example,dc=com --adminUID admin --adminPassword password --hostSource localhost \
        --portSource 24444 --hostDestination localhost --portDestination 34444 -X -n
        
        opendj-server-legacy/target/package/opendj2/bin/ldapsearch --port 3636 --hostname localhost --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll \
        --baseDN "ou=people,dc=example,dc=com" --searchScope sub "(uid=user.*)" dn  | grep ^dn: | wc -l | grep -q 100000
        
        echo "Stopping and removing OpenDJ instances"
        
        opendj-server-legacy/target/package/opendj1/bin/stop-ds
        opendj-server-legacy/target/package/opendj2/bin/stop-ds
        opendj-server-legacy/target/package/opendj3/bin/stop-ds
        
        rm -rf ./opendj-server-legacy/target/package/opendj1
        rm -rf ./opendj-server-legacy/target/package/opendj2
        rm -rf ./opendj-server-legacy/target/package/opendj3
 
    - name: Test on Windows
      if: runner.os == 'Windows'
      run:   |
        # Verify a stop took effect before moving on: wait until the server
        # releases the exclusive byte-range lock it holds on locks\server.lock.
        # The explicit Lock(0, 1) probe is required: a byte-range lock does not
        # prevent opening the file, so a bare Open() would always succeed.
        function Wait-ServerStopped($lockFile) {
          $lockFile = Join-Path $PWD $lockFile
          for ($i = 0; $i -lt 30; $i++) {
            if (-not (Test-Path $lockFile)) { return }
            try {
              $fs = [System.IO.File]::Open($lockFile, 'Open', 'ReadWrite', 'ReadWrite')
              try { $fs.Lock(0, 1); $fs.Unlock(0, 1); return } finally { $fs.Close() }
            } catch { Start-Sleep -Seconds 2 }
          }
          throw "The server still holds the lock on ${lockFile}: the stop did not take effect"
        }
        set OPENDJ_JAVA_ARGS="-server -Xmx512m"
        opendj-server-legacy\target\package\opendj\setup.bat -h localhost -p 1389 --ldapsPort 1636 --adminConnectorPort 4444 --enableStartTLS --generateSelfSignedCertificate --rootUserDN "cn=Directory Manager" --rootUserPassword password --baseDN dc=example,dc=com --sampleData 5000 --cli --acceptLicense --no-prompt
        opendj-server-legacy\target\package\opendj\bat\status.bat --hostname localhost --bindDN "cn=Directory Manager" --bindPassword password --trustAll
        opendj-server-legacy\target\package\opendj\bat\ldapsearch.bat --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "dc=example,dc=com" --searchScope base "(objectClass=*)" 1.1
        opendj-server-legacy\target\package\opendj\bat\ldapsearch.bat --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "dc=example,dc=com" --searchScope sub "(uid=user.*)" dn | find /c '"dn:"' | findstr "5000"
        # --- Load-testing tools smoke tests (server online, 5000 sample users) ---
        Write-Host "===== searchrate ====="
        opendj-server-legacy\target\package\opendj\bat\searchrate.bat --hostname localhost --port 1636 --useSSL --trustAll --bindDN "cn=Directory Manager" --bindPassword password --baseDN "ou=people,dc=example,dc=com" --searchScope sub -c 4 -t 1 -m 2000 -i 1 -g "rand(0,4999)" "(uid=user.%d)" 1.1
        if ($LASTEXITCODE -ne 0) { throw "searchrate failed with exit code $LASTEXITCODE" }
        Write-Host "===== authrate ====="
        opendj-server-legacy\target\package\opendj\bat\authrate.bat --hostname localhost --port 1636 --useSSL --trustAll -D "uid=user.%d,ou=people,dc=example,dc=com" -w password -c 4 -m 2000 -i 1 -g "rand(0,4999)"
        if ($LASTEXITCODE -ne 0) { throw "authrate failed with exit code $LASTEXITCODE" }
        Write-Host "===== modrate ====="
        opendj-server-legacy\target\package\opendj\bat\modrate.bat --hostname localhost --port 1636 --useSSL --trustAll --bindDN "cn=Directory Manager" --bindPassword password -b "uid=user.%d,ou=people,dc=example,dc=com" -c 4 -t 1 -m 2000 -i 1 -g "rand(0,4999)" "description:modrate-load-test"
        if ($LASTEXITCODE -ne 0) { throw "modrate failed with exit code $LASTEXITCODE" }
        Write-Host "===== addrate ====="
        @'
        define suffix=dc=example,dc=com
 
        branch: ou=People,[suffix]
        subordinateTemplate: person
 
        template: person
        rdnAttr: uid
        objectClass: top
        objectClass: person
        objectClass: organizationalPerson
        objectClass: inetOrgPerson
        uid: addrate.<sequential:0>
        cn: AddRate {uid}
        sn: AddRate
        userPassword: password
        '@ | Set-Content -Encoding ascii addrate.template
        opendj-server-legacy\target\package\opendj\bat\addrate.bat --hostname localhost --port 1636 --useSSL --trustAll --bindDN "cn=Directory Manager" --bindPassword password -c 4 -t 1 -m 2000 -i 1 -C fifo -s 100 addrate.template
        if ($LASTEXITCODE -ne 0) { throw "addrate failed with exit code $LASTEXITCODE" }
        opendj-server-legacy\target\package\opendj\bat\dsconfig.bat create-backend --hostname localhost --port 4444 --bindDN "cn=Directory Manager" --bindPassword password --backend-name=example2 --type je --set=base-dn:dc=example2,dc=com --set=enabled:true --no-prompt --trustAll
        opendj-server-legacy\target\package\opendj\bat\makeldif.bat -o test.ldif -c suffix=dc=example2,dc=com opendj-server-legacy\target\package\opendj\config\MakeLDIF\example.template
        opendj-server-legacy\target\package\opendj\bat\stop-ds.bat
        if ($LASTEXITCODE -ne 0) { throw "stop-ds.bat failed with exit code $LASTEXITCODE" }
        Wait-ServerStopped 'opendj-server-legacy\target\package\opendj\locks\server.lock'
        echo "4.9.9.0" > opendj-server-legacy\target\package\opendj\config\buildinfo
        opendj-server-legacy\target\package\opendj\upgrade.bat
        opendj-server-legacy\target\package\opendj\bat\import-ldif.bat --offline --ldifFile test.ldif --backendID=example2
        opendj-server-legacy\target\package\opendj\bat\rebuild-index.bat --offline --bindDN "cn=Directory Manager" --bindPassword password  --baseDN "dc=example2,dc=com" --rebuildAll
        opendj-server-legacy\target\package\opendj\bat\start-ds.bat
        opendj-server-legacy\target\package\opendj\bat\rebuild-index.bat --bindDN "cn=Directory Manager" --bindPassword password --baseDN "dc=example2,dc=com" --rebuildAll --trustAll
        opendj-server-legacy\target\package\opendj\bat\ldapsearch.bat --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "dc=example2,dc=com" --searchScope sub "(uid=user.*)" dn | find /c '"dn:"' | findstr "10000"
        opendj-server-legacy\target\package\opendj\bat\stop-ds.bat
        if ($LASTEXITCODE -ne 0) { throw "stop-ds.bat failed with exit code $LASTEXITCODE" }
        Wait-ServerStopped 'opendj-server-legacy\target\package\opendj\locks\server.lock'
        opendj-server-legacy\target\package\opendj\bat\windows-service.bat --enableService
        net start "OpenDJ Server"
        if ($LASTEXITCODE -ne 0) { throw "net start 'OpenDJ Server' failed with exit code $LASTEXITCODE" }
        for ($i=0; $i -lt 12; $i++) { try { $c = New-Object System.Net.Sockets.TcpClient('localhost', 1636); $c.Close(); break } catch { Start-Sleep -Seconds 5 } }
        opendj-server-legacy\target\package\opendj\bat\ldapsearch.bat --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "dc=example2,dc=com" --searchScope sub "(uid=user.*)" dn | find /c '"dn:"' | findstr "10000"
        net stop "OpenDJ Server"
        if ($LASTEXITCODE -ne 0) { throw "net stop 'OpenDJ Server' failed with exit code $LASTEXITCODE" }
        opendj-server-legacy\target\package\opendj\bat\windows-service.bat --disableService
 
    - name: Upload Windows exe artifacts
      if: runner.os == 'Windows'
      uses: actions/upload-artifact@v7
      with:
        name: windows-exe-${{ matrix.java }}
        retention-days: 5
        path: opendj-server-legacy/src/build-tools/windows/*.exe
 
    - name: Upload artifacts OpenDJ Server
      uses: actions/upload-artifact@v7
      with:
        name: ${{ matrix.os }}-${{ matrix.java }}
        retention-days: 5
        path:  |
            opendj-server-legacy/target/package/*.zip
            opendj-ldap-toolkit/target/*.zip
            opendj-packages/opendj-deb/opendj-deb-standard/target/*.deb
            opendj-packages/opendj-rpm/opendj-rpm-standard/target/rpm/opendj/RPMS/noarch/*.rpm
            opendj-packages/opendj-msi/opendj-msi-standard/target/*.msi
            opendj-packages/opendj-docker/target/Dockerfile.zip
            opendj-packages/opendj-openshift-template/*.yaml
            opendj-doc-generated-ref/target/*.zip
            opendj-dsml-servlet/target/*.war
            opendj-rest2ldap-servlet/target/*.war
  build-docker:
    needs: build-maven
    runs-on: 'ubuntu-latest'
    permissions:
      contents: read
      security-events: write
    services:
      registry:
        image: registry:3
        ports:
          - 5000:5000
    steps:
      - uses: actions/checkout@v6
        with:
          sparse-checkout: .github/benchmark
      - name: Download artifacts
        uses: actions/download-artifact@v8
        with:
          name: ubuntu-latest-11
      - name: Get latest release version
        shell: bash
        run:   |
          export git_version_last="$(curl -i -o - --silent https://api.github.com/repos/OpenIdentityPlatform/OpenDJ/releases/latest | grep -m1 "\"name\"" | cut -d\" -f4)" ; echo "last release: $git_version_last"
          echo "release_version=$git_version_last" >> $GITHUB_ENV
          echo "image_repository=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV
      - name: Docker meta
        id: meta
        uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
        with:
          images: |
            localhost:5000/${{ github.repository }}
          tags: |
            type=raw,value=${{ env.release_version }}
      - name: Set up QEMU
        uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
        with:
          driver-opts: network=host
      - name: Prepare Dockerfile
        shell: bash
        run: |
          unzip -d ./opendj-packages/opendj-docker ./opendj-packages/opendj-docker/target/Dockerfile.zip 
          cp ./opendj-server-legacy/target/package/opendj-*.zip ./opendj-packages/opendj-docker
          sed -i -E '/^#COPY opendj/s/^#//' ./opendj-packages/opendj-docker/Dockerfile
      - name: Build image (default)
        uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
        with:
          context: ./opendj-packages/opendj-docker
          file: ./opendj-packages/opendj-docker/Dockerfile
          build-args: |
            VERSION=${{ env.release_version }}
          platforms: linux/amd64, linux/arm64/8, linux/ppc64le, linux/s390x, linux/riscv64 #, linux/arm/v7
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
      - name: Docker test
        shell: bash
        run: |
          trap 'code=$?; echo "::group::container logs (test)"; docker logs test 2>&1 || true; echo "::endgroup::"; exit $code' ERR
          docker run --rm -it -d --memory="512m" --name=test localhost:5000/${GITHUB_REPOSITORY,,}:${{ env.release_version }}
          timeout 3m bash -c 'until docker inspect --format="{{json .State.Health.Status}}" test | grep -q \"healthy\"; do sleep 10; done'
          docker exec test 'sh' '-c' '/opt/opendj/bin/dsconfig create-backend --hostname localhost --port 4444 --bindDN "cn=Directory Manager" --bindPassword password --backend-name=example2 --type je --set=base-dn:dc=example2,dc=com --set=enabled:true --no-prompt --trustAll'
          docker exec test 'sh' '-c' '/opt/opendj/bin/makeldif -o /tmp/test.ldif -c suffix=dc=example2,dc=com /opt/opendj/data/config/MakeLDIF/example.template'
          docker exec test 'sh' '-c' '/opt/opendj/bin/stop-ds'
          docker exec test 'sh' '-c' '/opt/opendj/bin/import-ldif --offline --ldifFile /tmp/test.ldif --backendID=example2'
          docker exec test 'sh' '-c' '/opt/opendj/bin/rebuild-index --offline --bindDN "cn=Directory Manager" --bindPassword password  --baseDN "dc=example2,dc=com" --rebuildAll'
          docker exec test 'sh' '-c' '/opt/opendj/bin/start-ds'
          docker exec test 'sh' '-c' '/opt/opendj/bin/rebuild-index --bindDN "cn=Directory Manager" --bindPassword password --baseDN "dc=example2,dc=com" --rebuildAll --trustAll'
          docker exec test 'sh' '-c' '/opt/opendj/bin/ldapsearch --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "ou=people,dc=example2,dc=com" --searchScope sub "(uid=user.*)" dn | grep ^dn: | wc -l | grep -q 10000'
          docker kill test
      - name: Docker test custom password
        shell: bash
        run: |
          trap 'code=$?; echo "::group::container logs (test_custom)"; docker logs test_custom 2>&1 || true; echo "::endgroup::"; exit $code' ERR
          docker run --rm -it -d --memory="512m" -e ADD_BASE_ENTRY="--addBaseEntry" -e ROOT_PASSWORD=custom_password --name=test_custom localhost:5000/${GITHUB_REPOSITORY,,}:${{ env.release_version }}
          timeout 3m bash -c 'until docker inspect --format="{{json .State.Health.Status}}" test_custom | grep -q \"healthy\"; do sleep 10; done'
          docker exec test_custom 'sh' '-c' '/opt/opendj/bin/ldapsearch --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword custom_password --useSsl --trustAll --baseDN "dc=example,dc=com" --searchScope base "(objectClass=*)" 1.1'
          docker kill test_custom
      - name: Scan image for vulnerabilities (Trivy)
        # trivy resolves the image from the local Docker daemon, so only the runner's
        # linux/amd64 manifest is scanned; cache: false keeps the ~1GB trivy DBs from
        # evicting the m2-repository caches out of the repo's 10GB actions-cache quota
        uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
        with:
          image-ref: localhost:5000/${{ env.image_repository }}:${{ env.release_version }}
          format: sarif
          output: trivy-results.sarif
          severity: CRITICAL,HIGH
          limit-severities-for-sarif: true
          ignore-unfixed: true
          scanners: vuln
          cache: false
      - name: Upload Trivy report to GitHub Security
        uses: github/codeql-action/upload-sarif@v4
        # upload even if a preceding step failed, but not without a report to upload
        if: ${{ always() && hashFiles('trivy-results.sarif') != '' }}
        with:
          sarif_file: trivy-results.sarif
          # distinct from the docker-scan.yml categories, which track the published images
          category: trivy-build-default
      - name: Cache JMeter
        uses: actions/cache@v5
        with:
          path: ~/jmeter
          key: jmeter-5.6.3
      - name: Benchmark PDB vs JE
        shell: bash
        run: |
          A_BACKEND=je B_BACKEND=pdb THREADS=200 DURATION=150 bash .github/benchmark/compare-opendj.sh \
            "JE"  localhost:5000/${GITHUB_REPOSITORY,,}:${{ env.release_version }} \
            "PDB" localhost:5000/${GITHUB_REPOSITORY,,}:${{ env.release_version }}
      - name: Upload benchmark artifacts (PDB vs JE)
        if: always()
        uses: actions/upload-artifact@v7
        with:
          name: benchmark-pdb-vs-je
          path: |
            a/
            b/
            *.jtl
            *.docker.log
            *.jmeter.out
          if-no-files-found: warn
          retention-days: 3
      - name: Benchmark Build vs Release
        shell: bash
        run: |
          THREADS=200 DURATION=150 bash .github/benchmark/compare-opendj.sh \
            "Build"   localhost:5000/${GITHUB_REPOSITORY,,}:${{ env.release_version }} \
            "Release" openidentityplatform/opendj:latest
      - name: Upload benchmark artifacts
        if: always()
        uses: actions/upload-artifact@v7
        with:
          name: benchmark-build-vs-release
          path: |
            a/
            b/
            *.jtl
            *.docker.log
            *.jmeter.out
          if-no-files-found: warn
          retention-days: 3
 
  build-docker-alpine:
    needs: build-maven
    runs-on: 'ubuntu-latest'
    permissions:
      contents: read
      security-events: write
    services:
      registry:
        image: registry:3
        ports:
          - 5000:5000
    steps:
      - uses: actions/checkout@v6
        with:
          sparse-checkout: .github/benchmark
      - name: Download artifacts
        uses: actions/download-artifact@v8
        with:
          name: ubuntu-latest-11
      - name: Get latest release version
        shell: bash
        run:   |
          export git_version_last="$(curl -i -o - --silent https://api.github.com/repos/OpenIdentityPlatform/OpenDJ/releases/latest | grep -m1 "\"name\"" | cut -d\" -f4)" ; echo "last release: $git_version_last"
          echo "release_version=$git_version_last" >> $GITHUB_ENV
          echo "image_repository=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV
      - name: Docker meta 
        id: meta
        uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
        with:
          images: |
            localhost:5000/${{ github.repository }}
          tags: |
            type=raw,value=alpine
            type=raw,value=${{ env.release_version }}-alpine
      - name: Set up QEMU
        uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
        with:
          driver-opts: network=host
      - name: Prepare Dockerfile
        shell: bash
        run: |
          unzip -d ./opendj-packages/opendj-docker ./opendj-packages/opendj-docker/target/Dockerfile.zip 
          cp ./opendj-server-legacy/target/package/opendj-*.zip ./opendj-packages/opendj-docker
          sed -i -E '/^#COPY opendj/s/^#//' ./opendj-packages/opendj-docker/Dockerfile-alpine
      - name: Build image
        uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
        with:
          context: ./opendj-packages/opendj-docker
          file: ./opendj-packages/opendj-docker/Dockerfile-alpine
          build-args: |
            VERSION=${{ env.release_version }}
          platforms: linux/amd64, linux/arm64/8, linux/s390x, linux/ppc64le, linux/386, linux/riscv64
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
      - name: Docker test
        shell: bash
        run: |
          trap 'code=$?; echo "::group::container logs (test)"; docker logs test 2>&1 || true; echo "::endgroup::"; exit $code' ERR
          docker run --rm -it -d --memory="1g" --name=test localhost:5000/${GITHUB_REPOSITORY,,}:${{ env.release_version }}-alpine
          timeout 3m bash -c 'until docker inspect --format="{{json .State.Health.Status}}" test | grep -q \"healthy\"; do sleep 10; done'
          docker exec test 'sh' '-c' '/opt/opendj/bin/dsconfig create-backend --hostname localhost --port 4444 --bindDN "cn=Directory Manager" --bindPassword password --backend-name=example2 --type je --set=base-dn:dc=example2,dc=com --set=enabled:true --no-prompt --trustAll'
          docker exec test 'sh' '-c' '/opt/opendj/bin/makeldif -o /tmp/test.ldif -c suffix=dc=example2,dc=com /opt/opendj/data/config/MakeLDIF/example.template'
          docker exec test 'sh' '-c' '/opt/opendj/bin/stop-ds'
          docker exec test 'sh' '-c' '/opt/opendj/bin/import-ldif --offline --ldifFile /tmp/test.ldif --backendID=example2'
          docker exec test 'sh' '-c' '/opt/opendj/bin/rebuild-index --offline --bindDN "cn=Directory Manager" --bindPassword password  --baseDN "dc=example2,dc=com" --rebuildAll'
          docker exec test 'sh' '-c' '/opt/opendj/bin/start-ds'
          docker exec test 'sh' '-c' '/opt/opendj/bin/rebuild-index --bindDN "cn=Directory Manager" --bindPassword password --baseDN "dc=example2,dc=com" --rebuildAll --trustAll'
          docker exec test 'sh' '-c' '/opt/opendj/bin/ldapsearch --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "ou=people,dc=example2,dc=com" --searchScope sub "(uid=user.*)" dn | grep ^dn: | wc -l | grep -q 10000'
          docker kill test
      - name: Docker test custom password
        shell: bash
        run: |
          trap 'code=$?; echo "::group::container logs (test_custom)"; docker logs test_custom 2>&1 || true; echo "::endgroup::"; exit $code' ERR
          docker run --rm -it -d --memory="1g" -e ADD_BASE_ENTRY="--addBaseEntry" -e ROOT_PASSWORD=custom_password --name=test_custom localhost:5000/${GITHUB_REPOSITORY,,}:${{ env.release_version }}-alpine
          timeout 3m bash -c 'until docker inspect --format="{{json .State.Health.Status}}" test_custom | grep -q \"healthy\"; do sleep 10; done'
          docker exec test_custom 'sh' '-c' '/opt/opendj/bin/ldapsearch --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword custom_password --useSsl --trustAll --baseDN "dc=example,dc=com" --searchScope base "(objectClass=*)" 1.1'
          docker kill test_custom
      - name: Scan image for vulnerabilities (Trivy)
        # trivy resolves the image from the local Docker daemon, so only the runner's
        # linux/amd64 manifest is scanned; cache: false keeps the ~1GB trivy DBs from
        # evicting the m2-repository caches out of the repo's 10GB actions-cache quota
        uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
        with:
          image-ref: localhost:5000/${{ env.image_repository }}:${{ env.release_version }}-alpine
          format: sarif
          output: trivy-results.sarif
          severity: CRITICAL,HIGH
          limit-severities-for-sarif: true
          ignore-unfixed: true
          scanners: vuln
          cache: false
      - name: Upload Trivy report to GitHub Security
        uses: github/codeql-action/upload-sarif@v4
        # upload even if a preceding step failed, but not without a report to upload
        if: ${{ always() && hashFiles('trivy-results.sarif') != '' }}
        with:
          sarif_file: trivy-results.sarif
          # distinct from the docker-scan.yml categories, which track the published images
          category: trivy-build-alpine
      - name: Cache JMeter
        uses: actions/cache@v5
        with:
          path: ~/jmeter
          key: jmeter-5.6.3
      - name: Benchmark Build-alpine vs Release-alpine
        shell: bash
        run: |
          THREADS=200 DURATION=150 bash .github/benchmark/compare-opendj.sh \
            "Build-alpine"   localhost:5000/${GITHUB_REPOSITORY,,}:${{ env.release_version }}-alpine \
            "Release-alpine" openidentityplatform/opendj:alpine
      - name: Upload benchmark artifacts
        if: always()
        uses: actions/upload-artifact@v7
        with:
          name: benchmark-build-vs-release-alpine
          path: |
            a/
            b/
            *.jtl
            *.docker.log
            *.jmeter.out
          if-no-files-found: warn
          retention-days: 90
 
  test-deb:
    needs: build-maven
    runs-on: 'ubuntu-latest'
    steps:
      - uses: actions/checkout@v6
      - name: Download artifacts
        uses: actions/download-artifact@v8
        with:
          name: ubuntu-latest-11
      - name: Clean-room install + SysV start/stop (debian:12 container)
        shell: bash
        run: |
          docker run --rm -v "$PWD:/work" -w /work debian:12 bash -c '
            set -e
            export DEBIAN_FRONTEND=noninteractive
            DEB=$(ls opendj-packages/opendj-deb/opendj-deb-standard/target/*.deb | head -1)
            echo "Found $DEB"
            apt-get update
            apt-get install -y lintian
            lintian --info --no-tag-display-limit "$DEB" || true
            dpkg-deb -I "$DEB"
            # Both service files must be present (two greps: a single alternation
            # would pass with either one missing).
            dpkg-deb -c "$DEB" | grep "lib/systemd/system/opendj.service"
            dpkg-deb -c "$DEB" | grep "etc/init.d/opendj"
            # /etc/default/opendj must be a real conffile, so admin edits survive
            # upgrades and the file survives "apt remove".
            dpkg-deb -e "$DEB" /tmp/ctrl
            grep -qx "/etc/default/opendj" /tmp/ctrl/conffiles
            apt-get install -y "./$DEB"
            id opendj
            test "$(stat -c %U /opt/opendj)" = opendj
            # No JAVA_HOME in this clean container: Java resolves from the PATH
            # java that the package JRE dependency pulled in.
            runuser -u opendj -- /opt/opendj/setup --cli --no-prompt --acceptLicense --doNotStart \
              --rootUserDN "cn=Directory Manager" --rootUserPassword password \
              --hostname localhost --ldapPort 1389 --adminConnectorPort 4444 \
              --baseDN dc=example,dc=com --addBaseEntry
            /etc/init.d/opendj start
            bash .github/scripts/wait-for-ldap.sh 1389
            /etc/init.d/opendj status
            /etc/init.d/opendj stop
            apt-get purge -y opendj
          '
      - name: Live systemd install + start/stop (runner)
        shell: bash
        run: |
          DEB=$(ls opendj-packages/opendj-deb/opendj-deb-standard/target/*.deb | head -1)
          sudo apt-get update
          sudo apt-get install -y "$PWD/$DEB"
          test "$(stat -c '%U' /opt/opendj)" = opendj
          # postinst must have enabled the unit; do not enable by hand here or
          # the postinst registration would go untested.
          sudo systemctl is-enabled --quiet opendj
          # Before setup the unit's start condition must keep it from
          # start-bursting: "start" succeeds but no process may appear.
          sudo systemctl start opendj
          sleep 2
          if sudo systemctl is-failed --quiet opendj; then echo "unit failed before setup"; exit 1; fi
          # sudo/runuser/systemd strip JAVA_HOME -> the service uses the PATH java.
          # Privileged port 389: only AmbientCapabilities=CAP_NET_BIND_SERVICE lets
          # the non-root service bind it - the very reason it can drop root. setup
          # itself runs via runuser WITHOUT that capability, so it must skip its
          # own bind probe; the systemd start below performs the real 389 bind.
          sudo runuser -u opendj -- /opt/opendj/setup --cli --no-prompt --acceptLicense --doNotStart \
            --rootUserDN "cn=Directory Manager" --rootUserPassword password \
            --hostname localhost --ldapPort 389 --adminConnectorPort 4444 \
            --baseDN dc=example,dc=com --addBaseEntry --skipPortCheck
          sudo systemctl start opendj
          bash .github/scripts/wait-for-ldap.sh 389
          sudo systemctl is-active --quiet opendj
          echo "OpenDJ is active under systemd"
          sudo systemctl stop opendj
          sleep 3
          if sudo systemctl is-active --quiet opendj; then echo "still active"; exit 1; fi
          sudo apt-get purge -y opendj
 
  # Known coverage limit: the container has no booted systemd (/run/systemd/system
  # is absent), so only the SysV/chkconfig path of the scriptlets is exercised
  # here; start/stop through systemd is covered by the deb live-systemd jobs on
  # the runner. The enable registration IS asserted below - systemctl enable is
  # a pure symlink operation and works without a booted systemd.
  test-rpm:
    needs: build-maven
    runs-on: 'ubuntu-latest'
    steps:
      - uses: actions/checkout@v6
      - name: Download artifacts
        uses: actions/download-artifact@v8
        with:
          name: ubuntu-latest-11
      - name: Install and start/stop in Rocky Linux 9
        shell: bash
        run: |
          docker run --rm -v "$PWD:/work" -w /work rockylinux:9 bash -c '
            set -e
            RPM=$(ls opendj-packages/opendj-rpm/opendj-rpm-standard/target/rpm/opendj/RPMS/noarch/*.rpm | head -1)
            echo "Found $RPM"
            # Both service files must be shipped.
            rpm -qlp "$RPM" | grep "^/usr/lib/systemd/system/opendj.service$"
            rpm -qlp "$RPM" | grep "^/etc/init.d/opendj$"
            # The %pre user-creation dependency must be declared for minimal images.
            rpm -qp --requires "$RPM" | grep "shadow-utils"
            # A real EL9 host always has systemd, but the container image ships no
            # systemctl at all - %post then (correctly) skips the enable in silence.
            # Preinstall it as host infrastructure, deliberately NOT a package
            # Requires, so the is-enabled assertion below tests the enable path.
            dnf install -y systemd
            # No further manual dependencies: the package must pull everything else
            # itself (Requires).
            dnf install -y "$RPM"
            id opendj
            test "$(stat -c %U /opt/opendj)" = opendj
            # %post must have enabled the unit on initial install.
            systemctl is-enabled opendj.service
            # No JAVA_HOME here: Java resolves from the PATH java pulled in by
            # the java-headless dependency.
            runuser -u opendj -- /opt/opendj/setup --cli --no-prompt --acceptLicense --doNotStart \
              --rootUserDN "cn=Directory Manager" --rootUserPassword password \
              --hostname localhost --ldapPort 1389 --adminConnectorPort 4444 \
              --baseDN dc=example,dc=com --addBaseEntry
            /etc/init.d/opendj start
            bash .github/scripts/wait-for-ldap.sh 1389
            /etc/init.d/opendj status
            /etc/init.d/opendj stop
            rpm -e opendj
          '
 
  # Upgrade path: released 5.1.2 deb (root-owned, SysV) -> this build's deb, on the
  # runner's LIVE systemd. The new package must stop the running server, create the
  # opendj user, migrate ownership, run the upgrade tool and restart the server with
  # the old data - through deb-systemd-invoke and the native unit, the path a real
  # systemd host takes (docs: chap-upgrade).
  test-deb-upgrade:
    needs: build-maven
    runs-on: 'ubuntu-latest'
    steps:
      - uses: actions/checkout@v6
      - name: Download artifacts
        uses: actions/download-artifact@v8
        with:
          name: ubuntu-latest-11
      - name: Download released 5.1.2 deb
        shell: bash
        run: |
          curl -fsSL -o opendj-5.1.2.deb https://github.com/OpenIdentityPlatform/OpenDJ/releases/download/5.1.2/opendj_5.1.2-1_all.deb
          # Pin the upgrade source: a retagged release asset must fail loudly here,
          # not surface as an unrelated upgrade-job failure.
          echo "8f7f8bdd526b2d63eaef0621545be6aa55749cb60200d3bc2a2849d9ab69eb59  opendj-5.1.2.deb" | sha256sum -c -
      - name: Upgrade 5.1.2 -> new deb (live systemd on the runner)
        shell: bash
        run: |
          NEW=$(ls opendj-packages/opendj-deb/opendj-deb-standard/target/*.deb | head -1)
          echo "New deb: $NEW"
          sudo apt-get update
          # No manual dependencies: even the released 5.1.2 deb declares a JRE dependency.
          sudo apt-get install -y "$PWD/opendj-5.1.2.deb"
          # 5.1.2 model: no dedicated user, root-owned tree, SysV only - on this
          # booted host its unit comes from the systemd-sysv-generator.
          sudo /opt/opendj/setup --cli --no-prompt --acceptLicense --doNotStart \
            --rootUserDN "cn=Directory Manager" --rootUserPassword password \
            --hostname localhost --ldapPort 1389 --adminConnectorPort 4444 \
            --baseDN dc=example,dc=com --addBaseEntry
          sudo systemctl daemon-reload
          sudo /etc/init.d/opendj start
          bash .github/scripts/wait-for-ldap.sh 1389
          # Leave the server RUNNING: the new package must stop it, upgrade and
          # restart it - through deb-systemd-invoke and the native unit this time.
          sudo apt-get install -y "$PWD/$NEW"
          id opendj
          test "$(stat -c %U /opt/opendj)" = opendj
          test -f /opt/opendj/config/config.ldif
          # The systemd-hosted upgrade must leave the unit enabled and active.
          sudo systemctl is-enabled --quiet opendj
          sudo systemctl is-active --quiet opendj
          # The pre-upgrade data must be served again, now by the dedicated user.
          bash .github/scripts/wait-for-ldap.sh 1389
          test "$(stat -c %U /proc/$(cat /opt/opendj/logs/server.pid))" = opendj
          sudo systemctl stop opendj
          sleep 3
          if sudo systemctl is-active --quiet opendj; then echo "still active"; exit 1; fi
          sudo apt-get purge -y opendj
 
  test-rpm-upgrade:
    needs: build-maven
    runs-on: 'ubuntu-latest'
    steps:
      - uses: actions/checkout@v6
      - name: Download artifacts
        uses: actions/download-artifact@v8
        with:
          name: ubuntu-latest-11
      - name: Download released 5.1.2 rpm
        shell: bash
        run: |
          curl -fsSL -o opendj-5.1.2.rpm https://github.com/OpenIdentityPlatform/OpenDJ/releases/download/5.1.2/opendj-5.1.2-1.noarch.rpm
          # Pin the upgrade source: a retagged release asset must fail loudly here,
          # not surface as an unrelated upgrade-job failure.
          echo "9622ef7c3292f0c156c96029a647ca704064aa93b567e5608734fe07d224ec58  opendj-5.1.2.rpm" | sha256sum -c -
      - name: Upgrade 5.1.2 -> new rpm (Rocky Linux 9 container)
        shell: bash
        run: |
          docker run --rm -v "$PWD:/work" -w /work rockylinux:9 bash -c '
            set -e
            NEW=$(ls opendj-packages/opendj-rpm/opendj-rpm-standard/target/rpm/opendj/RPMS/noarch/*.rpm | head -1)
            echo "New rpm: $NEW"
            # No manual dependencies: the released 5.1.2 rpm already declares its runtime
            # requirements (#677), and everything the NEW package needs must come from its
            # own Requires.
            dnf install -y ./opendj-5.1.2.rpm
            # 5.1.2 model: no dedicated user, root-owned tree, SysV only
            /opt/opendj/setup --cli --no-prompt --acceptLicense --doNotStart \
              --rootUserDN "cn=Directory Manager" --rootUserPassword password \
              --hostname localhost --ldapPort 1389 --adminConnectorPort 4444 \
              --baseDN dc=example,dc=com --addBaseEntry
            /etc/init.d/opendj start
            bash .github/scripts/wait-for-ldap.sh 1389
            # Leave the server RUNNING: the new package must stop it, upgrade and restart it.
            dnf install -y "./$NEW"
            id opendj
            test "$(stat -c %U /opt/opendj)" = opendj
            test -f /opt/opendj/config/config.ldif
            # 5.1.2 was chkconfig-enabled and the native unit now shadows the
            # sysv-generator unit: %post must have migrated that enable state
            # (a pure symlink operation, asserted fine without booted systemd).
            systemctl is-enabled opendj.service
            # The package restarted the server; the pre-upgrade data must be served again,
            # now by the dedicated user.
            bash .github/scripts/wait-for-ldap.sh 1389
            test "$(stat -c %U /proc/$(cat /opt/opendj/logs/server.pid))" = opendj
            /etc/init.d/opendj stop
            rpm -e opendj
          '
 
  test-msi:
    needs: build-maven
    runs-on: 'windows-latest'
    steps:
      - name: Download artifacts
        uses: actions/download-artifact@v8
        with:
          name: windows-latest-11
      - name: Set up Java
        uses: actions/setup-java@v5
        with:
          java-version: '25'
          distribution: 'zulu'
      - name: Install MSI (silent)
        shell: pwsh
        run: |
          $msi = (Get-ChildItem -Recurse -Filter *.msi -Path opendj-packages/opendj-msi | Select-Object -First 1).FullName
          if (-not $msi) { throw "MSI not found in the windows-latest-11 artifact" }
          Write-Host "MSI: $msi"
          # No OPENDJ property: use the default install directory (a path with spaces),
          # which the server scripts must handle.
          $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i `"$msi`" /quiet /qn /norestart /l*v install.log"
          if ($p.ExitCode -ne 0) { Get-Content install.log -Tail 80; throw "msiexec /i failed: $($p.ExitCode)" }
          $root = @("C:\Program Files (x86)\OpenDJ","C:\Program Files\OpenDJ") | Where-Object { Test-Path "$_\setup.bat" } | Select-Object -First 1
          if (-not $root) { Get-Content install.log -Tail 80; throw "OpenDJ install root with setup.bat not found" }
          Write-Host "Installed to $root"
          "OPENDJ_ROOT=$root" | Out-File -FilePath $env:GITHUB_ENV -Append
      - name: Setup and start/stop the Windows service
        shell: pwsh
        run: |
          $root = $env:OPENDJ_ROOT
          $env:OPENDJ_JAVA_ARGS = "-server -Xmx512m"
          & "$root\setup.bat" -h localhost -p 1389 --ldapsPort 1636 --adminConnectorPort 4444 --enableStartTLS --generateSelfSignedCertificate --rootUserDN "cn=Directory Manager" --rootUserPassword password --baseDN dc=example,dc=com --addBaseEntry --cli --acceptLicense --no-prompt --doNotStart
          if ($LASTEXITCODE -ne 0) { throw "setup.bat failed: $LASTEXITCODE" }
          & "$root\bat\windows-service.bat" --enableService
          if ($LASTEXITCODE -ne 0) { throw "windows-service --enableService failed: $LASTEXITCODE" }
          net start "OpenDJ Server"
          if ($LASTEXITCODE -ne 0) { throw "net start failed: $LASTEXITCODE" }
          for ($i=0; $i -lt 12; $i++) { try { $c = New-Object System.Net.Sockets.TcpClient('localhost', 1636); $c.Close(); break } catch { Start-Sleep -Seconds 5 } }
          & "$root\bat\ldapsearch.bat" --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "dc=example,dc=com" --searchScope base "(objectClass=*)" 1.1
          if ($LASTEXITCODE -ne 0) { throw "ldapsearch failed: $LASTEXITCODE" }
          net stop "OpenDJ Server"
          if ($LASTEXITCODE -ne 0) { throw "net stop failed: $LASTEXITCODE" }
          & "$root\bat\windows-service.bat" --disableService
      - name: Uninstall MSI
        shell: pwsh
        run: |
          $msi = (Get-ChildItem -Recurse -Filter *.msi -Path opendj-packages/opendj-msi | Select-Object -First 1).FullName
          $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/x `"$msi`" /quiet /qn /norestart /l*v uninstall.log"
          if ($p.ExitCode -ne 0) { Get-Content uninstall.log -Tail 80; throw "msiexec /x failed: $($p.ExitCode)" }
          Write-Host "Uninstalled OK"