Skip to content

Commit f5dbed1

Browse files
committed
feat: initial scaffold for cq-demo-app-004
0 parents  commit f5dbed1

17 files changed

Lines changed: 1919 additions & 0 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
name: Deploy
2+
3+
on:
4+
push:
5+
branches: [main]
6+
workflow_dispatch:
7+
8+
permissions:
9+
id-token: write
10+
contents: read
11+
12+
env:
13+
AZURE_RG: rg-cq-demo-004
14+
APP_NAME: cq-demo-app-004
15+
LOCATION: canadacentral
16+
17+
jobs:
18+
deploy:
19+
runs-on: ubuntu-latest
20+
environment: prod
21+
steps:
22+
- uses: actions/checkout@v4
23+
24+
- name: Azure Login
25+
uses: azure/login@v2
26+
with:
27+
client-id: ${{ secrets.AZURE_CLIENT_ID }}
28+
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
29+
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
30+
31+
- name: Create Resource Group
32+
run: az group create --name ${{ env.AZURE_RG }} --location ${{ env.LOCATION }}
33+
34+
- name: Deploy Infrastructure
35+
id: infra
36+
run: |
37+
outputs=$(az deployment group create \
38+
--resource-group ${{ env.AZURE_RG }} \
39+
--template-file infra/main.bicep \
40+
--parameters appName=${{ env.APP_NAME }} \
41+
--query 'properties.outputs' -o json)
42+
echo "acrName=$(echo $outputs | jq -r '.acrName.value')" >> $GITHUB_OUTPUT
43+
echo "appServiceName=$(echo $outputs | jq -r '.appServiceName.value')" >> $GITHUB_OUTPUT
44+
45+
- name: Build and Push to ACR
46+
run: |
47+
az acr build \
48+
--registry ${{ steps.infra.outputs.acrName }} \
49+
--image ${{ env.APP_NAME }}:${{ github.sha }} \
50+
--image ${{ env.APP_NAME }}:latest \
51+
.
52+
53+
- name: Deploy to Web App for Containers
54+
uses: azure/webapps-deploy@v3
55+
with:
56+
app-name: ${{ steps.infra.outputs.appServiceName }}
57+
images: ${{ steps.infra.outputs.acrName }}.azurecr.io/${{ env.APP_NAME }}:${{ github.sha }}
58+
59+
- name: Health Check
60+
run: |
61+
APP_URL=$(az webapp show -g ${{ env.AZURE_RG }} -n ${{ steps.infra.outputs.appServiceName }} --query defaultHostName -o tsv)
62+
sleep 30
63+
curl -sf --retry 5 --retry-delay 10 "https://$APP_URL/actuator/health" || exit 1
64+
65+
- name: Summary
66+
run: |
67+
APP_URL=$(az webapp show -g ${{ env.AZURE_RG }} -n ${{ steps.infra.outputs.appServiceName }} --query defaultHostName -o tsv)
68+
echo "### ✅ ${{ env.APP_NAME }}" >> $GITHUB_STEP_SUMMARY
69+
echo "Deployed to: https://$APP_URL" >> $GITHUB_STEP_SUMMARY

