Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# The build context is now the repository root - every service image is built from here, with
# `dockerfile:` naming the module (see docker-compose.yml). Keep the context to source: anything
# else is either useless in an image or a leak.
.git/
.github/
.idea/
*.iml

# Build output, from every module.
target/
*/target/
.mvn/wrapper/maven-wrapper.jar

# The frontend is not in any service image; node_modules alone would dwarf the context.
frontend/

# Docs and local state.
docs/
*.md
*.log
.env
.env.*
data/
52 changes: 22 additions & 30 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,12 @@ env:

jobs:
java:
name: ${{ matrix.name }}
name: Java services
runs-on: ubuntu-latest
# The test report is published as a check run, which the read-only default token cannot create.
permissions:
contents: read
checks: write
strategy:
# One service failing should not hide the state of the other two.
fail-fast: false
matrix:
include:
- name: Library backend
path: Library-Management-System-Version-2
- name: Notification-Service
path: Notification-Service/Notification-Service
- name: Analytics-Service
path: Analytics-Service

steps:
- uses: actions/checkout@v4
Expand All @@ -50,42 +39,44 @@ jobs:
distribution: temurin
cache: maven

# The wrappers are committed from Windows, which does not carry the executable bit, so a
# Linux runner would fail with "Permission denied" before Maven ever starts.
# The wrapper is committed from Windows, which does not carry the executable bit, so a Linux
# runner would fail with "Permission denied" before Maven ever starts.
- name: Make the Maven wrapper executable
working-directory: ${{ matrix.path }}
run: chmod +x ./mvnw

# Checkstyle is bound to `validate`, so it would run inside `verify` anyway - but as its own
# step a style failure is legible at a glance instead of buried in test output.
- name: Checkstyle
working-directory: ${{ matrix.path }}
run: ./mvnw -B --no-transfer-progress checkstyle:check

# `verify` runs unit tests (surefire), integration tests (failsafe) and PMD.
# One reactor build now covers all three services: `verify` runs unit tests (surefire),
# integration tests (failsafe) and PMD in every module, in dependency order.
#
# --fail-at-end keeps what the old three-way matrix gave us for free: the reactor finishes
# every module it still can rather than stopping at the first failure, so one red service
# does not hide the state of the other two.
- name: Build and test
working-directory: ${{ matrix.path }}
run: ./mvnw -B --no-transfer-progress verify
run: ./mvnw -B --no-transfer-progress --fail-at-end verify

- name: Publish test report
# Reports are most wanted exactly when the previous step failed.
if: always()
uses: mikepenz/action-junit-report@v5
with:
report_paths: ${{ matrix.path }}/target/*-reports/TEST-*.xml
check_name: Tests - ${{ matrix.name }}
report_paths: '**/target/*-reports/TEST-*.xml'
check_name: Tests - Java services
fail_on_failure: true

- name: Upload reports on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: reports-${{ strategy.job-index }}
name: java-reports
path: |
${{ matrix.path }}/target/surefire-reports/
${{ matrix.path }}/target/failsafe-reports/
${{ matrix.path }}/target/pmd.xml
${{ matrix.path }}/target/checkstyle-result.xml
**/target/surefire-reports/
**/target/failsafe-reports/
**/target/pmd.xml
**/target/checkstyle-result.xml
retention-days: 7
if-no-files-found: ignore

Expand Down Expand Up @@ -148,11 +139,12 @@ jobs:

# The dev profile seeds from the bundled JSON fixture; the default profile would reach out to
# Open Library, which makes the suite depend on someone else's uptime.
# Run from the repository root and select the module: the wrapper lives at the root now, and
# the backend POM inherits from the root POM, so it cannot be built from its own directory.
- name: Start the backend
working-directory: Library-Management-System-Version-2
run: |
chmod +x ./mvnw
./mvnw -B --no-transfer-progress -DskipTests \
./mvnw -B --no-transfer-progress -pl Library-Management-System-Version-2 -DskipTests \
-Dcheckstyle.skip=true -Dpmd.skip=true \
spring-boot:run -Dspring-boot.run.profiles=dev > backend.log 2>&1 &
echo "started"
Expand All @@ -167,7 +159,7 @@ jobs:
sleep 1
done
echo "backend did not start within 60s"
tail -50 Library-Management-System-Version-2/backend.log
tail -50 backend.log
exit 1

- name: Run Playwright
Expand All @@ -182,7 +174,7 @@ jobs:
path: |
frontend/playwright-report/
frontend/test-results/
Library-Management-System-Version-2/backend.log
backend.log
retention-days: 7
if-no-files-found: ignore

Expand Down
9 changes: 0 additions & 9 deletions Analytics-Service/.dockerignore

This file was deleted.

19 changes: 14 additions & 5 deletions Analytics-Service/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
# ---- build ------------------------------------------------------------------------------------
# The JDK, Maven and the source tree stay in this stage; none of it reaches the image that runs.
#
# The build context is the repository root, not this directory - see docker-compose.yml. Since the
# parent POM arrived, a module cannot be built on its own: it inherits from the root POM, and Maven
# loads every module named in <modules> before pruning the reactor to the one being built. So all
# four POMs are copied, and only this module's sources.
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /build

