From 0e59fd5bad3878d69d61eca5f2519a02b3d49c5c Mon Sep 17 00:00:00 2001 From: Josh Rogers Date: Fri, 15 May 2026 23:44:53 -0500 Subject: [PATCH] Add Jenkinsfile for build, test, and Gitea image push Co-Authored-By: Claude Opus 4.7 (1M context) --- Jenkinsfile | 223 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 Jenkinsfile diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..ab98c32 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,223 @@ +// 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) + } + } +}