Skip to content

Releases: Drichards124/dbt-costgate

dbt-costgate v1.1.0

Choose a tag to compare

@github-actions github-actions released this 31 Jul 17:53
4e5ff89

Added

  • dbt-costgate config <key> explains one setting and shows the YAML that sets
    it
    , nested under the right section header so it can be pasted as-is. A
    section name (dbt-costgate config pricing) prints everything under it, and
    the leaf on its own (dbt-costgate config max_pct_increase) works as well as
    the dotted key, because the leaf is what the list displays. An unrecognised
    name exits 2 and suggests the nearest real one.
  • dbt-costgate config --verbose prints every setting's full explanation —
    what the command used to do by default.
  • dbt-costgate config takes --color auto|always|never, matching check.
  • dbt-costgate with no arguments now shows how to get started instead of a
    list of flags: the two commands that make up the local loop, the two more that
    turn it into a pull-request gate, and what to run when it cannot reach
    BigQuery.
  • dbt-costgate config --format json gained a summary field — the short
    label the list shows. Every existing field is unchanged.

Fixed

  • dbt-costgate config no longer runs off the side of the terminal. It
    printed each setting's whole explanation on one unwrapped line — up to 600
    characters — so on an 80-column terminal the text wrapped back to column 0 and
    sat flush against the next setting's name, with no blank line between entries.
    29 of its 42 lines were too wide to fit. Every screen now wraps to the width of
    the terminal it is printing to.

Changed

  • dbt-costgate config now lists one line per setting instead of twenty
    paragraphs.
    The list is grouped and indented the way the settings nest in
    .dbt-costgate.yml, so it doubles as a guide to the file's shape:

    pricing:
      region                      str    Force the pricing region
      usd_per_tib                 float  Flat USD/TiB rate for every region
    

    The full explanations moved behind two new forms rather than going away.

  • dbt-costgate init says which setting to uncomment first and no longer
    claims every key's note states its default, which was not true of all of them.

  • dbt-costgate config and the no-argument screen now follow your terminal
    width even when their output is piped or redirected
    , so dbt-costgate config | less and dbt-costgate config > settings.txt come out at the width of the
    window you ran them in rather than a fixed 100 columns. Set COLUMNS to pin it
    yourself; with no terminal anywhere — a CI log — it stays at 100.

    Which report goes where is deliberately unchanged: dbt-costgate check
    still renders at a fixed width whenever its output is not a terminal, so a
    report committed to a repo or read back out of a CI log does not depend on the
    window that produced it.

  • Paragraphs now stop at 100 characters on a wide terminal; tables still use
    the full width.
    Sentences set 200 characters wide are hard to read back —
    the eye loses the start of the next line — while a table is read down a column
    and a wide window buys it a model name that no longer needs truncating. This
    applies to explanatory text everywhere it appears: config, the no-argument
    screen, and a report's notes, notices, breach lines and footer.

    Nothing that gets written to a file or a CI log changes, because those already
    rendered at 100. The pricing header line is not capped either — it is a row of
    figures rather than a sentence, and capping it would split it across two lines
    on a terminal wide enough to show it on one.

dbt-costgate v1.0.5

Choose a tag to compare

@github-actions github-actions released this 28 Jul 01:32
fc49aae

Fixed

  • A report spanning regions priced differently no longer states one rate as if
    it applied to all of them.
    The header named every region it covered and then
    quoted a single rate beside them — whichever region happened to come first. The
    built-in table spans 1.8× (US at USD 6.25, southamerica-east1 at 11.25), so a
    change touching both understated by nearly half in the report's most prominent
    line. It now gives a range, and a single figure when every region agrees:

    dbt-costgate — region: EU, US, asia-northeast1 · on-demand USD 6.25–7.50/TiB · built-in table
    

    The per-region footer was always correct; only the summary above it was not.

  • An unusual model name no longer breaks the report. A name containing a
    newline split the row in half — in the terminal table and the pull-request
    comment. A name containing | spilled extra cells across the markdown table
    and shifted every figure after it into the wrong column. A name containing an
    escape character passed straight through to the terminal, so it could recolour
    everything printed after it.

    These are reachable rather than theoretical: a dbt model name comes from a
    filename, and a filename may contain any of them. Control characters are now
    shown as a space and pipes are escaped in markdown. The name is unchanged
    everywhere it is matched
    exclude, warn_only and renames still compare
    against exactly what the manifest holds, and --format json still reports it
    verbatim.

