Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add OpenAPI spec validation action and configure Spring profiles #31

Merged
merged 10 commits into from
Nov 19, 2024
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
166 changes: 166 additions & 0 deletions .github/workflows/validate-openapi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
name: Validate OpenAPI Spec and Generated Client Code

on:
pull_request:
branches: [main]
types: [opened, reopened, edited, synchronize]

jobs:
validate-openapi:
name: Validate OpenAPI Spec
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
env:
# Check whether server changes, openapi.yaml changes, or client openapi folder changes
PATHS_TO_CHECK: |
server/application-server/src/main/java/
server/application-server/openapi.yaml
client/src/app/core/modules/openapi/
outputs:
CHANGE_DETECTED: ${{ steps.check_changes.outputs.CHANGE_DETECTED }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Fetch all branches
run: |
git fetch origin ${{ github.base_ref }}:refs/remotes/origin/${{ github.base_ref }}
git fetch origin ${{ github.head_ref }}:refs/remotes/origin/${{ github.head_ref }}

- name: Check for changes in specified paths
id: check_changes
run: |
CHANGED_PATHS=$(git diff --name-only origin/${{ github.base_ref }} origin/${{ github.head_ref }} | grep -E "^($(echo "$PATHS_TO_CHECK" | tr '\n' '|'))")

if [[ -z "$CHANGED_PATHS" ]]; then
echo "CHANGE_DETECTED=true" >> "$GITHUB_OUTPUT"
echo "No OpenAPI changes detected."
else
echo "CHANGE_DETECTED=false" >> "$GITHUB_OUTPUT"
echo "OpenAPI changes detected in the following paths:"
echo "$CHANGED_PATHS"
fi

- name: Set up Java
if: steps.check_changes.outputs.CHANGE_DETECTED == 'true'
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'

- name: Run Gradle to generate OpenAPI docs
if: steps.check_changes.outputs.CHANGE_DETECTED == 'true'
working-directory: ./server/application-server
run: ./gradlew generateOpenApiDocs

- name: Check for OpenAPI spec differences
if: steps.check_changes.outputs.CHANGE_DETECTED == 'true'
id: check_openapi_spec
run: |
if git diff --exit-code ./server/application-server/openapi.yml; then
echo "OpenAPI spec is up-to-date."
else
echo "OpenAPI specs in openapi.yml differ from the generated version."
exit 1
fi

- name: Post comment about OpenAPI validation failure
if: failure() && steps.check_changes.outputs.CHANGE_DETECTED == 'true'
uses: marocchino/sticky-pull-request-comment@v2
with:
header: openapi-validation-yml
message: |
🚨 **OpenAPI Validation Failed** 🚨

The OpenAPI specs in `openapi.yml` differ from the generated version.
Please update the OpenAPI specs by running:
```bash
cd ./server/application-server
./gradlew generateOpenApiDocs
```
Commit and push the updated file.

- name: Remove sticky comment on OpenAPI validation success
if: success() || steps.check_changes.outputs.CHANGE_DETECTED == 'false'
uses: marocchino/sticky-pull-request-comment@v2
with:
header: openapi-validation-yml
delete: true

validate-client-code:
name: Validate Client Code
runs-on: ubuntu-latest
needs: validate-openapi
permissions:
contents: read
pull-requests: write
env:
CHANGE_DETECTED: ${{needs.validate-openapi.outputs.CHANGE_DETECTED}}
steps:
- name: Checkout code
if: env.CHANGE_DETECTED == 'true'
uses: actions/checkout@v4

- name: Set up Java
if: env.CHANGE_DETECTED == 'true'
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'

- name: Set up Node.js
if: env.CHANGE_DETECTED == 'true'
uses: actions/setup-node@v4
with:
node-version: '22'

- name: Install OpenAPI Generator CLI
if: env.CHANGE_DETECTED == 'true'
run: npm install -g @openapitools/openapi-generator-cli

- name: Generate client code
if: env.CHANGE_DETECTED == 'true'
run: |
npx openapi-generator-cli generate \
-i ./server/application-server/openapi.yaml \
-g typescript-angular \
-o ./client/src/app/core/modules/openapi \
--additional-properties fileNaming=kebab-case,withInterfaces=true --generate-alias-as-model

- name: Check for client code differences
if: env.CHANGE_DETECTED == 'true'
id: check_client_code
run: |
if git diff --exit-code ./client/src/app/core/modules/openapi; then
echo "Client code is up-to-date."
else
echo "Client code in /client/src/app/core/modules/openapi is not up-to-date."
exit 1

- name: Post comment about client code validation failure
if: failure() && env.CHANGE_DETECTED == 'true'
uses: marocchino/sticky-pull-request-comment@v2
with:
header: client-code-validation
message: |
🚨 **Client Code Validation Failed** 🚨

The client code in `/client/src/app/core/modules/openapi` is not up-to-date.
Please regenerate the client code by running:
```bash
npm install -g @openapitools/openapi-generator-cli
# In root directory
npx openapi-generator-cli generate -i ./server/application-server/openapi.yaml -g typescript-angular -o ./client/src/app/core/modules/openapi --additional-properties fileNaming=kebab-case,withInterfaces=true --generate-alias-as-model
```
Commit and push the updated files.

- name: Remove sticky comment on client code validation success
if: success() || env.CHANGE_DETECTED == 'false'
uses: marocchino/sticky-pull-request-comment@v2
with:
header: client-code-validation
delete: true
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,13 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/

# Node
/node_modules
npm-debug.log
yarn-error.log

.sdkmanrc

CMakeLists.txt.user
CMakeCache.txt
CMakeFiles
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
.gitignore
.openapi-generator-ignore
README.md
api.module.ts
api/api.ts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,18 +95,18 @@ export class StatusControllerService implements StatusControllerServiceInterface
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress.
*/
public healthCheck(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: '*/*', context?: HttpContext, transferCache?: boolean}): Observable<string>;
public healthCheck(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: '*/*', context?: HttpContext, transferCache?: boolean}): Observable<HttpResponse<string>>;
public healthCheck(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: '*/*', context?: HttpContext, transferCache?: boolean}): Observable<HttpEvent<string>>;
public healthCheck(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: '*/*', context?: HttpContext, transferCache?: boolean}): Observable<any> {
public healthCheck(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<string>;
public healthCheck(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<HttpResponse<string>>;
public healthCheck(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<HttpEvent<string>>;
public healthCheck(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<any> {

let localVarHeaders = this.defaultHeaders;

let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept;
if (localVarHttpHeaderAcceptSelected === undefined) {
// to determine the Accept header
const httpHeaderAccepts: string[] = [
'*/*'
'application/json'
];
localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts);
}
Expand Down
7 changes: 7 additions & 0 deletions openapitools.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "7.9.0"
}
}
6 changes: 3 additions & 3 deletions server/application-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Helios Application Server is a [Spring Boot application](https://spring.io/proje

## Prerequisites

- [Java 22](https://www.oracle.com/java/technologies/downloads/) (or higher)
- [Java 21](https://www.oracle.com/java/technologies/downloads/) (or higher)
- [Postgres 16](https://www.postgresql.org/download/) (is automatically started with the provided Docker Compose setup)

## Getting Started
Expand Down Expand Up @@ -34,10 +34,10 @@ Copy the file `.env.example` to `.env` and adjust the values to your needs. It i
$ cp .env.example .env
```

**4. Run the Application**
**4. Run the Application in Development Mode**

```bash
$ ./gradlew bootRun
$ ./gradlew bootRunDev
```

The application will be accessible at [http://localhost:8080/status/health](http://localhost:8080/status/health)
Expand Down
93 changes: 67 additions & 26 deletions server/application-server/build.gradle
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import org.springframework.boot.gradle.tasks.run.BootRun

plugins {
id 'java'
id 'war'
Expand All @@ -21,32 +23,17 @@ repositories {
mavenCentral()
}

def loadEnvFile() {
def env = [:]
def envFile = file('.env')
if (envFile.exists()) {
envFile.eachLine { line ->
line = line.trim()
if (line && !line.startsWith('#')) {
def keyValue = line.split('=', 2)
if (keyValue.length == 2) {
def key = keyValue[0].trim()
def value = keyValue[1].trim()
// Remove surrounding quotes if present
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
value = value.substring(1, value.length() - 1)
}
env[key] = value
}
}
// Detect if the generateOpenApiDocs task is being executed
gradle.taskGraph.whenReady { taskGraph ->
if (taskGraph.hasTask(':generateOpenApiDocs')) {
println "Generating OpenAPI documentation..."
// Add the H2 dependency dynamically
dependencies {
runtimeOnly 'com.h2database:h2'
}
} else {
println ".env file not found in project root."
}
return env
}


dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
Expand All @@ -62,17 +49,71 @@ dependencies {
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
useJUnitPlatform()
springBoot {
mainClass = "de.tum.cit.aet.helios.HeliosApplication"
}

// Custom bootRun task to force the user to specify an active profile
tasks.named("bootRun") {
doFirst {
def activeProfile = System.getProperty("spring.profiles.active")
?: project.findProperty("spring.profiles.active")
?: System.getenv("SPRING_PROFILES_ACTIVE")
if (!activeProfile) {
throw new GradleException("""
Error: 'spring.profiles.active' is not set.

Please specify an active profile using one of the following methods:

- Use a predefined task like 'bootRunDev'.
- Set the 'SPRING_PROFILES_ACTIVE' environment variable.
- Use the '-Dspring.profiles.active=<profile>' JVM argument.

Available profiles: dev, openapi (Don't use this profile directly).

Examples:

1. Using predefined task:
./gradlew bootRunDev

2. Setting environment variable:
export SPRING_PROFILES_ACTIVE=dev
./gradlew bootRun

3. Passing JVM argument:
./gradlew -Dspring.profiles.active=dev bootRun

4. Using project property:
./gradlew bootRun -Pspring.profiles.active=dev
"""
)
} else {
println "Running with profile: ${activeProfile}"
systemProperty "spring.profiles.active", activeProfile
}
}
}

tasks.register("bootRunDev", BootRun) {
group = "application"
description = "Runs the Spring Boot application with the dev profile"
// Set the active profile to 'dev'
systemProperty "spring.profiles.active", "dev"
classpath = sourceSets.main.runtimeClasspath
mainClass.set("de.tum.cit.aet.helios.HeliosApplication")
}


openApi {
apiDocsUrl = 'http://localhost:8080/v3/api-docs.yaml'
outputDir = file('.')
outputFileName = 'openapi.yaml'
def envVars = loadEnvFile()
// Set the active profile to 'openapi'
customBootRun {
args.set(["--spring.profiles.active=dev", "--DATASOURCE_URL=${envVars['DATASOURCE_URL'] ?: ''}", "--DATASOURCE_USERNAME=${envVars['DATASOURCE_USERNAME'] ?: ''}", "--DATASOURCE_PASSWORD=${envVars['DATASOURCE_PASSWORD'] ?: ''}"])
args.set(["--spring.profiles.active=openapi"])
}
}

tasks.named('test') {
useJUnitPlatform()
}
2 changes: 1 addition & 1 deletion server/application-server/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ paths:
"200":
description: OK
content:
'*/*':
application/json:
schema:
type: string
components: {}
7 changes: 7 additions & 0 deletions server/application-server/openapitools.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "7.9.0"
}
}
Loading