---
title: "Интеграция с CI"
description: "sqlc ci: sqlc diff против дрейфа генерата, github actions sqlc с пином версии, services postgres для интеграционных тестов, миграции и порядок шагов конвейера."
course: "sqlc: типобезопасный код из SQL-запросов"
url: "https://golangmaster.ru/courses/sqlc/lessons/22-ci/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 16
---

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

*Курс «sqlc: типобезопасный код из SQL-запросов», урок 22 из 25. ~16 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/sqlc/lessons/22-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/).

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

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

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

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

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

- Ничего: при следующем go build генерат соберётся автоматически из SQL-файлов
- ✅ CI с проверкой генерации упадёт: генерат в репозитории не соответствует конфигу и запросам
- Падёт локальная сборка у автора: без свежего генерата Go не компилируется
- Ошибка всплывёт только на проде при первом вызове нового метода

Сгенерированный пакет — обычные файлы, компилятору всё равно, свежие они или нет: сборка у автора пройдёт, ведь новый метод ему не нужен. Автогенерации в go build не существует — это и есть источник дрейфа. Единственное место, где расхождение ловится безлюдно и мгновенно, — конвейер: sqlc diff сравнит коммит с выводом генератора и завершится с ошибкой. На проде ошибка всплывёт только если кто-то вызовет несуществующий метод — и это уже авария, а не проверка.

### 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?

- diff использует кэш и работает быстрее на больших схемах
- ✅ diff не пишет файлы, а сравнивает вывод генерации с содержимым диска и падает при расхождении
- diff требует запущенной базы: он проверяет запросы исполнением
- diff запускает только первую запись списка sql, generate — все

Разница в записи результата: generate перезаписывает файлы пакета, diff оставляет дерево нетронутым и завершается кодом 1 при любой разнице — идеально для проверки «коммит соответствует конфигу». База не нужна ни generate, ни diff: оба работают по DDL-файлам, как фиксировал шестнадцатый урок. Кэша тут ни при чём, и записи списка sql diff обходит все — как и generate. Альтернатива через generate плюс git diff --exit-code существует, но штатная команда чище.

### Три способа поставить 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 в памяти по канону [девятнадцатого урока](/courses/sqlc/lessons/19-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-проверки?

- Ничего: контейнер всегда готов к первому запросу
- ✅ Тесты стартовали бы раньше готовности базы и падали с connection refused в первые секунды
- Контейнер не поднялся бы вовсе — options обязательны
- Миграции прошли бы, а тесты — нет

Сервис-контейнер стартует параллельно с job-ом, и Postgres несколько секунд инициализирует кластер: без health-проверки первый же Ping или миграция попадают в это окно и получают connection refused — классика «в CI падает, локально работает». health-cmd заставляет Actions ждать готовности: pg_isready периодически опрашивается с заданными интервалом и лимитом попыток, и шаги начинаются только после успеха. Options не обязательны для запуска контейнера — они обязательны для предсказуемого старта шагов.

### Установка 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 существуют, но два явных шага из канона делают то же самое без зависимости от чужого мейнтейнера.

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

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

**Что выведет программа?**

- Job зелёный: sqlc diff сравнивает только schema, а queries не трогает
- Job зелёный: новый запрос не ломает компиляцию старого пакета
- ✅ Job красный: sqlc diff обнаружил, что на диске нет метода ArchiveTodos, и завершился с кодом 1
- Job пропущен: diff работает только локально

sqlc diff сгенерирует пакет в памяти и сравнит с диском: в выводе генератора есть ArchiveTodos и новая константа, в репозитории их нет — diff завершится кодом 1, и job упадёт с куском diff в логе. Компиляция тут ни при чём: старый генерат собирается без нового запроса, и в этом весь подвох дрейфа. Вернётся зелёный только после sqlc generate и коммита обновлённых файлов — ровно тот цикл, который конвейер навязывает.

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

Проверка 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 из шестнадцатого урока](/courses/sqlc/lessons/16-migracii/)), затем тесты. Пропуск миграций — любимая ошибка: тесты падают с «relation does not exist» и уводят отладку в сторону sqlc, который ни при чём.

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

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

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

```go
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/...
```

Подсказка: Первый пропуск — имя действия, второй — та же команда, что ставит любые Go-инструменты из исходников, третий — главная команда этого урока, четвёртая — переменная из TestMain двадцать первого урока.

Правильные ответы в пропусках: `checkout`, `go install`, `sqlc diff`, `TEST_DATABASE_URL`

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

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

Правильный порядок строк:

1. `Зафиксировать пин sqlc: go:generate с @v1.27.0 и та же версия в workflow`
2. `Завести job sqlc-diff: checkout, установка sqlc, sqlc diff`
3. `Завести job unit: go test -short на фейках, sqlmock и sqlite`
4. `Поднять в job integration сервис postgres:16 с health-cmd pg_isready`
5. `Накатить миграции шагом go run ./cmd/migrate up перед тестами`
6. `Передать TEST_DATABASE_URL в env и запустить интеграционный набор`
7. `Закешировать модули Go между прогонами`
8. `Включить проверки обязательными для мержа в main`

### Чеклист sqlc ci

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

> **Не ставьте latest ни в одном шаге.** postgres:latest, sqlc без версии, actions без тега — каждый такой штрих превращает конвейер из проверки в лотерею: однажды в вторник после обеда обновится образ или генератор, и красный CI не будет иметь отношения к вашим коммитам. Пинуйте всё: образы по версии, действия по major-тегу, sqlc по полному релизу. Обновления — отдельным PR с осознанным diff, как и учил двадцатый урок в споре про генерат.

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

Остановитесь и посмотрите, что получилось. Дрейф генерата пойман 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 при разнице, база не нужна
- **Чем sqlc diff отличается от generate?** — diff не пишет файлы, а сравнивает и завершается с ошибкой; generate перезаписывает пакет. Оба читают DDL-файлы и все записи списка sql, база не нужна
- **Сколько job-ов в каноне и что в каждом?** — sqlc-diff: checkout, установка sqlc с пином, sqlc diff; unit: go test -short (фейки, sqlmock, sqlite); integration: services postgres, миграции, TEST_DATABASE_URL и полный набор
- **Зачем health-cmd в services postgres?** — Контейнер стартует параллельно с job-ом: без pg_isready шаги попадают в окно инициализации и получают connection refused; health-опции заставляют Actions ждать готовности
- **Какие способы установки sqlc в раннер?** — go install cmd/sqlc@v1.27.0 (версия в git, дольше из-за компиляции), релизный бинарник (быстро, URL в yaml), docker-образ sqlc/sqlc; версия пинуется всегда
- **Почему пин версии sqlc обязателен?** — Генерация детерминирована для одной версии: разные версии раскладывают файлы по-разному и diff падает «на пустом месте»; обновление — отдельный PR с diff генерата и полным прогоном

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

- 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 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-набор может оставаться компактным — по десятку тестов на диалектные фичи.
