This repository was archived by the owner on Jul 6, 2026. It is now read-only.
feat: prod hardening, security + bug fixes, runtime config, summary-s… #45
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: | |
| push: | |
| branches: | |
| - main | |
| - develop | |
| pull_request: | |
| branches: | |
| - main | |
| - develop | |
| permissions: | |
| contents: read | |
| security-events: write | |
| packages: write | |
| actions: read | |
| # Run Node20-based actions on Node24 (GitHub force-migrates 2026-06-16). Silences the | |
| # deprecation warnings and future-proofs the workflow. | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" | |
| jobs: | |
| secret-scan: | |
| name: Secret Scan (TruffleHog) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Run TruffleHog (verified secrets only) | |
| uses: trufflesecurity/trufflehog@main | |
| with: | |
| # The action mounts the workspace at /tmp inside the scanner container. | |
| # .trufflehog-exclude.txt skips deploy docs whose placeholder connection | |
| # strings (Password=__DB__) false-positive the SQLServer detector. | |
| extra_args: --results=verified,unknown --exclude-paths=/tmp/.trufflehog-exclude.txt | |
| quality: | |
| name: Lint, Test, Build | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| cache: npm | |
| cache-dependency-path: frontend/client/package-lock.json | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: "10.0.x" | |
| - name: Frontend lint | |
| shell: bash | |
| run: | | |
| if [ -f frontend/client/package.json ]; then | |
| cd frontend/client | |
| npm ci | |
| if npm run | grep -q " lint"; then | |
| npm run lint | |
| else | |
| echo "No frontend lint script found. Skipping." | |
| fi | |
| else | |
| echo "No frontend/client/package.json found. Skipping frontend lint." | |
| fi | |
| - name: Backend lint | |
| shell: bash | |
| run: | | |
| if [ -n "$(find backend -name '*.sln' -o -name '*.csproj')" ]; then | |
| dotnet tool update -g dotnet-format || true | |
| export PATH="$PATH:$HOME/.dotnet/tools" | |
| dotnet format backend --verify-no-changes --verbosity minimal || true | |
| else | |
| echo "No .NET solution/project found in backend/. Skipping backend lint." | |
| fi | |
| - name: Frontend test | |
| shell: bash | |
| run: | | |
| if [ -f frontend/client/package.json ]; then | |
| cd frontend/client | |
| if npm run | grep -q " test"; then | |
| npm test -- --ci | |
| else | |
| echo "No frontend test script found. Skipping." | |
| fi | |
| else | |
| echo "No frontend/client/package.json found. Skipping frontend tests." | |
| fi | |
| - name: Backend test | |
| shell: bash | |
| run: | | |
| if [ -n "$(find backend -name '*.sln' -o -name '*.csproj')" ]; then | |
| dotnet test backend --configuration Release --verbosity normal | |
| else | |
| echo "No .NET solution/project found in backend/. Skipping backend tests." | |
| fi | |
| - name: Frontend build | |
| shell: bash | |
| run: | | |
| if [ -f frontend/client/package.json ]; then | |
| cd frontend/client | |
| if npm run | grep -q " build"; then | |
| npm run build | |
| else | |
| echo "No frontend build script found. Skipping." | |
| fi | |
| else | |
| echo "No frontend/client/package.json found. Skipping frontend build." | |
| fi | |
| - name: Backend build | |
| shell: bash | |
| run: | | |
| if [ -n "$(find backend -name '*.sln' -o -name '*.csproj')" ]; then | |
| dotnet build backend --configuration Release --no-restore | |
| else | |
| echo "No .NET solution/project found in backend/. Skipping backend build." | |
| fi | |
| lighthouse: | |
| name: Lighthouse CI (Frontend Perf Budget) | |
| runs-on: ubuntu-latest | |
| needs: quality | |
| if: github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main') | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| cache: npm | |
| cache-dependency-path: frontend/client/package-lock.json | |
| - name: Install frontend deps | |
| working-directory: frontend/client | |
| run: npm ci | |
| - name: Build frontend (production) | |
| working-directory: frontend/client | |
| env: | |
| NEXT_PUBLIC_API_BASE_URL: "http://localhost:8081" | |
| NEXT_PUBLIC_KEYCLOAK_URL: "http://localhost:8090" | |
| NEXT_PUBLIC_SIGNALR_URL: "http://localhost:8081/hubs/messaging" | |
| run: npm run build | |
| - name: Run Lighthouse CI | |
| working-directory: frontend/client | |
| run: npx --yes @lhci/cli@0.13.x autorun --config=scripts/lighthouse.config.json | |
| env: | |
| LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }} | |
| - name: Upload Lighthouse reports | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: lighthouseci-reports | |
| path: frontend/client/.lighthouseci | |
| retention-days: 14 | |
| security: | |
| name: Trivy Scan | |
| runs-on: ubuntu-latest | |
| needs: quality | |
| if: always() | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| # Retry the base-image build: mcr.microsoft.com/dotnet/* intermittently returns | |
| # HTTP 403 on the manifest HEAD under load. A single transient blip here used to | |
| # fail the whole Trivy job, which silently skipped build-push -> release -> CD. | |
| # Retry up to 3x with backoff before treating it as a real failure. | |
| - name: Build backend image for security scan | |
| shell: bash | |
| run: | | |
| attempt=1; max=3 | |
| until docker buildx build \ | |
| --file devops/docker/backend/Dockerfile \ | |
| --load --tag loopless-backend:security \ | |
| backend; do | |
| if [ "$attempt" -ge "$max" ]; then | |
| echo "::error::backend security image build failed after ${max} attempts" | |
| exit 1 | |
| fi | |
| echo "::warning::build attempt ${attempt} failed (likely transient registry 403); retrying in 20s..." | |
| attempt=$((attempt + 1)); sleep 20 | |
| done | |
| - name: Build frontend image for security scan | |
| shell: bash | |
| run: | | |
| attempt=1; max=3 | |
| until docker buildx build \ | |
| --file devops/docker/frontend/Dockerfile \ | |
| --load --tag loopless-frontend:security \ | |
| frontend/client; do | |
| if [ "$attempt" -ge "$max" ]; then | |
| echo "::error::frontend security image build failed after ${max} attempts" | |
| exit 1 | |
| fi | |
| echo "::warning::build attempt ${attempt} failed (likely transient registry 403); retrying in 20s..." | |
| attempt=$((attempt + 1)); sleep 20 | |
| done | |
| - name: Run Trivy filesystem scan (SARIF) | |
| uses: aquasecurity/trivy-action@v0.36.0 | |
| with: | |
| scan-type: fs | |
| scan-ref: . | |
| format: sarif | |
| output: trivy-results.sarif | |
| severity: CRITICAL,HIGH | |
| - name: Enforce no CRITICAL/HIGH vulnerabilities (backend image) | |
| uses: aquasecurity/trivy-action@v0.36.0 | |
| with: | |
| scan-type: image | |
| image-ref: loopless-backend:security | |
| format: table | |
| severity: CRITICAL,HIGH | |
| ignore-unfixed: true | |
| exit-code: "1" | |
| - name: Enforce no CRITICAL/HIGH vulnerabilities (frontend image) | |
| uses: aquasecurity/trivy-action@v0.36.0 | |
| with: | |
| scan-type: image | |
| image-ref: loopless-frontend:security | |
| format: table | |
| severity: CRITICAL,HIGH | |
| ignore-unfixed: true | |
| exit-code: "1" | |
| - name: Upload Trivy SARIF report (artifact) | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: trivy-sarif | |
| path: trivy-results.sarif | |
| retention-days: 30 | |
| build-images: | |
| name: Build & Push Docker Images | |
| runs-on: ubuntu-latest | |
| needs: | |
| - quality | |
| - security | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_OWNER: ${{ github.repository_owner }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Log in to GHCR | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Compute lowercase image owner | |
| id: owner | |
| shell: bash | |
| run: echo "value=${IMAGE_OWNER,,}" >> "$GITHUB_OUTPUT" | |
| - name: Build & push backend image | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: backend | |
| file: devops/docker/backend/Dockerfile | |
| push: true | |
| tags: | | |
| ${{ env.REGISTRY }}/${{ steps.owner.outputs.value }}/loopless-backend:sha-${{ github.sha }} | |
| ${{ env.REGISTRY }}/${{ steps.owner.outputs.value }}/loopless-backend:staging | |
| ${{ env.REGISTRY }}/${{ steps.owner.outputs.value }}/loopless-backend:latest | |
| cache-from: type=gha,scope=backend | |
| cache-to: type=gha,mode=max,scope=backend | |
| - name: Build & push frontend image | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: frontend/client | |
| file: devops/docker/frontend/Dockerfile | |
| push: true | |
| # No build-args: public config (API/Keycloak URLs) is injected at RUNTIME via | |
| # window.__ENV (Helm sets APP_* on the frontend pod), so the image is env-agnostic. | |
| tags: | | |
| ${{ env.REGISTRY }}/${{ steps.owner.outputs.value }}/loopless-frontend:sha-${{ github.sha }} | |
| ${{ env.REGISTRY }}/${{ steps.owner.outputs.value }}/loopless-frontend:staging | |
| ${{ env.REGISTRY }}/${{ steps.owner.outputs.value }}/loopless-frontend:latest | |
| cache-from: type=gha,scope=frontend | |
| cache-to: type=gha,mode=max,scope=frontend | |
| - name: Build & push aiops image | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: devops/aiops | |
| file: devops/aiops/Dockerfile | |
| push: true | |
| tags: | | |
| ${{ env.REGISTRY }}/${{ steps.owner.outputs.value }}/loopless-aiops:sha-${{ github.sha }} | |
| ${{ env.REGISTRY }}/${{ steps.owner.outputs.value }}/loopless-aiops:latest | |
| cache-from: type=gha,scope=aiops | |
| cache-to: type=gha,mode=max,scope=aiops | |
| notify: | |
| name: Discord Notify | |
| runs-on: ubuntu-latest | |
| needs: | |
| - secret-scan | |
| - quality | |
| - lighthouse | |
| - security | |
| - build-images | |
| if: always() | |
| steps: | |
| - name: Send Discord notification | |
| if: env.DISCORD_WEBHOOK_URL != '' | |
| env: | |
| DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} | |
| SECRET_SCAN_RESULT: ${{ needs.secret-scan.result }} | |
| QUALITY_RESULT: ${{ needs.quality.result }} | |
| LIGHTHOUSE_RESULT: ${{ needs.lighthouse.result }} | |
| SECURITY_RESULT: ${{ needs.security.result }} | |
| IMAGES_RESULT: ${{ needs.build-images.result }} | |
| RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| shell: bash | |
| run: | | |
| STATUS="success" | |
| if [ "$SECRET_SCAN_RESULT" = "failure" ] || [ "$QUALITY_RESULT" != "success" ] || [ "$SECURITY_RESULT" != "success" ]; then | |
| STATUS="failed" | |
| fi | |
| if [ "$LIGHTHOUSE_RESULT" = "failure" ] || [ "$IMAGES_RESULT" = "failure" ]; then | |
| STATUS="failed" | |
| fi | |
| PAYLOAD=$(cat <<EOF | |
| { | |
| "content": "CI run **$STATUS** for `${{ github.repository }}` on `${{ github.ref_name }}`.\\nSecrets: **$SECRET_SCAN_RESULT** | Quality: **$QUALITY_RESULT** | Lighthouse: **$LIGHTHOUSE_RESULT** | Security: **$SECURITY_RESULT** | Images: **$IMAGES_RESULT**\\n$RUN_URL" | |
| } | |
| EOF | |
| ) | |
| curl -sS -X POST "$DISCORD_WEBHOOK_URL" \ | |
| -H "Content-Type: application/json" \ | |
| -d "$PAYLOAD" | |
| - name: Skip notification | |
| if: env.DISCORD_WEBHOOK_URL == '' | |
| run: echo "DISCORD_WEBHOOK_URL secret is not configured. Skipping notification." |