dbt-costgate v1.0.4

Choose a tag to compare

@github-actions github-actions released this 28 Jul 00:11
b9ea00f

Fixed

  • A very large change no longer produces a pull-request comment with no verdict
    in it.
    GitHub rejects a comment over 65,536 characters, so dbt-costgate cuts
    an oversized one to fit — from the end, which is where the verdict used to sit.
    Measured on a change where every model breached: past about 935 models the
    comment was a wall of cost rows, _…report truncated._, and no PASS or
    FAIL anywhere in it. The exit code was still right and CI still blocked; the
    part a person reads had lost its conclusion.

    The comment now leads with the verdict, and no list in it grows without limit.

Changed

  • The pull-request comment reads summary first, evidence after. The verdict
    and the net figure come above the table rather than below it:

    ### 💸 dbt-costgate — cost impact of this change (2 models)
    
    ❌ **Gate: FAIL**
    - fct_orders_daily: USD +13.19/run exceeds USD 5.00
    
    **Net increase:** USD 13.19/run · USD 395.70/month
    
    | Model | Baseline | This change | … |
    
  • A change touching more than 50 models shows the 50 largest and says how many
    it left out
    …and 950 more models, not shown — all of them are in --format json``. The same cap applies to the breach list, the per-model
    caveats and the not-estimated list. Under 50 models, nothing changes: no cap,
    no note.

    Rows are chosen by size, since the report is already ordered most expensive
    first. Terminal output is uncapped — it scrolls — and --format json
    still carries every model
    , so nothing is lost, only moved to where it fits.

dbt-costgate v1.0.3

Choose a tag to compare

@github-actions github-actions released this 27 Jul 23:43
9c261dc

Fixed

  • A negative number in any setting is now refused, instead of quietly turning
    the gate off.
    pricing.usd_per_tib: -6.25 — one typed minus sign — made
    every cost negative, so all three money thresholds stopped firing while the
    report still looked ordinary. A 3 TiB model against a USD 10 ceiling went from
    GATE: FAIL / exit 1 to GATE: PASS / exit 0. A negative run_frequency did
    the same to the per-month threshold.

    Ten of the twelve numeric settings accepted a negative value; only
    pricing.regions and pricing.free_tib_per_month refused one. All twelve now
    do, on the command line as well as in the file:

    dbt-costgate: .dbt-costgate.yml: pricing.usd_per_tib: must not be negative, got -6.25.
    dbt-costgate: --usd-per-tib: must not be negative, got -6.25.
    

    Zero is unaffected — it still means flat-rate slots for a rate, and a
    declared-but-spent allowance for the free tier. If you have a negative value in
    a config today, the run now stops at exit 2 and names the key rather than
    reporting a number nobody should act on.

  • A run frequency written as a fraction is refused rather than silently
    rounded down.
    run_frequency.default: 3.7 became 3, so every monthly
    figure was a fifth too low and nothing said so.

Changed

  • An unreadable config file now says what is wrong and where. The message
    used to carry four lines of parser internals — including a caret diagram and
    <unicode string> — flattened onto one line, which buried the only part worth
    reading. Now:

    dbt-costgate: .dbt-costgate.yml is not valid YAML: expected ',' or ']', but got
    '<stream end>' (line 3, column 1).
    
  • Config error messages read as sentences: must not be negative rather than
    must be >= 0, and the few that were missing a full stop have one.

dbt-costgate v1.0.2

Choose a tag to compare

