6 Commits

Author SHA1 Message Date
Peter Smit
39d55d908a Fix prettier issues
All checks were successful
CI / checks (push) Successful in 54s
2025-09-05 09:56:50 +02:00
Julen Dixneuf
409784672c Add health check endpoint and resolve locale detection bug (#387)
* Add health check API endpoint with database connectivity

* Update locale handling to fallback to default language on invalid input

* Add health check endpoints for application readiness and liveness

- Introduced `/api/health/readiness` endpoint to check if the application can serve requests, including database connectivity.
- Introduced `/api/health/liveness` endpoint to verify if the application is running independently of external dependencies.
- Updated the health check logic to streamline database connectivity checks and response handling.

* Refactor health check logic

---------

Co-authored-by: Julen Dixneuf <julen.d@padoa-group.com>
2025-09-05 09:53:12 +02:00
Izzy Irvine
d27cbdba47 Added pipeline to buid container and push to ghcr.io (#332) 2025-09-05 08:59:09 +02:00
Peter Smit
048ac4da0a Disable provenance and sbom to hide unknown/unknown builds 2025-09-05 08:55:55 +02:00
Izzy Irvine
a86e92e414 Added pipeline to buid container and push to ghcr.io 2025-09-05 08:54:22 +02:00
Peter Smit
76c58a7f61 Use vertical weblate chart in README 2025-09-05 08:18:31 +02:00
7 changed files with 175 additions and 2 deletions

45
.github/workflows/cd.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: Create and publish a Docker image
on:
push:
tags:
- '*'
jobs:
build-and-push-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login to Github Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Downcase repo
run: |
echo "REPO=${GITHUB_REPOSITORY@L}" >> "${GITHUB_ENV}"
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
ghcr.io/${{ env.REPO }}:${{ github.ref_name }}
ghcr.io/${{ env.REPO }}:latest
provenance: false # Disable provenance to avoid unknown/unknown
sbom: false # Disable sbom to avoid unknown/unknown

View File

@@ -50,7 +50,7 @@ You can easily add missing translations to the project or even add a new languag
Here is the current state of translation:
<a href="https://hosted.weblate.org/engage/spliit/">
<img src="https://hosted.weblate.org/widget/spliit/spliit/horizontal-auto.svg" alt="Translation status" />
<img src="https://hosted.weblate.org/widget/spliit/spliit/multi-auto.svg" alt="Translation status" />
</a>
## Run locally
@@ -68,6 +68,16 @@ Here is the current state of translation:
3. Run `npm run start-container` to start the postgres and the spliit2 containers
4. You can access the app by browsing to http://localhost:3000
You could also pull it from the container registry:
```docker pull ghcr.io/spliit-app/spliit:latest```
## Health check
The application has a health check endpoint that can be used to check if the application is running and if the database is accessible.
- `GET /api/health/readiness` or `GET /api/health` - Check if the application is ready to serve requests, including database connectivity.
- `GET /api/health/liveness` - Check if the application is running, but not necessarily ready to serve requests.
## Opt-in features
### Expense documents

View File

@@ -0,0 +1,7 @@
import { checkLiveness } from '@/lib/health'
// Liveness: Is the app itself healthy? (no external dependencies)
// If this fails, Kubernetes should restart the pod
export async function GET() {
return checkLiveness()
}

View File

@@ -0,0 +1,7 @@
import { checkReadiness } from '@/lib/health'
// Readiness: Can the app serve requests? (includes all external dependencies)
// If this fails, Kubernetes should stop sending traffic but not restart
export async function GET() {
return checkReadiness()
}

View File

@@ -0,0 +1,7 @@
import { checkReadiness } from '@/lib/health'
// Default health check - same as readiness (includes database check)
// This is readiness-focused for monitoring tools like uptime-kuma
export async function GET() {
return checkReadiness()
}

96
src/lib/health.ts Normal file
View File

@@ -0,0 +1,96 @@
import { prisma } from '@/lib/prisma'
export interface HealthCheckStatus {
status: 'healthy' | 'unhealthy'
services?: {
database?: {
status: 'healthy' | 'unhealthy'
error?: string
}
}
}
async function checkDatabase(): Promise<{
status: 'healthy' | 'unhealthy'
error?: string
}> {
try {
// Simple query to test database connectivity
await prisma.$queryRaw`SELECT 1`
return {
status: 'healthy',
}
} catch (error) {
return {
status: 'unhealthy',
error:
error instanceof Error ? error.message : 'Database connection failed',
}
}
}
function createHealthResponse(
data: HealthCheckStatus,
isHealthy: boolean,
): Response {
return new Response(JSON.stringify(data), {
status: isHealthy ? 200 : 503,
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Content-Type': 'application/json',
},
})
}
export async function checkReadiness(): Promise<Response> {
try {
const databaseStatus = await checkDatabase()
const services: HealthCheckStatus['services'] = {
database: databaseStatus,
}
// For readiness: healthy only if all services are healthy
const isHealthy = databaseStatus.status === 'healthy'
const healthStatus: HealthCheckStatus = {
status: isHealthy ? 'healthy' : 'unhealthy',
services,
}
return createHealthResponse(healthStatus, isHealthy)
} catch (error) {
const errorStatus: HealthCheckStatus = {
status: 'unhealthy',
services: {
database: {
status: 'unhealthy',
error:
error instanceof Error ? error.message : 'Readiness check failed',
},
},
}
return createHealthResponse(errorStatus, false)
}
}
export async function checkLiveness(): Promise<Response> {
try {
// Liveness: Only check if the app process is alive
// No database or external service checks - restarting won't fix those
const healthStatus: HealthCheckStatus = {
status: 'healthy',
// No services reported - we don't check them for liveness
}
return createHealthResponse(healthStatus, true) // Always 200 for liveness
} catch (error) {
// This should rarely happen, but if it does, the app needs restart
const errorStatus: HealthCheckStatus = {
status: 'unhealthy',
}
return createHealthResponse(errorStatus, false)
}
}

View File

@@ -17,7 +17,8 @@ function getAcceptLanguageLocale(requestHeaders: Headers, locales: Locales) {
try {
locale = match(languages, locales, defaultLocale)
} catch (e) {
// invalid language
// invalid language - fallback to default
locale = defaultLocale
}
return locale
}