From 5202fb2c7427ea139a56dfc9df4d1bffee0685ff Mon Sep 17 00:00:00 2001 From: bogdandina Date: Thu, 12 Mar 2026 09:22:34 +0200 Subject: [PATCH 01/10] feat(76477): remove the jarArtifactName and jarArtifact Path and hardcode them --- .github/workflows/ci-cd-java.yml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci-cd-java.yml b/.github/workflows/ci-cd-java.yml index 7eade61..906bf82 100644 --- a/.github/workflows/ci-cd-java.yml +++ b/.github/workflows/ci-cd-java.yml @@ -12,12 +12,6 @@ on: required: false type: boolean default: false - jarArtifactName: - required: false - type: string - jarArtifactPath: - required: false - type: string performRelease: required: false type: boolean @@ -104,8 +98,8 @@ jobs: if: ${{ inputs.uploadJarArtifact }} uses: actions/upload-artifact@v4 with: - name: ${{ inputs.jarArtifactName }} - path: ${{ inputs.jarArtifactPath }} + name: 'app.jar' + path: '/app/app.jar' - name: Set Docker Image Name run: | From 24d850856109e4164dfa34bf422dfc365f0fc29f Mon Sep 17 00:00:00 2001 From: bogdandina Date: Fri, 27 Mar 2026 10:05:00 +0200 Subject: [PATCH 02/10] feat(76471): check Java version alignment between Dockerfile and pom.xml --- .github/workflows/ci-cd-java.yml | 25 +++ ...e_java_version_consistency.cpython-313.pyc | Bin 0 -> 9300 bytes scripts/validate_java_version_consistency.py | 200 ++++++++++++++++++ 3 files changed, 225 insertions(+) create mode 100644 scripts/__pycache__/validate_java_version_consistency.cpython-313.pyc create mode 100644 scripts/validate_java_version_consistency.py diff --git a/.github/workflows/ci-cd-java.yml b/.github/workflows/ci-cd-java.yml index 906bf82..ec4712e 100644 --- a/.github/workflows/ci-cd-java.yml +++ b/.github/workflows/ci-cd-java.yml @@ -43,6 +43,23 @@ jobs: clean: 'true' fetch-depth: 2 + - name: Resolve shared workflow source + env: + WORKFLOW_REF: ${{ github.workflow_ref }} + run: | + SHARED_WORKFLOW_REPOSITORY="${WORKFLOW_REF%%/.github/workflows/*}" + SHARED_WORKFLOW_REF="${WORKFLOW_REF##*@}" + + echo "SHARED_WORKFLOW_REPOSITORY=${SHARED_WORKFLOW_REPOSITORY}" >> "$GITHUB_ENV" + echo "SHARED_WORKFLOW_REF=${SHARED_WORKFLOW_REF}" >> "$GITHUB_ENV" + + - name: Checkout shared workflow scripts + uses: actions/checkout@v4 + with: + repository: ${{ env.SHARED_WORKFLOW_REPOSITORY }} + ref: ${{ env.SHARED_WORKFLOW_REF }} + path: .shared-workflows + - name: Setup JDK uses: actions/setup-java@v4 with: @@ -50,6 +67,14 @@ jobs: java-version: '25' cache: 'maven' + - name: Validate Java version consistency + working-directory: ${{ inputs.workingDirectory }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + JAVA_TOOL_OPTIONS: "" + MAVEN_OPTS: "" + run: python3 "${GITHUB_WORKSPACE}/.shared-workflows/scripts/validate_java_version_consistency.py" + - name: Check code format and lint working-directory: ${{ inputs.workingDirectory }} run: | diff --git a/scripts/__pycache__/validate_java_version_consistency.cpython-313.pyc b/scripts/__pycache__/validate_java_version_consistency.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3252d4c216d32918e50e48365de5de9811390e91 GIT binary patch literal 9300 zcmcIqYit`=cAgnNX805(>S@RJn4%uEB+Ayqj$})gAF?bzq&3HM9E%o1i(`p4MQUd# z%ci{nw?GjpE>?CPm69zGrMp1nZGdRg0`)GoSZ6oeEZRS$XSZS|g5#q8lm3z)i!?!j z_S{R(kWv)KZP6?9&g0p*`|{$nUqNf7^rE|fy$P99Id$(sa6 zaO6=!$R0_7#MhKS;iE>-aMT7u&~h56b(|JzJ*R`3=JZfA91XRBW1u#22B=LwGiU5P zO%MTwAbchV(ViQQB}U0n(q;MrT@Q?qx!dSK!%qXh$1>b~lkgJt=q{8XT#v%Fr%*ck zNbPsMXB^p{YY`#6q|#fUoFKfU2aU9*aFimi%4SC*4wb2mCS-r*^ny()|Tmy|}L47i@63OtMal384wuD8>RpOq>eE z#$;OHC&Gad9-d8vLorzs<|8s45`&>pxS|AJ){A^V7#Wi_&{)=lA~9JvDnutITv}Oo zDG;9IWqKsc2O@k>)=i*(W!-onHZmq^VQdzP+$fH~uraY2#6!VAjQ3vvrTv$(TKh+$ z5iumj_{hi=_rw*sJQ5YgK`_2rOsIhF(4vdCp-K?yVIN9OPjZ+H*e3_E9Z)D zKA#+XV7AWmzuJFeY@U|P^@-klw0XIxJ9=MRCi0hgZy4rY*3_F!bzA2@IZu1GvLz3JL~s$4qqE``UUb17XHGm`U(j~7G_ux0$)*Cy)uXK2F+GfhoC2C!mrk(2K|q^THum=o z40?JG?BjZ|ISFV(f*osOfl-;62*hH%5D{fMcP5~nshY6>UXPHS{NTVm;qyXt z+#igNT;v5m^d)QtQE)SacnGQlu|nx=`o!VKR-*K|B)x1anKis-n5%oA`oMC_BH4B( zb)TDT$@XQtYoX?YhFcAbfxmG}_Kv&ej)ycfSuql3n*i`bj1*b{a$wkUy5VG)1b^!jMKWLB+L#%Gv?Ez+a##5m4o#@OsLcQ9kC<;Kh?ImjXg4a4yXIBY|;V)&_)8 zQACr*vI)GP02^I~2=GNa zU)`B1*|yLpnRchNyR%ksCDd;c+ldI-j697h;{_kYQ5_UTAg=+Q=u7ZK zTCaA!Le1cX&?d#j!zgF@BjBmtt1FPfY%KpAoheov z(R=ku?|~M;Y`q8V{52(ytm`onMbp#e>$cS@W{Bqh_>Z1Jd*+$7r@geIJ)WC?fZmG~ z<-PRN^?BO66z~zoOLY8|5Un5{;`E`+15bw!n^poezb1_ZR;KMJ9V>vRNiO<8ArIK_1|B zJQU&C*ci_u-pT@2vIskK9ppN60K0X^cRsacjy$evje2v$yB%(}_wock662As=w>){ zk!Kw(?T$uNHvtoYv#7v2cDT2@Tbj@<(54Akio@-)$lB-x9}$qN5sigJ*^`}@UO|WovQ9v=g{gq6NG{5<12jc`pciA<&&zr|gQ9HA zKb09FFy#-!G|5_A8vv91m{H3{%rmm*I^0yY=GMZW>&bwRet|zP8^ZxH<`J4xQHALF!jQcg`Fy3^dhfNo5D82L1=?^yZ9lAI8Mf7NgT{riV}w& z7%a)?0)5x8eeuX=m5$`$jD6#5_s#D4>iO?WKykLF?Tt(JmbAS^vbQA<-m_PHYO8!u zx#1n=Z6>v~|MzWwIKeHQ7*3xUmMZ@UtrRY#LYJ};_D#+NId^JkW7+S4`d zivg+T`K6lfbWOKZ({sluRrcM`Wo+e``Sbr`b3WWiR8@beA&fg7vc$ISQrW=I%I9@| zzx(IA|F-K+f4cSfEPty!F_0=7xM!+bwr)smJR(_-rkJDmZJXzWw9T<(s|Sr1>RXNot>Zn?Q--Yyl_B#x}Ux=$BN^cK|9rV}a8Q0jy)J1<<5}W+j8Wjh0UMu^4FLRV$l;eJ zLLpvMFcu&ijI|!yFe%ugU_k&OBDfJR1wFzNU_`_{suBQtUJYkP*yjXkvP@#{kWy+;Q6R9!gH};bTSsVHyspXSHKse{VsGxcyN|+%NQS?=;Tqv z4iPwN!e5>MQi&1=T_+E@G_rLhFcF&+ct7+pF&UG!F^F|!-N+a}auEZHOb!W%3%h7Y zAxxf|5TYZ{UeR@x!nayb!x0aL~jLKk@INN)XGIikab8hwqzMpQnRu@q;l_J5i1PZoUnwuO8_Li--J5Hus1qr2EKPxqp}Tqiv;7 z|3+K2e~Mg^~n7!GM26h(g+~m|BD!LK_tqAb+r#PywT*6cfWH z??6V#QD}2=8ZQa8wpG^#8%PQAhg2%hsYy7Vbm<3Nns_;03ox+p2|zRfA|*%xiF~|l z&3#BTK{OV}XyeXdcHA%#9d}}U^X6ObMaVZ78vm$t8xLtX=rx|ej++a5RWNs zg+6I~VkC;|7mSjv6%tEj4GXo`q3x4W!c+lX-oPQpZqkEe*Xf@U*C99t|D0QPSmb;>_boYA#eYUnt{VRGtiKsd z;{=(D17}biIHY>*^}Mr#@M>S63S`iZUsDR6t`p5QfvKwz-P(BZ0KAQET|cWvW)Xcs z)RfN$uZ^z8peK6Brl{sYi@m0-mzVVt%65Y7qVwvZ*0*X`Z5lNO$w!(z`4ndVVV2pu zm;nJGix9R!<q&XOu+sk7LA5S#sVO@W**%xvjFJ^Le>g=Aeapn zaJJEK+ z0_hHk_Gkq1)M&BJ>s@<8c-MM&Gsy&dZ zIe2IPoyj|+zdwT+T`9`?F3H#PjjhL=JNk> z481Z&GX=1ws{YI?rct0kS+M}DM;?5X8N(}bYVeF#3n2roh9?kAMCkv;sOpD7WmL@N z06Dxg1QfstaX`UDXfLBmfE5*3lG+1L=08Y!8ID0_p}bR8>(ga89NP`;)l?s6z+BPj zr2za)7y`{+9h_@g4MU~MGmy^IJwhV zkWgbMMPAHi%Y-vnYZn?f#>Pw7+&2M{jYYE(E>hMaq`;sV-@dAI9LKQ}0a4_G-!ed4 z#|B`6;szkkkon_9$c72SFsOjyELnRY8j8qzAoL?sL0QjVhOf3FMlGBS!ps+g3Va9_ z(1&7;ctym~ATJ2A7UW|tgRl#AWz8nM6eyC-x$A6Fi-8Hw!k@VS3q{$2r#9axdzob; zSryB$QZUp0!iFMBUMR4bBk^VXUI)Gm5LjM0J9=|;HheSu)0X!-{<`BQ zyBEz;c~@d^*<_j7HN9)5d%AnBdC9abZQ6ztHtmbiRQsVjfz-yslC?j@^gpQHbK5Uf zA5Ux9#K421;+d)GshMlj*Zyk%JN<9>|M=)al~mN4IQ)Py&d}5JjCtDphG+K0*Is;m zDCKCAnD*swKA3k)%#IbErc|Fe25|&qd}Zo~Q?I=I!@ zEjQdLwq>zvvH4E%ijJ}zB%!O4ve^wcH#{=v48|vB!eE|ZUS)0+|Apy+)__78Hp9A8 zTbdUp7k!HZcU+LRG8`xWq=CTZt4CXjt*1#5{Wpu>QT2Td+TXeLP|J)T;{Q;@?^jF` zp`UORRbf;iR)v$TI5nF!NF3OqFbrEIUToA4B3pj{EdmEoS(e1?8IJ}h!+f_82XPcT ziGxsqVkG$)VZi^2KO>Bf^mfwrsL=uyt0VV3vel8*k4}=MWc?$zmDE3SR6x~MP3l*U Zk%ZQqj7yrTPqoaSA6n56+6{QX{|3@aubKb= literal 0 HcmV?d00001 diff --git a/scripts/validate_java_version_consistency.py b/scripts/validate_java_version_consistency.py new file mode 100644 index 0000000..4182533 --- /dev/null +++ b/scripts/validate_java_version_consistency.py @@ -0,0 +1,200 @@ +import os +import re +import subprocess +import sys + + +def normalize_java_version(value): + if not value: + return None + + cleaned = value.strip().strip('"\'') + if not cleaned or cleaned.startswith('${'): + return None + + cleaned = cleaned.replace('JavaVersion.VERSION_', '') + + if cleaned.startswith('1.'): + parts = cleaned.split('.') + if len(parts) > 1 and parts[1].isdigit(): + return parts[1] + + match = re.search(r'(\d+)', cleaned) + if match: + return str(int(match.group(1))) + + return None + + +def extract_java_version_from_docker_tag(tag): + patterns = ( + r'(?:^|[._-])(\d+)(?:[._-])java(?:[._-])(jre|jdk)(?:$|[._-])', + r'(?:^|[._-])java(?:[._-])(\d+)(?:$|[._-])', + r'(?:^|[._-])(\d+)(?:[._-])(jre|jdk)(?:$|[._-])', + r'^(\d+)$', + ) + + for pattern in patterns: + match = re.search(pattern, tag, re.IGNORECASE) + if match: + return normalize_java_version(match.group(1)) + + return None + + +def resolve_args(value, args): + def replace(match): + variable_name = match.group(1) or match.group(2) + return args.get(variable_name, match.group(0)) + + return re.sub(r'\$\{([^}]+)\}|\$(\w+)', replace, value) + + +def parse_docker_java_version(dockerfile_path): + args = {} + images = [] + + with open(dockerfile_path, encoding='utf-8') as dockerfile: + for raw_line in dockerfile: + line = raw_line.split('#', 1)[0].strip() + if not line: + continue + + arg_match = re.match(r'^ARG\s+([A-Za-z_][A-Za-z0-9_]*)=(.+)$', line, re.IGNORECASE) + if arg_match: + args[arg_match.group(1)] = arg_match.group(2).strip() + continue + + from_match = re.match(r'^FROM\s+([^\s]+)', line, re.IGNORECASE) + if from_match: + image = resolve_args(from_match.group(1).strip(), args) + images.append(image) + + runtime_images = [image for image in images if image.lower() != 'scratch'] + if not runtime_images: + raise RuntimeError('Could not find a runtime image in Dockerfile.') + + image_ref = runtime_images[-1].split('@', 1)[0] + last_slash = image_ref.rfind('/') + last_colon = image_ref.rfind(':') + + if last_colon <= last_slash: + raise RuntimeError(f'Could not determine the Java tag from Docker image "{image_ref}".') + + tag = image_ref[last_colon + 1 :] + java_version = extract_java_version_from_docker_tag(tag) + if not java_version: + raise RuntimeError( + 'Could not determine the Java version from Docker tag ' + f'"{tag}". Expected a tag like "25", "25-jdk", or "1.0.2-25-java-jre".' + ) + + return java_version, image_ref + + +def evaluate_maven_property(expression): + result = subprocess.run( + [ + 'mvn', + '-q', + '-DforceStdout', + '-Dstyle.color=never', + 'help:evaluate', + f'-Dexpression={expression}', + ], + capture_output=True, + text=True, + check=False, + ) + + if result.returncode != 0: + return None + + lines = [line.strip() for line in result.stdout.splitlines() if line.strip()] + lines = [line for line in lines if not line.startswith('[')] + return lines[-1] if lines else None + + +def parse_maven_java_version(): + for expression in ( + 'maven.compiler.release', + 'maven.compiler.target', + 'java.version', + ): + value = evaluate_maven_property(expression) + normalized = normalize_java_version(value) + if normalized: + return normalized, f'pom.xml ({expression}={value})' + + raise RuntimeError( + 'Could not determine the Java version from pom.xml. ' + 'Expected maven.compiler.release, maven.compiler.target, or java.version.' + ) + + +def parse_gradle_java_version(gradle_path): + with open(gradle_path, encoding='utf-8') as gradle_file: + content = gradle_file.read() + + patterns = [ + r'jvmTarget\s*=\s*["\']([^"\']+)["\']', + r'jvmTarget\s*=\s*JavaVersion\.VERSION_?(\d+)', + r'languageVersion(?:\.set)?\s*\(?\s*JavaLanguageVersion\.of\((\d+)\)\s*\)?', + r'sourceCompatibility\s*=\s*JavaVersion\.VERSION_?(\d+)', + r'targetCompatibility\s*=\s*JavaVersion\.VERSION_?(\d+)', + ] + + for pattern in patterns: + match = re.search(pattern, content, re.MULTILINE | re.DOTALL) + if match: + value = match.group(1) + normalized = normalize_java_version(value) + if normalized: + return normalized, f'{os.path.basename(gradle_path)} ({value})' + + raise RuntimeError( + 'Could not determine the Java version from the Gradle build file. ' + 'Expected compileKotlin.kotlinOptions.jvmTarget or java.toolchain.languageVersion.' + ) + + +def main(): + dockerfile_path = os.path.join(os.getcwd(), 'Dockerfile') + if not os.path.exists(dockerfile_path): + raise RuntimeError(f'Dockerfile not found at {dockerfile_path}.') + + docker_version, docker_image = parse_docker_java_version(dockerfile_path) + + pom_path = os.path.join(os.getcwd(), 'pom.xml') + gradle_paths = [ + os.path.join(os.getcwd(), 'build.gradle.kts'), + os.path.join(os.getcwd(), 'build.gradle'), + ] + + if os.path.exists(pom_path): + build_version, build_source = parse_maven_java_version() + else: + existing_gradle_paths = [path for path in gradle_paths if os.path.exists(path)] + if not existing_gradle_paths: + raise RuntimeError( + 'Could not find pom.xml, build.gradle.kts, or build.gradle in the working directory.' + ) + build_version, build_source = parse_gradle_java_version(existing_gradle_paths[0]) + + if docker_version != build_version: + print( + 'Java version mismatch: ' + f'Dockerfile uses Java {docker_version} ({docker_image}), ' + f'but {build_source} resolves to Java {build_version}.', + file=sys.stderr, + ) + sys.exit(1) + + print( + 'Java version check passed: ' + f'Dockerfile uses Java {docker_version} and {build_source} matches.' + ) + + +if __name__ == '__main__': + main() From d6405213b1710cb2bb0576b20f60e1a24ae17675 Mon Sep 17 00:00:00 2001 From: bogdandina Date: Fri, 27 Mar 2026 10:07:00 +0200 Subject: [PATCH 03/10] feat(76471): fix the .gitignore and remove the scripts/__pycache__ --- .gitignore | 1 + ...date_java_version_consistency.cpython-313.pyc | Bin 9300 -> 0 bytes 2 files changed, 1 insertion(+) delete mode 100644 scripts/__pycache__/validate_java_version_consistency.cpython-313.pyc diff --git a/.gitignore b/.gitignore index b79d2d6..01b0c5b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .idea +scripts/__pycache__ /AGENTS.md diff --git a/scripts/__pycache__/validate_java_version_consistency.cpython-313.pyc b/scripts/__pycache__/validate_java_version_consistency.cpython-313.pyc deleted file mode 100644 index 3252d4c216d32918e50e48365de5de9811390e91..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9300 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 Zk%ZQqj7yrTPqoaSA6n56+6{QX{|3@aubKb= From 02eb2fa84a135451f064fe5312e38b0454ecdebf Mon Sep 17 00:00:00 2001 From: bogdandina Date: Mon, 30 Mar 2026 09:49:43 +0300 Subject: [PATCH 04/10] fix(76471): fix shared workflow scripts checkout using hardcoded repo and workflow_sha github.workflow_ref inside a reusable workflow returns the caller's workflow ref instead of the shared workflow's ref, causing the scripts checkout to target the wrong repository. Replace dynamic resolution with a hardcoded repository name and github.workflow_sha which correctly references the reusable workflow's commit SHA. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ci-cd-java.yml | 14 ++------------ .gitignore | 3 ++- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci-cd-java.yml b/.github/workflows/ci-cd-java.yml index ec4712e..e9be5f4 100644 --- a/.github/workflows/ci-cd-java.yml +++ b/.github/workflows/ci-cd-java.yml @@ -43,21 +43,11 @@ jobs: clean: 'true' fetch-depth: 2 - - name: Resolve shared workflow source - env: - WORKFLOW_REF: ${{ github.workflow_ref }} - run: | - SHARED_WORKFLOW_REPOSITORY="${WORKFLOW_REF%%/.github/workflows/*}" - SHARED_WORKFLOW_REF="${WORKFLOW_REF##*@}" - - echo "SHARED_WORKFLOW_REPOSITORY=${SHARED_WORKFLOW_REPOSITORY}" >> "$GITHUB_ENV" - echo "SHARED_WORKFLOW_REF=${SHARED_WORKFLOW_REF}" >> "$GITHUB_ENV" - - name: Checkout shared workflow scripts uses: actions/checkout@v4 with: - repository: ${{ env.SHARED_WORKFLOW_REPOSITORY }} - ref: ${{ env.SHARED_WORKFLOW_REF }} + repository: HSLdevcom/transitdata-shared-workflows + ref: ${{ github.workflow_sha }} path: .shared-workflows - name: Setup JDK diff --git a/.gitignore b/.gitignore index 01b0c5b..bd8b273 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea scripts/__pycache__ -/AGENTS.md +AGENTS.md +CLAUDE.md From de67a1aaeb79aa085bfaf9fe693dca23603e6aca Mon Sep 17 00:00:00 2001 From: bogdandina Date: Mon, 30 Mar 2026 09:51:53 +0300 Subject: [PATCH 05/10] feature(76471): update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index bd8b273..fa2e2d1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ scripts/__pycache__ AGENTS.md CLAUDE.md +.claude \ No newline at end of file From f4358461ac4dc63a0a09fc50b2db0c08adda9b09 Mon Sep 17 00:00:00 2001 From: bogdandina Date: Mon, 30 Mar 2026 10:02:36 +0300 Subject: [PATCH 06/10] feat(76471): potential fix --- .github/workflows/ci-cd-java.yml | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-cd-java.yml b/.github/workflows/ci-cd-java.yml index e9be5f4..d6ca377 100644 --- a/.github/workflows/ci-cd-java.yml +++ b/.github/workflows/ci-cd-java.yml @@ -43,11 +43,27 @@ jobs: clean: 'true' fetch-depth: 2 + - name: Resolve shared workflow ref + run: | + set -euo pipefail + SHARED_WORKFLOW_REF=$(grep -roh \ + 'transitdata-shared-workflows/.github/workflows/[^@]*@[^ "'\'']*' \ + "${GITHUB_WORKSPACE}/.github/workflows/" 2>/dev/null \ + | sed 's/.*@//' | head -1 || true) + + if [[ -z "${SHARED_WORKFLOW_REF}" ]]; then + echo "::warning::Could not detect shared workflow ref from caller workflows; falling back to main" + SHARED_WORKFLOW_REF="main" + fi + + echo "Resolved shared workflow ref: ${SHARED_WORKFLOW_REF}" + echo "SHARED_WORKFLOW_REF=${SHARED_WORKFLOW_REF}" >> "$GITHUB_ENV" + - name: Checkout shared workflow scripts uses: actions/checkout@v4 with: repository: HSLdevcom/transitdata-shared-workflows - ref: ${{ github.workflow_sha }} + ref: ${{ env.SHARED_WORKFLOW_REF }} path: .shared-workflows - name: Setup JDK From 06a10b532815659b96bd5f574dbf618bae3c090b Mon Sep 17 00:00:00 2001 From: bogdandina Date: Tue, 31 Mar 2026 14:27:47 +0300 Subject: [PATCH 07/10] test(76471): add unit tests for validate_java_version_consistency.py and CI workflow --- .github/workflows/ci-cd-java.yml | 1 + .github/workflows/ci.yml | 29 ++ .../test_validate_java_version_consistency.py | 278 ++++++++++++++++++ 3 files changed, 308 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 scripts/test_validate_java_version_consistency.py diff --git a/.github/workflows/ci-cd-java.yml b/.github/workflows/ci-cd-java.yml index d6ca377..8284fc0 100644 --- a/.github/workflows/ci-cd-java.yml +++ b/.github/workflows/ci-cd-java.yml @@ -43,6 +43,7 @@ jobs: clean: 'true' fetch-depth: 2 + # Required since custom scripts from /scripts are being used - name: Resolve shared workflow ref run: | set -euo pipefail diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f3da524 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: ci.yml + +permissions: + contents: read + +on: + pull_request: + push: + branches: + - main + +jobs: + test-scripts: + name: Test Python scripts + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install pytest + run: pip install pytest + + - name: Run script tests + run: pytest scripts/ -v diff --git a/scripts/test_validate_java_version_consistency.py b/scripts/test_validate_java_version_consistency.py new file mode 100644 index 0000000..dd0d666 --- /dev/null +++ b/scripts/test_validate_java_version_consistency.py @@ -0,0 +1,278 @@ +import pytest + +from validate_java_version_consistency import ( + extract_java_version_from_docker_tag, + normalize_java_version, + parse_docker_java_version, + parse_gradle_java_version, + resolve_args, +) + + +# --------------------------------------------------------------------------- +# normalize_java_version +# --------------------------------------------------------------------------- + +class TestNormalizeJavaVersion: + def test_plain_number(self): + assert normalize_java_version("25") == "25" + + def test_strips_whitespace(self): + assert normalize_java_version(" 17 ") == "17" + + def test_strips_single_quotes(self): + assert normalize_java_version("'21'") == "21" + + def test_strips_double_quotes(self): + assert normalize_java_version('"11"') == "11" + + def test_legacy_1x_format_java_8(self): + assert normalize_java_version("1.8") == "8" + + def test_legacy_1x_format_java_11(self): + assert normalize_java_version("1.11") == "11" + + def test_gradle_java_version_constant(self): + assert normalize_java_version("JavaVersion.VERSION_25") == "25" + + def test_gradle_java_version_constant_11(self): + assert normalize_java_version("JavaVersion.VERSION_11") == "11" + + def test_leading_zeros_stripped(self): + # int() conversion removes leading zeros + assert normalize_java_version("011") == "11" + + # --- sad paths --- + + def test_none_input(self): + assert normalize_java_version(None) is None + + def test_empty_string(self): + assert normalize_java_version("") is None + + def test_only_whitespace(self): + assert normalize_java_version(" ") is None + + def test_unexpanded_placeholder(self): + assert normalize_java_version("${java.version}") is None + + def test_no_digits(self): + assert normalize_java_version("abc") is None + + +# --------------------------------------------------------------------------- +# extract_java_version_from_docker_tag +# --------------------------------------------------------------------------- + +class TestExtractJavaVersionFromDockerTag: + def test_plain_number(self): + assert extract_java_version_from_docker_tag("25") == "25" + + def test_hsl_base_image_jre_tag(self): + assert extract_java_version_from_docker_tag("1.0.2-25-java-jre") == "25" + + def test_hsl_base_image_jdk_tag(self): + assert extract_java_version_from_docker_tag("1.0.2-25-java-jdk") == "25" + + def test_version_dash_jdk(self): + assert extract_java_version_from_docker_tag("25-jdk") == "25" + + def test_version_dash_jre(self): + assert extract_java_version_from_docker_tag("25-jre") == "25" + + def test_java_dash_version(self): + assert extract_java_version_from_docker_tag("java-11") == "11" + + def test_case_insensitive_jdk(self): + assert extract_java_version_from_docker_tag("17-JDK") == "17" + + # --- sad paths --- + + def test_latest_tag(self): + assert extract_java_version_from_docker_tag("latest") is None + + def test_empty_tag(self): + assert extract_java_version_from_docker_tag("") is None + + def test_no_java_version_hint(self): + assert extract_java_version_from_docker_tag("ubuntu") is None + + +# --------------------------------------------------------------------------- +# resolve_args +# --------------------------------------------------------------------------- + +class TestResolveArgs: + def test_curly_brace_syntax(self): + assert resolve_args("${BASE}", {"BASE": "ubuntu:22.04"}) == "ubuntu:22.04" + + def test_dollar_word_syntax(self): + assert resolve_args("$VERSION", {"VERSION": "1.0"}) == "1.0" + + def test_embedded_variable(self): + assert resolve_args("prefix-${TAG}-suffix", {"TAG": "foo"}) == "prefix-foo-suffix" + + def test_multiple_variables(self): + assert resolve_args("${A}/${B}", {"A": "x", "B": "y"}) == "x/y" + + def test_no_variables(self): + assert resolve_args("plain-string", {}) == "plain-string" + + # --- sad paths --- + + def test_missing_curly_brace_variable_left_intact(self): + assert resolve_args("${MISSING}", {}) == "${MISSING}" + + def test_missing_dollar_word_variable_left_intact(self): + assert resolve_args("$MISSING", {}) == "$MISSING" + + def test_empty_string(self): + assert resolve_args("", {}) == "" + + +# --------------------------------------------------------------------------- +# parse_docker_java_version (uses tmp_path fixture) +# --------------------------------------------------------------------------- + +class TestParseDockerJavaVersion: + def test_simple_jre_tag(self, tmp_path): + dockerfile = tmp_path / "Dockerfile" + dockerfile.write_text( + "FROM hsldevcom/infodevops-docker-base-images:1.0.2-25-java-jre\n" + ) + version, image_ref = parse_docker_java_version(str(dockerfile)) + assert version == "25" + assert "1.0.2-25-java-jre" in image_ref + + def test_arg_substitution(self, tmp_path): + dockerfile = tmp_path / "Dockerfile" + dockerfile.write_text( + "ARG BASE_TAG=1.0.2-25-java-jre\n" + "FROM hsldevcom/infodevops-docker-base-images:${BASE_TAG}\n" + ) + version, _ = parse_docker_java_version(str(dockerfile)) + assert version == "25" + + def test_multistage_uses_last_non_scratch(self, tmp_path): + dockerfile = tmp_path / "Dockerfile" + dockerfile.write_text( + "FROM hsldevcom/infodevops-docker-base-images:1.0.2-25-java-jdk AS build\n" + "FROM scratch\n" + "FROM hsldevcom/infodevops-docker-base-images:1.0.2-25-java-jre\n" + ) + version, image_ref = parse_docker_java_version(str(dockerfile)) + assert version == "25" + assert "java-jre" in image_ref + + def test_scratch_is_skipped_when_real_image_exists(self, tmp_path): + dockerfile = tmp_path / "Dockerfile" + dockerfile.write_text( + "FROM hsldevcom/infodevops-docker-base-images:1.0.2-17-java-jre AS base\n" + "FROM scratch\n" + ) + # scratch is not the last non-scratch image; base is + version, _ = parse_docker_java_version(str(dockerfile)) + assert version == "17" + + def test_comments_are_ignored(self, tmp_path): + dockerfile = tmp_path / "Dockerfile" + dockerfile.write_text( + "# syntax=docker/dockerfile:1\n" + "# This is a comment\n" + "FROM hsldevcom/infodevops-docker-base-images:1.0.2-11-java-jre\n" + ) + version, _ = parse_docker_java_version(str(dockerfile)) + assert version == "11" + + def test_image_with_digest_strips_digest(self, tmp_path): + dockerfile = tmp_path / "Dockerfile" + dockerfile.write_text( + "FROM hsldevcom/infodevops-docker-base-images:1.0.2-25-java-jre@sha256:abc123\n" + ) + version, _ = parse_docker_java_version(str(dockerfile)) + assert version == "25" + + # --- sad paths --- + + def test_no_from_instruction_raises(self, tmp_path): + dockerfile = tmp_path / "Dockerfile" + dockerfile.write_text("RUN echo hello\n") + with pytest.raises(RuntimeError, match="Could not find a runtime image"): + parse_docker_java_version(str(dockerfile)) + + def test_only_scratch_raises(self, tmp_path): + dockerfile = tmp_path / "Dockerfile" + dockerfile.write_text("FROM scratch\n") + with pytest.raises(RuntimeError, match="Could not find a runtime image"): + parse_docker_java_version(str(dockerfile)) + + def test_image_without_tag_raises(self, tmp_path): + dockerfile = tmp_path / "Dockerfile" + dockerfile.write_text("FROM ubuntu\n") + with pytest.raises(RuntimeError, match="Could not determine the Java tag"): + parse_docker_java_version(str(dockerfile)) + + def test_unrecognizable_java_version_in_tag_raises(self, tmp_path): + dockerfile = tmp_path / "Dockerfile" + dockerfile.write_text("FROM ubuntu:latest\n") + with pytest.raises(RuntimeError, match="Could not determine the Java version"): + parse_docker_java_version(str(dockerfile)) + + +# --------------------------------------------------------------------------- +# parse_gradle_java_version (uses tmp_path fixture) +# --------------------------------------------------------------------------- + +class TestParseGradleJavaVersion: + def test_jvm_target_string(self, tmp_path): + gradle = tmp_path / "build.gradle.kts" + gradle.write_text('compileKotlin { kotlinOptions { jvmTarget = "25" } }\n') + version, source = parse_gradle_java_version(str(gradle)) + assert version == "25" + assert "25" in source + + def test_jvm_target_java_version_constant(self, tmp_path): + gradle = tmp_path / "build.gradle.kts" + gradle.write_text("tasks.withType { jvmTarget = JavaVersion.VERSION_17 }\n") + version, _ = parse_gradle_java_version(str(gradle)) + assert version == "17" + + def test_language_version_set(self, tmp_path): + gradle = tmp_path / "build.gradle.kts" + gradle.write_text( + "java { toolchain { languageVersion.set(JavaLanguageVersion.of(21)) } }\n" + ) + version, _ = parse_gradle_java_version(str(gradle)) + assert version == "21" + + def test_source_compatibility(self, tmp_path): + gradle = tmp_path / "build.gradle" + gradle.write_text("sourceCompatibility = JavaVersion.VERSION_11\n") + version, _ = parse_gradle_java_version(str(gradle)) + assert version == "11" + + def test_target_compatibility(self, tmp_path): + gradle = tmp_path / "build.gradle" + gradle.write_text("targetCompatibility = JavaVersion.VERSION_11\n") + version, _ = parse_gradle_java_version(str(gradle)) + assert version == "11" + + def test_source_name_in_reported_source(self, tmp_path): + gradle = tmp_path / "build.gradle.kts" + gradle.write_text('compileKotlin { kotlinOptions { jvmTarget = "25" } }\n') + _, source = parse_gradle_java_version(str(gradle)) + assert "build.gradle.kts" in source + + # --- sad paths --- + + def test_no_matching_pattern_raises(self, tmp_path): + gradle = tmp_path / "build.gradle.kts" + gradle.write_text("plugins { kotlin(\"jvm\") }\n") + with pytest.raises(RuntimeError, match="Could not determine the Java version"): + parse_gradle_java_version(str(gradle)) + + def test_empty_file_raises(self, tmp_path): + gradle = tmp_path / "build.gradle.kts" + gradle.write_text("") + with pytest.raises(RuntimeError, match="Could not determine the Java version"): + parse_gradle_java_version(str(gradle)) From 22945f1e10af963cd9855d58d72d9def5609cd71 Mon Sep 17 00:00:00 2001 From: bogdandina Date: Thu, 2 Apr 2026 09:18:21 +0300 Subject: [PATCH 08/10] feat(76455): inline the test layer of the Dockerfile in the CI/CD for simplifying the microservice's Dockerfiles --- .github/workflows/ci-cd-java.yml | 46 +++++++---- .github/workflows/ci-cd-kotlin.yml | 75 +++++++++++++++++- .../test_validate_java_version_consistency.py | 79 +++++++++++++++++++ scripts/validate_java_version_consistency.py | 46 +++++++++++ 4 files changed, 227 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci-cd-java.yml b/.github/workflows/ci-cd-java.yml index 8284fc0..f888e31 100644 --- a/.github/workflows/ci-cd-java.yml +++ b/.github/workflows/ci-cd-java.yml @@ -26,11 +26,10 @@ on: runTestsInsideDocker: required: false type: boolean - default: false + default: true env: IMAGE_NAME_MIXED_CASE: "${{ github.repository }}" - TEST_STAGE: test jobs: build-check-test-push: @@ -87,7 +86,35 @@ jobs: run: | mvn spotless:check - - name: Run tests outside Docker + - name: Run unit tests inside Docker + if: ${{ inputs.runTestsInsideDocker }} + working-directory: ${{ inputs.workingDirectory }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_ACTOR_ARG: ${{ github.actor }} + DOCKER_BUILDKIT: "1" + run: | + cat > /tmp/Dockerfile.test << DOCKERFILE + # syntax=docker/dockerfile:1 + # check=error=true + FROM ${TEST_BASE_IMAGE} + WORKDIR /usr/app + ARG GITHUB_ACTOR=github-actions + COPY . . + COPY .mvn/settings.xml /root/.m2/settings.xml + RUN --mount=type=secret,id=github_token \ + export GITHUB_TOKEN="\$(cat /run/secrets/github_token)" && \ + export GITHUB_ACTOR="\$GITHUB_ACTOR" && \ + ./mvnw -B test + DOCKERFILE + docker build \ + --secret id=github_token,env=GITHUB_TOKEN \ + --build-arg "GITHUB_ACTOR=${GITHUB_ACTOR_ARG}" \ + -f /tmp/Dockerfile.test \ + . + + - name: Run unit tests outside Docker + if: ${{ !inputs.runTestsInsideDocker }} working-directory: ${{ inputs.workingDirectory }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -145,19 +172,6 @@ jobs: echo "IMAGE_NAME=${IMAGE_NAME}" >> "$GITHUB_ENV" - - name: Build & run tests inside Docker - if: ${{ inputs.runTestsInsideDocker }} - uses: docker/build-push-action@v6 - with: - context: ${{ inputs.workingDirectory }} - load: true - target: "${{ env.TEST_STAGE }}" - tags: "${{ env.IMAGE_NAME }}:${{ env.TEST_STAGE }}" - secrets: - github_token=${{ secrets.GITHUB_TOKEN }} - build-args: - GITHUB_ACTOR=${{ github.actor }} - - name: Build Docker Image uses: docker/build-push-action@v6 with: diff --git a/.github/workflows/ci-cd-kotlin.yml b/.github/workflows/ci-cd-kotlin.yml index da3bda8..bdfa3fe 100644 --- a/.github/workflows/ci-cd-kotlin.yml +++ b/.github/workflows/ci-cd-kotlin.yml @@ -21,6 +21,14 @@ on: required: false type: boolean default: false + runTestsInsideDocker: + required: false + type: boolean + default: true + hasIntegrationTests: + required: false + type: boolean + default: false env: IMAGE_NAME_MIXED_CASE: "${{ github.repository }}" @@ -36,6 +44,30 @@ jobs: clean: 'true' fetch-depth: 2 + # Required since custom scripts from /scripts are being used + - name: Resolve shared workflow ref + run: | + set -euo pipefail + SHARED_WORKFLOW_REF=$(grep -roh \ + 'transitdata-shared-workflows/.github/workflows/[^@]*@[^ "'\'']*' \ + "${GITHUB_WORKSPACE}/.github/workflows/" 2>/dev/null \ + | sed 's/.*@//' | head -1 || true) + + if [[ -z "${SHARED_WORKFLOW_REF}" ]]; then + echo "::warning::Could not detect shared workflow ref from caller workflows; falling back to main" + SHARED_WORKFLOW_REF="main" + fi + + echo "Resolved shared workflow ref: ${SHARED_WORKFLOW_REF}" + echo "SHARED_WORKFLOW_REF=${SHARED_WORKFLOW_REF}" >> "$GITHUB_ENV" + + - name: Checkout shared workflow scripts + uses: actions/checkout@v4 + with: + repository: HSLdevcom/transitdata-shared-workflows + ref: ${{ env.SHARED_WORKFLOW_REF }} + path: .shared-workflows + - name: Setup JDK uses: actions/setup-java@v4 with: @@ -43,18 +75,55 @@ jobs: java-version: '11' cache: 'gradle' + - name: Validate Java version consistency + env: + JAVA_TOOL_OPTIONS: "" + run: python3 "${GITHUB_WORKSPACE}/.shared-workflows/scripts/validate_java_version_consistency.py" + - name: Check code format and lint run: ./gradlew spotlessCheck env: GITHUB_ACTOR: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Run tests + - name: Run unit tests inside Docker + if: ${{ inputs.runTestsInsideDocker }} env: - GITHUB_ACTOR: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_ACTOR_ARG: ${{ github.actor }} + DOCKER_BUILDKIT: "1" run: | - ./gradlew test jacocoTestReport --stacktrace + cat > /tmp/Dockerfile.test << DOCKERFILE + # syntax=docker/dockerfile:1 + # check=error=true + FROM ${TEST_BASE_IMAGE} + WORKDIR /usr/app + ARG GITHUB_ACTOR=github-actions + COPY . . + RUN --mount=type=secret,id=github_token \ + export GITHUB_TOKEN="\$(cat /run/secrets/github_token)" && \ + export GITHUB_ACTOR="\$GITHUB_ACTOR" && \ + ./gradlew test --stacktrace --no-daemon + DOCKERFILE + docker build \ + --secret id=github_token,env=GITHUB_TOKEN \ + --build-arg "GITHUB_ACTOR=${GITHUB_ACTOR_ARG}" \ + -f /tmp/Dockerfile.test \ + . + + - name: Run unit tests + if: ${{ inputs.hasIntegrationTests == false }} + env: + GITHUB_ACTOR: ${{ github.actor }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./gradlew test jacocoTestReport --stacktrace + + - name: Run unit tests and integration tests + if: ${{ inputs.hasIntegrationTests }} + env: + GITHUB_ACTOR: ${{ github.actor }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./gradlew test integrationTest jacocoTestReport --stacktrace - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v5 diff --git a/scripts/test_validate_java_version_consistency.py b/scripts/test_validate_java_version_consistency.py index dd0d666..fd99a1f 100644 --- a/scripts/test_validate_java_version_consistency.py +++ b/scripts/test_validate_java_version_consistency.py @@ -4,6 +4,7 @@ extract_java_version_from_docker_tag, normalize_java_version, parse_docker_java_version, + parse_docker_jdk_image, parse_gradle_java_version, resolve_args, ) @@ -219,6 +220,84 @@ def test_unrecognizable_java_version_in_tag_raises(self, tmp_path): parse_docker_java_version(str(dockerfile)) +# --------------------------------------------------------------------------- +# parse_docker_jdk_image (uses tmp_path fixture) +# --------------------------------------------------------------------------- + +class TestParseDockerJdkImage: + def test_standard_multistage_returns_first_real_from(self, tmp_path): + dockerfile = tmp_path / "Dockerfile" + dockerfile.write_text( + "FROM hsldevcom/infodevops-docker-base-images:1.0.2-25-java-jdk AS base\n" + "FROM base AS test\n" + "FROM base AS build\n" + "FROM hsldevcom/infodevops-docker-base-images:1.0.2-25-java-jre\n" + ) + image = parse_docker_jdk_image(str(dockerfile)) + assert image == "hsldevcom/infodevops-docker-base-images:1.0.2-25-java-jdk" + + def test_arg_substitution(self, tmp_path): + dockerfile = tmp_path / "Dockerfile" + dockerfile.write_text( + "ARG BASE_TAG=1.0.2-25-java-jdk\n" + "FROM hsldevcom/infodevops-docker-base-images:${BASE_TAG} AS base\n" + "FROM hsldevcom/infodevops-docker-base-images:1.0.2-25-java-jre\n" + ) + image = parse_docker_jdk_image(str(dockerfile)) + assert "1.0.2-25-java-jdk" in image + + def test_single_stage_returns_only_image(self, tmp_path): + dockerfile = tmp_path / "Dockerfile" + dockerfile.write_text( + "FROM hsldevcom/infodevops-docker-base-images:1.0.2-25-java-jdk\n" + ) + image = parse_docker_jdk_image(str(dockerfile)) + assert image == "hsldevcom/infodevops-docker-base-images:1.0.2-25-java-jdk" + + def test_skips_scratch(self, tmp_path): + dockerfile = tmp_path / "Dockerfile" + dockerfile.write_text( + "FROM scratch\n" + "FROM hsldevcom/infodevops-docker-base-images:1.0.2-25-java-jdk AS base\n" + ) + image = parse_docker_jdk_image(str(dockerfile)) + assert "java-jdk" in image + + def test_skips_stage_aliases(self, tmp_path): + dockerfile = tmp_path / "Dockerfile" + dockerfile.write_text( + "FROM hsldevcom/infodevops-docker-base-images:1.0.2-25-java-jdk AS base\n" + "FROM base AS test\n" + ) + image = parse_docker_jdk_image(str(dockerfile)) + assert image == "hsldevcom/infodevops-docker-base-images:1.0.2-25-java-jdk" + + def test_comments_are_ignored(self, tmp_path): + dockerfile = tmp_path / "Dockerfile" + dockerfile.write_text( + "# syntax=docker/dockerfile:1\n" + "# check=error=true\n" + "FROM hsldevcom/infodevops-docker-base-images:1.0.2-25-java-jdk AS base\n" + ) + image = parse_docker_jdk_image(str(dockerfile)) + assert "java-jdk" in image + + def test_no_real_from_raises(self, tmp_path): + dockerfile = tmp_path / "Dockerfile" + dockerfile.write_text("FROM scratch\n") + with pytest.raises(RuntimeError, match="Could not find a JDK base image"): + parse_docker_jdk_image(str(dockerfile)) + + def test_only_aliases_raises(self, tmp_path): + dockerfile = tmp_path / "Dockerfile" + dockerfile.write_text( + "FROM base AS test\n" + "FROM build AS final\n" + ) + with pytest.raises(RuntimeError, match="Could not find a JDK base image"): + parse_docker_jdk_image(str(dockerfile)) + + # --------------------------------------------------------------------------- # parse_gradle_java_version (uses tmp_path fixture) # --------------------------------------------------------------------------- diff --git a/scripts/validate_java_version_consistency.py b/scripts/validate_java_version_consistency.py index 4182533..3a3c57a 100644 --- a/scripts/validate_java_version_consistency.py +++ b/scripts/validate_java_version_consistency.py @@ -50,6 +50,39 @@ def replace(match): return re.sub(r'\$\{([^}]+)\}|\$(\w+)', replace, value) +def _is_stage_alias(image): + """Return True if the image string is a multi-stage alias (e.g. 'base', 'build'), not a registry reference.""" + return ':' not in image and '/' not in image + + +def parse_docker_jdk_image(dockerfile_path): + """Return the image ref of the first real (non-scratch, non-alias) FROM stage. + + In the standard 4-stage Java Dockerfile pattern the first real FROM is the JDK + build/test image, e.g. hsldevcom/infodevops-docker-base-images:1.0.2-25-java-jdk. + """ + args = {} + + with open(dockerfile_path, encoding='utf-8') as dockerfile: + for raw_line in dockerfile: + line = raw_line.split('#', 1)[0].strip() + if not line: + continue + + arg_match = re.match(r'^ARG\s+([A-Za-z_][A-Za-z0-9_]*)=(.+)$', line, re.IGNORECASE) + if arg_match: + args[arg_match.group(1)] = arg_match.group(2).strip() + continue + + from_match = re.match(r'^FROM\s+([^\s]+)', line, re.IGNORECASE) + if from_match: + image = resolve_args(from_match.group(1).strip(), args) + if image.lower() != 'scratch' and not _is_stage_alias(image): + return image + + raise RuntimeError('Could not find a JDK base image in Dockerfile.') + + def parse_docker_java_version(dockerfile_path): args = {} images = [] @@ -158,11 +191,24 @@ def parse_gradle_java_version(gradle_path): ) +def _write_github_file(env_var_name, content): + """Append key=value to a GitHub Actions environment file if the path is set.""" + path = os.getenv(env_var_name) + if path: + with open(path, 'a') as f: + f.write(f'{content}\n') + + def main(): dockerfile_path = os.path.join(os.getcwd(), 'Dockerfile') if not os.path.exists(dockerfile_path): raise RuntimeError(f'Dockerfile not found at {dockerfile_path}.') + jdk_image = parse_docker_jdk_image(dockerfile_path) + print(f'JDK base image: {jdk_image}') + _write_github_file('GITHUB_OUTPUT', f'TEST_BASE_IMAGE={jdk_image}') + _write_github_file('GITHUB_ENV', f'TEST_BASE_IMAGE={jdk_image}') + docker_version, docker_image = parse_docker_java_version(dockerfile_path) pom_path = os.path.join(os.getcwd(), 'pom.xml') From 1d8679c431fa2a21d61e08ed321f1b63d7011034 Mon Sep 17 00:00:00 2001 From: bogdandina Date: Tue, 7 Apr 2026 09:18:49 +0300 Subject: [PATCH 09/10] feat(76471): fixes after PR review --- .github/workflows/ci-cd-java.yml | 5 +++-- .../test_validate_java_version_consistency.py | 19 ------------------- scripts/validate_java_version_consistency.py | 5 ----- 3 files changed, 3 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci-cd-java.yml b/.github/workflows/ci-cd-java.yml index f888e31..bd243a8 100644 --- a/.github/workflows/ci-cd-java.yml +++ b/.github/workflows/ci-cd-java.yml @@ -44,6 +44,7 @@ jobs: # Required since custom scripts from /scripts are being used - name: Resolve shared workflow ref + id: resolve_shared_workflow_ref run: | set -euo pipefail SHARED_WORKFLOW_REF=$(grep -roh \ @@ -57,13 +58,13 @@ jobs: fi echo "Resolved shared workflow ref: ${SHARED_WORKFLOW_REF}" - echo "SHARED_WORKFLOW_REF=${SHARED_WORKFLOW_REF}" >> "$GITHUB_ENV" + echo "shared_workflow_ref=${SHARED_WORKFLOW_REF}" >> "$GITHUB_OUTPUT" - name: Checkout shared workflow scripts uses: actions/checkout@v4 with: repository: HSLdevcom/transitdata-shared-workflows - ref: ${{ env.SHARED_WORKFLOW_REF }} + ref: ${{ steps.resolve_shared_workflow_ref.outputs.shared_workflow_ref }} path: .shared-workflows - name: Setup JDK diff --git a/scripts/test_validate_java_version_consistency.py b/scripts/test_validate_java_version_consistency.py index fd99a1f..e6ee0b6 100644 --- a/scripts/test_validate_java_version_consistency.py +++ b/scripts/test_validate_java_version_consistency.py @@ -145,15 +145,6 @@ def test_simple_jre_tag(self, tmp_path): assert version == "25" assert "1.0.2-25-java-jre" in image_ref - def test_arg_substitution(self, tmp_path): - dockerfile = tmp_path / "Dockerfile" - dockerfile.write_text( - "ARG BASE_TAG=1.0.2-25-java-jre\n" - "FROM hsldevcom/infodevops-docker-base-images:${BASE_TAG}\n" - ) - version, _ = parse_docker_java_version(str(dockerfile)) - assert version == "25" - def test_multistage_uses_last_non_scratch(self, tmp_path): dockerfile = tmp_path / "Dockerfile" dockerfile.write_text( @@ -236,16 +227,6 @@ def test_standard_multistage_returns_first_real_from(self, tmp_path): image = parse_docker_jdk_image(str(dockerfile)) assert image == "hsldevcom/infodevops-docker-base-images:1.0.2-25-java-jdk" - def test_arg_substitution(self, tmp_path): - dockerfile = tmp_path / "Dockerfile" - dockerfile.write_text( - "ARG BASE_TAG=1.0.2-25-java-jdk\n" - "FROM hsldevcom/infodevops-docker-base-images:${BASE_TAG} AS base\n" - "FROM hsldevcom/infodevops-docker-base-images:1.0.2-25-java-jre\n" - ) - image = parse_docker_jdk_image(str(dockerfile)) - assert "1.0.2-25-java-jdk" in image - def test_single_stage_returns_only_image(self, tmp_path): dockerfile = tmp_path / "Dockerfile" dockerfile.write_text( diff --git a/scripts/validate_java_version_consistency.py b/scripts/validate_java_version_consistency.py index 3a3c57a..348de9b 100644 --- a/scripts/validate_java_version_consistency.py +++ b/scripts/validate_java_version_consistency.py @@ -69,11 +69,6 @@ def parse_docker_jdk_image(dockerfile_path): if not line: continue - arg_match = re.match(r'^ARG\s+([A-Za-z_][A-Za-z0-9_]*)=(.+)$', line, re.IGNORECASE) - if arg_match: - args[arg_match.group(1)] = arg_match.group(2).strip() - continue - from_match = re.match(r'^FROM\s+([^\s]+)', line, re.IGNORECASE) if from_match: image = resolve_args(from_match.group(1).strip(), args) From 885a6fa4e6528ede19d27725e8b5680eafed3861 Mon Sep 17 00:00:00 2001 From: bogdandina Date: Tue, 7 Apr 2026 09:27:46 +0300 Subject: [PATCH 10/10] feat(76471): fixes++ --- .github/workflows/ci-cd-java.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-cd-java.yml b/.github/workflows/ci-cd-java.yml index bd243a8..007217c 100644 --- a/.github/workflows/ci-cd-java.yml +++ b/.github/workflows/ci-cd-java.yml @@ -92,7 +92,7 @@ jobs: working-directory: ${{ inputs.workingDirectory }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_ACTOR_ARG: ${{ github.actor }} + GITHUB_ACTOR: ${{ github.actor }} DOCKER_BUILDKIT: "1" run: | cat > /tmp/Dockerfile.test << DOCKERFILE @@ -100,17 +100,17 @@ jobs: # check=error=true FROM ${TEST_BASE_IMAGE} WORKDIR /usr/app - ARG GITHUB_ACTOR=github-actions COPY . . COPY .mvn/settings.xml /root/.m2/settings.xml RUN --mount=type=secret,id=github_token \ + --mount=type=secret,id=github_actor \ export GITHUB_TOKEN="\$(cat /run/secrets/github_token)" && \ - export GITHUB_ACTOR="\$GITHUB_ACTOR" && \ + export GITHUB_ACTOR="\$(cat /run/secrets/github_actor)" && \ ./mvnw -B test DOCKERFILE docker build \ --secret id=github_token,env=GITHUB_TOKEN \ - --build-arg "GITHUB_ACTOR=${GITHUB_ACTOR_ARG}" \ + --secret id=github_actor,env=GITHUB_ACTOR \ -f /tmp/Dockerfile.test \ .