@github-actions github-actions released this 27 Jul 23:17
69a8c68

Changed

  • The line about a resource dbt-costgate does not price no longer says its one
    fact twice.
    It read "country_codes changed but is not priced — seeds are not
    priced"
    . The first half now says what happened and the second says why:

    dbt-costgate: country_codes changed but is not in the report — seeds are not priced.
    

    Same for the --select form ("… was selected but is not in the report — …").
    Grep for is not priced still matches; grep for the full old sentence does not.
    It is also the more accurate wording for an ephemeral model, whose cost really
    is priced — in the rows of the models that inline it.

Fixed

  • A changed analysis is now told that "analyses are not priced", rather than
    "analysiss are not priced". The plural was built by adding a letter.
  • A changed Python model's explanation no longer runs two dashes together.
    It was "… is not priced — Python models are not priced — dbt-costgate
    estimates SQL scans"
    ; the second dash is now a semicolon.

Docs

  • New: What dbt-costgate does not price.
    Seeds, snapshots, tests, analyses, hooks, Python models and ephemeral models
    get no row in the report, and the tool has always said so on stderr — but no
    shipped page explained why, or what each one actually costs. The section names
    all seven and is blunt about the part that matters: "not priced" is not
    "free."
    A snapshot is a MERGE and a test is a SELECT; both scan real
    bytes that dbt-costgate does not put a number on. Ephemeral models are the
    exception — their cost is counted, in the rows of the models that inline them.
  • Materialized views and BigQuery scripts are now documented, in both the
    usage accuracy notes and the explained-page caveats. Their siblings were
    already there; these two were not. A materialized view's figure is the cost of
    building it once, and BigQuery bills each automatic refresh on top. A model
    using DECLARE or BEGIN reads low — the only caveat on the page that
    errs that way — because BigQuery can only price the statements it can resolve
    before the script runs.

dbt-costgate v1.0.1

Choose a tag to compare

@github-actions github-actions released this 27 Jul 22:41
62fae87

Fixed

  • --select no longer throws away the whole report because one name cannot be
    priced.
    Naming a seed, a snapshot or an ephemeral model alongside real models
    used to end the run at exit 2 with nothing printed — even when sixteen other
    models had answers. That is reachable from an ordinary CI line, because
    dbt ls --select state:modified --resource-type model includes ephemerals.

    Those names are now reported on stderr and the run carries on, which is what
    the change-detection path has always done:

    dbt-costgate: int_names_00 was selected but is not priced — ephemeral models
      have no relation of their own; their SQL is inlined into the models that
      select from them, and the cost shows up there.
    

    Two things deliberately did not change. A --select naming only unpriced
    nodes is still exit 2 — nothing was gated, and that has to stay loud. A name
    nobody recognises is still exit 2 with a spelling suggestion, so a stale list
    cannot quietly check nothing.

  • A BigQuery outage no longer hangs your CI job for up to forty minutes per
    model.
    deadline_seconds (default 60) was bounding nothing. Client.query
    takes two independent retries and dbt-costgate set only one, leaving
    job_retry at its 2400-second default to re-drive the whole thing underneath.
    Measured against real BigQuery, a 5-second deadline ran for 179 seconds over
    42 attempts
    . It now gives up inside the deadline — the same case takes 2.1
    seconds.

  • A dry-run that BigQuery could not serve now says so. When the retries run
    out, the client raises a RetryError wrapping the real failure, which fell
    through every check and was reported as the dry-run failed. You now get the
    message that was always meant for this:

    not estimated — BigQuery was unavailable and the retries ran out
    

    This affects the reported reason only. That kind was already treated as an
    operational failure, so no exit code or verdict changes.

dbt-costgate v1.0.0

Choose a tag to compare

@github-actions github-actions released this 27 Jul 21:35
e84e46c

