Skip to content

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.