dev #39

Merged
jkuijperm merged 7 commits from dev into main 2026-10-01 10:14:44 +00:00
Showing only changes of commit 6bc6a35e30 - Show all commits

View File

@ -4,8 +4,8 @@ pipeline {
agent none
environment {
// Jenkins has no Docker daemon, so everything Docker-related happens on
// the NAS over SSH. Absolute paths are required: the remote
// Everything Docker-related happens on the NAS over SSH: Jenkins has no
// daemon of its own. Absolute paths are required — the remote
// non-interactive PATH does not include /usr/local/bin.
NAS_HOST = '192.168.1.140'
NAS_USER = 'jenkins-ci'
@ -26,8 +26,7 @@ pipeline {
stages {
// Nested stages share one agent allocation, so the venv built in the
// first stage is still there for the ones that follow.
// Nested stages share one agent allocation.
stage('CI') {
agent any
options { timeout(time: 30, unit: 'MINUTES') }
@ -68,7 +67,6 @@ pipeline {
}
// Catches models changed without generating the migration.
// Catches models changed without generating the migration.
stage('Check migrations') {
steps {
sshagent(credentials: ['nas-ssh']) {
@ -105,18 +103,20 @@ pipeline {
-e DB_ENGINE=sqlite3 \
${IMAGE}-test:${SHA} \
sh -c 'pytest --cov --junitxml=/tmp/junit.xml >&2; \
rc=\\$?; cat /tmp/junit.xml; exit \\$rc'" \
rc=\\$?; cat /tmp/junit.xml; exit \\$rc'" \
> junit.xml
'''
}
}
post {
always {
// allowEmptyResults: false is deliberate. A build where
// pytest died before collecting anything produces no XML,
// and that must fail rather than pass quietly.
junit allowEmptyResults: false, testResults: 'junit.xml'
}
}
}
}
stage('Smoke test image') {
steps {
@ -176,8 +176,8 @@ pipeline {
if (rc != 0) {
// Printing the plan also proves the database is reachable.
// If the non-zero status above was a connection failure
// rather than pending migrations, this fails loudly
// instead of asking for approval on a false premise.
// rather than pending migrations, this fails loudly instead
// of asking for approval on a false premise.
sh '''
SHA=$(echo "${GIT_COMMIT}" | cut -c1-7)
ssh ${SSH_OPTS} ${NAS_USER}@${NAS_HOST} \
@ -217,8 +217,8 @@ pipeline {
sh '''
SHA=$(echo "${GIT_COMMIT}" | cut -c1-7)
# Always back up, even with no migrations pending: this is
# the restore point for this particular deployment.
# Always back up, even with no migrations pending: this is the
# restore point for this particular deployment.
ssh ${SSH_OPTS} ${NAS_USER}@${NAS_HOST} "sudo ${NAS_BACKUP}"
# Migrate with the new image before the running container is
@ -230,21 +230,20 @@ pipeline {
--env-file ${APP_ENV} \
${IMAGE}:${SHA} python manage.py migrate --noinput"
# Point compose at the image that was just tested, then swap
# the container. Writing the tag to .env leaves a readable
# record of what is deployed, and makes rollback a one-line
# edit plus "docker-compose up -d web".
# Point compose at the image that was just tested, then swap the
# container. Writing the tag to .env leaves a readable record of
# what is deployed and makes rollback a one-line edit.
ssh ${SSH_OPTS} ${NAS_USER}@${NAS_HOST} \
"echo WEB_IMAGE=${IMAGE}:${SHA} > ${COMPOSE_DIR}/.env \
&& cd ${COMPOSE_DIR} \
&& sudo ${NAS_COMPOSE} up -d web"
&& cd ${COMPOSE_DIR} \
&& sudo ${NAS_COMPOSE} up -d web"
'''
script {
// The Host header must match ALLOWED_HOSTS or Django answers
// 400 regardless of health. A 301 is the expected reply here:
// SECURE_SSL_REDIRECT sends plain HTTP to HTTPS, and reaching
// that redirect already proves the app processed the request.
// The Host header must match ALLOWED_HOSTS or Django answers 400
// regardless of health. A 301 is expected: SECURE_SSL_REDIRECT
// sends plain HTTP to HTTPS, and reaching that redirect already
// proves the app processed the request.
def code = sh(returnStdout: true, script: '''
ssh ${SSH_OPTS} ${NAS_USER}@${NAS_HOST} \
"sleep 10 && curl -s -o /dev/null -w '%{http_code}' \