Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 22 из 25 ~16 мин +50 XP

Интеграция с CI

Тема урока — sqlc ci: как конвейер проверки ловит то, что компилятор и локальные привычки пропускают. Девятнадцать уроков назад sqlc стал частью вашего цикла разработки: поменял запрос — прогнал sqlc generate — закоммитил. Двадцатый урок научил коммитить генерат и пинить версию генератора в go:generate. Осталось последнее звено: человек иногда забывает перегенерировать, и тогда репозиторий уезжает в дрейф — источники говорят одно, сгенерированный пакет другое. Ловит это не человек: sqlc github actions связка делает проверку автоматической, и этот урок собирает её из деталей, которые вы уже знаете. Урок отвечает на шесть вопросов. Первый: что именно ломается без CI и почему дрейф генерата — самый дорогой из «дешёвых» багов. Второй: sqlc diff — команда, которая за секунды сравнивает коммит с тем, что выдал бы текущий конфиг, и не требует базы вовсе. Третий: полный пример GitHub Actions — от checkout до интеграционного job-а с services postgres. Четвёртый: установка sqlc в раннер — три способа и почему пин версии не обсуждается. Пятый: интеграционные тесты go в конвейере — сервис-контейнер, миграции из шестнадцатого урока и TEST_DATABASE_URL из двадцать первого. Шестой: порядок шагов и чеклист sqlc ci, по которому пайплайн собирается за вечер. Финал — мостик к следующему уроку про sqlc vet, статические правила для запросов. План курса — на [странице курса sqlc](/courses/sqlc/); решение коммитить генерат — [в двадцатом уроке](/courses/sqlc/lessons/20-organizaciya/), тестовая пирамида — [в двадцать первом](/courses/sqlc/lessons/21-testirovanie/).

Содержание урока · 11 разделов

Зачем sqlc в CI: дрейф генерата#

Начните с инвентаризации того, что может поехать в main незаметно. Поменять запрос и забыть sqlc generate — верхний пункт списка: пакет собирается, компилятор доволен, потому что генерат в репозитории по-прежнему старый и самодостаточный; расхождение проявится позже, у коллеги, который потянет ветку и получит метод, отсутствующий в его генерате, или у ревьюера, читающего один SQL, а в diff видящего другой. Второй пункт — новая версия sqlc у кого-то в команде: без пина генерация тихо меняет формат, и diff в ревью пестрит шумом. Третий — конфиг и схема разъехались: новая миграция добавлена, queries не переписаны, и только генерация знает, что пакет уменьшился. Все три случая объединяет одно: ни один не ломает сборку на машине виновника, и потому ручная дисциплина против них не работает.

Задача sqlc ci — перевести эти проверки из памяти людей в конвейер. Сгенерированный код детерминирован: один конфиг, одна схема, одна версия sqlc дают байт в байт одинаковый пакет, — и на этой детерминированности стоит вся механика. CI знает ровно три факта: содержимое репозитория, конфиг и пиннутую версию генератора. Этого достаточно, чтобы за секунды ответить на главный вопрос «соответствует ли коммит тому, что выдал бы его конфиг» — и уронить сборку, если нет. Всё остальное в этом уроке — оформление этого вопроса в yaml и дополнение его тестами из двадцать первого урока, которым CI даёт постоянную площадку с докером и настоящей базой.

Проверка: что сломается без CI

Разработчик добавил запрос в db/queries/todos/queries.sql, закоммитил и запушил, забыв sqlc generate. Что произойдёт дальше?

sqlc diff: проверка за секунды, база не нужна#

Команда sqlc diff делает ровно одно: запускает генерацию так же, как sqlc generate, но не пишет файлы, а сравнивает результат с тем, что лежит на диске, — и завершается с ненулевым кодом, если нашлась разница. Это вся семантика, и в ней вся ценность: проверка не требует ни докера, ни базы, ни сети — только файлов репозитория и бинарника sqlc. Прогон занимает секунды даже на большом конфиге, поэтому diff ставят первым job-ом: он дешевле всего и падает раньше всего. Отдельные записи списка sql проверяются разом: три движка из двадцатого урока — три пакета, один прогон.