1.0 because the last thing standing in its way is done. The v0.11.0 notes
said the number was blocked on one thing: every test faked
google.cloud.bigquery.Client, so the one part of this tool that talks to a
warehouse had never met a warehouse. It has now — see
docs/qa/live-bigquery-2026-07-27.md. The
dry-run response parses, the error classification holds against live BigQuery
exceptions, and an end-to-end run's byte counts match a direct dry-run exactly.
It also turned up the bug below, which is the argument for doing it.

Nothing here changes a gated figure, a threshold, an exit code or a verdict.
1.0 is a statement about confidence and about the promise that comes with it —
from here, breaking changes wait for 2.0.

Fixed

  • A mistyped dataset name no longer tells you to log in again. BigQuery
    answers 403 — not 404 — for a dataset or project that does not exist, so a
    typo used to end the run with could not estimate any model (check credentials/permissions). Try gcloud auth application-default login and exit
    2. The exit code is unchanged, but both messages now name the other cause:

    not estimated — BigQuery refused the dry-run — either it is not allowed, or
    the dataset or project does not exist
    

    Check the names in your --select and your model SQL before reaching for IAM.

  • max_tib_total and max_usd_total breaches now name numbers you can tell
    apart.
    Both sides were rendered at two decimal places, so a small cap
    produced a breach line that compared a number to itself:

    - fct_names: 0.00 TiB/run exceeds cap 0.00 TiB
    - fct_names: USD 0.00/run exceeds cap USD 0.00
    

    A 1 GiB ceiling (max_tib_total: 0.001) is an ordinary thing to set and lands
    right in that range. Byte figures now use the same units as the table, and
    money widens its decimals only when two places would round an amount away:

    - fct_names: 147.61 MiB/run exceeds cap 104.86 MiB
    - fct_names: USD 0.0009/run exceeds cap USD 0.0001
    

    Ordinary caps are unchanged — USD 0.84/run exceeds cap USD 0.50 still reads
    at two places. Only the wording moved; gating, exit codes and verdicts are
    identical.

dbt-costgate v0.11.0

Choose a tag to compare

@github-actions github-actions released this 27 Jul 00:36
1aea046

Added

  • You can declare your BigQuery free tier. pricing.free_tib_per_month tells
    dbt-costgate what your on-demand allowance is, and the report shows where the
    change lands against it:

    pricing:
      free_tib_per_month: 1
    run_frequency:
      default: 30      # required — an allowance is per month, so it needs a month
    dbt-costgate — region: US · on-demand USD 6.25/TiB · built-in table · first 1 TiB/month free
    
      Net increase: USD 13.19/run · USD 395.63/month
      Monthly scan for these models: 85.35 TiB — past the 1 TiB/month you declared free
    

    It subtracts nothing. No cost figure, threshold, verdict or exit code
    changes — set it and the gate behaves exactly as it did. The allowance belongs
    to your whole billing account and is drawn down by every other query anyone
    runs, which dbt-costgate cannot see, so deducting it would mean assuming it is
    still unspent. A gate that forgives the first TiB of a regression on an
    unverified assumption is worse than one that over-reports honestly.

    Three things worth knowing before you set it. "For these models" is literal —
    a pull request covers a handful of models, so the figure is not your project's
    monthly scan and certainly not your account's. Without a run_frequency there
    is no monthly figure to compare against, and the new
    free-tier-needs-run-frequency notice says so rather than letting the setting
    sit there doing nothing. And the tier is an on-demand allowance, so all of this
    is suppressed under capacity/Editions pricing.

    Unset — the default — changes nothing anywhere.

  • --format json gains two fields: pricing.free_tib_per_month (what you
    declared, or null) and net.monthly_scan_bytes (raw bytes these models are
    projected to scan in a month, or null when no run frequency is set). Unlike
    the other net figures, the monthly total is absolute rather than a difference,
    so it is present in absolute mode too.


A manual QA pass drove the packaged CLI end to end against a real dbt project
and found 21 defects. This release fixes all of them, and rebuilds the terminal
report around a readable table.

Read the breaking changes first if you run dbt-costgate in CI — a gate that
could not check a model now fails instead of passing, and that is deliberate.

