Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
.env
venv
.venv
**/__pycache__
**/*.pyc
.idea/
.git/
.github/
/log/**
30 changes: 28 additions & 2 deletions .github/workflows/backend-postgres-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,36 @@ concurrency:
cancel-in-progress: true

jobs:
docker-image:
name: Production Docker image
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4

- name: Build production Dockerfile without publishing
run: docker build --tag procollab-prod-dependencies-ci .

- name: Check production configuration inside the image
run: |
docker run --rm \
-e DJANGO_SETTINGS_MODULE=procollab.settings_ci \
-e DJANGO_SECRET_KEY=ci-only-image-test-secret \
procollab-prod-dependencies-ci python manage.py check

- name: Test native CV PDF and persisted credentials inside the image
run: |
docker run --rm \
-e DEBUG=True \
-e NEXTGEN_SURFACE_ENABLED=False \
-e DJANGO_SECRET_KEY=ci-only-image-test-secret \
procollab-prod-dependencies-ci python manage.py test \
core.test_dependency_compatibility users.tests.test_cv_native_pdf --verbosity 2

postgres-tests:
name: Backend PostgreSQL CI
runs-on: ubuntu-latest
timeout-minutes: 30
timeout-minutes: 60

services:
postgres:
Expand Down Expand Up @@ -64,7 +90,7 @@ jobs:

- name: Install dependencies
run: |
python -m pip install "poetry==1.2.2"
python -m pip install "poetry==2.5.1"
poetry config virtualenvs.create true
poetry install --no-interaction --no-ansi --no-root

Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ jobs:
uses: actions/cache@v4
with:
path: ~/.local
key: poetry-1.2.2-${{ hashFiles('**/poetry.lock') }}
key: poetry-2.5.1-${{ hashFiles('**/poetry.lock') }}

- uses: snok/install-poetry@v1
with:
version: 1.2.2
version: 2.5.1
virtualenvs-create: true
virtualenvs-in-project: true

Expand All @@ -58,7 +58,7 @@ jobs:
pydeps-

- name: Install dependencies
run: poetry install
run: poetry install --no-root --no-interaction

- name: Run production surface quarantine tests
run: >-
Expand Down
8 changes: 3 additions & 5 deletions .github/workflows/release-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ jobs:
uses: actions/cache@v4
with:
path: ~/.local
key: poetry-1.2.2-0
key: poetry-2.5.1-0

- uses: snok/install-poetry@v1
with:
version: 1.2.2
version: 2.5.1
virtualenvs-create: true
virtualenvs-in-project: true

Expand All @@ -79,9 +79,7 @@ jobs:
path: .venv
key: pydeps-${{ hashFiles('**/poetry.lock') }}

- run: poetry install

- run: poetry run pip install setuptools
- run: poetry install --no-root --no-interaction

- name: Run tests
run: poetry run python manage.py test
Expand Down
8 changes: 5 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
FROM python:3.11

