Skip to content

Development: Port new gender contracts #4436

Development: Port new gender contracts

Development: Port new gender contracts #4436

Workflow file for this run

name: PR check
on:
pull_request:
paths-ignore:
- 'README.md'
- 'CODE_OF_CONDUCT.md'
- 'CONTRIBUTING.md'
- 'LICENSE'
- 'SECURITY.md'
- '../../docs-github/**'
push:
branches:
- main
tags: '[0-9]+.[0-9]+.[0-9]+'
release:
types:
- created
workflow_dispatch:
concurrency:
group: ci-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
env:
CI: true
NODE_VERSION: 24
JAVA_VERSION: 25
GRADLE_VERSION: '9.1.0'
RUN_ALL_TESTS: ${{ (github.event_name == 'pull_request' && github.event.pull_request.draft == false) || github.event.repository.default_branch == github.ref_name }}
jobs:
# ============================================
# VALIDATION - Schnelle Pre-Checks
# ============================================
validate-pr-title:
name: Validate PR Title
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 1
steps:
- uses: Slashgear/action-check-pr-title@v5.0.1
with:
regexp: '^`(Bugfix|Development|Documentation|Test|General)`:\s[A-Z].*$'
validate-gradle-wrapper:
name: Validate Gradle Wrapper
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- uses: actions/checkout@v6
- uses: gradle/actions/wrapper-validation@v5
with:
min-wrapper-count: 1
# ============================================
# SERVER - Tests & Style
# ============================================
server-quality:
name: Server Quality & Tests
runs-on: ubuntu-latest
timeout-minutes: 60
needs: validate-gradle-wrapper
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: |
17
${{ env.JAVA_VERSION }}
cache: 'gradle'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
with:
gradle-version: ${{ env.GRADLE_VERSION }}
# --- Code Style Checks ---
- name: βœ… Java Code Style (Spotless)
run: ./gradlew spotlessCheck -Pprod
- name: βœ… Java Documentation (Checkstyle)
run: ./gradlew checkstyleMain -x webapp -Pprod
if: success() || failure()
- name: βœ… Java Architecture Tests
run: ./gradlew test -DincludeTags='ArchitectureTest' -x webapp -Pprod
if: success() || failure()
# --- Unit Tests ---
- name: βœ… Java Unit Tests
if: ${{ env.RUN_ALL_TESTS }}
run: |
set -o pipefail
./gradlew --console=plain test jacocoTestReport -x webapp -Pprod jacocoTestCoverageVerification | tee tests.log
- name: Print Failed Tests
if: failure()
run: |
FAILED_TESTS=$(grep "Test >.* FAILED\$" tests.log || true)
if [ -n "$FAILED_TESTS" ]; then
echo "$FAILED_TESTS"
exit 1
else
echo "No failed tests."
fi
# --- Upload Results ---
- name: Upload JUnit Test Results
if: success() || failure()
uses: actions/upload-artifact@v7
with:
name: junit-test-results
path: build/test-results/test/*.xml
- name: Upload Coverage Report
if: success() || failure()
uses: actions/upload-artifact@v7
with:
name: coverage-report-server
path: build/reports/jacoco/test/
# --- Test Reports ---
- name: Annotate Test Results
uses: ashley-taylor/junit-report-annotations-action@f9c1a5cbe28479439f82b80a5402a6d3aa1990ac
if: always() && github.event.pull_request.user.login != 'dependabot[bot]'
with:
access-token: ${{ secrets.GITHUB_TOKEN }}
path: build/test-results/test/*.xml
numFailures: 99
- name: Test Report
uses: dorny/test-reporter@v2
if: success() || failure()
with:
name: Server Tests & Architecture
path: build/test-results/test/*.xml
reporter: java-junit
- name: Post Coverage Comment
if: failure() && github.event_name == 'pull_request'
uses: peter-evans/create-or-update-comment@v5
with:
token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.pull_request.number }}
body: |
πŸ“Š **Server Test Coverage Too Low**
**πŸ” View coverage locally:**
```bash
./gradlew test jacocoTestReport
open build/reports/jacoco/test/html/index.html
```
**🌐 View coverage from GitHub:**
Download the "coverage-report-server" artifact from this workflow run.
# ============================================
# CLIENT - Tests, Style & Compilation
# ============================================
client-quality:
name: Client Quality & Tests
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: Install Dependencies
run: pnpm install --frozen-lockfile
# --- Style Checks ---
- name: βœ… TypeScript Formatting
run: pnpm run prettier:check
- name: βœ… TypeScript Code Style (Lint)
run: pnpm run lint
if: success() || failure()
# --- Compilation ---
- name: βœ… TypeScript Compilation
run: pnpm run compile:ts
- name: βœ… TypeScript Test Files Compilation
run: pnpm run compile:ts:tests
# --- Tests ---
- name: βœ… TypeScript Tests
run: pnpm run test:ci
- name: βœ… TypeScript Tests (Selection)
run: pnpm run test-diff:ci
if: success() || failure()
# --- Upload Results ---
- name: Upload Coverage Report
if: success() || failure()
uses: actions/upload-artifact@v7
with:
name: coverage-report-client
path: build/test-results/vitest/coverage/
- name: Post Coverage Comment
if: failure() && github.event_name == 'pull_request'
uses: peter-evans/create-or-update-comment@v5
with:
token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.pull_request.number }}
body: |
πŸ“Š **Client Test Coverage Too Low**
**πŸ” View coverage locally:**
```bash
pnpm run test:ci
open build/test-results/vitest/coverage/index.html
```
**🌐 View coverage from GitHub:**
Download the "coverage-report-client" artifact from this workflow run.
# ============================================
# OPENAPI - Generation & Auto-commit (nur mit Label)
# ============================================
openapi-generation:
name: OpenAPI Generation & Auto-commit
if: |
github.event_name == 'pull_request' &&
contains(github.event.pull_request.labels.*.name, 'server')
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.ref }}
fetch-depth: 0
token: ${{ secrets.BOT_USER_TOKEN }}
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: ${{ env.JAVA_VERSION }}
cache: 'gradle'
- name: Setup pnpm
uses: pnpm/action-setup@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: Install Node Dependencies
run: pnpm install --frozen-lockfile
- name: Start Keycloak
run: |
docker compose -f docker/local-setup/services.yml up -d keycloak
echo "Waiting for Keycloak to be healthy..."
for i in {1..60}; do
if curl -sSf http://localhost:9080/realms/tumidpldap/.well-known/openid-configuration >/dev/null 2>&1; then
echo "βœ… Keycloak is up"
break
fi
echo "Waiting ($i/60)..."
sleep 2
done
if ! curl -sSf http://localhost:9080/realms/tumidpldap/.well-known/openid-configuration >/dev/null 2>&1; then
echo "❌ Keycloak did not start in time."
docker compose -f docker/local-setup/services.yml logs keycloak || true
docker compose -f docker/local-setup/services.yml down --remove-orphans --volumes || true
exit 1
fi
- name: Generate OpenAPI Spec
run: ./gradlew generateApiDocs -x webapp
- name: Generate Client Code
run: ./gradlew openApiGenerate
- name: Stop Keycloak
if: always()
run: docker compose -f docker/local-setup/services.yml down --remove-orphans --volumes || true
- name: Format Generated Client Code
run: pnpm exec prettier --write ./src/main/webapp/app/generated
- name: Check for Changes
id: check_changes
run: |
git add openapi/openapi.yaml src/main/webapp/app/generated -f
if git diff --cached --quiet; then
echo "no_changes_detected=true" >> $GITHUB_OUTPUT
else
echo "no_changes_detected=false" >> $GITHUB_OUTPUT
fi
- name: Commit Changes
if: steps.check_changes.outputs.no_changes_detected == 'false'
run: |
git config --local user.name "github-actions[bot]"
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git commit -m "chore: update OpenAPI spec and generated client"
git push https://x-access-token:${{ secrets.BOT_USER_TOKEN }}@github.com/${{ github.repository }} HEAD:${{ github.event.pull_request.head.ref }}
- name: Comment on PR
run: |
COMMENT=$([[ "${{ steps.check_changes.outputs.no_changes_detected }}" == "true" ]] && echo "πŸ€– No OpenAPI or client changes needed." || echo "πŸ€– OpenAPI spec and client code auto-updated and committed.")
curl -s -X POST \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Content-Type: application/json" \
-d "{\"body\":\"$COMMENT\"}" \
"https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments"
# ============================================
# SERVER STARTUP - Validierung dass Server startet UND LIGHTHOUSE - Performance & Accessibility Scans
# ============================================
lighthouse-scan:
name: Lighthouse Scan (${{ matrix.role }})
runs-on: ubuntu-latest
timeout-minutes: 20
if: github.event_name == 'pull_request' || github.event_name == 'push' || github.event_name == 'workflow_dispatch'
env:
TARGET_URL: ${{ vars.AET_CLIENT_URL || 'http://localhost:4200' }}
strategy:
fail-fast: false
matrix:
include:
- role: 'Professor'
username: 'professor1@docapply.local'
password: 'professor'
auth_method: 'keycloak'
keycloak_realm: 'tumidpldap'
paths: |
/my-positions
/job/create
/evaluation/application
/research-group/info
/interviews/overview
- role: 'Applicant'
username: 'applicant2@docapply.local'
password: 'applicant'
auth_method: 'server'
# Applicants authenticate via DocApply's internal user management (POST /api/auth/login),
# not Keycloak; this realm is unused for the 'server' auth method.
keycloak_realm: ''
paths: |
/
/job-overview
/application/form
/application/overview
/settings
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
with:
gradle-version: ${{ env.GRADLE_VERSION }}
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: ${{ env.JAVA_VERSION }}
cache: 'gradle'
- name: Setup pnpm
uses: pnpm/action-setup@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
# --- Start Keycloak ---
- name: Start Keycloak
run: |
docker compose -f docker/local-setup/services.yml up -d keycloak mysql
echo "Waiting for Keycloak to be healthy..."
for i in {1..60}; do
if curl -sSf http://localhost:9080/realms/tumidpldap/.well-known/openid-configuration >/dev/null 2>&1; then
echo "βœ… Keycloak is up"
break
fi
echo "Waiting ($i/60)..."
sleep 2
done
if ! curl -sSf http://localhost:9080/realms/tumidpldap/.well-known/openid-configuration >/dev/null 2>&1; then
echo "❌ Keycloak did not start in time."
docker compose -f docker/local-setup/services.yml logs keycloak || true
docker compose -f docker/local-setup/services.yml down --remove-orphans --volumes || true
exit 1
fi
# --- Install Client Dependencies for Local Angular Dev Server ---
- name: Install Client Dependencies
if: ${{ !vars.AET_CLIENT_URL }}
run: pnpm install --frozen-lockfile
- name: Start Angular Dev Server
if: ${{ !vars.AET_CLIENT_URL }}
run: |
echo "Starting Angular dev server..."
node prebuild.mjs --develop
pnpm exec ng serve --host 0.0.0.0 > client.log 2>&1 &
echo "CLIENT_PID=$!" >> $GITHUB_ENV
echo "Waiting for Angular dev server to start..."
for i in {1..60}; do
if curl -sSf http://localhost:4200 >/dev/null 2>&1; then
echo "βœ… Angular dev server started successfully!"
break
fi
echo "Waiting ($i/60)..."
sleep 5
done
if ! curl -sSf http://localhost:4200 >/dev/null 2>&1; then
echo "❌ Angular dev server did not start in time."
cat client.log
exit 1
fi
# --- Start Server ---
- name: Start Spring Boot Server
run: |
echo "Starting Spring Boot server..."
# The prod profile fails fast unless these security-critical secrets are provided
# (see AppTokenKeyConfiguration + ProductionSecretsGuard). Generate ephemeral, throwaway
# values for this short-lived Lighthouse server; they never leave the runner.
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out /tmp/app_token_key.pem 2>/dev/null
openssl pkey -in /tmp/app_token_key.pem -pubout -out /tmp/app_token_pub.pem 2>/dev/null
export APP_TOKEN_RSA_PRIVATE_KEY="$(cat /tmp/app_token_key.pem)"
export APP_TOKEN_RSA_PUBLIC_KEY="$(cat /tmp/app_token_pub.pem)"
export OTP_HMAC_SECRET="$(openssl rand -base64 32)"
export APP_WEBAUTHN_RP_ID="docapply.local"
export KEYCLOAK_ADMIN_TUM_CLIENT_SECRET="$(openssl rand -base64 32)"
./gradlew -Pprod bootRun > server.log 2>&1 &
echo "SERVER_PID=$!" >> $GITHUB_ENV
echo "Waiting for server to start..."
for i in {1..60}; do
if curl -sSf http://localhost:8080/actuator/health >/dev/null 2>&1; then
echo "βœ… Server started successfully!"
break
fi
if grep -q "Application 'DocApply' is running!" server.log; then
echo "βœ… Server started successfully!"
break
fi
echo "Waiting ($i/60)..."
sleep 3
done
if ! curl -sSf http://localhost:8080/actuator/health >/dev/null 2>&1 && ! grep -q "Application 'DocApply' is running!" server.log; then
echo "❌ Server failed to start!"
cat server.log
exit 1
fi
- name: Import Lighthouse Test Data
run: |
echo "Importing application test data for Lighthouse users..."
MYSQL_CONTAINER=$(docker compose -f docker/local-setup/services.yml ps -q mysql)
if [ -z "$MYSQL_CONTAINER" ]; then
echo "❌ MySQL container is not running."
exit 1
fi
while IFS= read -r file; do
echo "Running $file"
docker exec -i "$MYSQL_CONTAINER" mysql -h 127.0.0.1 -P 3306 -u root --password="" docapply < "$file"
done < <(find src/main/resources/testdata -type f -name "*.sql" ! -name "00_drop_all_tables.sql" ! -path "*/combined/*" | sort)
- name: Build Lighthouse URL List
id: lighthouse-urls
shell: bash
run: |
{
echo 'urls<<EOF'
while IFS= read -r path; do
if [ -n "$path" ]; then
echo "${TARGET_URL}${path}"
fi
done <<< '${{ matrix.paths }}'
echo 'EOF'
} >> "$GITHUB_OUTPUT"
# --- Run Lighthouse ---
# The treosh/lighthouse-ci-action ships its own bundled @lhci/cli, so no
# separate install step is needed.
- name: βœ… Run Lighthouse for ${{ matrix.role }}
uses: treosh/lighthouse-ci-action@3e7e23fb74242897f95c0ba9cabad3d0227b9b18
with:
urls: ${{ steps.lighthouse-urls.outputs.urls }}
configPath: ./lighthouserc.json
uploadArtifacts: true
temporaryPublicStorage: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TEST_USERNAME: ${{ matrix.username }}
TEST_PASSWORD: ${{ matrix.password }}
TEST_AUTH_METHOD: ${{ matrix.auth_method }}
TEST_KEYCLOAK_REALM: ${{ matrix.keycloak_realm }}
# --- Cleanup ---
- name: Cleanup Servers
if: always()
run: |
echo "Stopping servers..."
kill $CLIENT_PID || true
kill $SERVER_PID || true
# --- Upload Logs on Failure ---
- name: Upload Client Log
if: failure() && !vars.AET_CLIENT_URL
uses: actions/upload-artifact@v7
with:
name: lighthouse-client-log-${{ matrix.role }}
path: client.log
- name: Upload Server Log
if: failure()
uses: actions/upload-artifact@v7
with:
name: lighthouse-server-log-${{ matrix.role }}
path: server.log