Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .github/workflows/examples/cute-dbt-report-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
# repo's own dogfood lives at `.github/workflows/report-preview.yml`,
# invoking the same shape against committed example fixtures.
#
# Walkthrough + variations (dbt-parse-in-CI, fork-PR workarounds, Pages
# preview) live at
# https://breezy-bays-labs.github.io/cute-dbt/recipes/ci-sticky-comment.html
# Walkthrough + variations (dbt-parse-in-CI, dbt-fusion standalone-binary
# compile, fork-PR workarounds, Pages preview, and the PrDiff-on-the-PR's-
# own-diff self-dogfood pattern) live at
# https://breezy-bays-labs.github.io/cute-dbt/recipes/github-actions-pr-review.html

name: cute-dbt report preview

Expand Down
188 changes: 183 additions & 5 deletions .github/workflows/report-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
# fork PRs. Same-repo branches are unaffected. To get fork-PR coverage,
# split this into a `pull_request` regenerate workflow + a
# `workflow_run`-triggered comment workflow with its own writable
# token (see `book/src/recipes/ci-sticky-comment.md` § Fork PRs for the
# two well-trodden workarounds).
# token (see `book/src/recipes/github-actions-pr-review.md` § Fork PRs
# for the two well-trodden workarounds).
#
# Consumer-recipe copy lives at
# `.github/workflows/examples/cute-dbt-report-preview.yml` (subdirectory
Expand Down Expand Up @@ -113,6 +113,153 @@ jobs:
retention-days: 7
if-no-files-found: warn

prdiff-preview:
# Self-dogfood the --pr-diff flow on THIS PR's own changes
# (cute-dbt#118). Distinct from the baseline `regenerate` matrix
# above: that one renders the committed example fixtures in
# BASELINE mode; this one runs --pr-diff against the PR's OWN git
# diff scoped to `dbt-project/`, using a CI-recompiled EPHEMERAL
# fusion manifest. A PR that edits `dbt-project/` therefore
# self-shows its rendered PrDiff diff (the inline SQL diff #111, the
# block-precise updated-test detection + YAML drawer diff #96, etc.)
# in the same sticky comment.
#
# Empty-diff handling: an in-step guard (the "Diff dbt-project/"
# step) detects when the PR touches no `dbt-project/` path and sets
# an output that short-circuits the expensive install/compile/render
# steps below — the job ends green with no artifact, never a
# misleading empty report, never a hard failure. (Chose an in-step
# guard over a `dorny/paths-filter`-style gate so no new third-party
# action enters the supply chain.)
#
# Manifest is EPHEMERAL — recompiled in CI from the PR HEAD, never
# committed — so the `root_path` absolute-path leak lesson (which
# applies only to *committed* manifests) does not apply here.
name: PrDiff preview (this PR's own dbt-project changes)
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
outputs:
# Surfaced so `sticky-comment` can tell "PR changed dbt-project/" (a real
# dbt-project row was uploaded) from "didn't" (no row). This job has NO
# job-level `if:`, so it always runs to completion and its `.result` is
# `success` even on non-dbt PRs — `.result` cannot distinguish the two;
# `touched` can.
touched: ${{ steps.diff.outputs.touched }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
# Full history so the base...head diff resolves locally; the
# base SHA is otherwise not guaranteed present on a shallow
# PR-head checkout.
fetch-depth: 0
persist-credentials: false

- name: Diff dbt-project/
id: diff
env:
# Pass the SHAs through env: — the official safe pattern for
# any `${{ }}` interpolation in a run: block. base.sha/head.sha
# are deterministic + fork-safe (no reliance on `origin` remote
# semantics or branch-name resolution).
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
mkdir -p target/report-preview
# Exclude the committed `target/` (the manifest cute-dbt consumes):
# a PR that touches only the committed manifest has no model/test
# SOURCE change to diff, so it should NOT trigger the full
# fusion-install + compile + render for an empty-scope report.
git diff --unified=0 "${BASE_SHA}...${HEAD_SHA}" \
-- dbt-project/ ':(exclude)dbt-project/target/' \
> target/report-preview/prdiff.patch
if [ -s target/report-preview/prdiff.patch ]; then
echo "touched=true" >> "$GITHUB_OUTPUT"
echo "This PR touches dbt-project/ — rendering PrDiff preview."
else
echo "touched=false" >> "$GITHUB_OUTPUT"
echo "This PR does not touch dbt-project/ — skipping PrDiff preview (no artifact)."
fi

# All steps below run ONLY when the PR touched dbt-project/, so a
# non-dbt PR pays no fusion-install / compile / cargo-build cost
# and the job ends green with no artifact.
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
if: steps.diff.outputs.touched == 'true'
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
if: steps.diff.outputs.touched == 'true'

- name: Install dbt-fusion (standalone binary, pinned)
if: steps.diff.outputs.touched == 'true'
env:
# Pinned to a specific fusion version — never floating `latest`
# (cute-dbt#118 hard constraint). NO pip / NO `uv pip`: fusion
# is a standalone Rust binary (consistent with #114). The
# installer drops `dbt` into ~/.local/bin.
FUSION_VERSION: 2.0.0-preview.177
run: |
set -euo pipefail
# `--version VER` pins the install (installer usage: "--version VER
# Install version VER"). `--update` is for updating an existing
# install, not a fresh CI runner.
curl -fsSL https://public.cdn.getdbt.com/fs/install/install.sh \
| sh -s -- --version "$FUSION_VERSION"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
# Fail loud if the pin did not land the intended release (the PATH
# update above only applies to LATER steps, so call dbt by full path).
installed=$("$HOME/.local/bin/dbt" --version | head -1)
echo "installed: $installed"
case "$installed" in
*"$FUSION_VERSION"*) ;;
*) echo "::error::expected dbt-fusion $FUSION_VERSION, got: $installed"; exit 1 ;;
esac

