Практический GitHub Actions: полноценный CI/CD для FastAPI + PostgreSQL


❯ Вступление
В этой статье будет разбираться написание GitHub Actions пайплайна на основе моего стенда cicd-practice на GitHub (подробно все можно посмотреть там). Стенд состоит из FastAPI (веб API на Python) и PostgreSQL. Все сервисы разворачиваются в Docker с помощью Docker Compose (микросервисы) на VDS от Timeweb Cloud. Подробнее об остальных инструментах будет ниже, в отдельном блоке.
Для части приложения я также написал около 90 Pytest тестов, которые будут воспроизводиться в пайплайне, но об этом позже.
Backend-часть также будет разбираться, но менее подробно, для общего понимания работы. Уточню, что я не являюсь backend-разработчиком, поэтому часть приложения нужна только для симуляции рабочей архитектуры, поверх которой будет строиться CI/CD.
Немного терминов
Пайплайн — задачи организованные в виде непрерывного конвейера для автоматизации работы и передачи результатов от одного этапа к другому
CI (Continuous Integration) — это практика автоматической сборки и тестирования кода каждый раз, когда разработчик добавляет новые изменения в общий репозиторий.
CD (Continuous Delivery / Deployment) — автоматическая подготовка проверенного кода к релизу или его автоматический запуск на сервере для пользователей
❯ Подробнее про инструменты
Ниже перечислены некоторые основные используемые инструменты:
Python 3.13 — основной язык проекта.
FastAPI — создание REST API.
Uvicorn — запуск FastAPI-приложения.
PostgreSQL 18 — основная и тестовая базы данных.
SQLAlchemy — работа с базой данных через ORM.
Alembic — миграции схемы базы данных.
Pydantic — валидация данных и конфигурации.
JWT / OAuth2 — аутентификация и авторизация пользователей.
Docker — упаковка приложения в контейнеры.
Docker Compose — запуск backend, PostgreSQL, pgAdmin и тестов.
GitHub Actions — автоматизация CI/CD.
Ruff — проверка качества Python-кода.
pytest — запуск unit- и интеграционных тестов.
Bandit — статический анализ безопасности Python-кода.
pip-audit — проверка зависимостей на уязвимости.
GitHub Container Registry — хранение Docker-образов.
SSH Action — деплой приложения на удалённый сервер.
pgAdmin — веб-интерфейс для управления PostgreSQL.
❯ Docker Compose (про каждый микросервис)
Ниже представлен основной docker-compose.yml файл, в котором находятся все развертываемые сервисы.
Основными двумя контейнерами являются backend и main_db, pgadmin4 используется как интерфейс для удобного взаимодействия с базой.
test_db и tests контейнеры нужны только для интеграционных и юнит тестов.
Интеграционные тесты — это проверка совместной работы нескольких модулей.
Юнит тесты — отдельные проверки определенных частей кода, функций.
services:
test_db: # Тестовая база данных
image: postgres:18
environment:
- POSTGRES_DB=${TEST_DB_NAME}
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_USER=${POSTGRES_USER}
networks:
- backend
healthcheck: # Проверка готовности СУБД перед запуском тестов
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${TEST_DB_NAME}"]
interval: 5s
timeout: 5s
retries: 5
ports:
- 5433:5432 # Порт 5433, чтобы не конфликтовать с main_db
command: >
postgres -c listen_addresses='*'
backend: # Основное FastAPI приложение
image: ${BACKEND_IMAGE:-ghcr.io/canntstand/cicd-practice-backend:latest} # Образ из GHCR после CI/CD
networks:
- backend
volumes:
- ./backend:/backend # Синхронизация кода с хостом для hot-reload
environment:
- DB_URL=${DB_URL}
- SECRET_KEY=${SECRET_KEY}
- REFRESH_SECRET_KEY=${REFRESH_SECRET_KEY}
- ALGORITHM=${ALGORITHM}
- TOKEN_EXPIRE_MINUTES=${TOKEN_EXPIRE_MINUTES}
- REFRESH_TOKEN_EXPIRE_MINUTES=${REFRESH_TOKEN_EXPIRE_MINUTES}
- REFRESH_ALGORITHM=${REFRESH_ALGORITHM}
command: > # Накат миграций Alembic и запуск Uvicorn
sh -c "alembic upgrade head && uvicorn app.main:app
--host 0.0.0.0 --port 8000 --reload --reload-dir /backend/app"
depends_on:
main_db:
condition: service_healthy # Ожидание готовности основной БД
ports:
- "8000:8000"
pgadmin4: # Веб-интерфейс для работы с базой
image: dpage/pgadmin4:latest
ports:
- 5050:80
environment:
- PGADMIN_DEFAULT_EMAIL=${PGADMIN_DEFAULT_EMAIL}
- PGADMIN_DEFAULT_PASSWORD=${PGADMIN_DEFAULT_PASSWORD}
depends_on:
- main_db
volumes:
- pgadmin4_data:/var/lib/pgadmin # Хранение настроек pgAdmin
networks:
- backend
tests: # Контейнер для автоматического запуска тестов
build:
context: .
dockerfile: Dockerfile.tests # Изолированный Dockerfile для тестов
networks:
- backend
environment:
- DB_URL=${TEST_DB_URL}
- SECRET_KEY=${SECRET_KEY}
- REFRESH_SECRET_KEY=${REFRESH_SECRET_KEY}
- ALGORITHM=${ALGORITHM}
- TOKEN_EXPIRE_MINUTES=${TOKEN_EXPIRE_MINUTES}
- REFRESH_TOKEN_EXPIRE_MINUTES=${REFRESH_TOKEN_EXPIRE_MINUTES}
- REFRESH_ALGORITHM=${REFRESH_ALGORITHM}
command: > # Накат миграций на тестовую БД и запуск Pytest
sh -c "set -e;
cd backend && alembic upgrade head;
cd .. && pytest"
tty: true
stdin_open: true
volumes:
- ./tests:/test/tests
- ./backend:/test/backend
depends_on:
test_db:
condition: service_healthy # Запуск тестов только после готовности test_db
main_db: # Основная база данных (PostgreSQL)
image: postgres:18
environment:
- POSTGRES_DB=${DB_NAME}
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_USER=${POSTGRES_USER}
ports:
- 5432:5432
networks:
- backend
volumes:
- pg_main_data:/var/lib/postgresql/ # Хранение постоянных данных БД
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${DB_NAME}"]
interval: 5s
timeout: 5s
retries: 5
command: >
postgres -c listen_addresses='*'
volumes: # Именованные тома для сохранения данных
pg_main_data:
pgadmin4_data:
networks: # Изолированная сеть для взаимодействия контейнеров
backend:
driver: bridge❯ Архитектура Backend
Ниже представлены 3 схемы, которые отображают общую работу приложения.



