Skip to content

Commit 22945f1

Browse files
committed
feat(76455): inline the test layer of the Dockerfile in the CI/CD for simplifying the microservice's Dockerfiles
1 parent 06a10b5 commit 22945f1

4 files changed

Lines changed: 227 additions & 19 deletions

File tree

.github/workflows/ci-cd-java.yml

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,10 @@ on:
2626
runTestsInsideDocker:
2727
required: false
2828
type: boolean
29-
default: false
29+
default: true
3030

3131
env:
3232
IMAGE_NAME_MIXED_CASE: "${{ github.repository }}"
33-
TEST_STAGE: test
3433

3534
jobs:
3635
build-check-test-push:
@@ -87,7 +86,35 @@ jobs:
8786
run: |
8887
mvn spotless:check
8988
90-
- name: Run tests outside Docker
89+
- name: Run unit tests inside Docker
90+
if: ${{ inputs.runTestsInsideDocker }}
91+
working-directory: ${{ inputs.workingDirectory }}
92+
env:
93+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
94+
GITHUB_ACTOR_ARG: ${{ github.actor }}
95+
DOCKER_BUILDKIT: "1"
96+
run: |
97+
cat > /tmp/Dockerfile.test << DOCKERFILE
98+
# syntax=docker/dockerfile:1
99+
# check=error=true
100+
FROM ${TEST_BASE_IMAGE}
101+
WORKDIR /usr/app
102+
ARG GITHUB_ACTOR=github-actions
103+
COPY . .
104+
COPY .mvn/settings.xml /root/.m2/settings.xml
105+
RUN --mount=type=secret,id=github_token \
106+
export GITHUB_TOKEN="\$(cat /run/secrets/github_token)" && \
107+
export GITHUB_ACTOR="\$GITHUB_ACTOR" && \
108+
./mvnw -B test
109+
DOCKERFILE
110+
docker build \
111+
--secret id=github_token,env=GITHUB_TOKEN \
112+
--build-arg "GITHUB_ACTOR=${GITHUB_ACTOR_ARG}" \
113+
-f /tmp/Dockerfile.test \
114+
.
115+
116+
- name: Run unit tests outside Docker
117+
if: ${{ !inputs.runTestsInsideDocker }}
91118
working-directory: ${{ inputs.workingDirectory }}
92119
env:
93120
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -145,19 +172,6 @@ jobs:
145172
146173
echo "IMAGE_NAME=${IMAGE_NAME}" >> "$GITHUB_ENV"
147174
148-
- name: Build & run tests inside Docker
149-
if: ${{ inputs.runTestsInsideDocker }}
150-
uses: docker/build-push-action@v6
151-
with:
152-
context: ${{ inputs.workingDirectory }}
153-
load: true
154-
target: "${{ env.TEST_STAGE }}"
155-
tags: "${{ env.IMAGE_NAME }}:${{ env.TEST_STAGE }}"
156-
secrets:
157-
github_token=${{ secrets.GITHUB_TOKEN }}
158-
build-args:
159-
GITHUB_ACTOR=${{ github.actor }}
160-
161175
- name: Build Docker Image
162176
uses: docker/build-push-action@v6
163177
with:

.github/workflows/ci-cd-kotlin.yml

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ on:
2121
required: false
2222
type: boolean
2323
default: false
24+
runTestsInsideDocker:
25+
required: false
26+
type: boolean
27+
default: true
28+
hasIntegrationTests:
29+
required: false
30+
type: boolean
31+
default: false
2432

2533
env:
2634
IMAGE_NAME_MIXED_CASE: "${{ github.repository }}"
@@ -36,25 +44,86 @@ jobs:
3644
clean: 'true'
3745
fetch-depth: 2
3846

47+
# Required since custom scripts from /scripts are being used
48+
- name: Resolve shared workflow ref
49+
run: |
50+
set -euo pipefail
51+
SHARED_WORKFLOW_REF=$(grep -roh \
52+
'transitdata-shared-workflows/.github/workflows/[^@]*@[^ "'\'']*' \
53+
"${GITHUB_WORKSPACE}/.github/workflows/" 2>/dev/null \
54+
| sed 's/.*@//' | head -1 || true)
55+
56+
if [[ -z "${SHARED_WORKFLOW_REF}" ]]; then
57+
echo "::warning::Could not detect shared workflow ref from caller workflows; falling back to main"
58+
SHARED_WORKFLOW_REF="main"
59+
fi
60+
61+
echo "Resolved shared workflow ref: ${SHARED_WORKFLOW_REF}"
62+
echo "SHARED_WORKFLOW_REF=${SHARED_WORKFLOW_REF}" >> "$GITHUB_ENV"
63+
64+
- name: Checkout shared workflow scripts
65+
uses: actions/checkout@v4
66+
with:
67+
repository: HSLdevcom/transitdata-shared-workflows
68+
ref: ${{ env.SHARED_WORKFLOW_REF }}
69+
path: .shared-workflows
70+
3971
- name: Setup JDK
4072
uses: actions/setup-java@v4
4173
with:
4274
distribution: 'temurin'
4375
java-version: '11'
4476
cache: 'gradle'
4577