Breaking

  • A gate that could not check a model now fails the run (exit 1). Previously
    it reported GATE: PASS and exit 0. There were four ways into this and all of
    them are ordinary mistakes: a baseline compiled a different way from the
    branch, a model whose dry-run returned no size, a baseline with no compiled
    SQL, and a run where nothing at all could be estimated. The report warned in
    some of these cases, but CI reads the exit code.

    In practice this fires when a threshold is configured and a model could not be
    measured. If that is expected for a particular model — an external table your
    service account cannot see, say — accept it by name:

    exclude:
      - external_events

    To stop blocking entirely while you sort it out, fail_on: never reports the
    breaches and exits 0. There is no new setting to learn.

    One case fails regardless of thresholds: a run where every selected model
    failed to estimate. PASS there does not mean "I checked and found nothing
    wrong" — it means the gate never ran. The usual causes are an unbuilt dev
    schema, the wrong --project, or a deferred build that never happened.

  • A config error now exits 2 instead of 1. ADR-0008 reserves 1 for a
    threshold breach; a malformed .dbt-costgate.yml was exiting 1 with a Python
    stack trace, so CI reported a YAML typo as a cost regression. A failed
    --output write moved for the same reason.

  • .dbt-costgate.yml is now validated, and rejects what it used to ignore.
    A file that was quietly half-applied will now fail with a message naming the
    key. Three things change:

    • exclude: my_model written as a bare string used to become a list of single
      characters and match nothing. A scalar is now read as a one-item list, so
      this works as written.
    • fail_on: no is YAML for the boolean false. It matched neither never nor
      warn and fell through to the strictest setting — the opposite of what
      someone writing "no" means. fail_on and report.format are now checked
      against their allowed values.
    • An unknown key is refused, with the nearest documented key as a hint. A typo
      like thresholds.max_usd_totl used to leave you with no threshold at all.
  • A manifest compiled for another warehouse is refused (exit 2). Nothing
    checked metadata.adapter_type, so pointing dbt-costgate at a Snowflake,
    Postgres or duckdb project produced confident BigQuery dollar figures for SQL
    BigQuery was never going to run. Manifests without the field still run.

Changed

  • The terminal report is a table. It used to write each model as a sentence,
    so figures started at a different column on every row, nothing lined up, and a
    priced diff row ran to 108 characters and wrapped:

      fct_orders_daily  (full-refresh): 819.20 GiB → 2.91 TiB   +264%   USD +13.19/run   USD +316.46/month (24 runs)
    

    Now:

      MODEL                             BASELINE     CURRENT    Δ %     Δ / RUN    Δ / MONTH  RUNS
      ────────────────  ────────────  ──────────  ──────────  ─────  ──────────  ───────────  ────
      fct_orders_daily  full-refresh  819.20 GiB    2.91 TiB  +264%  USD +13.19  USD +395.63    30
    

    Per-model warnings moved below the table, into a NOTES block keyed by model.
    Nothing was removed — (24 runs) became the RUNS column.

    It adapts to the terminal width, giving up columns in a fixed order and saying
    which it hid; the model name and the per-run cost are never among them. Below
    60 columns it prints one block per model instead. Output redirected to a file
    or a pipe always renders at a fixed width, so a captured report does not depend
    on the window that produced it.

  • Percentages are printed at the precision they need. A 0.4% increase over a
    max_pct_increase: 0.3 limit used to produce the breach line +0% exceeds 0%,
    which reads like a bug rather than a correct failure.

  • Unpriced (slot-priced) reports are ordered by size. With every rate at 0
    the sort had nothing to work with and rows came out in arrival order.

  • The incremental and full-refresh footnotes say which figure is the big
    one.
    Every fact in the old wording was correct — "the figure is one run
    against the table as already built, so it does not gate rebuild cost" — and it
    took a second read to work out that the number on screen is the cheap case and
    the expensive one is not in the report at all. Each now says what the figure
    is, what it is not, and which of the two is larger, in that order:

    incremental — rows tagged incremental show one run against a table that
    already exists. A full rebuild scans far more, and nothing here measures it,
    so no threshold on this report can catch a rebuild getting expensive.

