From 906ffc6b0c2ba3679423174cda89755873812ce6 Mon Sep 17 00:00:00 2001 From: JKuijperM Date: Wed, 30 Sep 2026 18:10:25 +0200 Subject: [PATCH] Updating files to change python to 3.11 --- .dockerignore | 2 - Dockerfile | 23 +++-- expenses_manager/Jenkinsfile | 143 ++++++++++++++------------ expenses_manager/requirements-dev.txt | 11 ++ 4 files changed, 104 insertions(+), 75 deletions(-) create mode 100644 expenses_manager/requirements-dev.txt diff --git a/.dockerignore b/.dockerignore index 8897ed7..309316d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -26,8 +26,6 @@ # CI and test configuration, not needed at runtime. **/Jenkinsfile -**/pytest.ini -**/tests/ # Documentation and planning, not needed at runtime. **/*.md diff --git a/Dockerfile b/Dockerfile index 08fd0cb..82555c2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,5 @@ -FROM python:3.11-slim +FROM python:3.11-slim AS runtime -# Fail fast and log straight to stdout, so `docker-compose logs` is useful. ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 @@ -11,16 +10,14 @@ WORKDIR /app COPY expenses_manager/requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -# Copy the Django project itself, not the repository root, so that /app is -# the directory holding manage.py. The previous version copied the repo root -# into /app/expenses_manager, which left manage.py and wsgi.py one level -# deeper than the gunicorn command expected. +# Copy the Django project itself, not the repository root, so /app is the +# directory holding manage.py. COPY expenses_manager/ . # Collect static files at build time so the running container never needs to # write to its own filesystem. SECRET_KEY is required by settings.py when -# DEBUG is False, but collectstatic never uses it, so a throwaway value is -# enough — the real one comes from the environment at runtime. +# DEBUG is False but collectstatic never uses it, so a throwaway value is +# enough — the real one comes from the environment at runtime. RUN SECRET_KEY=build-time-placeholder \ DEBUG=False \ ALLOWED_HOSTS=localhost \ @@ -28,4 +25,12 @@ RUN SECRET_KEY=build-time-placeholder \ EXPOSE 8000 -CMD ["gunicorn", "expenses_manager.wsgi:application", "--bind", "0.0.0.0:8000"] \ No newline at end of file +CMD ["gunicorn", "expenses_manager.wsgi:application", "--bind", "0.0.0.0:8000"] + + +# The production image plus the test-only dependencies. Tests then run on +# exactly the Python version and packages production uses; the virtualenv this +# replaces ran on Python 3.14 while production runs 3.11. +FROM runtime AS test + +RUN pip install --no-cache-dir -r requirements-dev.txt \ No newline at end of file diff --git a/expenses_manager/Jenkinsfile b/expenses_manager/Jenkinsfile index 84f266f..e42f860 100644 --- a/expenses_manager/Jenkinsfile +++ b/expenses_manager/Jenkinsfile @@ -38,74 +38,89 @@ pipeline { options { timeout(time: 30, unit: 'MINUTES') } stages { - stage('Setup Python') { - steps { - sh ''' - cd expenses_manager - python --version - python -m venv venv - venv/bin/pip install --upgrade pip - venv/bin/pip install -r requirements.txt - ''' - } - } - - // Catches models changed without generating the migration. - stage('Check migrations') { - steps { - sh ''' - cd expenses_manager - venv/bin/python manage.py makemigrations --check --dry-run - ''' - } - } - - stage('Run tests') { - steps { - sh ''' - cd expenses_manager - venv/bin/pytest --cov --junitxml=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: 'expenses_manager/junit.xml' - } - } - } - - stage('Build image') { + stage('Build images') { steps { sshagent(credentials: ['nas-ssh']) { sh ''' SHA=$(echo "${GIT_COMMIT}" | cut -c1-7) TAG=$(echo "${BRANCH_NAME}" | tr '/' '-') - # Ship the workspace that was just tested rather than - # cloning again: what gets built is then provably the - # same code the tests ran against. + # Ship the workspace rather than cloning again on the + # NAS: what gets built is then provably the same code + # this build checked out. ssh ${SSH_OPTS} ${NAS_USER}@${NAS_HOST} \ "rm -rf ${CI_DIR} && mkdir -p ${CI_DIR}" - tar -czf - --exclude=./expenses_manager/venv \ - --exclude=./.git . \ + tar -czf - --exclude=./.git . \ | ssh ${SSH_OPTS} ${NAS_USER}@${NAS_HOST} \ "tar -xzf - -C ${CI_DIR}" + # The test image is the runtime image plus test + # dependencies, so the second build reuses every layer + # of the first. ssh ${SSH_OPTS} ${NAS_USER}@${NAS_HOST} \ "cd ${CI_DIR} && sudo ${NAS_DOCKER} build \ + --target runtime \ -t ${IMAGE}:${SHA} -t ${IMAGE}:${TAG} ." + + ssh ${SSH_OPTS} ${NAS_USER}@${NAS_HOST} \ + "cd ${CI_DIR} && sudo ${NAS_DOCKER} build \ + --target test \ + -t ${IMAGE}-test:${SHA} ." ''' } } } - // A successful build only proves the Dockerfile is syntactically - // sound. Django's own checks prove the image can actually start. + // Catches models changed without generating the migration. + stage('Check migrations') { + steps { + sshagent(credentials: ['nas-ssh']) { + sh ''' + SHA=$(echo "${GIT_COMMIT}" | cut -c1-7) + ssh ${SSH_OPTS} ${NAS_USER}@${NAS_HOST} \ + "sudo ${NAS_DOCKER} run --rm \ + -e SECRET_KEY=ci-only-value \ + -e DEBUG=True \ + -e DB_ENGINE=sqlite3 \ + ${IMAGE}-test:${SHA} \ + python manage.py makemigrations --check --dry-run" + ''' + } + } + } + + stage('Run tests') { + steps { + sshagent(credentials: ['nas-ssh']) { + // pytest output goes to stderr so it shows up in this log, + // while stdout carries only the JUnit XML. That avoids + // copying a root-owned file back from the NAS, which + // jenkins-ci could not read. + sh ''' + SHA=$(echo "${GIT_COMMIT}" | cut -c1-7) + ssh ${SSH_OPTS} ${NAS_USER}@${NAS_HOST} \ + "sudo ${NAS_DOCKER} run --rm \ + -e SECRET_KEY=ci-only-value \ + -e DEBUG=True \ + -e DB_ENGINE=sqlite3 \ + ${IMAGE}-test:${SHA} \ + sh -c 'pytest --cov --junitxml=/tmp/junit.xml >&2; \ + 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 { sshagent(credentials: ['nas-ssh']) { @@ -120,22 +135,22 @@ pipeline { ${IMAGE}:${SHA} python manage.py check" ''' - // A migrations directory without __init__.py is a namespace - // package, and Django deliberately ignores those: the app then - // appears to have no migrations at all, migrate --check always - // reports nothing pending, and a real migration would silently - // never be applied. This is what .gitignore line 6 caused. - sh ''' - SHA=$(echo "${GIT_COMMIT}" | cut -c1-7) - ssh ${SSH_OPTS} ${NAS_USER}@${NAS_HOST} \ - "sudo ${NAS_DOCKER} run --rm \ - -e SECRET_KEY=smoke-test-only \ - -e DEBUG=False \ - -e ALLOWED_HOSTS=localhost \ - -e DB_ENGINE=sqlite3 \ - ${IMAGE}:${SHA} python manage.py showmigrations expenses" \ - | grep -q '0001_initial' - ''' + // A migrations directory without __init__.py is a namespace + // package, and Django deliberately ignores those: the app + // then appears to have no migrations at all, migrate --check + // always reports nothing pending, and a real migration would + // silently never be applied. + sh ''' + SHA=$(echo "${GIT_COMMIT}" | cut -c1-7) + ssh ${SSH_OPTS} ${NAS_USER}@${NAS_HOST} \ + "sudo ${NAS_DOCKER} run --rm \ + -e SECRET_KEY=smoke-test-only \ + -e DEBUG=False \ + -e ALLOWED_HOSTS=localhost \ + -e DB_ENGINE=sqlite3 \ + ${IMAGE}:${SHA} python manage.py showmigrations expenses" \ + | grep -q '0001_initial' + ''' } } } diff --git a/expenses_manager/requirements-dev.txt b/expenses_manager/requirements-dev.txt new file mode 100644 index 0000000..f728dfa --- /dev/null +++ b/expenses_manager/requirements-dev.txt @@ -0,0 +1,11 @@ +-r requirements.txt + +colorama==0.4.6 +coverage==7.13.1 +iniconfig==2.3.0 +pluggy==1.6.0 +Pygments==2.19.2 +pytest==9.0.2 +pytest-cov>=4.0 +pytest-django==4.11.1 +toml==0.10.2 \ No newline at end of file