Learning Platform
Глоссарий Troubleshooting
Урок 14.01 · 30 мин
Средний
GitHub ActionsCI/CDWorkflowsSecretsprofiles.yml

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.
NOTE

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:

  1. Перейти в Settings -> Secrets and variables -> Actions.
  2. Добавить secrets: SNOWFLAKE_ACCOUNT, SNOWFLAKE_USER, SNOWFLAKE_PASSWORD, SNOWFLAKE_ROLE, и т.д.
  3. В 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 интерполируется напрямую в файл — нужна осторожность чтобы не закоммитить случайно.

DANGER

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

Структурно:

  1. Job lint — pre-commit run на всех файлах. Если падает — dbt-build не запустится (через needs:).
  2. Job dbt-build — полный dbt build на изолированной схеме pr_<PR_NUMBER>.
  3. Cleanupif: 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:

  1. Создайте .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
  1. Создайте 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
  1. Откройте PR. В разделе Checks увидите workflow прогон. Через 1-3 минуты — статус (зелёный/красный).

  2. Добавьте concurrency: блок и закоммитьте ещё раз — увидите как старый run отменяется.

Бонус: добавьте step с dbt test --select state:modified+ --defer --state ./prod-manifest/ — но он упадёт, потому что нет prod-manifest. Это подготовка к следующему уроку.


Ключевые выводы

  1. workflow.yml — YAML-файл в .github/workflows/. Триггеры (on:), jobs, steps. Базовая структура для dbt CI.
  2. Triggers: pull_request (на PR), push (на merge в main), schedule (cron), workflow_dispatch (manual).
  3. Secrets: хранятся в GitHub Settings -> Secrets. Передаются в workflow через secrets.NAME или env vars. profiles.yml использует env_var() для credentials.
  4. DuckDB в CI — нулевая стоимость, нет credentials, мгновенный startup. Подходит для синтетических данных.
  5. Cleanup PR schema — через macro drop_pr_schema в if: always() step. Без cleanup — рост заброшенных schemas в warehouse.
  6. Кеширование: cache: 'pip' для setup-python, actions/cache для dbt_packages. Экономит минуты на каждом запуске.
  7. Concurrency cancel-in-progress — отменяет старые runs при новых коммитах в PR. Экономит compute.
  8. workflow_call — переиспользуемые workflows. Используется для shared логики между CI и prod deploy.
