Files
Josh Rogers 0e59fd5bad
YesChef/pipeline/head There was a failure building this commit
Add Jenkinsfile for build, test, and Gitea image push
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 23:44:53 -05:00

224 lines
8.9 KiB
Groovy

// Jenkins pipeline for YesChef.
//
// Stages:
// 1. Restore + build the .NET backend solution
// 2. Run backend unit tests
// 3. Run backend integration tests against a sidecar Postgres container named `postgres`
// (Testcontainers also works because the docker socket is mounted)
// 4. Install, type-check, unit-test, and build the SvelteKit frontend
// 5. Build the backend and frontend Docker images from their existing Dockerfiles
//
// Requires the Jenkins Docker Pipeline plugin, an agent with Docker available, and a
// workspace user that can talk to the Docker daemon (the host docker socket is mounted
// into the build containers so Testcontainers can spawn its own DB instances).
pipeline {
agent any
options {
timestamps()
timeout(time: 45, unit: 'MINUTES')
buildDiscarder(logRotator(numToKeepStr: '20'))
disableConcurrentBuilds()
}
environment {
DOTNET_SDK_IMAGE = 'mcr.microsoft.com/dotnet/sdk:10.0'
NODE_IMAGE = 'node:22-slim'
POSTGRES_IMAGE = 'postgres:17'
POSTGRES_DB = 'yeschef'
POSTGRES_USER = 'yeschef'
POSTGRES_PASSWORD = 'yeschef'
// Gitea container registry target. The registry lives on the same host as
// Gitea itself; the owner is the Gitea user/org that owns the images.
GITEA_REGISTRY = 'git.therogersfamily.tech'
GITEA_OWNER = 'josh'
// Jenkins credential ID for the Gitea push. Type: giteaPersonalAccessToken
// (the gitea-personal-access-token plugin). The PAT must have at least
// `write:package` scope. The token is bound as a Secret String at push time
// and fed to `docker login --password-stdin` using GITEA_OWNER as the
// username.
GITEA_CREDENTIALS = 'gitea-ci'
BACKEND_IMAGE = "${GITEA_REGISTRY}/${GITEA_OWNER}/yeschef-api"
FRONTEND_IMAGE = "${GITEA_REGISTRY}/${GITEA_OWNER}/yeschef-web"
IMAGE_TAG = "${env.BUILD_NUMBER}"
// Keep NuGet / npm caches inside the workspace so they survive across
// docker.inside containers without needing a shared host mount.
DOTNET_CLI_TELEMETRY_OPTOUT = '1'
NUGET_PACKAGES = "${env.WORKSPACE}/.nuget"
npm_config_cache = "${env.WORKSPACE}/.npm"
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Backend: restore & build') {
agent {
docker {
image "${DOTNET_SDK_IMAGE}"
reuseNode true
}
}
steps {
sh 'dotnet --info'
sh 'dotnet restore src/backend/YesChef.slnx'
sh 'dotnet build src/backend/YesChef.slnx -c Release --no-restore'
}
}
stage('Backend: unit tests') {
agent {
docker {
image "${DOTNET_SDK_IMAGE}"
reuseNode true
}
}
steps {
// global.json lives in src/backend and switches dotnet test to MTP mode,
// which requires --project (positional project paths are rejected).
dir('src/backend') {
sh '''
dotnet test \
--project YesChef.Api.UnitTests/YesChef.Api.UnitTests.csproj \
-c Release --no-build \
--report-trx --results-directory ../../TestResults/backend-unit
'''
}
}
post {
always {
junit allowEmptyResults: true, testResults: 'TestResults/backend-unit/**/*.trx'
}
}
}
stage('Backend: integration tests') {
steps {
script {
// Sidecar Postgres reachable from the build container at host `postgres`.
// Testcontainers can still spin up additional DBs because the host docker
// socket is mounted into the SDK container below.
docker.image("${POSTGRES_IMAGE}").withRun(
"-e POSTGRES_DB=${POSTGRES_DB}" +
" -e POSTGRES_USER=${POSTGRES_USER}" +
" -e POSTGRES_PASSWORD=${POSTGRES_PASSWORD}"
) { pg ->
// Wait for the DB to accept connections.
docker.image("${POSTGRES_IMAGE}").inside("--link ${pg.id}:postgres") {
sh '''
for i in $(seq 1 30); do
if pg_isready -h postgres -U yeschef >/dev/null 2>&1; then
echo "postgres is ready"
exit 0
fi
sleep 2
done
echo "postgres did not become ready in time" >&2
exit 1
'''
}
docker.image("${DOTNET_SDK_IMAGE}").inside(
"--link ${pg.id}:postgres" +
" -v /var/run/docker.sock:/var/run/docker.sock" +
" -e ConnectionStrings__DefaultConnection=Host=postgres;Port=5432;Database=${POSTGRES_DB};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD}"
) {
dir('src/backend') {
sh '''
dotnet test \
--project YesChef.Api.IntegrationTests/YesChef.Api.IntegrationTests.csproj \
-c Release \
--report-trx --results-directory ../../TestResults/backend-integration
'''
}
}
}
}
}
post {
always {
junit allowEmptyResults: true, testResults: 'TestResults/backend-integration/**/*.trx'
}
}
}
stage('Frontend') {
agent {
docker {
image "${NODE_IMAGE}"
reuseNode true
}
}
stages {
stage('Install') {
steps {
dir('src/frontend') { sh 'npm ci' }
}
}
stage('Type check') {
steps {
dir('src/frontend') { sh 'npm run check' }
}
}
stage('Unit tests') {
steps {
dir('src/frontend') { sh 'npm run test:unit' }
}
}
stage('Build') {
steps {
dir('src/frontend') { sh 'npm run build' }
}
}
}
}
stage('Docker images') {
steps {
script {
def backendImg = docker.build("${BACKEND_IMAGE}:${IMAGE_TAG}", 'src/backend/YesChef.Api')
def frontendImg = docker.build("${FRONTEND_IMAGE}:${IMAGE_TAG}", 'src/frontend')
backendImg.tag('latest')
frontendImg.tag('latest')
// Gitea PAT credential (giteaPersonalAccessToken) — exposes the
// token as a Secret String, so bind with `string` and feed it to
// docker login on stdin. Username for the package registry is the
// Gitea owner.
withCredentials([string(credentialsId: "${GITEA_CREDENTIALS}", variable: 'GITEA_TOKEN')]) {
sh """
set +x
echo "\$GITEA_TOKEN" | docker login ${GITEA_REGISTRY} -u ${GITEA_OWNER} --password-stdin
"""
try {
sh "docker push ${BACKEND_IMAGE}:${IMAGE_TAG}"
sh "docker push ${BACKEND_IMAGE}:latest"
sh "docker push ${FRONTEND_IMAGE}:${IMAGE_TAG}"
sh "docker push ${FRONTEND_IMAGE}:latest"
} finally {
sh "docker logout ${GITEA_REGISTRY} || true"
}
}
}
}
}
}
post {
always {
archiveArtifacts artifacts: 'TestResults/**/*.trx', allowEmptyArchive: true, fingerprint: false
cleanWs(deleteDirs: true, notFailBuild: true)
}
}
}