RUN apt update --no-install-recommends -y
RUN apt-get update && apt-get install -y --no-install-recommends \
libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz-subset0 fontconfig libjpeg62-turbo libopenjp2-7 \
&& rm -rf /var/lib/apt/lists/*

ENV PYTHONFAULTHANDLER=1 \
PYTHONUNBUFFERED=1 \
PYTHONHASHSEED=random \
PIP_NO_CACHE_DIR=off \
PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=100 \
POETRY_VERSION=1.2.2
POETRY_VERSION=2.5.1

RUN pip install "poetry==$POETRY_VERSION"

Expand All @@ -23,7 +25,7 @@ RUN poetry config virtualenvs.create false \
&& poetry install --no-root

RUN mkdir /procollab/staticfiles
RUN mkdir /procollab/static
RUN mkdir -p /procollab/static /procollab/log

COPY . /procollab/

Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ Backend API для продукта Procollab.

## Базовые команды

Используются Python 3.11, Poetry 2.5.1 и PostgreSQL 14 или новее.

```bash
poetry install
poetry install --no-root
poetry run python manage.py migrate
poetry run python manage.py runserver
poetry run python manage.py test
Expand All @@ -28,4 +30,5 @@ poetry run python manage.py test
- [Архитектура](docs/architecture.md)
- [API](docs/api.md)
- [Инфраструктура и деплой](docs/devops-state.md)
- [Подготовка обновления зависимостей prod](docs/prod-dependencies-20261103.md)
- [Доменные модули](docs/modules/readme.md)
49 changes: 49 additions & 0 deletions core/test_dependency_compatibility.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Compatibility of persisted credentials, static assets and image uploads."""

from io import BytesIO
from types import SimpleNamespace

from django.contrib.auth.hashers import check_password
from django.core.files.storage import storages
from django.test import SimpleTestCase, override_settings
from PIL import Image
from whitenoise.storage import CompressedStaticFilesStorage

from files.helpers import convert_image_to_webp


@override_settings(
PASSWORD_HASHERS=[
"django.contrib.auth.hashers.BCryptPasswordHasher",
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
]
)
class PersistedPasswordCompatibilityTests(SimpleTestCase):
# Synthetic fixtures produced by the previous Django 4.2.11 / bcrypt 4.1.2.
def test_existing_long_bcrypt_password_still_authenticates(self):
encoded = "bcrypt$$2b$04$qs8/QCj6RsBopogj..srsuvHeg/qOtoW44PjQvsji2su7J6I9z7TK"
self.assertTrue(check_password("legacy-password-" + "x" * 100, encoded))
self.assertFalse(check_password("different-password-" + "x" * 100, encoded))

def test_existing_pbkdf2_password_still_authenticates(self):
encoded = (
"pbkdf2_sha256$390000$migration-test-salt$"
"cGWmSyv+hMl8eGjeBZL5rFJkIvidubO6dg6yOT99zIM="
)
self.assertTrue(check_password("legacy-secret-2026", encoded))
self.assertFalse(check_password("different-secret", encoded))


class DependencyIntegrationTests(SimpleTestCase):
def test_static_assets_keep_whitenoise_compression(self):
self.assertIsInstance(storages["staticfiles"], CompressedStaticFilesStorage)

def test_uploaded_rgba_image_can_be_converted_to_webp(self):
upload = BytesIO()
Image.new("RGBA", (8, 6), (40, 80, 120, 51)).save(upload, format="PNG")
upload.seek(0)
encoded = convert_image_to_webp(SimpleNamespace(file=upload))
with Image.open(BytesIO(encoded.buffer())) as image:
self.assertEqual(image.format, "WEBP")
self.assertEqual(image.size, (8, 6))
self.assertEqual(image.getchannel("A").getextrema(), (51, 51))
107 changes: 107 additions & 0 deletions docs/prod-dependencies-20261103.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Подготовка обновления зависимостей prod, 3 ноября 2026

PR подготовлен от `master` (`f16709f8b8a7422f4ed5392bd4d7024e2e5a13a3`).
Он переносит обновление зависимостей на код prod. Функции dev, новые режимы
программ и переключение `NEXTGEN_SURFACE_ENABLED` в него не входят.
Python остается в серии 3.11, Django ограничен веткой 5.2 LTS.

## Изменения

| Пакет | Было | Стало |
| --- | --- | --- |
| Django | 4.2.11 | 5.2.18 |
| DRF | 3.15.1 | 3.18.3 |
| SimpleJWT | 5.3.1 | 5.5.1 |
| Requests | 2.31.0 | 2.34.2 |
| Pillow | 9.5.0 | 13.0.0 |
| WeasyPrint | 62.3 | 70.0 |
| cryptography | 42.0.5 | 50.0.2 |
| Celery | 5.4.0 | 5.6.3 |
| django-celery-beat | 2.6.0 | 2.9.0 |
| psycopg2-binary | 2.9.9 | 2.9.13 |

Основные версии совпадают с проверенными на dev. Из lock-файла исключены пакеты
для проверки dev-контрактов; всего проверена 141 запись, включая альтернативы
для разных версий Python. Ограничение `bcrypt>=4.3,<5` сохраняет проверку старых
bcrypt-паролей длиной более 72 байт.

`STATICFILES_STORAGE` заменен на `STORAGES`: по умолчанию остается файловое
хранилище, для статики — WhiteNoise со сжатием. Docker получает Pango/HarfBuzz
для настоящего PDF-движка. Poetry 2.5.1 и `install --no-root` согласованы в Docker,
общем CI, PostgreSQL CI и существующем release workflow.

Обновленная django-rest-passwordreset переопределяет выбор ограничителей
частоты запросов. В нашем подклассе явно сохранен `PostOnlyScopedRateThrottle`
со scope `auth_reset_password`: штатный лимит сброса пароля не отключается.
Несуществующий email получает общий успешный ответ библиотеки, без раскрытия
наличия аккаунта; ограничение частоты применяется и к нему.

## Миграции и проверка данных

Новая `users.0066_alter_userachievement_files` привязана к собственным миграциям
master: `users.0065_expand_university_directory` и `files.0007`. Она фиксирует
`through_fields` в состоянии модели; `sqlmigrate` на PostgreSQL дает `(no-op)`.
Также применяются новые миграции beat, passwordreset, taggit и token_blacklist.

Локальная репетиция выполнялась на отдельной синтетической PostgreSQL 16:

1. Исходный master и Django 4.2.11 создали схему, пользователя, программу,
поданный проект, анкету, достижение с файлом и access/refresh-токены.
2. Кандидат с Django 5.2.18 применил новые миграции. Сравнены хеши строк всех
таблиц users/projects/partner_programs/files/project_rates: исходные данные
сохранились, пароль и связь достижения с файлом работают.
3. Старые access/refresh приняты; refresh успешно обновлен. Старый Django 4.2.11
после миграций также прочитал те же записи и связи.

Это проверка совместимости prod-кода на искусственных данных, не репетиция
на копии рабочей базы. Сетевые обращения к внешним сервисам в ней запрещены.

## Автоматические проверки

В PR CI сохранены отдельный прогон карантина prod API с выключенным NEXTGEN,
проверки блокировок/ограничений PostgreSQL и полный набор с включенным NEXTGEN.
Сборка production Dockerfile выполняется без публикации образа. Внутри образа
проверяются Django, старые пароли, WebP и настоящие PDF: скачивание резюме и
вложение в письмо через локальную тестовую почту.

Обновление не меняет обработчик регистрации с внешнего сайта; его существующие
API-тесты входят в полный набор. Исправление защиты внешней регистрации —
отдельная задача и не считается выполненным обновлением библиотек.

`pip-audit` на 3 ноября 2026 проверил все 141 lock-запись по PyPI: известных
уязвимостей и пропущенных записей нет. Это сведения о Python-пакетах,
не полный аудит приложения или системных библиотек Linux-образа.

Итоговые ссылки CI и точные результаты находятся в описании PR #853.

## До разрешения на выкладку

Ничего из этого PR не сливается и не выкатывается автоматически в рамках
подготовки. Release workflow не запускался; рабочая база не мигрировалась,
контейнеры prod не перезапускались и реальные письма не отправлялись.

Для проверки работающего prod подготовлен отдельный служебный workflow, который
читает состояние контейнеров и выполняет SELECT в транзакции READ ONLY. SSH
подключение требует независимо подтвержденного ключа prod. Проверка сервера
отложена до этапа перед выкладкой; в рамках подготовки PR доступа к prod нет.

Перед выпуском остаются обязательные условия:

- Подтвердить фактические PostgreSQL 14+, исходный коммит/образ prod, состояние
миграций, свободное место и достаточные права роли мигратора.
- Сверить рабочий коммит с master: в master есть самостоятельные изменения после
последнего известного release-коммита; их нельзя считать автоматически
проверенными или уже установленными только по состоянию ветки.
- Создать свежую защищенную резервную копию prod, проверить ее восстановление
в изолированную базу и применить там миграции кандидата. Сохранить старый образ
и конфигурацию. Резервная копия dev не является копией prod.
- Выбрать окно выкладки и действия при ошибке. Восстановление всей базы после
возобновления записи может потерять новые пользовательские изменения; оно
требует отдельного решения, а не автоматического отката к старому архиву.
- После отдельно разрешенной выкладки проверить действующий фронт, вход/refresh,
профиль, файлы, программу/проект, внешнюю регистрацию, админку, PDF и очередь.
Реальную регистрацию или отправку писем согласовывать по конкретным адресатам;
тесты PR не используют настоящих пользователей.

В текущем release workflow нет автоматического backup. Зеленый CI сам по себе
не снимает перечисленные условия допуска к prod.
Loading
Loading