Added

  • --color auto|always|never (default auto): colour when stdout is a
    terminal, off when piped, and off whenever NO_COLOR is set.

  • Deleting a model is reported as the saving it is. Removing a model is the
    most direct cost reduction a change can make, and it produced no row and no
    credit — a branch deleting a 411 GiB/run model reported Net change: none.
    Deleted models are dry-run from the baseline, tagged deleted, and never
    gated: a removal cannot raise cost.

  • A change to an ephemeral model now selects the models that inline it. Its
    SQL ends up inside them, so widening a filter there is a real cost change —
    but on the local path, which is what the pre-commit hook runs, it selected
    nothing at all.

  • A changed seed, snapshot, Python model or ephemeral model is named. These
    are out of scope for pricing, which is fine; being indistinguishable from
    "nothing changed" is not, and a snapshot really does run a MERGE.

  • A --select name that matches nothing is an error (exit 2), listing the
    names with a suggestion for near misses, and saying when the model exists but
    is a kind dbt-costgate does not price. It used to select nothing in silence, so
    a CI job building its list from dbt ls checked nothing the day that list went
    stale.

  • A materialized view says its figure is not the recurring cost. It is priced
    like a plain view, but BigQuery bills each automatic refresh separately.

  • New notice new-models-not-percentage-gated. max_pct_increase needs a
    before and an after, so it cannot cover a model that has no baseline. If it is
    your only threshold, the report now names the new models that went through
    ungated and points at max_usd_total / max_tib_total, which need no
    baseline. Silence it like any other notice.

  • JSON gains skip_reason and is_deleted per model. gateable alone could
    not tell a consumer whether you excluded a model or the gate could not measure
    it, and those mean opposite things on a dashboard.

Fixed

  • A model that scanned 0 B on the baseline now breaches max_pct_increase.
    There is no ratio to a zero baseline, so the threshold silently did not apply
    to exactly the model it should catch hardest.

  • The net line no longer totals a comparison the report just called invalid.
    Having warned that a model's two figures cannot be subtracted, it went on to
    headline the subtraction. Naming that model in exclude: does not bring the
    total back either — an exclusion says "do not fail the build over this", not
    "the two figures are comparable after all".

  • A permanent SQL error is no longer reported as a transient one. The status
    code was matched anywhere in the message, so 400 Syntax error … at [500:3]
    came back as "BigQuery was unavailable and the retries ran ...

Read more

dbt-costgate v0.10.0

Choose a tag to compare

@github-actions github-actions released this 26 Jul 19:36
9d595a1

Security

  • Reports no longer reproduce BigQuery's error message. A model that could
    not be dry-run is now described by what went wrong, not by what BigQuery said:

    > • **fct_orders** — not estimated — BigQuery rejected the compiled SQL
    

    where it previously printed the message verbatim. BigQuery quotes the query it
    was given, and compiled SQL can embed secrets templated through env_var() or
    vars — the same reason reports have always excluded compiled SQL. Because a
    report becomes a pull-request comment, that message could carry query text,
    dataset and table names, and the service-account identity into a comment that
    is public on a public repository.

    The full message is still available, printed to stderr and so kept in the
    job log:

    dbt-costgate: fct_orders: invalid_sql: 400 Syntax error: Unexpected ...
    

    This changes report text in all three formats, including the JSON error
    field. If you match on that string, match on the new wording — or read
    models[].error alongside the job log rather than parsing the warehouse
    message out of a comment.

Fixed

  • A report long enough to be truncated now posts. The sticky comment is
    capped at GitHub's size limit; the cut could land inside a multi-byte
    character, and the resulting comment was rejected. It is now shortened to a
    whole line.

dbt-costgate v0.9.0

Choose a tag to compare