- name: Compile dbt-project (ephemeral manifest, offline)
if: steps.diff.outputs.touched == 'true'
# `profiles.yml` lives in-project (duckdb `:memory:`), so compile
# succeeds fully offline — no warehouse, no secrets, no network.
# There are no dbt packages, so there is no `dbt deps` step.
# Output target/manifest.json is EPHEMERAL — used only for this
# render, never committed.
working-directory: dbt-project
run: |
set -euo pipefail
dbt --version
dbt compile --profiles-dir .

- name: Render PrDiff report
if: steps.diff.outputs.touched == 'true'
# Same-revision contract: the manifest was just compiled at HEAD
# and the diff is base...head, so the diff hunks line up with the
# working-tree source — what makes the inline SQL diff (#111) and
# block-precise updated-test detection (#96) trustworthy. The
# `@`-prefix tells cute-dbt to read the diff from the file.
run: |
set -euo pipefail
cargo run --quiet --locked --bin cute-dbt -- \
--manifest dbt-project/target/manifest.json \
--pr-diff @target/report-preview/prdiff.patch \
--project-root dbt-project \
--out target/report-preview/dbt-project-report.html
ls -l target/report-preview/dbt-project-report.html

- name: Upload PrDiff report artifact
# Only upload when we actually rendered — a skipped (non-dbt) PR
# produces no artifact, so the sticky comment shows no
# dbt-project row. `if-no-files-found: error` would mask a real
# render failure, so guard on the touched flag instead.
if: always() && steps.diff.outputs.touched == 'true'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
# Name is globbed by the sticky-comment job's
# `report-preview-*` selector.
name: report-preview-dbt-project
path: target/report-preview/dbt-project-report.html
retention-days: 7
if-no-files-found: warn

