diff --git a/expenses_manager/Jenkinsfile b/expenses_manager/Jenkinsfile index 0b37eb8..80f8303 100644 --- a/expenses_manager/Jenkinsfile +++ b/expenses_manager/Jenkinsfile @@ -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}' \