@github-actions github-actions released this 26 Jul 17:43
68234b4

Added

  • models[].basis in the JSON reportfull_refresh, incremental_form,
    direct, or null when it could not be established. It says which query shape
    was dry-run, and so whether that model's figure is a rebuild or a single run.
    is_incremental cannot answer this: it is true for both. Additive — every
    existing field is unchanged.

  • dbt-costgate init — writes a starter .dbt-costgate.yml:

    dbt-costgate init

    Config discovery has always worked, but nothing told you to create the file,
    and the documented example had every setting filled in — copy it and you had a
    configured project rather than a starting point. The written file documents
    every setting with its default, its type and an example value, and leaves all
    of them commented out, so it changes nothing until you uncomment one. It
    refuses to overwrite an existing config, including one under either of the
    other discovered names, and takes --project-dir when your dbt project is not
    the directory you are standing in.

  • A published container image, so CI that isn't GitHub Actions can run the
    same check without a Python environment of its own:

    docker pull ghcr.io/drichards124/dbt-costgate:v0.9.0
    docker run --rm -v "$PWD:/workspace" ghcr.io/drichards124/dbt-costgate:v0.9.0 check

    Every release publishes both :vX.Y.Z and :latest. Pin the version tag —
    :latest moves under you, which is the one thing you do not want from the job
    that decides whether a pull request merges. The repository also ships the
    Dockerfile itself, so you can build it and push it to your own registry:

    docker build -t dbt-costgate .
    docker run --rm -v "$PWD:/workspace" dbt-costgate check

    The image runs as a non-root user, mounts your project at /workspace, and
    contains dbt-costgate only — not dbt, so compile in the image you already use
    for that and hand this one the target/. The
    usage guide
    has a GitLab pipeline that authenticates keylessly, with no service-account key
    anywhere. A published image on ghcr.io is wired but not switched on yet;
    build it yourself or push it to your own registry until it lands.

  • A pre-commit hook, id dbt-costgate. If your team runs
    pre-commit, adding this repository to your
    .pre-commit-config.yaml catches an expensive change on your own machine
    rather than in review. The config block to copy is in the
    usage guide.

    It runs at the pre-push stage, so install it with the stage named:

    pre-commit install --hook-type pre-push

    A plain pre-commit install will not fire it. Pre-push rather than pre-commit
    because the check needs a compiled target and a BigQuery round-trip per changed
    model — too slow to sit in front of every commit. It needs the same two things
    the CLI does (dbt compile first, BigQuery via ADC), and every check flag is
    available through args:. Requires pre-commit 3.2.0 or newer.

Changed

  • The incremental caveat is now one footnote instead of one line per model.
    A change touching five incrementals printed the same sentence five times,
    which pushed the caveats that are about a specific model — a dynamic filter,
    a missing baseline — under a wall of repeats. Reports now tag the rows and
    explain the tag once:

      fct_orders_daily  (full-refresh): 819.20 GiB → 2.91 TiB   +264%
      fct_events_hourly (full-refresh): 40.10 GiB → 44.02 GiB   +10%
    
      ⚠ full-refresh — for the rows tagged above, the figure is the full-refresh
        scan, not an incremental run.
    

    The per-row full-refresh tag is unchanged, so you can still see exactly
    which models it covers, and no other warning is collapsed. If you parse the
    terminal or markdown output, note the string incremental — figure is the full-refresh scan no longer appears in either. The JSON payload is
    unchanged
    models[].warnings still carries that warning per model, since
    a machine reader has no repetition problem to solve.

  • Priced reports now disclose the free tier they do not deduct, in the footer
    beside the rate they used:

    Priced from the first byte scanned: BigQuery's 1 TiB/month on-demand free
    tier is per billing account, so it is disclosed here and never deducted.
    

    Not new behaviour — costs have always been priced from the first byte — but it
    was stated only in the docs, which is the wrong place for it: someone comparing
    a report against a bill has the report in front of them. The allowance is drawn
    down by every other query the billing account runs that month, which a dry-run
    cannot see, so deducting it would mean guessing. A figure therefore reads high
    by at most one TiB's worth, and a gate that over-reports is safer than one that
    lets a regression through.

    Not configurable, by design: a setting could only mean "assume the tier is
    still unspent", a claim about the whole billing account this tool cannot check.
    The line does not appear when you have set pricing.usd_per_tib: 0.00 — that
    report quotes no money for it to adjust, and the tier is an on-demand allowance
    that does not apply under capacity/Editions pricing at all. Nothing about the
    gate, the breaches or the exit code changes; if you parse reports, note this
    adds a line to the terminal footer and a <br/> segment to the markdown one.
    The JSON payload is unchanged.

    The docs previously called the free tier "not modeled by default", which
    implied a setting that has never existed. That wording is gone.

  • The docs now say plainly that dbt-costgate prices compute, not storage.
    BigQuery meters the two separately, and a dry-run reports the bytes a query
    would scan — a compute figure that carries no storage information. Nothing
    about what the tool measures has changed; it is now stated as the scope it has
    always been, under
    what it will not do,
    with the case worth knowing called out: a view becoming a table, or an
    incremental becoming a full table, moves real money on a meter nothing here
    watches.