sticky-comment:
# Builds and posts the sticky PR comment. Reads the upstream matrix
# artifacts via the GitHub API to assemble per-artifact clickable
Expand All @@ -121,7 +268,7 @@ jobs:
# indicates a real permission misconfig that should surface).
name: Post sticky preview comment
runs-on: ubuntu-latest
needs: [regenerate]
needs: [regenerate, prdiff-preview]
if: always() && github.event_name == 'pull_request'
permissions:
# actions: read — GET /repos/{owner}/{repo}/actions/runs/{run_id}/artifacts
Expand All @@ -137,6 +284,8 @@ jobs:
RUN_ID: ${{ github.run_id }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
MATRIX_RESULT: ${{ needs.regenerate.result }}
PRDIFF_RESULT: ${{ needs.prdiff-preview.result }}
PRDIFF_TOUCHED: ${{ needs.prdiff-preview.outputs.touched }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
Expand All @@ -150,13 +299,24 @@ jobs:
"/repos/${REPO}/actions/runs/${RUN_ID}/artifacts" \
--jq '[.artifacts[] | select(.name | startswith("report-preview-"))]' \
|| echo '[]')
# Build one table row per artifact. The dbt-project artifact
# (cute-dbt#118) is the PR's OWN --pr-diff render, so it gets a
# distinct label vs. the baseline example previews; it is also
# absent entirely on PRs that don't touch `dbt-project/` (the
# prdiff-preview job skips upload), so this never claims a row
# that isn't there.
rows=$(printf '%s' "$artifacts_json" \
| jq -r --arg repo "$REPO" --arg run "$RUN_ID" '
def row_label:
if . == "dbt-project"
then "dbt-project (PrDiff — this PR'"'"'s own changes)"
else . end;
if length == 0 then
"| _(no artifacts uploaded — see [run log](https://github.com/\($repo)/actions/runs/\($run)) for details)_ | — |"
else
map(
"| `" + (.name | sub("^report-preview-"; "")) + "-report.html`"
(.name | sub("^report-preview-"; "")) as $slug
| "| " + ($slug | row_label) + " — `" + $slug + "-report.html`"
+ " | [Download](https://github.com/" + $repo + "/actions/runs/" + $run + "/artifacts/" + (.id | tostring) + ") |"
) | join("\n")
end
Expand All @@ -169,12 +329,30 @@ jobs:
cancelled) echo "Run cancelled (likely superseded by a newer push)." ;;
*) echo "Regenerate result: \`${MATRIX_RESULT}\`." ;;
esac)
# Surface the PrDiff-preview job's fate. The job has NO job-level
# `if:`, so it runs to completion even on non-dbt PRs and reports
# `result: success` regardless — `.result` cannot tell "touched
# dbt-project/" from "didn't". Branch on its `touched` OUTPUT
# instead ('true' only when the PR changed dbt-project/ source;
# empty string if the job was ever genuinely skipped/cancelled, which
# the non-'true' branch absorbs into the no-row message).
if [ "${PRDIFF_TOUCHED}" = "true" ]; then
prdiff_line=$(case "$PRDIFF_RESULT" in
success) echo "This PR touches \`dbt-project/\` — the **dbt-project (PrDiff)** row below renders this PR's own \`--pr-diff\` diff." ;;
failure) echo "⚠️ The PrDiff self-preview job failed — see the [run log](${RUN_URL})." ;;
*) echo "" ;;
esac)
else
prdiff_line="_This PR doesn't touch \`dbt-project/\`, so there's no PrDiff self-preview row._"
fi
cat > target/sticky/body.md <<MARKDOWN
## 📄 Rendered report preview

${status_line}

| Example | Artifact |
${prdiff_line}

| Preview | Artifact |
|---|---|
${rows}