78+
- name: Validate Java version consistency
79+
env:
80+
JAVA_TOOL_OPTIONS: ""
81+
run: python3 "${GITHUB_WORKSPACE}/.shared-workflows/scripts/validate_java_version_consistency.py"
82+
4683
- name: Check code format and lint
4784
run: ./gradlew spotlessCheck
4885
env:
4986
GITHUB_ACTOR: ${{ github.actor }}
5087
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
5188

52-
- name: Run tests
89+
- name: Run unit tests inside Docker
90+
if: ${{ inputs.runTestsInsideDocker }}
5391
env:
54-
GITHUB_ACTOR: ${{ github.actor }}
5592
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
93+
GITHUB_ACTOR_ARG: ${{ github.actor }}
94+
DOCKER_BUILDKIT: "1"
5695
run: |
57-
./gradlew test jacocoTestReport --stacktrace
96+
cat > /tmp/Dockerfile.test << DOCKERFILE
97+
# syntax=docker/dockerfile:1
98+
# check=error=true
99+
FROM ${TEST_BASE_IMAGE}
100+
WORKDIR /usr/app
101+
ARG GITHUB_ACTOR=github-actions
102+
COPY . .
103+
RUN --mount=type=secret,id=github_token \
104+
export GITHUB_TOKEN="\$(cat /run/secrets/github_token)" && \
105+
export GITHUB_ACTOR="\$GITHUB_ACTOR" && \
106+
./gradlew test --stacktrace --no-daemon
107+
DOCKERFILE
108+
docker build \
109+
--secret id=github_token,env=GITHUB_TOKEN \
110+
--build-arg "GITHUB_ACTOR=${GITHUB_ACTOR_ARG}" \
111+
-f /tmp/Dockerfile.test \
112+
.
113+
114+
- name: Run unit tests
115+
if: ${{ inputs.hasIntegrationTests == false }}
116+
env:
117+
GITHUB_ACTOR: ${{ github.actor }}
118+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
119+
run: ./gradlew test jacocoTestReport --stacktrace
120+
121+
- name: Run unit tests and integration tests
122+
if: ${{ inputs.hasIntegrationTests }}
123+
env:
124+
GITHUB_ACTOR: ${{ github.actor }}
125+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
126+
run: ./gradlew test integrationTest jacocoTestReport --stacktrace
58127

59128
- name: Upload coverage reports to Codecov
60129
uses: codecov/codecov-action@v5

scripts/test_validate_java_version_consistency.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
extract_java_version_from_docker_tag,
55
normalize_java_version,
66
parse_docker_java_version,
7+
parse_docker_jdk_image,
78
parse_gradle_java_version,
89
resolve_args,
910
)
@@ -219,6 +220,84 @@ def test_unrecognizable_java_version_in_tag_raises(self, tmp_path):
219220
parse_docker_java_version(str(dockerfile))
220221

221222

223+
# ---------------------------------------------------------------------------
224+
# parse_docker_jdk_image (uses tmp_path fixture)
225+
# ---------------------------------------------------------------------------
226+
227+
class TestParseDockerJdkImage:
228+
def test_standard_multistage_returns_first_real_from(self, tmp_path):
229+
dockerfile = tmp_path / "Dockerfile"
230+
dockerfile.write_text(
231+
"FROM hsldevcom/infodevops-docker-base-images:1.0.2-25-java-jdk AS base\n"
232+
"FROM base AS test\n"
233+
"FROM base AS build\n"
234+
"FROM hsldevcom/infodevops-docker-base-images:1.0.2-25-java-jre\n"
235+
)
236+
image = parse_docker_jdk_image(str(dockerfile))
237+
assert image == "hsldevcom/infodevops-docker-base-images:1.0.2-25-java-jdk"
238+
239+
def test_arg_substitution(self, tmp_path):
240+
dockerfile = tmp_path / "Dockerfile"
241+
dockerfile.write_text(
242+
"ARG BASE_TAG=1.0.2-25-java-jdk\n"
243+
"FROM hsldevcom/infodevops-docker-base-images:${BASE_TAG} AS base\n"
244+
"FROM hsldevcom/infodevops-docker-base-images:1.0.2-25-java-jre\n"
245+
)
246+
image = parse_docker_jdk_image(str(dockerfile))
247+
assert "1.0.2-25-java-jdk" in image
248+
249+
def test_single_stage_returns_only_image(self, tmp_path):
250+
dockerfile = tmp_path / "Dockerfile"
251+
dockerfile.write_text(
252+
"FROM hsldevcom/infodevops-docker-base-images:1.0.2-25-java-jdk\n"
253+
)
254+
image = parse_docker_jdk_image(str(dockerfile))
255+
assert image == "hsldevcom/infodevops-docker-base-images:1.0.2-25-java-jdk"
256+
257+
def test_skips_scratch(self, tmp_path):
258+
dockerfile = tmp_path / "Dockerfile"
259+
dockerfile.write_text(
260+
"FROM scratch\n"
261+
"FROM hsldevcom/infodevops-docker-base-images:1.0.2-25-java-jdk AS base\n"
262+
)
263+
image = parse_docker_jdk_image(str(dockerfile))
264+
assert "java-jdk" in image
265+
266+
def test_skips_stage_aliases(self, tmp_path):
267+
dockerfile = tmp_path / "Dockerfile"
268+
dockerfile.write_text(
269+
"FROM hsldevcom/infodevops-docker-base-images:1.0.2-25-java-jdk AS base\n"
270+
"FROM base AS test\n"
271+
)
272+
image = parse_docker_jdk_image(str(dockerfile))
273+
assert image == "hsldevcom/infodevops-docker-base-images:1.0.2-25-java-jdk"
274+
275+
def test_comments_are_ignored(self, tmp_path):
276+
dockerfile = tmp_path / "Dockerfile"
277+
dockerfile.write_text(
278+
"# syntax=docker/dockerfile:1\n"
279+
"# check=error=true\n"
280+
"FROM hsldevcom/infodevops-docker-base-images:1.0.2-25-java-jdk AS base\n"
281+
)
282+
image = parse_docker_jdk_image(str(dockerfile))
283+
assert "java-jdk" in image
284+
285+
def test_no_real_from_raises(self, tmp_path):
286+
dockerfile = tmp_path / "Dockerfile"
287+
dockerfile.write_text("FROM scratch\n")
288+
with pytest.raises(RuntimeError, match="Could not find a JDK base image"):
289+
parse_docker_jdk_image(str(dockerfile))
290+
291+
def test_only_aliases_raises(self, tmp_path):
292+
dockerfile = tmp_path / "Dockerfile"
293+
dockerfile.write_text(
294+
"FROM base AS test\n"
295+
"FROM build AS final\n"
296+
)
297+
with pytest.raises(RuntimeError, match="Could not find a JDK base image"):
298+
parse_docker_jdk_image(str(dockerfile))
299+
300+
222301
# ---------------------------------------------------------------------------
223302
# parse_gradle_java_version (uses tmp_path fixture)
224303
# ---------------------------------------------------------------------------