Пара уточнений, которые экономят вечер отладки. diff сравнивает сгенерированные файлы, а не git-состояние: если генерат вообще не коммитится (выбор из двадцатого урока против дефолта), проверять не с чем — сравнение всегда упадёт или всегда пройдёт в зависимости от реализации; коммит генерата и diff идут парой. Альтернативный паттерн, который вы встретите в чужих конвейерах, — sqlc generate в CI-копии и git diff --exit-code: то же сравнение руками. Штатный diff делает это чище и без правки рабочего дерева. За пределами этого урока остаётся пара команд для больших команд: sqlc push и sqlc verify из sqlc Cloud — схема и запросы загружаются в облако, и каждая новая версия проверяется на регрессии против истории; удобно при строжайших требованиях, но требует аккаунта, а базовая гигиена живёт в локальном diff. Ещё дальше — sqlc vet, статические правила для запросов: их разбор — тема следующего урока.

Локальная проверка теми же командами

GO
# локально — перед коммитом, чтобы CI не ругался первым
$ sqlc diff
# тишина и код 0 — генерат свежий

# после правки queries без генерации:
$ sqlc diff
--- internal/store/pgdb/todos.sql.go
+++ <generated>
@@ -14,7 +14,12 @@
-func (q *Queries) ListTodosByUser(ctx context.Context, userID int64) ([]Todo, error)
+func (q *Queries) ListTodosByPriority(...)  ...
exit status 1

Формат вывода — обычный diff между файлом на диске и тем, что выдал бы генератор: по нему сразу видно, какой пакет и какой метод отстал от SQL. Код возврата — единственное, что читает CI. Заметьте порядок в жизни команды: diff полезен и локально, перед пушем, — но его сила в том, что он не доверяет памяти и гоняется в конвейере при каждом push независимо от того, что «точно обновили».

Проверка: механика sqlc diff

Чем sqlc diff отличается от sqlc generate при прогоне в CI?

Три способа поставить sqlc в раннер

GO
# Способ 1: сборка из исходников — версия живёт в yaml
- run: go install github.com/sqlc-dev/sqlc/cmd/sqlc@v1.27.0

# Способ 2: релизный бинарник — секунды вместо минут
- run: |
    curl -sSL https://github.com/sqlc-dev/sqlc/releases/download/v1.27.0/sqlc_1.27.0_linux_amd64.tar.gz \
      | tar -xz -C /usr/local/bin sqlc

# Способ 3: docker-образ — там, где раннер живёт в контейнерах
- run: docker run --rm -v "$PWD:/src" -w /src sqlc/sqlc:1.27.0 diff

Один и тот же sqlc diff тремя путями к бинарнику. Первый способ — дефолт урока: пин виден прямо в шаге и версионируется вместе с репозиторием, цена — холодная компиляция, которую кеш setup-go сглаживает. Второй — самый быстрый, но URL релиза придётся поднимать руками при обновлении пина. Третий органичен для докеризированных раннеров: тег образа и есть пин. Какой бы путь ни выбрали, diff требует лишь одной вещи — чтобы версия была той же, что у автора генерата.

GitHub Actions: три job-а, один пайплайн#

Соберите конвейер из трёх job-ов, каждый — про свою полосу из пирамиды тестов. Первый sqlc-diff: checkout, установка sqlc с пином, sqlc diff — дрейф ловится за секунды на каждом push и pull request. Второй unit: go test -short — домен на фейках, контракты на sqlmock, sqlite в памяти по канону девятнадцатого урока; по договорённости двадцать первого урока этот набор не требует докера и проходит на любом раннере. Третий integration: сервис-контейнер PostgreSQL прямо в job-е, миграции из шестнадцатого урока, TEST_DATABASE_URL в окружении — и полный набор против настоящего диалекта.

Пара слов про механику Actions, чтобы yaml читался как проза. actions/checkout выгружает репозиторий, actions/setup-go ставит Go и кеширует модули — кеш экономит минуты на каждом прогоне. Job-ы по умолчанию идут параллельно: diff не ждёт тестов, что и нужно — падение проверки генерации должно быть видно сразу. Секция services поднимает контейнер рядом с job-ом и следит за его здоровьем через health-cmd: шаги не начнутся, пока pg_isready не ответит. Секреты и пароли в тестовой базе — не секреты: контейнер живёт минуты, но DSN всё равно уезжает в env шага, а не в код. Такой пайплайн — типовой ответ на запрос sqlc github actions, и его можно копировать в проект как есть, поменяв путь к миграциям и версию Go.

.github/workflows/ci.yml: полный канон урока

GO
name: ci

on:
  push:
    branches: [main]
  pull_request:

