Хочете мати швидкий зворотний зв’язок про помилки в коді ще до пушу на віддалений репозиторій? Зробімо свій локальний 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. ❤️

Прокоментувати
На сайті відображається лише твоє ім'я та коментар. Електронна пошта зберігається виключно для зв'язку з тобою за потреби та в жодному разі не передається стороннім особам.