scripts/validate_java_version_consistency.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,39 @@ def replace(match):
5050
return re.sub(r'\$\{([^}]+)\}|\$(\w+)', replace, value)
5151

5252

53+
def _is_stage_alias(image):
54+
"""Return True if the image string is a multi-stage alias (e.g. 'base', 'build'), not a registry reference."""
55+
return ':' not in image and '/' not in image
56+
57+
58+
def parse_docker_jdk_image(dockerfile_path):
59+
"""Return the image ref of the first real (non-scratch, non-alias) FROM stage.
60+
61+
In the standard 4-stage Java Dockerfile pattern the first real FROM is the JDK
62+
build/test image, e.g. hsldevcom/infodevops-docker-base-images:1.0.2-25-java-jdk.
63+
"""
64+
args = {}
65+
66+
with open(dockerfile_path, encoding='utf-8') as dockerfile:
67+
for raw_line in dockerfile:
68+
line = raw_line.split('#', 1)[0].strip()
69+
if not line:
70+
continue
71+
72+
arg_match = re.match(r'^ARG\s+([A-Za-z_][A-Za-z0-9_]*)=(.+)$', line, re.IGNORECASE)
73+
if arg_match:
74+
args[arg_match.group(1)] = arg_match.group(2).strip()
75+
continue
76+
77+
from_match = re.match(r'^FROM\s+([^\s]+)', line, re.IGNORECASE)
78+
if from_match:
79+
image = resolve_args(from_match.group(1).strip(), args)
80+
if image.lower() != 'scratch' and not _is_stage_alias(image):
81+
return image
82+
83+
raise RuntimeError('Could not find a JDK base image in Dockerfile.')
84+
85+
5386
def parse_docker_java_version(dockerfile_path):
5487
args = {}
5588
images = []
@@ -158,11 +191,24 @@ def parse_gradle_java_version(gradle_path):
158191
)
159192

160193

194+
def _write_github_file(env_var_name, content):
195+
"""Append key=value to a GitHub Actions environment file if the path is set."""
196+
path = os.getenv(env_var_name)
197+
if path:
198+
with open(path, 'a') as f:
199+
f.write(f'{content}\n')
200+
201+
161202
def main():
162203
dockerfile_path = os.path.join(os.getcwd(), 'Dockerfile')
163204
if not os.path.exists(dockerfile_path):
164205
raise RuntimeError(f'Dockerfile not found at {dockerfile_path}.')
165206

207+
jdk_image = parse_docker_jdk_image(dockerfile_path)
208+
print(f'JDK base image: {jdk_image}')
209+
_write_github_file('GITHUB_OUTPUT', f'TEST_BASE_IMAGE={jdk_image}')
210+
_write_github_file('GITHUB_ENV', f'TEST_BASE_IMAGE={jdk_image}')
211+
166212
docker_version, docker_image = parse_docker_java_version(dockerfile_path)
167213

168214
pom_path = os.path.join(os.getcwd(), 'pom.xml')

0 commit comments

Comments
 (0)