jobs:
  sqlc-diff:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: "1.23"
      - name: Install sqlc
        run: go install github.com/sqlc-dev/sqlc/cmd/sqlc@v1.27.0
      - name: Check generated code is fresh
        run: sqlc diff

  unit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: "1.23"
      - run: go test -short ./...

  integration:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_USER: tracker
          POSTGRES_PASSWORD: secret
          POSTGRES_DB: tracker_test
        ports:
          - 5432:5432
        options: >-
          --health-cmd "pg_isready -U tracker"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: "1.23"
      - name: Run migrations
        run: go run ./cmd/migrate up
        env:
          DATABASE_URL: "postgres://tracker:secret@localhost:5432/tracker_test?sslmode=disable"
      - name: Integration tests
        run: go test -tags=integration ./internal/store/...
        env:
          TEST_DATABASE_URL: "postgres://tracker:secret@localhost:5432/tracker_test?sslmode=disable"

Три полосы пирамиды в одном файле. sqlc-diff ставит пиннутую версию и не доверяет памяти: любой push с несвежим генератом красный через полминуты. unit требует только Go. integration поднимает postgres:16 с health-проверкой, накатывает миграции тем же инструментом, что и на прод, и кормит тесты через TEST_DATABASE_URL — переменную, на которой тесты из двадцать первого урока умеют скипаться. Заметьте пины в шагах: checkout@v4, setup-go@v5, sqlc@v1.27.0 — конвейер воспроизводим, а не «как повезёт сегодня».

Проверка: health и порядок старта

В job integration контейнер postgres объявлен в services с options --health-cmd "pg_isready". Что сломалось бы без health-проверки?

Установка sqlc в раннер: три способа и пин#

Первый способ — go install github.com/sqlc-dev/sqlc/cmd/sqlc@v1.27.0: сборка из исходников на раннере. Минус — время: холодная компиляция занимает минуты, но setup-go кеширует модули, и повторные прогоны сходятся к десяткам секунд; плюс — версия задана прямо в yaml, ничего вне git не существует. Второй способ — готовый бинарник с релизов GitHub: один curl-распаковка в /usr/local/bin, секунды вместо минут; цену платите строкой с URL конкретного релиза, которую придётся обновлять руками. Третий — docker-образ sqlc/sqlc: docker run с примонтированным репозиторием; естественно там, где раннер и так живёт в докере.

Какой бы способ вы ни выбрали, версия фиксируется явно — это не рекомендация, а условие корректности всей проверки. Генерация детерминирована для одной версии: две разные версии sqlc могут разложить пробелы, комментарии или порядок полей по-разному, и diff начнёт падать «на пустом месте» — не потому, что генерат устарел, а потому, что раннер думает другим генератором. Пин закрывает и обратную сторону: новая версия sqlc с ломающими изменениями не подорвёт конвейер внезапно — вы поднимаете пин осознанно, читаете changelog, прогоняете diff и тесты, и коммит обновлённого генерата попадает в ревью как обычное изменение. Готовые сторонние actions для установки sqlc существуют, но два явных шага из канона делают то же самое без зависимости от чужого мейнтейнера.

Предскажите вывод конвейера

Прочитайте код и выберите, что он выведет:

# состояние репозитория в PR:
#   db/queries/todos/queries.sql — добавлен запрос -- name: ArchiveTodos :exec
#   internal/store/pgdb/... — генерат: коммита генерации НЕ было
# job sqlc-diff:
#   run: sqlc diff

Дрейф и его лечение: кто генерирует в команде#

Проверка diff в CI закрывает вопрос «поймали ли», но стоит объяснить и «почему так». Существует альтернативная модель: генерат не коммитится, CI сам запускает sqlc generate и подкладывает результат в артефакт сборки. Она выглядит чище в git и разбивается о быт: разработчик без установленного sqlc не собирает проект локально; ревьюер не видит в PR, как изменились структуры и методы; откат релиза — не revert-коммит, а повторная генерация, требующая точной версии инструмента. Поэтому дефолт из двадцатого урока остаётся: генерат в git, diff в CI как охрана этой договорённости.

Когда дрейф всё же случился (а он случится хотя бы один раз, чтобы вы полюбили job sqlc-diff), починка тривиальна и одинакова всегда: pull свежей main, sqlc generate, посмотреть diff в ревью — не подгонять ли его под чей-то незакоммиченный эксперимент — и коммит. Автоматизировать этот коммит ботом (генерировать в CI и пушить в ветку) можно, но взвесьте: автоматический push в ревью — источник гонок и сюрпризов, а экономия — одна команда, которую никто не забудет после первого красного конвейера. Для монорепозиториев добавляется штрих: paths-фильтры в триггере, чтобы sqlc-diff гонялся только когда изменились sqlc.yaml, db/ или внутренние пакеты; но держите его обязательным для любых изменений слоя данных — фильтр экономит секунды, дрейф стоит доверия.

Миграции и порядок шагов: кто за что отвечает#