# Dependencies first, so a source-only change does not re-download the world.
# POMs first, so a source-only change does not re-download the world.
COPY pom.xml .
RUN mvn -B -q dependency:go-offline
COPY Library-Management-System-Version-2/pom.xml Library-Management-System-Version-2/
COPY Notification-Service/pom.xml Notification-Service/
COPY Analytics-Service/pom.xml Analytics-Service/
RUN mvn -B -q -pl Analytics-Service -am dependency:go-offline

COPY src ./src
COPY Analytics-Service/src ./Analytics-Service/src
# Checkstyle, PMD and the tests run in CI; repeating them here only makes images slow to build.
RUN mvn -B -q clean package -DskipTests -Dcheckstyle.skip=true -Dpmd.skip=true \
&& mv target/*.jar /build/app.jar
RUN mvn -B -q -pl Analytics-Service -am clean package \
-DskipTests -Dcheckstyle.skip=true -Dpmd.skip=true \
&& mv Analytics-Service/target/*.jar /build/app.jar

# ---- run --------------------------------------------------------------------------------------
FROM eclipse-temurin:21-jre-alpine AS runtime
Expand Down
6 changes: 4 additions & 2 deletions Analytics-Service/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ components — see the [root README](../README.md) for the whole picture.
Needs a Kafka broker on **localhost:9094**.

```bash
./mvnw spring-boot:run # http://localhost:9095
cd .. # the repository root
./mvnw -pl Analytics-Service spring-boot:run # http://localhost:9095
```

Without a broker the service still starts and simply never sees an event
Expand Down Expand Up @@ -78,5 +79,6 @@ rebuilt from the topic, so nothing here is a source of truth. The consumer reads
## Testing

```bash
./mvnw test
cd .. # the repository root
./mvnw -pl Analytics-Service test
```
104 changes: 16 additions & 88 deletions Analytics-Service/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,22 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<!--
The repository root, which in turn has spring-boot-starter-parent as its own parent. The Java
release, the plugin versions, and the Checkstyle, PMD, Lombok and surefire wiring all live
there; groupId and version are inherited, so only the artifactId is set here.
-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.4</version>
<relativePath/>
<groupId>app</groupId>
<artifactId>library-management-system</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<groupId>spring-boot</groupId>

<artifactId>Analytics-Service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Analytics-Service</name>
<description>Borrowing statistics, fed by loan events from Kafka</description>
<properties>
<java.version>21</java.version>
</properties>

<dependencies>
<dependency>
Expand Down Expand Up @@ -51,97 +53,23 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- For the container healthcheck. Only /actuator/health is exposed; see the properties. -->

<!-- For the container healthcheck. Only /actuator/health is exposed; see the properties. -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
</dependencies>

<build>
<plugins>
<!--
Checkstyle and PMD, sharing the rulesets in the repository root so all three
services are held to the same standard from one file. Runs at validate/verify and
fails the build; skip with -Dcheckstyle.skip=true / -Dpmd.skip=true.
Configured in the parent's pluginManagement and only switched on here. The compiler,
surefire, Checkstyle and PMD plugins reach this module from the parent on their own.
-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.6.0</version>
<dependencies>
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>10.21.1</version>
</dependency>
</dependencies>
<configuration>
<configLocation>${maven.multiModuleProjectDirectory}/../config/checkstyle/checkstyle.xml</configLocation>
<consoleOutput>true</consoleOutput>
<failOnViolation>true</failOnViolation>
<violationSeverity>error</violationSeverity>
<includeTestSourceDirectory>false</includeTestSourceDirectory>
</configuration>
<executions>
<execution>
<id>checkstyle</id>
<phase>validate</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-pmd-plugin</artifactId>
<version>3.26.0</version>
<configuration>
<rulesets>
<ruleset>${maven.multiModuleProjectDirectory}/../config/pmd/ruleset.xml</ruleset>
</rulesets>
<printFailingErrors>true</printFailingErrors>
<failOnViolation>true</failOnViolation>
<includeTests>false</includeTests>
<targetJdk>${java.version}</targetJdk>
</configuration>
<executions>
<execution>
<id>pmd</id>
<phase>verify</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>


<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/** Boots Analytics-Service. */
@SpringBootApplication
public class Application {

/** Starts the service. */
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import springboot.analytics.health.StreamHealth;
import springboot.analytics.service.LoanStatisticsService;

/** The service's only inbound path: it is fed by the topic, never called over HTTP to be told things. */
/** The service's only inbound path: it is fed by the topic, never told anything over HTTP. */
@Component
@RequiredArgsConstructor
@Slf4j
Expand All @@ -19,6 +19,7 @@ public class LoanEventListener {
@KafkaListener(
topics = "${library.events.topic:library.loans}",
groupId = "${spring.kafka.consumer.group-id:analytics-service}")
/** Records one loan event: marks the stream alive, then folds it into the totals. */
public void onLoanEvent(LoanEvent event) {
log.info("Received {} for '{}'", event.type(), event.bookTitle());
streamHealth.recordEvent();
Expand Down
Loading
Loading