Хочете мати швидкий зворотний зв’язок про помилки в коді ще до пушу на віддалений репозиторій? Зробімо свій локальний CI, який запускає лінтери й тести автоматично — за допомогою Git hooks, Docker на Linux і user-сервісів systemd. Це працює на будь-якому дистрибутиві, комфортно інтегрується у ваш робочий процес і не ламає середовище розробки для python в Linux. А ще ми напишемо зручні bash скрипти і додамо фоновий режим. 🚀

План такий: швидкі перевірки перед комітом і пушем (hooks), відтворюване середовище в контейнері, плюс фонова перевірка через systemd.path. Як бонус — альтернативи, включно з cron та systemd timers, і короткий GUI-варіант.

Вимоги і підготовка середовища

Потрібні встановлені Git, Docker та базові інструменти. Приклад для Debian/Ubuntu:

sudo apt update
sudo apt install -y docker.io git python3-venv
sudo usermod -aG docker "$USER"
newgrp docker  # оновити групи без перезавантаження сесії

Перевірте, що Docker працює без sudo:

docker run --rm hello-world

Крок 1. Git hooks для миттєвого фідбеку

Hooks виконуються автоматично під час подій Git. Ми додамо два: pre-commit (швидкі перевірки змінених файлів) і pre-push (повніші тести перед відправкою).

Створюємо pre-commit

Скрипт лінтить тільки проіндексовані Python-файли, а потім запускає швидкі тести в Docker.

cat > .git/hooks/pre-commit <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

# Знайти проіндексовані .py файли
files=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\\.py$' || true)

# Побудувати образ для локального CI (тихо, щоб не засмічувати вивід)
echo "[local CI] building Docker image..."
docker build -t localci:py312 -q .

# Якщо є змінені Python-файли — швидко лінтимо їх
if [[ -n "$files" ]]; then
  echo "[local CI] linting staged files..."
  docker run --rm -v "$PWD":/app -w /app localci:py312 bash -lc \
    "ruff \\"$files\\" && black --check \\"$files\\" && mypy \\"$files\\""
fi

# Швидкий прогін тестів (мінімальний набір)
echo "[local CI] running quick tests..."
docker run --rm -v "$PWD":/app -w /app localci:py312 bash -lc \
  'pytest -q -m "not slow"'

exit 0
EOF
chmod +x .git/hooks/pre-commit

Створюємо pre-push

Перед пушем — повні тести у відтворюваному середовищі.

cat > .git/hooks/pre-push <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

echo "[local CI] full test suite before push..."
docker build -t localci:py312 -q .
docker run --rm -v "$PWD":/app -w /app localci:py312 bash -lc \
  'ruff . && black --check . && mypy . && pytest -q'

exit 0
EOF
chmod +x .git/hooks/pre-push

Крок 2. Docker середовище для CI

Все буде запускатися в одному контейнері, тож різні комп’ютери даватимуть однаковий результат. Спершу опишемо dev-залежності, потім Dockerfile.

cat > requirements-dev.txt <<'EOF'
ruff==0.4.5
black==24.2.0
mypy==1.10.0
pytest==8.2.1
EOF

cat > Dockerfile <<'EOF'
FROM python:3.12-slim
WORKDIR /app