Разложите конвейер по ролям — и порядок шагов станет очевидным, а не магией. Job sqlc-diff не касается живых баз вообще: генератору нужен только DDL из db/migrations как файлы — проверка схемы и запросов компилятором sqlc. Job unit тоже: sqlite-контур создаёт базу сам. Единственный job, где база настоящая, — integration, и внутри него порядок жёсткий: контейнер поднялся и ответил pg_isready, миграции накатились тем же инструментом, что и на прод (golang-migrate из шестнадцатого урока), затем тесты. Пропуск миграций — любимая ошибка: тесты падают с «relation does not exist» и уводят отладку в сторону sqlc, который ни при чём.

Отдельный вопрос — миграции при деплое: конвейер проверки и конвейер выката — разные сущности. CI отвечает на «правильно ли написано», CD-шаг отвечает на «успешно ли накатится»: миграции перед выкаткой новой версии приложения, в порядке, который шестнадцатый урок разбирал на примере schema_migrations. В маленьких проектах шаг migrate up живёт прямо в деплой-job-е CI, в больших — в отдельном пайплайне релиза; для урока важно одно: тестовая база integration-job-а и продовая база не связаны, и успех тестов ничего не накатывает на прод. И последний штрих порядка: держите diff первым, unit вторым, integration третьим — не потому, что Actions этого требуют (job-ы параллельны), а потому, что так логи читаются сверху вниз от дешёвых проверок к дорогим.

Впишите шаги job-а integration

Заполните пропуски: действие выгрузки репозитория, команда проверки генерации, имя переменной с DSN и команда установки sqlc с пином.

jobs: sqlc-diff: runs-on: ubuntu-latest steps: - uses: actions/@v4 # выгрузить репозиторий - name: Install sqlc run: github.com/sqlc-dev/sqlc/cmd/sqlc@v1.27.0 - name: Check generated code run: integration: services: postgres: image: postgres:16 steps: - name: Integration tests env: : "postgres://tracker:secret@localhost:5432/tracker_test?sslmode=disable" run: go test -tags=integration ./internal/store/...

Соберите пайплайн sqlc ci по шагам

Расставьте шаги настройки конвейера в рабочем порядке.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Чеклист sqlc ci#

  • sqlc diff — первый и обязательный job на каждый push и pull request.
  • Версия sqlc пиннута в двух местах и совпадает: go:generate-директива и шаг установки в workflow.
  • Интеграционный job держит postgres в services с health-проверкой, а не «надеется, что успеет».
  • Миграции накатываются тем же инструментом, что на прод, до запуска тестов.
  • DSN тестовой базы живёт в env шага; TEST_DATABASE_URL отсутствует — тесты скипаются, а не падают.
  • Обновление пина — отдельный PR с diff генерата и полным прогоном, а не правка на лету.
  • Кеш модулей Go включён: конвейер быстрый — значит, его не начнут обходить.

Что дальше: конвейер проверяет форму — очередь за смыслом#

Остановитесь и посмотрите, что получилось. Дрейф генерата пойман job-ом sqlc-diff за секунды и без базы. Пирамида тестов из двадцать первого урока получила постоянную площадку: быстрые наборы на каждый push, полный Postgres-набор с миграциями — на pull request. Пин версии делает всю проверку воспроизводимой, а чеклист — её обслуживаемой. Конвейер отвечает на вопрос «правильно ли собран слой данных»: конфиг соответствует коммиту, запросы соответствуют схеме, поведение соответствует тестам.

Остаётся вопрос глубже: сами запросы могут быть правильными по синтаксису и всё равно плохими — сканировать всю таблицу без индекса, забывать WHERE, дублировать условия, возвращать лишние колонки. Ловить это руками в ревью можно, но sqlc умеет проверять запросы по правилам: команда sqlc vet исполняет декларированные в конфиге проверки — от «в каждом запросе есть WHERE» до прогона EXPLAIN против настоящей базы и ловли full scan-ов на этапе пуша. Следующий урок посвящён sqlc vet: правилам, EXPLAIN-проверкам и тому, как встроить статический анализ SQL в тот же конвейер, который вы только что собрали. Проверка генерации сказала «код свежий» — vet скажет «код хороший».

Карточки: sqlc в CI

Термин

Что такое дрейф генерата и кто его ловит?

нажмите, чтобы перевернуть
Определение