Dockerfile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
FROM maven:3.9-eclipse-temurin-21 AS build
2+
WORKDIR /app
3+
COPY pom.xml .
4+
RUN mvn dependency:go-offline
5+
COPY src/ ./src/
6+
RUN mvn package -DskipTests
7+
8+
FROM eclipse-temurin:21-jre-alpine
9+
WORKDIR /app
10+
COPY --from=build /app/target/*.jar app.jar
11+
EXPOSE 8080
12+
ENTRYPOINT ["java", "-jar", "app.jar"]

README.md

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# cq-demo-app-004 — Student Enrollment System (Java/Spring Boot)
2+
3+
Code Quality demo application #4 — a **Student Enrollment Management System** built with Java 21 and Spring Boot 3.3. This app contains **intentional code quality violations** for scanner demonstration purposes.
4+
5+
## Intentional Violations
6+
7+
| Category | Count | Location |
8+
|----------|-------|----------|
9+
| **High Cyclomatic Complexity** | 3 | `StudentService.processEnrollment()` (CCN > 15), `StudentController.enrollStudent()`, `GradeCalculator.calculateGPA()` |
10+
| **Code Duplication** | 4 | Validation logic duplicated across `StudentService`, `CourseService`, `StudentController`, `CourseController` |
11+
| **Missing Javadoc** | 10+ | All public classes and methods across `controller/`, `service/`, `util/` |
12+
| **Magic Numbers** | 15+ | `GradeCalculator` (0.3, 0.7, 65, 70, 80, 90, 100), `StudentService` (2.0, 3.5, 21) |
13+
| **Long Methods** | 3 | `processEnrollment()` > 100 lines, `calculateClassStatistics()` > 50 lines, `calculateGPA()` > 50 lines |
14+
| **System.out.println** | 12+ | Throughout all classes instead of SLF4J logger |
15+
| **Raw Types** | 2 | `ReportFormatter.formatStudentReport(List)`, `formatCourseReport(List)` |
16+
| **String Concatenation in Loops** | 5 | `ReportFormatter` — all formatting methods |
17+
| **Unused Private Methods** | 3 | `ReportFormatter.padRight()`, `padLeft()`, `repeatChar()` |
18+
| **Low Test Coverage** || Only `contextLoads()` test — < 10% coverage |
19+
20+
## API Endpoints
21+
22+
### Students
23+
| Method | Path | Description |
24+
|--------|------|-------------|
25+
| GET | `/api/students` | List all students |
26+
| GET | `/api/students/{id}` | Get student by ID |
27+
| GET | `/api/students/{id}/summary` | Get student summary |
28+
| POST | `/api/students` | Create a student |
29+
| PUT | `/api/students/{id}` | Update a student |
30+
| DELETE | `/api/students/{id}` | Delete a student |
31+
| POST | `/api/students/{id}/enroll` | Enroll in a course |
32+
| GET | `/api/students/{id}/validate` | Validate enrollment eligibility |
33+
34+
### Courses
35+
| Method | Path | Description |
36+
|--------|------|-------------|
37+
| GET | `/api/courses` | List all courses |
38+
| GET | `/api/courses/{id}` | Get course by ID |
39+
| GET | `/api/courses/{id}/summary` | Get course summary |
40+
| GET | `/api/courses/code/{code}` | Get course by code |
41+
| POST | `/api/courses` | Create a course |
42+
| PUT | `/api/courses/{id}` | Update a course |
43+
| DELETE | `/api/courses/{id}` | Delete a course |
44+
| POST | `/api/courses/{id}/validate-enrollment` | Validate course enrollment |
45+
| GET | `/api/courses/{id}/schedule-conflicts` | Check schedule conflicts |
46+
47+
### Health
48+
| Method | Path | Description |
49+
|--------|------|-------------|
50+
| GET | `/actuator/health` | Spring Actuator health check |
51+
52+
## Tech Stack
53+
54+
- **Java 21** with **Spring Boot 3.3**
55+
- **Maven** build system
56+
- **JaCoCo** for coverage reporting
57+
- **Checkstyle** (Google style) for lint analysis
58+
- **Docker** multi-stage build
59+
60+
## Run Locally
61+
62+
Build and run with Docker (works in GitHub Codespaces):
63+
64+
```bash
65+
docker build -t cq-demo-app-004 .
66+
docker run -p 8080:8080 cq-demo-app-004
67+
```
68+
69+
Then browse to [http://localhost:8080/api/students](http://localhost:8080/api/students).
70+
71+
### Run with Maven (requires Java 21)
72+
73+
```bash
74+
mvn spring-boot:run
75+
```
76+
77+
### Run Tests
78+
79+
```bash
80+
mvn test
81+
```
82+
83+
### Generate Coverage Report
84+
85+
```bash
86+
mvn test jacoco:report
87+
```
88+
89+
Coverage report is generated at `target/site/jacoco/index.html`.
90+
91+
### Run Checkstyle
92+
93+
```bash
94+
mvn checkstyle:check
95+
```
96+
97+
## Azure Deployment
98+
99+
This app deploys as a Docker container to Azure Web App for Containers via the GitHub Actions workflow in `.github/workflows/deploy.yml`.
100+
101+
Infrastructure is defined in `infra/main.bicep` and provisions:
102+
- Azure Container Registry (ACR) with globally unique name
103+
- App Service Plan (Linux, B1)
104+
- Web App for Containers
105+
106+
All resource names use `uniqueString(resourceGroup().id)` for global uniqueness across multiple workshop participants.

infra/main.bicep

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
@description('Logical app name used for resource naming')
2+
param appName string = 'cq-demo-app-004'
3+
4+
@description('Azure region for all resources')
5+
param location string = resourceGroup().location
6+
7+
var suffix = uniqueString(resourceGroup().id)
8+
var acrName = 'acr${suffix}'
9+
var appServicePlanName = 'plan-${appName}-${suffix}'
10+
var appServiceName = '${appName}-${suffix}'
11+
12+
resource acr 'Microsoft.ContainerRegistry/registries@2023-07-01' = {
13+
name: acrName
14+
location: location
15+
sku: {
16+
name: 'Basic'
17+
}
18+
properties: {
19+
adminUserEnabled: true
20+
}
21+
}
22+
23+
resource appServicePlan 'Microsoft.Web/serverfarms@2023-12-01' = {
24+
name: appServicePlanName
25+
location: location
26+
kind: 'linux'
27+
sku: {
28+
name: 'B1'
29+
tier: 'Basic'
30+
}
31+
properties: {
32+
reserved: true
33+
}
34+
}
35+
36+
resource webApp 'Microsoft.Web/sites@2023-12-01' = {
37+
name: appServiceName
38+
location: location
39+
kind: 'app,linux,container'
40+
identity: {
41+
type: 'SystemAssigned'
42+
}
43+
properties: {
44+
serverFarmId: appServicePlan.id
45+
siteConfig: {
46+
linuxFxVersion: 'DOCKER|${acr.properties.loginServer}/${appName}:latest'
47+
appSettings: [
48+
{
49+
name: 'DOCKER_REGISTRY_SERVER_URL'
50+
value: 'https://${acr.properties.loginServer}'
51+
}
52+
{
53+
name: 'DOCKER_REGISTRY_SERVER_USERNAME'
54+
value: acr.listCredentials().username
55+
}
56+
{
57+
name: 'DOCKER_REGISTRY_SERVER_PASSWORD'
58+
value: acr.listCredentials().passwords[0].value
59+
}
60+
{
61+
name: 'WEBSITES_ENABLE_APP_SERVICE_STORAGE'
62+
value: 'false'
63+
}
64+
]
65+
}
66+
}
67+
}
68+
69+
output acrName string = acr.name
70+
output acrLoginServer string = acr.properties.loginServer
71+
output appServiceName string = webApp.name
72+
output appUrl string = 'https://${webApp.properties.defaultHostName}'

pom.xml

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
5+
https://maven.apache.org/xsd/maven-4.0.0.xsd">
6+
<modelVersion>4.0.0</modelVersion>
7+
8+
<parent>
9+
<groupId>org.springframework.boot</groupId>
10+
<artifactId>spring-boot-starter-parent</artifactId>
11+
<version>3.3.5</version>
12+
<relativePath/>
13+
</parent>
14+
15+
<groupId>com.example</groupId>
16+
<artifactId>cq-demo-app-004</artifactId>
17+
<version>1.0.0</version>
18+
<name>cq-demo-app-004</name>
19+
<description>Student Enrollment Management System — Code Quality Demo App 004 (Java/Spring Boot)</description>
20+
21+
<properties>
22+
<java.version>21</java.version>
23+
</properties>
24+
25+
<dependencies>
26+
<dependency>
27+
<groupId>org.springframework.boot</groupId>
28+
<artifactId>spring-boot-starter-web</artifactId>
29+
</dependency>
30+
<dependency>
31+
<groupId>org.springframework.boot</groupId>
32+
<artifactId>spring-boot-starter-actuator</artifactId>
33+
</dependency>
34+
<dependency>
35+
<groupId>org.springframework.boot</groupId>
36+
<artifactId>spring-boot-starter-test</artifactId>
37+
<scope>test</scope>
38+
</dependency>
39+
</dependencies>
40+
41+
<build>
42+
<plugins>
43+
<plugin>
44+
<groupId>org.springframework.boot</groupId>
45+
<artifactId>spring-boot-maven-plugin</artifactId>
46+
</plugin>
47+
<plugin>
48+
<groupId>org.apache.maven.plugins</groupId>
49+
<artifactId>maven-checkstyle-plugin</artifactId>
50+
<version>3.4.0</version>
51+
<configuration>
52+
<configLocation>google_checks.xml</configLocation>
53+
<consoleOutput>true</consoleOutput>
54+
<failsOnError>false</failsOnError>
55+
<linkXRef>false</linkXRef>
56+
</configuration>
57+
</plugin>
58+
<plugin>
59+
<groupId>org.jacoco</groupId>
60+
<artifactId>jacoco-maven-plugin</artifactId>
61+
<version>0.8.12</version>
62+
<executions>
63+
<execution>
64+
<goals>
65+
<goal>prepare-agent</goal>
66+
</goals>
67+
</execution>
68+
<execution>
69+
<id>report</id>
70+
<phase>test</phase>
71+
<goals>
72+
<goal>report</goal>
73+
</goals>
74+
</execution>
75+
</executions>
76+
</plugin>
77+
</plugins>
78+
</build>
79+
</project>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.example.cqdemo;
2+
3+
import org.springframework.boot.SpringApplication;
4+
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
6+
@SpringBootApplication
7+
public class CqDemoApplication {
8+
9+
public static void main(String[] args) {
10+
SpringApplication.run(CqDemoApplication.class, args);
11+
}
12+
}

0 commit comments

Comments
 (0)