Repository navigation
Chore/repository hardening #162
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: CI | |
| on: | |
| pull_request: | |
| workflow_dispatch: | |
| concurrency: | |
| group: ci-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| jobs: | |
| # ------------------------------------------------------------------ | |
| # Changed-path detection | |
| # ------------------------------------------------------------------ | |
| changes: | |
| name: detect-changes | |
| runs-on: ubuntu-latest | |
| outputs: | |
| web_client: ${{ steps.manual.outputs.web_client || steps.pr_changes.outputs.web_client }} | |
| steps: | |
| - name: Default changed paths for non-PR runs | |
| id: manual | |
| if: github.event_name != 'pull_request' | |
| run: echo "web_client=true" >> "$GITHUB_OUTPUT" | |
| - name: Checkout repository | |
| if: github.event_name == 'pull_request' | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Detect changed paths | |
| id: pr_changes | |
| if: github.event_name == 'pull_request' | |
| uses: dorny/paths-filter@v3 | |
| with: | |
| filters: | | |
| web_client: | |
| - 'web-client/**' | |
| # ------------------------------------------------------------------ | |
| # Java / Spring Boot - organization-service | |
| # ------------------------------------------------------------------ | |
| organization-service: | |
| name: organization-service (Java) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| defaults: | |
| run: | |
| working-directory: services/spring-organization | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up JDK 21 (Temurin) | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: '21' | |
| cache: gradle | |
| - name: Grant execute permission for gradlew | |
| run: chmod +x ./gradlew | |
| - name: Lint (Checkstyle) | |
| run: ./gradlew --no-daemon checkstyleMain | |
| - name: Build & test | |
| run: ./gradlew --no-daemon build | |
| - name: Upload test reports | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: organization-service-reports | |
| path: | | |
| services/spring-organization/build/reports/** | |
| services/spring-organization/build/test-results/** | |
| if-no-files-found: ignore | |
| # ------------------------------------------------------------------ | |
| # Java / Spring Boot - member-service | |
| # ------------------------------------------------------------------ | |
| member-service: | |
| name: member-service (Java) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| defaults: | |
| run: | |
| working-directory: services/spring-member | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up JDK 21 (Temurin) | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: '21' | |
| cache: gradle | |
| - name: Grant execute permission for gradlew | |
| run: chmod +x ./gradlew | |
| - name: Lint (Checkstyle) | |
| run: ./gradlew --no-daemon checkstyleMain | |
| - name: Build & test | |
| run: ./gradlew --no-daemon build | |
| - name: Upload test reports | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: member-service-reports | |
| path: | | |
| services/spring-member/build/reports/** | |
| services/spring-member/build/test-results/** | |
| if-no-files-found: ignore | |
| # ------------------------------------------------------------------ | |
| # Java / Spring Boot - event-service | |
| # ------------------------------------------------------------------ | |
| event-service: | |
| name: event-service (Java) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| defaults: | |
| run: | |
| working-directory: services/spring-event | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up JDK 21 (Temurin) | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: '21' | |
| cache: gradle | |
| - name: Grant execute permission for gradlew | |
| run: chmod +x ./gradlew | |
| - name: Lint (Checkstyle) | |
| run: ./gradlew --no-daemon checkstyleMain | |
| - name: Build & test | |
| run: ./gradlew --no-daemon build | |
| - name: Upload test reports | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: event-service-reports | |
| path: | | |
| services/spring-event/build/reports/** | |
| services/spring-event/build/test-results/** | |
| if-no-files-found: ignore | |
| # ------------------------------------------------------------------ | |
| # Java / Spring Boot - feedback-service | |
| # ------------------------------------------------------------------ | |
| feedback-service: | |
| name: feedback-service (Java) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| defaults: | |
| run: | |
| working-directory: services/spring-feedback | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up JDK 21 (Temurin) | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: '21' | |
| cache: gradle | |
| - name: Grant execute permission for gradlew | |
| run: chmod +x ./gradlew | |
| - name: Lint (Checkstyle) | |
| run: ./gradlew --no-daemon checkstyleMain | |
| - name: Build & test | |
| run: ./gradlew --no-daemon build | |
| - name: Upload test reports | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: feedback-service-reports | |
| path: | | |
| services/spring-feedback/build/reports/** | |
| services/spring-feedback/build/test-results/** | |
| if-no-files-found: ignore | |
| # ------------------------------------------------------------------ | |
| # Java / Spring Boot - finance-service | |
| # ------------------------------------------------------------------ | |
| finance-service: | |
| name: finance-service (Java) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| defaults: | |
| run: | |
| working-directory: services/spring-finance | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up JDK 21 (Temurin) | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: '21' | |
| cache: gradle | |
| - name: Grant execute permission for gradlew | |
| run: chmod +x ./gradlew | |
| - name: Lint (Checkstyle) | |
| run: ./gradlew --no-daemon checkstyleMain | |
| - name: Build & test | |
| run: ./gradlew --no-daemon build | |
| - name: Upload test reports | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: finance-service-reports | |
| path: | | |
| services/spring-finance/build/reports/** | |
| services/spring-finance/build/test-results/** | |
| if-no-files-found: ignore | |
| # ------------------------------------------------------------------ | |
| # Java / Spring Boot - letter-service | |
| # ------------------------------------------------------------------ | |
| letter-service: | |
| name: letter-service (Java) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| defaults: | |
| run: | |
| working-directory: services/spring-letter | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up JDK 21 (Temurin) | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: '21' | |
| cache: gradle | |
| - name: Grant execute permission for gradlew | |
| run: chmod +x ./gradlew | |
| - name: Lint (Checkstyle) | |
| run: ./gradlew --no-daemon checkstyleMain | |
| - name: Build & test | |
| run: ./gradlew --no-daemon build | |
| - name: Upload test reports | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: letter-service-reports | |
| path: | | |
| services/spring-letter/build/reports/** | |
| services/spring-letter/build/test-results/** | |
| if-no-files-found: ignore | |
| # ------------------------------------------------------------------ | |
| # Python - py-genai-helper | |
| # ------------------------------------------------------------------ | |
| py-genai-helper: | |
| name: py-genai-helper (Python) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| defaults: | |
| run: | |
| working-directory: services/py-genai-helper | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python 3.12 | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.12' | |
| cache: pip | |
| cache-dependency-path: services/py-genai-helper/requirements.txt | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -r requirements.txt | |
| pip install -r requirements-dev.txt | |
| - name: Lint (ruff) | |
| run: ruff check . | |
| - name: Run tests (pytest) | |
| run: pytest -q | |
| # ------------------------------------------------------------------ | |
| # Node / React - web-client | |
| # ------------------------------------------------------------------ | |
| web-client: | |
| name: web-client (Node) | |
| needs: changes | |
| if: ${{ needs.changes.outputs.web_client == 'true' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| defaults: | |
| run: | |
| working-directory: web-client | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # Must run before setup-node so its `cache: pnpm` can find the store. | |
| - name: Set up pnpm | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| version: 9.15.9 | |
| - name: Set up Node.js 24 | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '24' | |
| cache: pnpm | |
| cache-dependency-path: web-client/pnpm-lock.yaml | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Typecheck (TypeScript) | |
| run: pnpm typecheck | |
| - name: Lint (ESLint) | |
| run: pnpm lint | |
| - name: Run tests with coverage (Vitest) | |
| run: pnpm test:coverage | |
| - name: Upload coverage report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: web-client-coverage | |
| path: web-client/coverage/ | |
| if-no-files-found: ignore | |
| - name: Install Playwright browsers | |
| run: pnpm exec playwright install --with-deps chromium | |
| - name: Run E2E tests (Playwright) | |
| run: pnpm e2e | |
| - name: Upload Playwright report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: web-client-playwright-report | |
| path: web-client/playwright-report/ | |
| if-no-files-found: ignore | |
| - name: Build (Vite) | |
| run: pnpm build | |
| # ------------------------------------------------------------------ | |
| # Whole-system Docker build | |
| # ------------------------------------------------------------------ | |
| docker-build: | |
| name: docker-build | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| if: ${{ always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }} | |
| needs: | |
| - member-service | |
| - organization-service | |
| - event-service | |
| - feedback-service | |
| - finance-service | |
| - letter-service | |
| - py-genai-helper | |
| - web-client | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Prepare env files | |
| run: | | |
| cp services/py-genai-helper/.env.example services/py-genai-helper/.env | |
| cp services/spring-letter/.env.example services/spring-letter/.env | |
| - name: Build all images via docker compose | |
| run: docker compose -f infra/docker-compose.yml build | |
| - name: Verify expected image tags exist | |
| # Compose tags images as `<project>-<service>:latest`; project name | |
| # defaults to the working directory and can differ between hosts. | |
| run: | | |
| set -e | |
| missing=0 | |
| for img in \ | |
| team-devoops-organization-service:latest \ | |
| team-devoops-member-service:latest \ | |
| team-devoops-event-service:latest \ | |
| team-devoops-feedback-service:latest \ | |
| team-devoops-finance-service:latest \ | |
| team-devoops-letter-service:latest \ | |
| team-devoops-py-genai-helper:latest \ | |
| team-devoops-web-client:latest; do | |
| if ! docker image inspect "$img" >/dev/null 2>&1; then | |
| echo "::error::Expected image '$img' not found after compose build." | |
| missing=1 | |
| fi | |
| done | |
| if [ "$missing" -ne 0 ]; then | |
| echo "Available images:" | |
| docker images | |
| exit 1 | |
| fi | |
| # ------------------------------------------------------------------ | |
| # CodeQL SAST - vulnerability scan of first-party code. | |
| # ------------------------------------------------------------------ | |
| codeql: | |
| name: CodeQL (${{ matrix.language }}) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: read | |
| security-events: write | |
| actions: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - language: java-kotlin | |
| build-mode: none | |
| - language: python | |
| build-mode: none | |
| - language: javascript-typescript | |
| build-mode: none | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Initialize CodeQL | |
| uses: github/codeql-action/init@v3 | |
| with: | |
| languages: ${{ matrix.language }} | |
| build-mode: ${{ matrix.build-mode }} | |
| queries: security-extended | |
| - name: Perform CodeQL analysis | |
| uses: github/codeql-action/analyze@v3 | |
| with: | |
| category: /language:${{ matrix.language }} | |
| # ------------------------------------------------------------------ | |
| # OpenAPI lint | |
| # ------------------------------------------------------------------ | |
| openapi-lint: | |
| name: openapi-lint | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Check OpenAPI file is non-empty | |
| id: check | |
| run: | | |
| if [ -s api/openapi.yaml ]; then | |
| echo "has_spec=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "has_spec=false" >> "$GITHUB_OUTPUT" | |
| echo "::notice::api/openapi.yaml is empty - skipping Spectral lint" | |
| fi | |
| - name: Lint OpenAPI with Spectral | |
| if: steps.check.outputs.has_spec == 'true' | |
| uses: stoplightio/spectral-action@6416fd018ae38e60136775066eb3e98172143141 # v0.8.13 | |
| with: | |
| file_glob: 'api/openapi.yaml' | |
| # ------------------------------------------------------------------ | |
| # Helm chart lint + render + schema validation. | |
| # ------------------------------------------------------------------ | |
| helm-validate: | |
| name: helm-validate | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Helm | |
| uses: azure/setup-helm@v4 | |
| - name: Lint chart | |
| run: helm lint infra/helm/team-devoops | |
| - name: Render templates | |
| run: | | |
| helm template team-devoops infra/helm/team-devoops \ | |
| --set global.image.tag=ci-validate > /tmp/rendered.yaml | |
| test -s /tmp/rendered.yaml | |
| - name: Install kubeconform | |
| run: | | |
| curl -sSLo kubeconform.tar.gz \ | |
| https://github.com/yannh/kubeconform/releases/latest/download/kubeconform-linux-amd64.tar.gz | |
| tar xf kubeconform.tar.gz | |
| sudo mv kubeconform /usr/local/bin/ | |
| - name: Validate rendered manifests | |
| run: kubeconform -strict -summary /tmp/rendered.yaml | |
| # ------------------------------------------------------------------ | |
| # Aggregate gate - use this single check for branch protection. | |
| # ------------------------------------------------------------------ | |
| ci-success: | |
| name: ci-success | |
| runs-on: ubuntu-latest | |
| if: always() | |
| needs: | |
| - changes | |
| - member-service | |
| - organization-service | |
| - event-service | |
| - feedback-service | |
| - finance-service | |
| - letter-service | |
| - py-genai-helper | |
| - web-client | |
| - docker-build | |
| - codeql | |
| - openapi-lint | |
| - helm-validate | |
| steps: | |
| - name: Verify all required jobs succeeded | |
| run: | | |
| results='${{ toJSON(needs) }}' | |
| echo "Job results: $results" | |
| if echo "$results" | jq -e ' | |
| to_entries[] | |
| | select(.value.result == "failure" or .value.result == "cancelled") | |
| ' > /dev/null; then | |
| echo "::error::One or more required jobs did not succeed." | |
| exit 1 | |
| fi | |
| # web-client and docker-build are allowed to be skipped together | |
| # (docker-build cascades when web-client is skipped via path filter) | |
| if echo "$results" | jq -e ' | |
| to_entries[] | |
| | select(.value.result == "skipped" and (.key | IN("web-client", "docker-build") | not)) | |
| ' > /dev/null; then | |
| echo "::error::A required job was skipped unexpectedly." | |
| exit 1 | |
| fi | |
| echo "All required jobs passed." |