Расхождение коммита и вывода sqlc generate (забытая генерация, другая версия); ловит job с sqlc diff — сравнение диска с выводом генератора, код 1 при разнице, база не нужна

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • sqlc ci существует ради дрейфа генерата: забытая sqlc generate не ломает сборку, и только конвейер ловит расхождение коммита с конфигом безлюдно и мгновенно.
  • sqlc diff генерирует в памяти и сравнивает с диском: ненулевой код при разнице, база и докер не нужны, все записи списка sql проверяются одним прогоном.
  • Канон github actions sqlc — три job-а: sqlc-diff (установка с пином + diff), unit (go test -short), integration (services postgres:16 с health-cmd, миграции, TEST_DATABASE_URL).
  • Установка sqlc в раннер — go install с пином, релизный бинарник или docker-образ; версия фиксируется явно, потому что разные версии дают разный генерат и ломают diff.
  • Порядок внутри integration жёсткий: контейнер готов по pg_isready, миграции накатываются прод-инструментом, только потом тесты; тестовая база с продом не связана.
  • Никакого latest: образы, действия и sqlc пинуются; обновление пина — отдельный PR с diff генерата; следующий рубеж — sqlc vet и статические правила для запросов.

Домашнее задание

Первое: соберите канон урока в своём репозитории — три job-а в .github/workflows/ci.yml — и сломайте его нарочно: измените queries без генерации и убедитесь, что sqlc-diff падает с понятным diff в логе. Второе: поднимите пин sqlc на версию свежее и прогоните diff: если файлы изменились, коммитните обновлённый генерат отдельным PR и письменно опишите, что поменялось. Третье: добавьте paths-фильтр на sqlc-diff и проверьте на двух push-ах — с изменением в db/ и без — что фильтр работает. Четвёртое: соберите integration-job с services postgres, health-проверкой и миграциями; выньте health-cmd и дождитесь connection refused на миграциях, затем верните — зафиксируйте в шпаргалке, сколько секунд экономит ожидание готовности и сколько нервов. Пятое: настройте кеш модулей Go и сравните время холодного и тёплого прогона sqlc-diff с go install. Шестое: переведите локальный TestMain из двадцать первого урока на TEST_DATABASE_URL и проверьте оба сценария CI локально через act или в форк-репозитории: с переменной — полный набор, без — скип по testing.Short. Седьмое: напишите правила команды по обновлению sqlc: кто поднимает пин, что читается перед обновлением, какие job-ы обязаны быть зелёными, — и положите в README рядом с go:generate-директивой.

Личные заметкиПусто
Хранятся только в вашем браузере (sqlc · Интеграция с CI)

Вопросы и ответы по уроку

sqlc diff падает в CI, хотя локально всё зелёное. С чего начать?

С версии генератора: локальный бинарник почти наверняка отличается от пина в workflow. Сверьте sqlc version с шагом установки и go:generate-директивой из двадцатого урока. Вторая причина — конфиг: CI видит репозиторий как есть, а у вас локально могли остаться непрокоммиченные правки sqlc.yaml или db/. Третья — порядок файлов: генерация чувствительна к содержимому папок, и файл, добавленный в .gitignore, у раннера отсутствует. Чинится всегда одинаково: одинаковый пин, чистое дерево, sqlc generate, коммит.

Можно ли в CI генерировать код и коммитить его автоматически?

Технически да: job с sqlc generate и push в ветку через GITHUB_TOKEN. Практически взвесьте: автоматический push в PR — источник гонок с ревью, а сам паттерн приучает к «генерат появится сам», что и порождает дрейф. Дефолт, который выдерживает годы: генерат коммитит человек вместе с изменением SQL, CI проверяет diff и роняет пуш при забытой генерации. Автогенерацию оставьте проектам, где слой данных собирается ботами целиком, — там она естественна.

Интеграционные тесты в CI медленные. Что резать первым?

Не набор, а частоту: полная интеграция нужна на pull request и merge в main, а не на каждый push в черновую ветку — разделите триггеры. Внутри набора резать дешевле всего пересоздание базы: один контейнер на job, а не на тест, и миграции один раз в TestMain, как в двадцать первом уроке. Кеш модулей и сборки съедает ещё минуты. А вот sqlite-набор и sqlmock-контракты не резать: они и есть причина, почему Postgres-набор может оставаться компактным — по десятку тестов на диалектные фичи.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

Следующая ступень учебного пути

ent25 уроков~13 чпрактика25 уроков уже доступны

ent: графовая ORM с кодогенерацией

Курс ent: схемы, поля, связи, запросы, хуки, privacy, миграции и GraphQL. 25 уроков с примерами и проектом микроблога.

  • Проектировать схемы и связи в ent
  • Генерировать код и жить с кодогенерацией
  • Писать запросы builder-стилем с типами
  • Строить хуки, интерсепторы и privacy-правила