Проверка знанийKnowledge check
В GitHub Actions workflow.yml настроен step \`dbt build\` который читает profiles.yml через env_var() с креденшелами Snowflake. На каждом PR создаётся изолированная схема \`pr_123\`. Через год DBA жалуется что в warehouse 500+ заброшенных схем pr_*. Что не так и как починить?
ОтветAnswer
Проблема — **отсутствует cleanup PR-схем после закрытия PR**. Каждый PR создавал `pr_<number>` для изолированного dbt build, но никто их не удалял после merge / close. За год накопилось 500+ заброшенных.\n\n**Решения:**\n\n1. **Cleanup в workflow после dbt build** (минимум):\n\n```yaml\n- name: Cleanup PR schema\n if: always() # даже при fail\n run: dbt run-operation drop_pr_schema --args "{pr_number: ${{ github.event.pull_request.number }}}"\n```\n\nMacro `drop_pr_schema`: `DROP SCHEMA pr_<num> CASCADE`.\n\nПроблема: cleanup работает только если build job дошёл до этого шага. Если runner упал — схема остаётся.\n\n2. **Cleanup на закрытие PR**:\n\n```yaml\non:\n pull_request:\n types: [closed]\n\njobs:\n cleanup:\n runs-on: ubuntu-latest\n steps:\n - run: dbt run-operation drop_pr_schema --args "{pr_number: ${{ github.event.pull_request.number }}}"\n```\n\nЗапускается при close (merge или close без merge). Надёжнее.\n\n3. **Scheduled cleanup для оркфанов** (на случай если что-то проскользнуло):\n\n```yaml\non:\n schedule:\n - cron: '0 3 * * 0' # каждое воскресенье\n\njobs:\n cleanup-orphans:\n runs-on: ubuntu-latest\n steps:\n - run: dbt run-operation drop_old_pr_schemas --args "{days: 30}"\n```\n\nMacro находит все `pr_*` старше 30 дней и удаляет. Защита от любых edge cases.\n\n**Очистка существующих 500 схем**: разовый run-operation macro, который перечисляет `pr_*` (через `information_schema.schemata` или Snowflake `SHOW SCHEMAS`) и дропает все.\n\n**Главный урок**: каждый side effect в CI (создание схем, файлов, ресурсов) должен иметь явный cleanup механизм. Иначе через год storage/compute счёт удваивается из-за заброшенных артефактов.
Проверка знанийKnowledge check
Команда хочет запускать dbt build только когда менялись dbt файлы (models/, macros/), но не на PR с изменениями только в README или scripts/. Как настроить и какие подводные камни?
ОтветAnswer
Решение — `paths` filter в `on:`:\n\n```yaml\non:\n pull_request:\n branches: [main]\n paths:\n - 'models/**'\n - 'tests/**'\n - 'macros/**'\n - 'seeds/**'\n - 'snapshots/**'\n - 'dbt_project.yml'\n - 'packages.yml'\n - '.github/workflows/dbt-ci.yml' # сам workflow тоже\n```\n\nЕсли PR не трогает эти файлы, workflow не запускается. Экономит CI минуты, особенно на больших командах где много non-dbt PR-ов.\n\n**Подводные камни:**\n\n1. **paths-ignore vs paths.** Альтернатива — `paths-ignore: ['docs/**', 'README.md']`. Inverse логика. Сложно поддерживать когда добавляются новые non-dbt пути.\n\n2. **Required check проблема.** Если в branch protection rules стоит 'Require dbt-build check', а workflow не запустился из-за paths — PR не сможет смержиться (статус 'pending' навсегда). **Решение** — использовать **GitHub workflow ARG-trick**: workflow всегда запускается, но job skips если нет релевантных изменений:\n\n```yaml\non:\n pull_request:\n branches: [main]\n\njobs:\n check-paths:\n runs-on: ubuntu-latest\n outputs:\n should-run: ${{ steps.check.outputs.should-run }}\n steps:\n - uses: actions/checkout@v4\n with:\n fetch-depth: 0\n - id: check\n run: |\n if git diff --name-only origin/main...HEAD | grep -E '^(models|tests|macros|seeds|snapshots|dbt_project\\.yml|packages\\.yml)' > /dev/null; then\n echo "should-run=true" >> $GITHUB_OUTPUT\n else\n echo "should-run=false" >> $GITHUB_OUTPUT\n fi\n\n dbt-build:\n needs: check-paths\n if: needs.check-paths.outputs.should-run == 'true'\n runs-on: ubuntu-latest\n steps:\n - ...\n```\n\nТеперь `dbt-build` check всегда регистрируется в GitHub, но реально запускается только при релевантных изменениях. Это совместимо с branch protection.\n\n3. **Cross-file dependencies.** Изменение в .github/workflows/dbt-ci.yml само по себе должно триггерить workflow (чтобы протестировать новый workflow). Не забывайте добавлять путь к workflow.yml в `paths`.\n\n4. **draft PR.** Можно дополнительно фильтровать `if: github.event.pull_request.draft == false` — не запускать тяжёлые тесты на draft PR.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 6. В workflow.yml команда настроила `paths: ['models/**']` чтобы CI не запускался на PR без dbt изменений. Но в branch protection стоит required check 'dbt-build'. PR с изменением только в README не может смержиться (статус 'pending' навсегда). Как структурно решить?

Закончили урок?

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

Войдите чтобы оценить урок

Прогресс модуля
0 из 5