Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,11 @@ AI_CONNECT_TIMEOUT_SECONDS=5
AI_TIMEOUT_SECONDS=90
AI_RATE_LIMIT_CAPACITY=5
AI_RATE_LIMIT_REFILL_PER_MINUTE=2

# Monitoring stack (docker-compose.monitoring.yml — optional overlay).
# Grafana is never public: reach it via SSH tunnel only:
# ssh -L 3000:localhost:3001 user@vps # then http://localhost:3000
# Generate a strong password once on the host:
# echo "GRAFANA_ADMIN_PASSWORD=$(openssl rand -base64 24)" >> .env
GRAFANA_ADMIN_USER=admin
GRAFANA_ADMIN_PASSWORD=change_me_strong_password
10 changes: 10 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Force LF line endings for files that run on the Linux VPS, so they
# work regardless of the contributor's OS / git autocrlf setting.
# (CRLF in a shell script breaks the shebang: "bad interpreter ^M".)
*.sh text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
Caddyfile text eol=lf
Makefile text eol=lf
Dockerfile text eol=lf
mvnw text eol=lf
49 changes: 49 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
version: 2

updates:
# ── Backend (Maven) ──────────────────────────────────────
- package-ecosystem: maven
directory: /apps/backend
schedule:
interval: weekly
open-pull-requests-limit: 5
labels: [dependencies, backend]
groups:
backend-minor-patch:
update-types: [minor, patch]

# ── Frontend (npm/pnpm) ──────────────────────────────────
- package-ecosystem: npm
directory: /apps/frontend
schedule:
interval: weekly
open-pull-requests-limit: 5
labels: [dependencies, frontend]
groups:
frontend-minor-patch:
update-types: [minor, patch]
ignore:
# Next.js is an internal fork — never auto-bump (see apps/frontend/AGENTS.md)
- dependency-name: next

# ── Docker base images ───────────────────────────────────
- package-ecosystem: docker
directory: /apps/backend
schedule:
interval: weekly
labels: [dependencies, docker]
- package-ecosystem: docker
directory: /apps/frontend
schedule:
interval: weekly
labels: [dependencies, docker]

# ── GitHub Actions ───────────────────────────────────────
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
labels: [dependencies, github-actions]
groups:
actions:
update-types: [minor, patch]
96 changes: 0 additions & 96 deletions .github/workflows/build-services.yml

This file was deleted.

217 changes: 217 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
name: CI

# Fast feedback on every push / PR to dev and main.
# Heavy security scanning lives in security.yml (main + nightly).
on:
push:
branches: [dev, main]
pull_request:
branches: [dev, main]

# Cancel superseded runs on the same ref.
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
changes:
name: Detect changed paths
runs-on: ubuntu-latest
outputs:
frontend: ${{ steps.filter.outputs.frontend }}
backend: ${{ steps.filter.outputs.backend }}
migrations: ${{ steps.filter.outputs.migrations }}
i18n: ${{ steps.filter.outputs.i18n }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3
id: filter
with:
filters: |
frontend:
- 'apps/frontend/**'
backend:
- 'apps/backend/**'
migrations:
- 'apps/backend/src/main/resources/db/migration/**'
i18n:
- 'apps/frontend/messages/**'

frontend:
name: Frontend — lint, typecheck, build
needs: changes
if: needs.changes.outputs.frontend == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: apps/frontend
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
with:
version: '9.15.0'
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'
cache: pnpm
cache-dependency-path: apps/frontend/pnpm-lock.yaml
- name: Install
run: pnpm install --frozen-lockfile
- name: Lint
run: pnpm lint
- name: Type-check
run: pnpm typecheck
- name: Build
run: pnpm build

backend:
name: Backend — compile, test, coverage
needs: changes
if: needs.changes.outputs.backend == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
defaults:
run:
working-directory: apps/backend
# Ephemeral Postgres for @SpringBootTest (context load runs Flyway against it).
# Created before the steps, reachable at localhost:5432, destroyed with the runner.
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: codestar
POSTGRES_PASSWORD: codestar
POSTGRES_DB: codestardb
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U codestar -d codestardb"
--health-interval 10s --health-timeout 5s --health-retries 5
env:
DB_URL: jdbc:postgresql://localhost:5432/codestardb
DB_USER: codestar
DB_PASSWORD: codestar
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4
with:
java-version: '17'
distribution: temurin
cache: maven
- name: Make mvnw executable
run: chmod +x ./mvnw
- name: Compile
run: ./mvnw compile -B --no-transfer-progress
- name: Test + coverage
run: ./mvnw verify -B --no-transfer-progress
- name: Upload coverage report
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: jacoco-coverage
path: apps/backend/target/site/jacoco/
retention-days: 7
if-no-files-found: warn
compression-level: 6

governance:
name: Codestar governance (migrations, i18n)
needs: changes
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0 # full history so guards can diff against the base branch
- name: Flyway migration governance
if: needs.changes.outputs.migrations == 'true'
env:
BASE_REF: origin/${{ github.base_ref || 'main' }}
run: bash scripts/ci/flyway-governance.sh
- name: i18n key parity
if: needs.changes.outputs.i18n == 'true'
run: bash scripts/ci/i18n-parity.sh

secrets:
name: Secret scan (gitleaks)
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
# Run the gitleaks binary directly: the GitHub Action requires a paid
# license for organisation repos, the open-source binary does not.
- name: Run gitleaks
env:
GITLEAKS_VERSION: 8.21.2
run: |
curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
| tar -xz gitleaks
./gitleaks detect --source . --redact --verbose --exit-code 1

dependency-review:
name: Dependency review
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/dependency-review-action@e58c696e52cac8e62d61cc21fda89565d71505d7 # v4.3.0
with:
fail-on-severity: high
comment-summary-in-pr: on-failure

docker:
name: Docker — compose build
needs: [frontend, backend]
if: |
always() &&
!contains(needs.*.result, 'failure') &&
!contains(needs.*.result, 'cancelled') &&
(needs.frontend.result == 'success' || needs.backend.result == 'success')
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Create env file
run: cp .env.example .env
- name: Build images
run: docker compose build

# Single required status check for branch protection.
ci-required:
name: CI required
needs: [frontend, backend, governance, secrets, docker]
if: always()
runs-on: ubuntu-latest
steps:
- name: Verify no required job failed
run: |
results='${{ join(needs.*.result, ',') }}'
echo "Upstream results: $results"
case "$results" in
*failure*|*cancelled*) echo "A required job failed."; exit 1 ;;
*) echo "All required jobs passed (or were skipped)." ;;
esac

# Discord notification. No-ops when DISCORD_WEBHOOK_URL is unset (fork-safe).
notify:
name: Discord notification
needs: [frontend, backend, governance, secrets, docker]
if: always()
runs-on: ubuntu-latest
steps:
- uses: sarisia/actions-status-discord@eb045afee445dc055c18d3d90bd0f244fd062708 # v1
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_URL }}
status: ${{ (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) && 'failure' || 'success' }}
title: "CI pipeline"
username: Codestar CI
Loading
Loading