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