Fixed

  • A baseline with no compiled SQL is no longer reported as a basis mismatch.
    When the baseline manifest had no compiled code for a model, it was still
    assigned a query shape — and if your branch happened to compile to the other
    one, the report said:

    ⚠ mixed basis — baseline is full_refresh, current is incremental_form;
      recompile the baseline the same way
    

    naming a shape for SQL that was never compiled, directly beside the warning
    saying the baseline had no compiled SQL at all. Recompiling could not fix it,
    because the mismatch was not real.

    Such a model is still reported and still not gated — with no baseline
    bytes, the whole current scan reads as an increase, and a threshold firing on
    that is firing on a missing measurement rather than a regression. That
    outcome is now decided where the missing baseline is detected. Previously it
    fell out of the bogus mismatch, which meant it only applied when your branch
    compiled to the other shape: the same missing baseline gated or did not gate
    depending on something unrelated to it. If you have such a model, expect the
    spurious mixed basis line to disappear and the gating to stay off in cases
    where it was previously inconsistent.

    Unestimated rows also no longer carry a full-refresh / incremental tag,
    and models[].basis is null for them in JSON — there is no figure for a
    basis to describe.

  • An incremental model compiled against its existing table is no longer
    labelled full-refresh.
    An incremental has two prices — the cost to rebuild
    the table, and the cost of one run against the table as it already stands —
    and which one a dry-run measures is decided by how the model was compiled.
    Every incremental row was tagged full-refresh and told "figure is the
    full-refresh scan" regardless, so a figure that was one incremental run read
    as rebuild cost. On a large fact table those differ by orders of magnitude,
    and the mislabelled one reads low.

    This was not a rare case: a prod-run manifest captures incrementals in their
    incremental form, which the usage guide already documents.

    Rows are now labelled from the basis actually measured — full-refresh or the
    new incremental tag — with a footnote under the table for each one present:

      fct_orders_daily  (incremental): 92.16 GiB → 112.64 GiB   +22%   USD +0.13/run
    
      ⚠ incremental — for the rows tagged above, the figure is one run against the
        table as already built, so it does not gate rebuild cost.
    

    If you have a max_usd_total / max_tib_total ceiling on an incremental
    believing it capped rebuild cost, check the tag: on an incremental row it
    does not, and never did — the label was what said otherwise. The same applies
    to run_frequency, which should count rebuilds for a full-refresh row and
    runs for an incremental one.

    Two things change in output. The incremental tag is new, so anything matching
    on the literal full-refresh will no longer see these rows. And in JSON,
    models[].warnings carries a different sentence for an incremental-form model.
    models[].is_incremental is unchanged — it is true for both shapes, which
    is why it could never have answered this.