In CI¶
stevin ships as a GitHub Action. It runs plan, apply or drift, puts the result in
the job summary, and — on a pull request — posts it as a comment, updating its own
comment on every push instead of adding another.
uses: kostavo-oss/stevin@v0 follows the newest 0.x release; pin a release tag
(@v0.3.0a1) to hold one still. The action runs the stevin of its own version, so the
two never disagree.
Credentials¶
The action talks to your workspace the way the CLI does, through the Databricks SDK's unified auth. Give the job the environment variables, from repository or environment secrets:
env:
DATABRICKS_HOST: ${{ secrets.DATABRICKS_HOST }}
DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }}
DATABRICKS_WAREHOUSE_ID: ${{ secrets.DATABRICKS_WAREHOUSE_ID }}
A service principal with OAuth (DATABRICKS_CLIENT_ID / DATABRICKS_CLIENT_SECRET)
works the same way, and is the better choice for anything that applies.
Plan on every pull request¶
name: stevin
on:
pull_request:
paths: ["tables/**", "stevin.yml"]
permissions:
contents: read
pull-requests: write # to comment
jobs:
plan:
runs-on: ubuntu-latest
env:
DATABRICKS_HOST: ${{ secrets.DATABRICKS_HOST }}
DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }}
DATABRICKS_WAREHOUSE_ID: ${{ secrets.DATABRICKS_WAREHOUSE_ID }}
steps:
- uses: actions/checkout@v4
- id: plan
uses: kostavo-oss/stevin@v0
with:
target: prod
- uses: actions/upload-artifact@v4
with:
name: plan
path: ${{ steps.plan.outputs.plan-file }}
The comment shows the summary, an alert for anything destructive, expensive or
impossible, then each object as a comparison — what it is now, what it becomes, and the
sentence about the difference — with the numbered steps and the SQL folded away
underneath. It is the same comparison stevin ui shows, so the review on
the pull request and the one on a laptop can't describe the same plan differently. Only
the rows that moved are in the table; the rest are counted under it.
It is rendered from the plan file, so it shows exactly what apply of that file would
run. A very large plan steps down: the comparison without the SQL, then the list of
changes, then the table names — and says which of those it did.
Apply on merge¶
Review the plan on the pull request; apply it when it merges. A protected environment adds a human approval in between.
name: stevin apply
on:
push:
branches: [main]
paths: ["tables/**", "stevin.yml"]
jobs:
apply:
runs-on: ubuntu-latest
environment: prod # required reviewers go here
env:
DATABRICKS_HOST: ${{ secrets.DATABRICKS_HOST }}
DATABRICKS_CLIENT_ID: ${{ secrets.DATABRICKS_CLIENT_ID }}
DATABRICKS_CLIENT_SECRET: ${{ secrets.DATABRICKS_CLIENT_SECRET }}
DATABRICKS_WAREHOUSE_ID: ${{ secrets.DATABRICKS_WAREHOUSE_ID }}
steps:
- uses: actions/checkout@v4
- uses: kostavo-oss/stevin@v0
with:
command: apply
target: prod
command: apply plans, puts the plan in the job summary, and runs exactly that plan.
The summary and the outputs are written before anything is applied, so a run that fails
halfway still shows the plan it was running, with a line under it saying that it
failed. It
is made fresh at merge time, so it is always against the tables as they are now; apply
refuses it anyway if they move in between. Set allow-destructive: true only if you
mean it — without it, a plan that drops anything stops before running a single
statement. apply keeps its run history in the target's history_schema, so the
principal needs to be allowed to write there.
Catch drift nightly¶
drift asks whether apply would do anything. A hand edit in the catalog, a table
dropped outside stevin, a spec merged but never applied — all show up. Unmanaged
objects don't: stevin never claimed them.
name: stevin drift
on:
schedule:
- cron: "0 6 * * 1-5"
workflow_dispatch:
jobs:
drift:
runs-on: ubuntu-latest
env:
DATABRICKS_HOST: ${{ secrets.DATABRICKS_HOST }}
DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }}
DATABRICKS_WAREHOUSE_ID: ${{ secrets.DATABRICKS_WAREHOUSE_ID }}
steps:
- uses: actions/checkout@v4
- uses: kostavo-oss/stevin@v0
with:
command: drift
target: prod
The job fails when there is drift, with the details in the job summary. Set
fail-on-drift: false to report without failing, and branch on the has-changes
output instead.
On the command line, stevin drift exits 0 when live tables match their specs,
2 when they have drifted, and 1 when something went wrong — the convention
terraform plan -detailed-exitcode uses.
Inputs and outputs¶
| Input | Default | |
|---|---|---|
target |
(required) | The target in stevin.yml. |
command |
plan |
plan, apply or drift. |
config |
(found) | The project file, relative to working-directory. Left out, it is looked for there and above. |
working-directory |
. |
Where the project lives. |
clone |
false |
plan and apply: SHALLOW CLONE before risky steps. |
allow-destructive |
false |
apply only: let the plan drop something. |
comment |
true |
Comment on the pull request, if there is one. |
fail-on-drift |
true |
drift only: fail the job on drift. |
github-token |
github.token |
Needs pull-requests: write to comment. |
| Output | |
|---|---|
has-changes |
true if apply would do something (or, for drift, if there is drift). |
plan-file |
The plan as JSON, for stevin apply. Empty for drift. |
markdown-file |
The rendered comment. |