Bugfix: Fix excessive api requests triggering genderdecoder
#4382
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |