Skip to content
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

feat: prod hardening, security + bug fixes, runtime config, summary-s…

feat: prod hardening, security + bug fixes, runtime config, summary-s… #45

Workflow file for this run

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