Skip to content

Chore/repository hardening #162

Chore/repository hardening

Chore/repository hardening #162

Workflow file for this run

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."