Expand Down
64 changes: 63 additions & 1 deletion book/src/recipes/github-actions-pr-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,9 @@ re-run button) is common.
`--manifest target/manifest.json`.
- **A different dbt version / engine:** edit the `pip install` line. Both
dbt-core 1.8+ and dbt-fusion 2.0-preview emit manifest schema v12, which
cute-dbt reads identically — see [How it works](../how-it-works.md).
cute-dbt reads identically — see [How it works](../how-it-works.md). To
compile with **dbt-fusion** instead of dbt-core, see the standalone-binary
variant in § 11 below (no pip).
- **Custom comment body:** the `message:` is plain markdown; add a model
count, a CHANGELOG link, whatever your reviewers want.
- **Branch protection:** make the `review` job a required status check so
Expand Down Expand Up @@ -444,3 +446,63 @@ actually touched, so a change confined to a `config:` block won't mark the
`unit_tests:` in the same file as updated.

See [Features](../features/index.md) for the full fidelity matrix.

## 11. Variant: compile with dbt-fusion (standalone binary, no pip)

[dbt-fusion](https://docs.getdbt.com/docs/fusion/about-fusion) is a
standalone Rust binary — **no Python, no pip, no virtualenv**. If your
project compiles under fusion, swap the `setup-python` + `pip install` +
`~/.dbt/profiles.yml` steps in § 4 / § 5 for the installer below. Everything
else (the diff + render core in § 3, the sticky comment, the trigger
patterns) is unchanged — fusion and dbt-core both emit manifest schema v12,
which cute-dbt reads identically.

```yaml
# Install dbt-fusion via the official standalone-binary installer,
# PINNED to a specific version (never floating `latest`). The
# installer drops `dbt` into ~/.local/bin.
- name: Install dbt-fusion
env:
FUSION_VERSION: 2.0.0-preview.177 # edit: pin your fusion version
run: |
set -euo pipefail
# `--version VER` pins the install; `--update` only updates an
# existing install (wrong for a fresh CI runner).
curl -fsSL https://public.cdn.getdbt.com/fs/install/install.sh \
| sh -s -- --version "$FUSION_VERSION"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
Comment thread
cmbays marked this conversation as resolved.
# Fail loud if the pin didn't land the intended release (PATH update
# above applies only to later steps, so call dbt by full path here).
"$HOME/.local/bin/dbt" --version | head -1 | grep -q "$FUSION_VERSION" \
|| { echo "::error::expected dbt-fusion $FUSION_VERSION"; exit 1; }

# Compile. With an in-project `profiles.yml` (duckdb `:memory:`),
# compile runs fully offline — no warehouse, no secrets, no network.
# If your project has no dbt packages there is no `dbt deps` step.
- name: Compile dbt project (dbt-fusion)
working-directory: dbt_project # edit: your project dir
run: |
set -euo pipefail
dbt --version
dbt compile --profiles-dir . # profiles.yml committed in-project
```

Notes:

- **Pin the version.** A floating `latest` makes CI non-reproducible and
can break on a fusion release. Pin a `2.0.0-preview.NNN` tag you have
tested (the installer takes `--version` via `sh -s -- --version <tag>`,
no `v` prefix).
- **In-project `profiles.yml` + `--profiles-dir .`** is the offline-friendly
setup — `compile` only parses and renders SQL, so the duckdb `:memory:`
target is never materialized and needs no secrets. (Alternatively keep
the `~/.dbt/profiles.yml` approach from § 4.)
- **Deprecated test-arg format.** Fusion rejects dbt's deprecated
generic-test argument format that dbt-core only warns about. If
`dbt compile` errors on that, run the official autofix ephemerally (no
venv, no pip): `uvx dbt-autofix@latest deprecations --path .`.
- This is exactly the path the cute-dbt repo's own
[`report-preview.yml`](https://github.com/breezy-bays-labs/cute-dbt/blob/main/.github/workflows/report-preview.yml)
uses to self-dogfood `--pr-diff` against an embedded fusion example
project — CI recompiles an **ephemeral** manifest at the PR head and
renders the PR's own diff into the sticky comment.
Loading