# Інструменти для складання залежностей
RUN apt-get update \
 && apt-get install -y --no-install-recommends git build-essential \
 && rm -rf /var/lib/apt/lists/*

# Кешуємо залежності окремим шаром
COPY requirements-dev.txt /tmp/requirements-dev.txt
RUN pip install --no-cache-dir -r /tmp/requirements-dev.txt

ENV PYTHONDONTWRITEBYTECODE=1 \\
    PYTHONUNBUFFERED=1

# Типовий командний прогін (можна змінити)
CMD ["bash", "-lc", "pytest -q"]
EOF

# Ручна перевірка образу
docker build -t localci:py312 .
docker run --rm -v "$PWD":/app -w /app localci:py312

Тепер і hooks, і ручні запуски використовують одне й те саме ізольоване середовище.

Крок 3. Автоматичний фон: systemd.path стежить за змінами

Хочете, щоб тести бігали автоматично, коли гілка оновлюється (merge/pull)? Зробимо user-сервіс systemd, що тригериться при зміні файлів у Git.

Один сервіс + path-юнит

Створімо сервіс, який збирає образ і запускає тести в контейнері. Вкажіть шлях до свого репозиторію і гілки.

mkdir -p ~/.config/systemd/user

cat > ~/.config/systemd/user/localci.service <<'EOF'
[Unit]
Description=Local CI run for my-project

[Service]
Type=oneshot
WorkingDirectory=/home/USER/path/to/project
ExecStart=/usr/bin/env bash -lc 'git fetch --all -q || true; docker build -t localci:py312 -q .; docker run --rm -v "$PWD":/app -w /app localci:py312 bash -lc "pytest -q"'
TimeoutStartSec=0
EOF

# Слідкуємо за зміною HEAD поточної гілки (наприклад, main)
cat > ~/.config/systemd/user/localci.path <<'EOF'
[Unit]
Description=Watch repo changes for local CI

[Path]
PathChanged=/home/USER/path/to/project/.git/refs/heads/main
PathChanged=/home/USER/path/to/project/requirements-dev.txt

[Install]
WantedBy=default.target
EOF

systemctl --user daemon-reload
systemctl --user enable --now localci.path

# Щоб працювало без відкритої сесії (опційно):
loginctl enable-linger "$USER"

Тепер при зміні гілки main сервіс автоматично пройде тести у фоні. Перевірити стан можна так:

systemctl --user status localci.service
systemctl --user list-units --type=path

Альтернативні способи

  • Framework pre-commit: встановлює лінтери через YAML-конфіг і керує хуками. Зручно, якщо не хочете писати свої bash скрипти.
  • tox/nox: стандартизують запуск середовищ і команд, легко перенести конфіг у CI/CD.
  • Podman як заміна Docker без root; командами йде подібно.
  • cron та systemd timers: замість *.path можна періодично запускати тести. Наприклад, кожні 5 хвилин:
cat > ~/.config/systemd/user/localci.timer <<'EOF'
[Unit]
Description=Periodic Local CI

[Timer]
OnBootSec=2m
OnUnitActiveSec=5m
Unit=localci.service

[Install]
WantedBy=timers.target
EOF

systemctl --user daemon-reload
systemctl --user enable --now localci.timer

GUI-спосіб (якщо доречно)

VS Code допоможе запускати все однією кнопкою: додайте tasks.json із командами docker build і docker run, увімкніть Problem Matchers для pytest/flake8/ruff, а через розширення Docker переглядайте логи контейнера. Так ви поєднаєте комфорт GUI Linux із надійністю контейнерів і hooks. 🔧

FAQ

Hooks не виконуються або "Permission denied"

Переконайтеся, що файли мають право на виконання:

chmod +x .git/hooks/pre-commit .git/hooks/pre-push

"Got permission denied while trying to connect to the Docker daemon"

Додайте себе в групу docker і перезапустіть сесію:

sudo usermod -aG docker "$USER"
newgrp docker

Занадто повільний build

Тримайте залежності в окремому шарі, не копіюйте весь проєкт до кроку інсталяції, використовуйте фіксовані версії і не чистіть кеш шарів без потреби. Для pip--no-cache-dir, для Docker — реюз шарів.

systemd user не стартує

Перевірте статус і журнали, а також lingering:

systemctl --user status localci.path localci.service
journalctl --user -u localci.service -e
loginctl enable-linger "$USER"

Тести локально проходять, у контейнері — ні

Перевірте залежності, шляхи і версії Python. Запускайте все через контейнер і локально однаковими командами. Розгляньте tox, щоб вирівняти середовища.

Де зберігати секрети?

Не комітьте їх у репозиторій. Використовуйте змінні середовища або окремий .env, який ігнорується .gitignore. Для контейнера — --env-file.

Порада від Kernelka

Тримайте pre-commit максимально швидким (до 5–10 секунд): лінтьте тільки змінені файли, а повні тести запускайте у pre-push або у фоні через systemd. Маленькі кроки — велика стабільність релізів!

Підсумок

  • Git hooks дають миттєвий фідбек перед комітом/пушем.
  • Docker гарантує відтворюване середовище перевірок.
  • systemd path/timer автоматизує фонові прогони.
  • Альтернативи: pre-commit, tox/nox, Podman, cron.
  • Швидкість — через кеші й таргетовані перевірки.

Готово! Ваш локальний CI дисциплінує кодову базу і економить час команди — ще до того, як задачі дістануться віддаленого CI. ❤️