Теперь приведу пример кода аутентификации для наглядности.
У меня логика эндпоинтов разделена на 2 части:
В routers: функции, которые принимают веб-запросы, вызывают функции из services и отдают результат.
В services: функции, которые отвечают за работу непосредственно с базой данных.
routers/auth.py:
# Импорт основных компонентов FastAPI
from fastapi import APIRouter, Cookie, Depends, HTTPException, Response
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
# Внутренние модули проекта
from ..database.database import get_db
from ..services import auth
from ..utils.dependencies import get_current_user, refresh_access_token
from ..validation import schemas
# Создание роутера с базовым префиксом /api и тегами для Документации (Swagger)
router = APIRouter(prefix="/api", tags=["Auth", "API"])
@router.post("/register", status_code=201)
def register(body: schemas.User, db: Session = Depends(get_db)):
"""Регистрация нового пользователя."""
# Вызов сервиса регистрации для записи пользователя в БД
is_success = auth.register(body, db)
if is_success:
return {"message": "Successfully registered"}
raise HTTPException(500, detail="Registration failed")
@router.post("/login", status_code=200, response_model=schemas.TokenResp)
def login(form: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
"""Аутентификация пользователя по логину/паролю и выдача JWT-токенов."""
return auth.login(form, db)
@router.delete("/logout", status_code=204)
def logout(
refresh_token: str = Cookie(None),
db: Session = Depends(get_db),
user_id: int = Depends(get_current_user), # Проверка авторизации через access_token
):
if not refresh_token:
raise HTTPException(400, detail="Refresh token missing")
# Удаление токена из базы и очистка client-side Cookie
if auth.logout(refresh_token, db):
response = Response(status_code=204)
response.delete_cookie("access_token")
response.delete_cookie("refresh_token")
return response
raise HTTPException(500, detail="something went wrong in logout function")
@router.post("/refresh", status_code=200, response_model=schemas.TokenResp)
def refresh_token_pair(
refresh_token: str = Cookie(None),
db: Session = Depends(get_db)
):
"""Обновление пары токенов (Access + Refresh) с использованием Refresh-токена из Cookie."""
if not refresh_token:
raise HTTPException(400, detail="Refresh token missing")
return refresh_access_token(refresh_token, db)services/auth.py:
import datetime
# FastAPI и инструменты безопасности
from fastapi import HTTPException, Response
from fastapi.security import OAuth2PasswordRequestForm
# Работа с JWT и ORM SQLAlchemy
from jose import jwt
from sqlalchemy import delete
from sqlalchemy.orm import Session
# Внутренние модули приложения
from ..config import settings as ss
from ..database import models
from ..utils.dependencies import create_token_pair
from ..utils.exc import db_exc_check
from ..utils.hash import hash_pwd, verify_pwd
from ..validation import schemas
from . import users
@db_exc_check
def register(body: schemas.User, db: Session) -> bool:
"""Хеширует пароль, создает нового пользователя и сохраняет его в БД."""
body.password = hash_pwd(body.password)
user = users.create_user(body, db)
db.commit()
return bool(user)
@db_exc_check
def login(
form: OAuth2PasswordRequestForm, db: Session, response: Response = Response()
) -> schemas.TokenResp:
"""
Аутентифицирует пользователя, сбрасывает старые refresh-токены,
генерирует новую пару токенов и устанавливает их в HttpOnly Cookie.
"""
# Поиск пользователя и проверка валидности пароля
user = users.get_user_by_form(form, db)
if not user or not verify_pwd(form.password, user.password):
raise HTTPException(400, detail="Invalid credentials")
# Аннулирование предыдущих refresh-токенов пользователя (сессии)
db.execute(delete(models.RefreshToken).where(models.RefreshToken.owner == user.id))
# Генерация новой пары токенов и запись refresh-токена в БД
token_pair = create_token_pair(user.id)
_save_refresh_token(db, user.id, token_pair.refresh_token)
# Установка безопасных HttpOnly Cookie с токенами в ответ
response.set_cookie(
key="access_token",
value=token_pair.access_token,
httponly=True,
secure=True,
max_age=15 * 60, # 15 минут
)
response.set_cookie(
key="refresh_token",
value=token_pair.refresh_token,
httponly=True,
secure=True,
max_age=7 * 24 * 60 * 60, # 7 дней
)
db.commit()
return token_pair
def _save_refresh_token(db: Session, user_id: int, refresh_token: str):
"""Декодирует exp из JWT и сохраняет refresh-токен в базу данных."""
expires = datetime.datetime.fromtimestamp(
jwt.decode(
refresh_token,
ss.REFRESH_SECRET_KEY,
algorithms=[ss.REFRESH_ALGORITHM],
)["exp"],
tz=datetime.timezone.utc,
)
db.add(models.RefreshToken(token=refresh_token, owner=user_id, expires_at=expires))
db.commit()
@db_exc_check
def logout(refresh_token: str, db: Session) -> bool:
"""Удаляет указанный refresh-токен из базы данных для завершения сессии."""
db.execute(
delete(models.RefreshToken).where(models.RefreshToken.token == refresh_token)
)
db.commit()
return True❯ Кратко про Pytest
Pytest в этом проекте используется для автоматической проверки работы приложения и базы данных. Он запускает тесты, проверяет API, бизнес-логику и корректность схемы данных, а затем сообщает, что прошло, а что сломалось.
В проекте есть набор тестов в папке tests, и CI-пайплайн запускает их автоматически после сборки и перед деплоем, чтобы ловить ошибки.
Файлы с тестами начинаются с test_, поэтому Pytest находит их автоматически.
Пример функций тестов
class TestLogin: # Тесты для эндпоинта авторизации /api/login
# Успешная авторизация с правильным логином и паролем (код 200)
def test_post_login_returns_200(self, register: None, client: TestClient):
resp = client.post(
"/api/login",
data={"username": "example", "password": "example123"},
)
assert resp.status_code == 200
# Попытка входа с неверным логином или неверным паролем (код 400)
@pytest.mark.parametrize(
"username,password", [("false", "example123"), ("example", "false")]
)
def test_post_login_with_invalid_credentials_returns_400(
self, register: None, username: str, password: str, client: TestClient
):
resp = client.post(
"/api/login",
data={"username": username, "password": password},
)
assert resp.status_code == 400
# Передача незаполненных полей — ошибка валидации FastAPI (код 422)
@pytest.mark.parametrize(
"data",
[
({"username": None, "password": "example123"}),
({"username": "example", "password": None}),
],
)
def test_post_login_with_missing_fields_returns_422(
self, register: None, data: dict, client: TestClient
):
resp = client.post(
"/api/login",
data=data,
)
assert resp.status_code == 422❯ Кратко про Alembic
Для этой статьи данный инструмент не так сильно важен, поэтому кратко напишу зачем он вообще нужен, и можно идти дальше.
Alembic в проекте используется для управления структурой базы данных PostgreSQL.
Вместо ручного изменения таблиц всё фиксируется в отдельной миграции: например, добавление столбца, индекса, ограничения или новой таблицы.
Миграции хранятся в backend/alembic/versions и выполняются последовательно через функции upgrade() и downgrade(), что позволяет как обновлять базу, так и откатывать изменения.
Alembic получает модели SQLAlchemy и подключается к базе через DB_URL, благодаря этому таблицы базы остаются синхронизированными с кодом приложения.
❯ Архитектура GitHub Actions пайплайна
Мой CI/CD состоит из:
Линтинг кода и конфигов
Интеграционные и юнит тесты
Проверки безопасности
Сборка docker образов и отправка в ghcr.io
Подключение к серверу, скачивание образов и репозитория, развертывание базы данных и приложения.


❯ Настройка Continuous Integration (CI)
Главная задача этапа CI — убедиться, что каждый pull запрос или коммит в основную ветку содержит рабочий код, проходящий линтеры и интеграционные тесты.
Главный файл пайплайна хранится в .github/workflows/ci-cd.yml.
Линтинг конфигов и кода
name: CI/CD Pipeline # Название пайплайна
on: # Когда запускать
push:
branches: ["main"] # При пуше в ветку main
pull_request:
branches: ["main"] # При создании/обновлении PR в main
env: # Переменные
REGISTRY: ghcr.io # Реестр для Docker-образов
IMAGE_NAME: ${{ github.repository }} # Название репозитория
jobs:
lint: # Задача проверки кода
name: Code & Config Linting
runs-on: ubuntu-latest # Запускаем на Ubuntu
steps:
- uses: actions/checkout@v4 # Скачиваем код репозитория
- name: Yamllint # Проверка файла docker-compose
uses: karancode/yamllint-github-action@v2.1.1
with:
yamllint_file_or_dir: "docker-compose.yml"
yamllint_strict: false
- name: Set up Python # Установка Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Run Ruff (Linter) # Проверка кода линтером
run: |
pip install ruff
ruff check . # Проверяем все .py файлыПроверки безопасности
security: # Задача проверки безопасности
name: Security Checks
runs-on: ubuntu-latest # Запускаем на Ubuntu
permissions: # Права для GITHUB_TOKEN
security-events: write # Разрешение на отправку отчетов по безопасности
contents: read # Разрешение на чтение кода
steps:
- uses: actions/checkout@v4 # Скачиваем код репозитория
- name: Set up Python # Установка Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Security audit for dependencies # Проверка зависимостей на известные уязвимости
uses: pypa/gh-action-pip-audit@v1.1.0
with:
inputs: backend/requirements.txt
ignore-vulns: PYSEC-2026-1325 # Игнорируем уязвимость ecdsa (зависимость python-jose, нет апдейтов)
- name: Perform Bandit Analysis # Сканирование кода Python на уязвимости (Bandit)
uses: PyCQA/bandit-action@v1
with:
path: "backend" # Проверяемая директорияТестирование работы
tests: # Задача запуска тестов
name: Integration & Unit Tests
runs-on: ubuntu-latest # Запускаем на Ubuntu
steps:
- uses: actions/checkout@v4 # Скачиваем код репозитория
- name: Create .env file # Создаем .env с переменными (из secrets/vars или со значениями по умолчанию)
run: | # На практике значения по умолчанию лучше не добавлять, чтобы избежать неожиданностей в продакшене, даже если все тесты прошли успешно
cat << EOF > .env
TEST_DB_NAME=${{ vars.TEST_DB_NAME || 'test_db' }}
DB_NAME=${{ vars.DB_NAME || 'main_db' }}
DB_PASSWORD=${{ secrets.DB_PASSWORD || 'example123' }}
POSTGRES_USER=${{ vars.POSTGRES_USER || 'example' }}
TEST_DB_URL=postgresql://${{ vars.POSTGRES_USER || 'example' }}:${{ secrets.DB_PASSWORD || 'example123' }}@test_db:5432/${{ vars.TEST_DB_NAME || 'test_db' }}
DB_URL=postgresql://${{ vars.POSTGRES_USER || 'example' }}:${{ secrets.DB_PASSWORD || 'example123' }}@main_db:5432/${{ vars.DB_NAME || 'main_db' }}
SECRET_KEY=${{ secrets.SECRET_KEY || 'example_key' }}
REFRESH_SECRET_KEY=${{ secrets.REFRESH_SECRET_KEY || 'example_refresh' }}
ALGORITHM=${{ vars.ALGORITHM || 'HS256' }}
REFRESH_ALGORITHM=${{ vars.REFRESH_ALGORITHM || 'HS256' }}
TOKEN_EXPIRE_MINUTES=30
REFRESH_TOKEN_EXPIRE_MINUTES=1440
PGADMIN_DEFAULT_EMAIL=admin@example.com
PGADMIN_DEFAULT_PASSWORD=example123
EOF
- name: Start services # Поднимаем базы данных и ждем их полной готовности (--wait)
run: docker compose up -d --wait main_db test_db
- name: Run tests via Docker Compose # Запускаем тесты и удаляем тестовый контейнер после финиша (--rm)
run: docker compose run --rm tests❯ Настройка Continuous Deployment (CD)
После того как код прошел все проверки на этапе CI, его необходимо автоматически доставить на сервер, a все собранные образы нужно отправить в registry.
Cборка образов и отправка в ghcr.io
build-and-push: # Задача сборки и публикации Docker-образа
name: Build & Push Docker Image
needs: [lint, tests, security] # Ждем успешного выполнения прошлых трех задач
if: github.ref == 'refs/heads/main' && github.event_name == 'push' # Запуск только при прямом пуше в main (не для PR)
runs-on: ubuntu-latest # Запускаем на чистой Ubuntu
permissions: # Права для загрузки образов
contents: read # Чтение кода
packages: write # Запись в реестр пакетов (GHCR)
steps:
- uses: actions/checkout@v4 # Скачиваем код репозитория
- name: Log in to GitHub Container Registry # Авторизация в GHCR с помощью системного токена
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker # Автосоздание тегов (latest и SHA коммита)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-backend
tags: |
type=raw,value=latest
type=sha,prefix=
- name: Set up Docker Buildx # Включение Buildx для поддержки кэширования
uses: docker/setup-buildx-action@v3
- name: Build and push Docker image # Сборка Dockerfile из директории backend и отправка в GHCR
uses: docker/build-push-action@v5
with:
context: ./backend
file: ./backend/Dockerfile
push: true # Публикуем собранный образ
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha # Использование кэша GitHub Actions для ускорения сборки
cache-to: type=gha,mode=maxДеплой на сервер
Теперь стоит уточнить, что CI/CD прежде всего нужен для автоматизации и безопасности при изменении кодовой базы. Это значит, что он не должен отвечать за первоначальную настройку сервера. Для этого лучше подойдет, к примеру, Ansible.
В данный момент для работы на сервере нужно:
Установить Git
Установить Docker Engine
Склонировать репозиторий по пути /opt/cicd-practice
deploy: # Задача деплоя на удаленный сервер
name: Deploy to Server
needs: [build-and-push] # Ждем успешного завершения сборки и пуша образа
if: github.ref == 'refs/heads/main' && github.event_name == 'push' # Запуск только при пуше в main
runs-on: ubuntu-latest # Запускаем на Ubuntu
steps:
- name: Deploy via SSH # Подключение к серверу по SSH и запуск команд
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
set -e # Остановка выполнения при любой ошибке
cd /opt/cicd-practice # Переход в директорию проекта на сервере
git pull origin main # Обновление исходного кода из репозитория
cat << EOF > .env # Создание .env файла с переменными окружения
TEST_DB_NAME=${{ vars.TEST_DB_NAME || 'test_db' }}
DB_NAME=${{ vars.DB_NAME || 'main_db' }}
DB_PASSWORD=${{ secrets.DB_PASSWORD || 'example123' }}
POSTGRES_USER=${{ vars.POSTGRES_USER || 'example' }}
TEST_DB_URL=postgresql://${{ vars.POSTGRES_USER || 'example' }}:${{ secrets.DB_PASSWORD || 'example123' }}@test_db:5432/${{ vars.TEST_DB_NAME || 'test_db' }}
DB_URL=postgresql://${{ vars.POSTGRES_USER || 'example' }}:${{ secrets.DB_PASSWORD || 'example123' }}@main_db:5432/${{ vars.DB_NAME || 'main_db' }}
SECRET_KEY=${{ secrets.SECRET_KEY || 'example_key' }}
REFRESH_SECRET_KEY=${{ secrets.REFRESH_SECRET_KEY || 'example_refresh' }}
ALGORITHM=${{ vars.ALGORITHM || 'HS256' }}
REFRESH_ALGORITHM=${{ vars.REFRESH_ALGORITHM || 'HS256' }}
TOKEN_EXPIRE_MINUTES=30
REFRESH_TOKEN_EXPIRE_MINUTES=1440
PGADMIN_DEFAULT_EMAIL=admin@example.com
PGADMIN_DEFAULT_PASSWORD=example123
BACKEND_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-backend:latest
EOF
echo "${{ secrets.DEPLOY_TOKEN || secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin # Авторизация сервера в GHCR
docker compose pull backend # Скачивание обновленного Docker-образа
docker compose up -d --remove-orphans backend pgadmin4 main_db # Перезапуск сервисов в фоновом режиме❯ Подготовка переменных
Для CI/CD в .env значения хранить нельзя ни в коем случае, иначе они будут видны при выполнении пайплайна. Поэтому, в нашем случае, переменные нужно создать в самом GitHub.
Repository Secrets — это зашифрованные конфиденциальные данные (пароли, API-ключи, SSH-ключи), которые маскируются в логах и недоступны для просмотра в интерфейсе после создания.
Repository Variables — это открытые конфигурационные параметры (имена баз данных, порты, URL, флаги), которые хранятся в открытом виде и отображаются в логах выполнения workflows.
Repository Secrets
SERVER_HOST— IP-адрес или доменное имя вашего VPS/сервера.SERVER_USER— имя SSH-пользователя на сервере.SSH_PRIVATE_KEY— приватный SSH-ключ для подключения к серверу.DB_PASSWORD— пароль от СУБД PostgreSQL.SECRET_KEY— секретный ключ приложения для подписи Access JWT-токенов.REFRESH_SECRET_KEY— секретный ключ приложения для подписи Refresh-токенов.
Repository Variables
POSTGRES_USER— имя пользователя (логин) PostgreSQL.DB_NAME— название основной базы данных.TEST_DB_NAME— название базы данных для запуска тестов.ALGORITHM— алгоритм шифрования Access JWT-токена.REFRESH_ALGORITHM— алгоритм шифрования Refresh-токена.
❯ Заключение
Мы пошагово создали надежный CI/CD-пайплайн для проекта на FastAPI и PostgreSQL. Теперь при создании pull request код автоматически проверяется на ошибки стилистики и проходит интеграционные тесты с реальной базой данных. После одобрения изменений код бесшовно деплоится на сервер.
Полный исходный код проекта с готовыми конфигурационными файлами и выполненными пайплайнами доступен в репозитории canntstand/cicd-practice.
Если вам интересна тема CI/CD, автоматизации и разработки, делюсь своими практическими заметками и опытом в Telegram-канале My Tech Notes.
Может быть интересно:

Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram‑канале ↩
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.