GitHub Actions для dbt: workflow.yml, secrets, full-build pipeline
В предыдущем модуле вы настроили локальные quality gates (pre-commit, sqlfluff, dbt-checkpoint). Они работают на машине разработчика и могут быть обойдены — --no-verify, забытый pre-commit install, и так далее.
GitHub Actions — это enforcement слой. Workflow запускается на серверах GitHub, не зависит от локального окружения разработчика, и работает как required check для merge. В этом уроке разберём структуру workflow.yml специально под dbt, secrets management для profiles.yml, и соберём базовый full-build pipeline.
В следующих уроках добавим Slim CI, manifest storage, defer и source freshness gate. Сейчас — фундамент.
Анатомия workflow.yml
Минимальный workflow:
# .github/workflows/dbt-ci.yml
name: dbt CI
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
dbt-build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dbt
run: pip install dbt-duckdb==1.10.0
- name: dbt deps
run: dbt deps
- name: dbt build
run: dbt build --target ci
Разбор по слоям:
| Слой | Что делает |
|---|---|
name | Имя workflow, видно в GitHub UI. |
on | Триггеры. pull_request — на PR, push на main — на каждый merge в main. |
jobs | Параллельные jobs. Каждый — отдельный runner. |
runs-on | Тип runner. ubuntu-latest — стандартный Linux, бесплатный 2000 минут/месяц для private. |
steps | Последовательные шаги внутри job. Каждый шаг — это uses (action) или run (shell команда). |
Триггеры — когда запускать
on:
# На каждом PR в main
pull_request:
branches: [main]
paths:
- 'models/**'
- 'tests/**'
- 'macros/**'
- 'seeds/**'
- 'snapshots/**'
- 'dbt_project.yml'
- 'packages.yml'
# На merge в main
push:
branches: [main]
# Manual trigger
workflow_dispatch:
# Schedule
schedule:
- cron: '0 8 * * *' # каждое утро в 8:00 UTC
paths— если PR не трогает эти файлы, workflow не запускается. Экономит CI время на не-dbt PR-ах (документация, dev-only changes).workflow_dispatch— позволяет вручную запустить через GitHub UI. Полезно для ad-hoc прогонов.schedule— cron. Используется для nightly full rebuilds, source freshness checks.
schedule всегда работает от ветки main (или default branch). Нельзя запустить cron от feature branch. Поэтому schedule-jobs обычно для production maintenance (refresh, source freshness), не для CI feature branches.
Secrets: безопасное хранение credentials
В dbt проекте есть profiles.yml с warehouse credentials. Это секрет. Никогда не коммитить.
В GitHub:
- Перейти в Settings -> Secrets and variables -> Actions.
- Добавить secrets:
SNOWFLAKE_ACCOUNT,SNOWFLAKE_USER,SNOWFLAKE_PASSWORD,SNOWFLAKE_ROLE, и т.д. - В workflow обращаться через
secrets.SECRET_NAME.
Способ 1: profiles.yml в репо с env vars
# profiles.yml (в корне репо, не в ~/.dbt/)
jaffle_shop:
target: dev
outputs:
dev:
type: duckdb
path: 'dev.duckdb'
threads: 4
ci:
type: snowflake
account: ${'{{'} env_var("SNOWFLAKE_ACCOUNT") {'}}'}
user: ${'{{'} env_var("SNOWFLAKE_USER") {'}}'}
password: ${'{{'} env_var("SNOWFLAKE_PASSWORD") {'}}'}
role: ${'{{'} env_var("SNOWFLAKE_ROLE") {'}}'}
database: ${'{{'} env_var("SNOWFLAKE_DATABASE") {'}}'}
warehouse: ${'{{'} env_var("SNOWFLAKE_WAREHOUSE") {'}}'}
schema: pr_${'{{'} env_var("DBT_PR_NUMBER") {'}}'}
threads: 8
И в workflow:
jobs:
dbt-build:
runs-on: ubuntu-latest
env:
DBT_PROFILES_DIR: .
SNOWFLAKE_ACCOUNT: ${'{{'} secrets.SNOWFLAKE_ACCOUNT {'}}'}
SNOWFLAKE_USER: ${'{{'} secrets.SNOWFLAKE_USER {'}}'}
SNOWFLAKE_PASSWORD: ${'{{'} secrets.SNOWFLAKE_PASSWORD {'}}'}
SNOWFLAKE_ROLE: ${'{{'} secrets.SNOWFLAKE_ROLE {'}}'}
SNOWFLAKE_DATABASE: ANALYTICS
SNOWFLAKE_WAREHOUSE: COMPUTE_WH
DBT_PR_NUMBER: ${'{{'} github.event.pull_request.number {'}}'}
steps:
- uses: actions/checkout@v4
- name: Install dbt
run: pip install dbt-snowflake==1.10.0
- name: dbt build
run: |
dbt deps
dbt build --target ci
Ключевое: DBT_PROFILES_DIR: . указывает что profiles.yml лежит в корне репо.
Способ 2: генерировать profiles.yml в CI step
- name: Generate profiles.yml
run: |
mkdir -p ~/.dbt
cat > ~/.dbt/profiles.yml << EOF
jaffle_shop:
target: ci
outputs:
ci:
type: snowflake
account: ${'{{'} secrets.SNOWFLAKE_ACCOUNT {'}}'}
user: ${'{{'} secrets.SNOWFLAKE_USER {'}}'}
password: ${'{{'} secrets.SNOWFLAKE_PASSWORD {'}}'}
...
EOF
Этот подход проще для маленьких профилей, но secrets.X интерполируется напрямую в файл — нужна осторожность чтобы не закоммитить случайно.
profiles.yml с реальными credentials НИКОГДА не коммитить. Даже в gitignore. Используйте env_var() ссылки или генерируйте файл в CI. Если случайно закоммитили — credentials скомпрометированы, нужно rotate в warehouse немедленно.
DuckDB target для CI: нулевая стоимость
Если у вас DuckDB как dev warehouse — можно использовать его и в CI:
# profiles.yml
jaffle_shop:
outputs:
ci:
type: duckdb
path: 'ci.duckdb'
threads: 4
extensions:
- parquet
- httpfs
В workflow:
- name: Setup CI seeds
run: |
# Download production seeds from S3 / R2 / fixtures
aws s3 sync s3://my-bucket/dbt-ci-seeds seeds/
env:
AWS_ACCESS_KEY_ID: ${'{{'} secrets.AWS_ACCESS_KEY_ID {'}}'}
AWS_SECRET_ACCESS_KEY: ${'{{'} secrets.AWS_SECRET_ACCESS_KEY {'}}'}
- name: dbt build
run: dbt build --target ci
DuckDB в CI:
- Не требует credentials (нет внешнего warehouse).
- Бесплатный.
- Запускается мгновенно (нет cold start).
- Подходит для синтетических тестовых данных, не для production-volume.
Базовый full-build pipeline
Airflow: CI/CD интеграция — GitHub Actions, ruff, upgrade-check# .github/workflows/dbt-ci.yml
name: dbt CI
on:
pull_request:
branches: [main]
paths:
- 'models/**'
- 'tests/**'
- 'macros/**'
- 'dbt_project.yml'
- 'packages.yml'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install pre-commit
- run: pre-commit run --all-files
dbt-build:
runs-on: ubuntu-latest
needs: lint
env:
DBT_PROFILES_DIR: .
SNOWFLAKE_ACCOUNT: ${'{{'} secrets.SNOWFLAKE_ACCOUNT {'}}'}
SNOWFLAKE_USER: ${'{{'} secrets.SNOWFLAKE_USER {'}}'}
SNOWFLAKE_PASSWORD: ${'{{'} secrets.SNOWFLAKE_PASSWORD {'}}'}
DBT_PR_NUMBER: ${'{{'} github.event.pull_request.number {'}}'}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
- name: Install dbt
run: pip install dbt-snowflake==1.10.0
- name: dbt deps
run: dbt deps
- name: dbt parse
run: dbt parse --target ci
- name: dbt build
run: dbt build --target ci --fail-fast
- name: Cleanup PR schema
if: always()
run: |
dbt run-operation drop_pr_schema --args "{pr_number: ${'{{'} github.event.pull_request.number {'}}'}}" --target ci
Структурно:
- Job
lint— pre-commit run на всех файлах. Если падает —dbt-buildне запустится (черезneeds:). - Job
dbt-build— полный dbt build на изолированной схемеpr_<PR_NUMBER>. - Cleanup —
if: always()— runs даже при fail. Удаляет PR schema чтобы не плодились пустые schemas в warehouse.
Macro для cleanup
В macros/cleanup.sql:
{% macro drop_pr_schema(pr_number) %}
{% set schema_name = 'pr_' ~ pr_number %}
{% set sql %}
DROP SCHEMA IF EXISTS {'{{ schema_name }}'} CASCADE
{% endset %}
{'{{ run_query(sql) }}'}
{% do log("Dropped schema " ~ schema_name, info=true) %}
{% endmacro %}
Cleanup критически важен. Без него каждый PR создаёт схему pr_123, pr_124, и через год у вас 2000+ заброшенных схем.
Workflow_call: переиспользуемые workflows
В реальном проекте можно вынести общую логику в отдельный workflow и переиспользовать:
# .github/workflows/dbt-build-reusable.yml
name: dbt build (reusable)
on:
workflow_call:
inputs:
target:
required: true
type: string
secrets:
SNOWFLAKE_ACCOUNT:
required: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install dbt-snowflake==1.10.0
- run: dbt build --target ${'{{'} inputs.target {'}}'}
env:
SNOWFLAKE_ACCOUNT: ${'{{'} secrets.SNOWFLAKE_ACCOUNT {'}}'}
Использование:
# .github/workflows/dbt-ci.yml
jobs:
ci-build:
uses: ./.github/workflows/dbt-build-reusable.yml
with:
target: ci
secrets:
SNOWFLAKE_ACCOUNT: ${'{{'} secrets.SNOWFLAKE_ACCOUNT {'}}'}
# .github/workflows/dbt-prod.yml
jobs:
prod-build:
uses: ./.github/workflows/dbt-build-reusable.yml
with:
target: prod
secrets:
SNOWFLAKE_ACCOUNT: ${'{{'} secrets.SNOWFLAKE_ACCOUNT_PROD {'}}'}
Кеширование зависимостей
pip install dbt-snowflake занимает 30-60 секунд. На каждом запуске — медленно.
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip' # автоматически кеширует pip downloads
- name: Install dbt
run: pip install -r requirements.txt
Также кешировать dbt_packages (зависимости dbt):
- name: Cache dbt_packages
uses: actions/cache@v4
with:
path: dbt_packages/
key: dbt-packages-${'{{'} hashFiles('packages.yml') {'}}'}
- name: dbt deps
run: dbt deps
Ключ кеша — хеш packages.yml. Если файл не менялся — берём кеш. С 10-30 секунд до 0.
Concurrency: cancel old runs
Если PR обновляется коммитом, новый workflow запускается. Старый — продолжает работать вхолостую. Решение:
concurrency:
group: ${'{{'} github.workflow {'}}'}-${'{{'} github.ref {'}}'}
cancel-in-progress: true
jobs:
...
group — идентификатор группы. Для PR — dbt-ci-pull_request-123. Новый run в той же группе отменяет старый.
Экономия минут и compute. Особенно важно для тяжёлых full-build pipelines.
Чувствительные данные в логах
Иногда secrets могут случайно попасть в логи (если разработчик echo $SNOWFLAKE_PASSWORD). GitHub автоматически маскирует known secrets:
Run dbt build --target ci
Password: ***
Но это не панацея. Доп. меры:
- Не использовать
set -xилиbash -x. - Не делать
echo $SECRET_VAR. - В custom logging не печатать env vars.
В dbt_project.yml можно явно скрыть переменные:
# не делать так
on-run-start:
- "{'{{'} log('Snowflake user: ' ~ env_var('SNOWFLAKE_USER'), info=true) {'}}'}"
Это попадёт в dbt logs, которые архивируются как artifacts -> потенциальная утечка.
Попробуй сам
В вашем dbt-проекте на GitHub:
- Создайте
.github/workflows/dbt-ci.yml:
name: dbt CI
on:
pull_request:
branches: [main]
jobs:
dbt-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
- run: pip install dbt-duckdb==1.10.0
- run: dbt deps
- run: dbt build --target dev
- Создайте feature branch:
git checkout -b feat/test-ci
echo "-- comment" >> models/staging/stg_customers.sql
git add models/staging/stg_customers.sql
git commit -m "test CI"
git push -u origin feat/test-ci
-
Откройте PR. В разделе Checks увидите workflow прогон. Через 1-3 минуты — статус (зелёный/красный).
-
Добавьте
concurrency:блок и закоммитьте ещё раз — увидите как старый run отменяется.
Бонус: добавьте step с dbt test --select state:modified+ --defer --state ./prod-manifest/ — но он упадёт, потому что нет prod-manifest. Это подготовка к следующему уроку.
Ключевые выводы
- workflow.yml — YAML-файл в
.github/workflows/. Триггеры (on:), jobs, steps. Базовая структура для dbt CI. - Triggers:
pull_request(на PR),push(на merge в main),schedule(cron),workflow_dispatch(manual). - Secrets: хранятся в GitHub Settings -> Secrets. Передаются в workflow через
secrets.NAMEили env vars. profiles.yml используетenv_var()для credentials. - DuckDB в CI — нулевая стоимость, нет credentials, мгновенный startup. Подходит для синтетических данных.
- Cleanup PR schema — через macro
drop_pr_schemaвif: always()step. Без cleanup — рост заброшенных schemas в warehouse. - Кеширование:
cache: 'pip'для setup-python,actions/cacheдля dbt_packages. Экономит минуты на каждом запуске. - Concurrency cancel-in-progress — отменяет старые runs при новых коммитах в PR. Экономит compute.
- workflow_call — переиспользуемые workflows. Используется для shared логики между CI и prod deploy.