This doc walks through every stage of getting a change from a feature
branch to a tagged release on GitHub, including which CI workflow fires at
each step and why. It is the operational counterpart to the high-level
design in specs/ci-rework/README.md.
If you are new to the project, read CONTRIBUTING.md first.
feature/* ─────PR─────► main ────push─────► release-candidate/X.Y.Z ──tag──► X.Y.Z
│ │ │ │
▼ ▼ ▼ ▼
Tier 1 CI Tier 2 CI Tier 3 CI publish_docs
(pr.yml) (main.yml) (release.yml) (existing)
no secrets secrets, Snowflake + secrets, full release tag
lint disabled¹ BigQuery, latest dbt version matrix triggers
docs site
¹ Lint is currently in Tier 2 only because the package's models call adapter methods at compile time and need a real Snowflake connection. Tracked for fix in specs §12.2.
- Fork the repo, clone your fork, create a branch.
- Make your changes. Use
./scripts/ci/test.sh <warehouse>to test locally against any warehouse you have access to. Postgres / Trino / SQL Server run in Docker containers — no warehouse credentials needed for these. - Open a PR against
brooklyn-data/dbt_artifacts:main.
What you'll see on your PR:
- Tier 1 CI runs (
pr.yml). One job per local-runnable warehouse (Postgres, Trino, SQL Server). Each spins up a container, runs the integration tests, tears down. - No lint, no Snowflake / BigQuery / Databricks signal on your PR. This is deliberate — those require secrets, which forks cannot access by design. A maintainer will run the higher-tier checks for you after they merge (Stage 2).
- The matrix is configured with
fail-fast: false, so a break in one warehouse does not hide results from the others.
If Tier 1 passes, a maintainer will review. If it fails, fix it locally
(./scripts/ci/test.sh <warehouse>) and push again — the same CI fires
on every push.
Same flow as a fork. Internal contributors also PR against main
— there is no shortcut, no privileged branch. The PR-level CI is
identical to what a fork sees. The only difference is internal
contributors typically have their own warehouse credentials and can
run all warehouses locally before opening the PR.
After review and approval, the maintainer merges the PR. This is the trust boundary — only code merged through a reviewed PR ever sees repository secrets.
What fires on push to main (main.yml):
| Job | What it does | Secrets used |
|---|---|---|
lint |
sqlfluff against models/ |
Snowflake (templater needs a real connection) |
integration-local (matrix) |
Re-runs Postgres / Trino / SQL Server against merged code | None |
integration-snowflake |
Latest dbt-snowflake against the test Snowflake account |
Snowflake |
integration-bigquery |
Latest dbt-bigquery via Workload Identity Federation |
GCP (WIF — keyless) |
If Tier 2 fails after a merge, the maintainer either:
- Reverts the merge commit on
main, or - Pushes a follow-up fix (which itself goes through a PR and Tier 1 again, then Tier 2 reruns on push).
This is also where Snowflake / BigQuery regressions get caught for changes contributed by forks who couldn't run those tests themselves.
When you have a set of merged changes ready to ship (~monthly cadence),
cut the release candidate. Recommended path: use the cutter, which
auto-bumps the version in dbt_project.yml and README.md, creates
the branch, commits, and pushes — one step:
Via GitHub UI (workflow_dispatch):
- Actions tab → "Cut release-candidate" workflow → "Run workflow"
- Pick
patch,minor, ormajor(or paste an explicitX.Y.Z) - Click "Run"
Or locally (same script — single source of truth):
git checkout main && git pull
./scripts/release/cut-candidate.py --minor # or --patch / --major / --version X.Y.ZThis automation is bump, not merge: the maintainer still reviews Tier 3 results and drives the tag manually (Stage 4).
What fires (release.yml):
| Job | What it does |
|---|---|
lint |
Same as Tier 2 |
version-matrix |
42 entries: every supported (warehouse, dbt_version) pair, plus an unversioned "latest" per warehouse. max-parallel: 8. |
integration-databricks-stub |
Visible-but-skipped placeholder until Databricks is reactivated (see specs §12.3). |
This typically takes 30–40 minutes. Watch the Actions tab. If anything goes red, you have two choices:
- Fix forward on the release-candidate branch. Push commits
directly to
release-candidate/X.Y.Z— Tier 3 reruns. Use this when the fix is small and clearly part of the release scope. - Fix via a PR to
main, then re-cut. Delete the release-candidate branch, fix on a new feature branch through the normal PR flow, then re-create the release-candidate branch from the updatedmain. Use this when the fix is substantive enough to deserve PR review.
Once release.yml is green on the candidate branch:
- Open a PR from
release-candidate/X.Y.Ztomain(if there were commits made directly to the candidate branch). Otherwise skip — the candidate branch already matchesmainat the head it was created from. - Merge the PR (or confirm the branches are in sync).
- Create a GitHub Release:
- Tag:
X.Y.Z - Target:
main(the merge commit if you opened a PR, or the candidate branch head if it matches) - Title and notes summarizing the changes
- Tag:
- Publish.
What fires: publish_docs_on_release.yml
rebuilds the docs site. dbt Hub picks up the new release within an hour
via dbt-labs/hubcap.
- Delete the
release-candidate/X.Y.Zbranch. It's served its purpose. Keep it around only if you anticipate a same-day hotfix targeting the same minor.
For a critical bug on a released version:
hotfix/critical-bug ───PR───► main ───push───► release-candidate/X.Y.Z+1
│ │ │
▼ ▼ ▼
Tier 1 CI Tier 2 CI Tier 3 CI
then tag
Hotfixes are not a separate code path — they're feature branches with a narrow scope. Same PR → main → release-candidate → tag flow as Stage 1–4. The only difference is urgency: maintainers may compress the timeline (e.g., merge same-day rather than holding for a batch).
If a hotfix targets an older minor (you cannot fix-forward to the
latest), branch from the tag of that minor instead of main, then PR
into a release-candidate/X.Y.Z+1 branch directly. This is rare and
not currently automated — talk to maintainers before attempting.
| File | Tier | When it runs | Secrets |
|---|---|---|---|
pr.yml |
1 | pull_request → main |
None |
main.yml |
2 | push → main, manual |
Snowflake, GCP WIF |
release.yml |
3 | push → release-candidate/**, manual, Mondays 06:00 UTC (weekly regression on main) |
Snowflake, GCP WIF |
cut-release-candidate.yml |
— | workflow_dispatch only — auto-bumps and pushes a release-candidate/X.Y.Z branch |
contents: write (push only) |
publish_docs_on_release.yml |
4 | Release tag created | (none — docs only) |
Every Monday at 06:00 UTC the full Tier 3 matrix runs against the
current head of main. This catches drift from upstream dbt adapter
releases that land between our scheduled releases — e.g., a new
dbt-snowflake minor that breaks our package. If the weekly run
fails, treat it like any other Tier 2/3 failure: investigate, fix
forward via a PR, or pin the offending adapter version.
CONTRIBUTING.md— how to set up your local environment and submit a first PRdocs/MAINTAINERS.md— maintainer-specific guidance (warehouse credentials, release procedure detail)scripts/ci/README.md— script-by-script reference for the local CI shimsspecs/ci-rework/README.md— design rationale, threat model, tech debt backlog