The GitHub Action
Konstruct is also a GitHub Action. It reviews every pull request column by column, posts the review as a comment (updated in place on each push), writes it to the job summary, annotates each breaking change, and fails the check when a change removes a column that something downstream still reads:
# .github/workflows/konstruct.yml
name: konstruct
on: pull_request
permissions:
contents: read
pull-requests: write
jobs:
lineage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: kodexlabsio/konstruct@v0
Input |
Default |
Purpose |
|---|---|---|
|
|
Where the dbt project, SQL or DAGs are |
|
the pull request’s base branch |
Commit to compare with |
|
|
Post the review on the pull request |
|
|
Fail the check on a breaking change |
|
|
Run |
|
|
Arguments for |
|
none |
Konstruct extras, such as |
The outputs breaking, gate, summary, report (markdown) and json let later steps act on the result. The Action installs Konstruct from its own checkout, which compiles it: it sets up a toolchain (dtolnay/rust-toolchain) for that, and later steps in the job see it. Pin a release (@v0.7.0) for a fixed version; @v0 moves to each new 0.x release. docs/examples/pull-request.yml shows the same steps written out, for adding a dbt parse or other setup first.
Reading a Warehouse
With --no-warehouse left out of build-args, the build reads the warehouses your own profile names, but skips any that the repository’s konstruct.toml or profiles.yml names, because the pull request under review could change them (see Trusting a Project’s Connections). For pull requests, write the profile from the workflow’s secrets outside the checkout and point DBT_PROFILES_DIR at it:
- name: Write the dbt profile
run: printf '%s' "$PROFILE" > "$RUNNER_TEMP/profiles.yml"
env:
PROFILE: ${{ secrets.DBT_PROFILE }}
- uses: kodexlabsio/konstruct@v0
with:
build-args: --no-dbt
extras: snowflake
env:
DBT_PROFILES_DIR: ${{ runner.temp }}
On a branch whose content is already reviewed, such as a push to the default branch, --trust-config in build-args (or KONSTRUCT_TRUST_CONFIG: "1" in env) uses the repository’s own connections and, without --no-dbt, runs its dbt project. Never set either for pull requests from forks, or for any pull request whose konstruct.toml and profiles.yml have not been reviewed.
The Gate
konstruct gate is the check a change must pass before it is proposed. A coding agent runs it before opening a pull request (the Konstruct skills, the plugin’s hook and the gate MCP tool tell it to), and gate: true makes the Action run the same check:
- uses: kodexlabsio/konstruct@v0
with:
gate: true
It fails when the change:
Rule |
Fails when |
|---|---|
|
It removes a column that a model, task or dashboard still reads, whatever |
|
A term approved in |
|
A column it adds or changes, or one that changes because of it (a new filter on its relation, a change upstream), computes a business idea differently from an existing column (another filter, source, join or aggregation), as |
|
A data law that |
|
A column a repository listed in |
|
A column or an exposure it adds or changes, or one that changes because of it, receives sensitive data, or a relation it changes reads data sensitive at the base through SQL whose columns are not known, and nothing on the base marks the model or exposure to hold it (see Sensitive Data and Erasure) |
|
A table or materialised view it adds or changes copies a column the warehouse masks, unsanitised, with no masking policy of its own |
|
It takes a |
A conflict between columns the change does not touch does not fail it; a change to either side of an existing conflict does, until the two agree. Two columns bound to different terms in definitions.yml, such as net and gross revenue, are two definitions on purpose and do not fail it. In the Action the gate uses the graph built with build-args, so it runs no dbt the build did not. Each finding names the file and says how to fix it; the Action adds the findings to the review and annotates each one.
$ konstruct gate
Gate failed: 1 finding to fix before proposing the change.
conflicting-definition: gross.revenue computes 'revenue' differently from ltv.revenue (filter
WHERE: orders.status <> 'void')
file sql/gross.sql
fix Compute it the way the existing column does, or name the difference in definitions.yml.