Skip to content

#346 — render: link PR number + title in the change-context banner - #363

Merged
cmbays merged 5 commits into
mainfrom
render-346-pr-link
Jun 13, 2026
Merged

cmbays merged 5 commits into
mainfrom
render-346-pr-link

Conversation

@cmbays

@cmbays cmbays commented Jun 13, 2026 •

Copy link
Copy Markdown
Contributor

Links the PR in the report change-context banner: PR # — <title> as an to the PR. Gen-time via --pr-url/--pr-title/--pr-number flags + a [pr] TOML section (flags override config); the review subcommand derives them from gh pr view (same call + title/url). Zero-egress-safe ( = navigation, fires nothing at view-time; title askama-escaped). Synthetic config-pr-showcase fixture + regenerated diff-showcase golden; jaffle/playground/macro-heavy byte-identical.

NOTE: committed by the orchestrator after the builder finished + verified clippy/fmt/nextest(2005)/bdd(222)/headless+zero-egress locally but was disk-blocked before crap4rs + push. CI runs the full suite.

Closes #346


Open in Stage

Summary by CodeRabbit

Release Notes

  • New Features

    • Added PR change-context banner rendering in PR-diff reports, displaying PR link and title customizable via new --pr-url, --pr-title, --pr-number CLI arguments or [pr] TOML config section.
    • Enhanced cute-dbt review command to automatically capture and pass PR metadata from GitHub CLI.
  • Documentation

    • Updated examples with new PR showcase configuration.

--pr-url/--pr-title/--pr-number flags + [pr] TOML section (flags override config);
review-mode derives them from gh pr view (same call, +title/url). Renders
PR #<n> linked in the --pr-diff banner (a-href = zero-egress-safe navigation,
title askama-escaped). Synthetic config-pr-showcase fixture + regenerated
diff-showcase golden; jaffle/playground/macro-heavy byte-identical.

Closes #346

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 13, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@cmbays, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 31 minutes and 18 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f0c7b111-24af-4643-b3e3-30547adb4b4a

📥 Commits

Reviewing files that changed from the base of the PR and between 99a4037 and 87ddb76.

📒 Files selected for processing (6)
  • .github/workflows/ci.yml
  • src/cli/mod.rs
  • src/domain/config.rs
  • src/domain/mod.rs
  • tests/headless_toggle.rs
  • tests/steps/pr_diff_scoping.rs
📝 Walkthrough

Walkthrough

This PR implements optional PR-context banners for cute-dbt report in PR-diff mode. Users can supply PR metadata via CLI flags, config file, or—in review mode—automatic extraction from gh pr view. The rendered banner displays a linked PR number and title, with scope gating and HTML escaping for security, and gracefully degrades when no context is supplied.

Changes

PR context banner linking

Layer / File(s) Summary
Domain model for PR context
src/domain/config.rs, src/domain/mod.rs
Introduces PrConfig with optional url, title, number fields and PrConfig::resolve() returning Option<PrRef> only when URL and title are present/non-blank; derives PR number from URL /pull/<n> segment when explicit number absent. Full test coverage for deserialization, resolution, and URL parsing tolerance.
CLI arguments and resolution logic
src/cli/args.rs, src/cli/mod.rs
Adds --pr-url, --pr-title, --pr-number to ReportArgs and implements resolve_pr_ref() merging CLI values over config, enabling explicit flag override of [pr] table fields.
Review command PR info extraction and threading
src/cli/review.rs
Extends PrInfo with title and url, updates parse_pr_info() to extract from gh pr view --json, threads resolved PR through base-detection ladder into ComposeInputs, and populates ReportArgs fields for downstream rendering.
Render payload, template binding, and scope gating
src/adapters/render.rs, templates/report.html
Adds PrRefPayload struct and optional pr_ref field to ReportPayload with From<&PrRef> conversion; extends render_report_with_externals() signature with pr_ref: Option<&PrRef>, implements scope gating (only applies when scope_source == ScopeSource::PrDiff), and updates template to render linked "PR #n — title" banner clause; includes new test harness validating linked clause, HTML escaping of title, and graceful absence when pr_ref is None.
Feature tests and step implementations
features/pr_diff_scoping.feature, tests/steps/pr_diff_scoping.rs
Adds three BDD scenarios validating PR banner rendering with supplied context (linked clause formatted "PR #77"), HTML escaping of metacharacters in title, and graceful degradation without context. Implements corresponding step definitions running cute-dbt report with --pr-url/--pr-title and asserting rendered HTML structure.
Workflow integration, fixtures, and documentation
.github/workflows/ci.yml, .github/workflows/report-preview.yml, examples/README.md, tests/fixtures/*
Extends CI example matrix and preview workflow to conditionally thread config via CONFIG env var; adds prdiff-preview job passing PR_URL/PR_TITLE/PR_NUMBER from GitHub event to cute-dbt report --pr-diff; documents regeneration with new --config flag; introduces synthetic config-pr-showcase.toml fixture for golden diff-showcase output.
Renderer call site updates
tests/headless_toggle.rs
Updates seven test helpers and assertions to pass None for new pr_ref parameter to render_report_with_externals(), maintaining compatibility across all existing test paths.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 A report that links back home,
PR #42 no more alone—
Click the banner, see the light,
Title escaped, secure and bright,
From diff to source, one hop's delight!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly references issue #346 and accurately summarizes the main change: linking PR number and title in the change-context banner.
Linked Issues check ✅ Passed The pull request implements all acceptance criteria from issue #346: adds PR context rendering in the banner with linked PR number, supports graceful degradation, provides flags and config section, preserves zero-egress with links and escaped titles, and maintains byte-identical goldens.
Out of Scope Changes check ✅ Passed All changes are scoped to issue #346 objectives: PR banner rendering, CLI flags, config section, review mode integration, template updates, fixture additions, and test coverage for the new feature.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch render-346-pr-link

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ghost

ghost commented Jun 13, 2026 •

Copy link
Copy Markdown

Ready to review this PR? Stage has broken it down into 7 individual chapters for you:

Title
1 Define PR reference domain models
2 Implement PR link rendering logic
3 Wire PR flags through CLI
4 Enhance review command with PR metadata
5 Update CI workflows and fixtures
6 Update documentation and golden reports
7 Verify PR link behavior with tests
Open in Stage

Chapters generated by Stage for commit 87ddb76 on Jun 13, 2026 8:14pm UTC.

@github-actions

github-actions Bot commented Jun 13, 2026 •

Copy link
Copy Markdown
Contributor

📄 Rendered report preview

All golden examples regenerated cleanly.

🟡 Golden examples

Committed to examples/ and byte-identity gated — the canonical reports contributors and consumers browse. Stable across PRs.

Report View Download
jaffle-shop-report.html ▶ Open ↗ ⬇ Download
macro-heavy-report.html ▶ Open ↗ ⬇ Download
playground-report.html ▶ Open ↗ ⬇ Download
diff-showcase-report.html ▶ Open ↗ ⬇ Download

🐶 Live dogfood preview

This PR doesn't touch dbt-project/, so there's no live dogfood preview.

🧭 Explore preview

The two-page cute-dbt explore explorer — dag.html (model lineage) + tests.html (unit-test viewer). Same golden/live split as the report.

🟡 Golden explore

The committed examples/explore/ playground golden (the full synthetic playground manifest). Byte-identity gated in Example report check. Stable across PRs.

Page View Download
explore/dag.html ▶ Open ↗ ⬇ Download
explore/tests.html ▶ Open ↗ ⬇ Download

🐶 Live explore

This PR doesn't touch dbt-project/, so there's no live explore preview.

▶ Open ↗ opens the report or explorer in your browser in one
click — published to this repo's GitHub Pages under
/pr-363/.
⬇ Download fetches the same self-contained HTML as a workflow
artifact (auth-gated; works fully offline). Either way the report
makes zero external resource requests.

The Pages preview may take ~1 min to update after this comment
posts. On PRs from forks the Open link is unavailable (read-only
token) — use Download.

Alternative: GitHub CLI
# gh CLI >= 2.63 extracts into ./report-preview-playground/.
gh run download 27477844847 -R breezy-bays-labs/cute-dbt -n report-preview-playground
open report-preview-playground/playground-report.html

Posted by report-preview.yml for 87ddb76f4383fa79e64c577d6435bb31ba377bef. Affordance only — never blocks merge.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements a feature (cute-dbt#346) that links the change-context banner in the generated report to the source pull request. It introduces new CLI flags (--pr-url, --pr-title, --pr-number) and a [pr] configuration section, while also automatically extracting PR details from gh pr view during review runs. The PR title is properly HTML-escaped in the template to prevent XSS. However, a security review identified a potential XSS vulnerability where an unvalidated PR URL could be used to inject malicious pseudo-protocols like javascript: or data: into the href attribute. It is recommended to validate that the resolved URL starts with a safe scheme like http:// or https://.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/domain/config.rs
The `[`PrConfig`](crate::domain::PrConfig)` link is redundant — PrConfig
is imported at module scope, so `[`PrConfig`]` auto-resolves to the same
item. Trips rustdoc::redundant-explicit-links under -D warnings (the
cargo-doc CI gate). Doc-comment-only; no behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/adapters/render.rs (1)

3062-3092: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restrict pr_ref.url to a safe GitHub PR URL before rendering.

Line 3068 and Line 3092 forward the raw URL from CLI/TOML/review into a clickable href and the embedded payload. HTML escaping won't neutralize javascript: or data: schemes, so a crafted --pr-url can turn the local report into a click-triggered script sink and break the "navigation-only / zero-egress" contract. Canonicalize this from trusted PR parts, or drop anything outside https://github.com/.../pull/....

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/adapters/render.rs` around lines 3062 - 3092, The pr_ref value assigned
into payload.pr_ref (see pr_ref, PrRefPayload::from and the ReportTemplate field
pr_ref) must be canonicalized/validated before embedding or rendering: parse the
candidate URL and allow only the exact GitHub PR form (scheme https, host
github.com, path matching /{owner}/{repo}/pull/{number}); reconstruct a safe
canonical URL from owner/repo/number and drop or null out pr_ref if it fails
validation; ensure the sanitized/None pr_ref is what is passed into
payload.pr_ref and therefore into payload_json_for_html_script and the
ReportTemplate so no raw user-provided URL (including javascript: or data:
schemes) can be emitted.
🧹 Nitpick comments (2)
src/cli/review.rs (2)

1023-1030: ⚡ Quick win

Clarify documentation: "manifest" is the wrong term here.

Lines 1024–1025 and 1028 refer to "the manifest" when describing the title and url fields, but these fields are extracted from the gh pr view JSON response, not from dbt's manifest.json. Using "manifest" in this context could confuse maintainers who might think this is related to the dbt manifest file.

📝 Suggested doc wording
-    /// The PR title (cute-dbt#346) — feeds the change-context banner link.
-    /// Empty when the manifest carries no title (the banner then renders
-    /// link-free — both a url and a title are required).
+    /// The PR title (cute-dbt#346) — feeds the change-context banner link.
+    /// Empty when `gh pr view` returns no title (the banner then renders
+    /// link-free — both a url and a title are required).
     pub title: String,
-    /// The PR's GitHub URL (cute-dbt#346) — the `<a href>` the banner
-    /// links to. Empty when absent (banner renders link-free).
+    /// The PR's GitHub URL (cute-dbt#346) — the `<a href>` the banner
+    /// links to. Empty when `gh pr view` returns no URL (banner renders link-free).
     pub url: String,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/review.rs` around lines 1023 - 1030, Update the field docs for the
struct fields title and url in src/cli/review.rs (the pub title: String and pub
url: String lines) to replace the word "manifest" with a clearer source
description such as "the GitHub PR view JSON (gh pr view response)" or "the
GitHub PR JSON returned by `gh pr view`", so the comments read that title and
url are extracted from the GitHub PR view JSON response rather than from dbt's
manifest.json.

1699-1699: 💤 Low value

Optional: Avoid cloning pr_info by reordering.

Line 1699 clones facts.pr_info before moving it into the returned tuple. Since facts is only used once more (line 1700), you could reorder to avoid the clone:

♻️ Suggested reordering
         let facts = gather_base_facts(toplevel, args.base.as_deref())?;
-        let pr_info = facts.pr_info.clone();
         let (base, rung) = decide_base(&facts)?;
+        let pr_info = facts.pr_info;
         (base, rung, pr_info)

The performance gain is small (one avoided allocation per review run), but the code is equally clear.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/review.rs` at line 1699, Avoid the unnecessary clone of
facts.pr_info: instead move it out of facts (e.g., let pr_info = facts.pr_info;
or destructure with let Facts { pr_info, .. } = facts;) and then use the
remaining fields of facts as before; this eliminates the extra allocation while
preserving behavior—ensure that subsequent use of facts only accesses other
fields (or borrow as needed) so moving pr_info is valid.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/steps/pr_diff_scoping.rs`:
- Around line 1178-1194: The test banner_shows_escaped_title currently only
asserts that raw_title (if it contains '<') does not appear verbatim in html;
update it to also assert that an escaped form or a safe substring of the title
is present so the title is actually rendered and escaped. Concretely, compute a
safe_part from raw_title (e.g., keep alphanumeric and whitespace characters as
suggested) and if safe_part.trim() is non-empty assert that
html.contains(&safe_part), and additionally verify the escaped form for '<' by
checking html.contains(&raw_title.replace("<", "&lt;")) (and similarly for other
common escapes like '>' -> "&gt;", '&' -> "&amp;", '"' -> "&quot;") so the test
both fails if the title was omitted and ensures proper escaping; make these
checks inside banner_shows_escaped_title using the existing raw_title and html
variables.

---

Outside diff comments:
In `@src/adapters/render.rs`:
- Around line 3062-3092: The pr_ref value assigned into payload.pr_ref (see
pr_ref, PrRefPayload::from and the ReportTemplate field pr_ref) must be
canonicalized/validated before embedding or rendering: parse the candidate URL
and allow only the exact GitHub PR form (scheme https, host github.com, path
matching /{owner}/{repo}/pull/{number}); reconstruct a safe canonical URL from
owner/repo/number and drop or null out pr_ref if it fails validation; ensure the
sanitized/None pr_ref is what is passed into payload.pr_ref and therefore into
payload_json_for_html_script and the ReportTemplate so no raw user-provided URL
(including javascript: or data: schemes) can be emitted.

---

Nitpick comments:
In `@src/cli/review.rs`:
- Around line 1023-1030: Update the field docs for the struct fields title and
url in src/cli/review.rs (the pub title: String and pub url: String lines) to
replace the word "manifest" with a clearer source description such as "the
GitHub PR view JSON (gh pr view response)" or "the GitHub PR JSON returned by
`gh pr view`", so the comments read that title and url are extracted from the
GitHub PR view JSON response rather than from dbt's manifest.json.
- Line 1699: Avoid the unnecessary clone of facts.pr_info: instead move it out
of facts (e.g., let pr_info = facts.pr_info; or destructure with let Facts {
pr_info, .. } = facts;) and then use the remaining fields of facts as before;
this eliminates the extra allocation while preserving behavior—ensure that
subsequent use of facts only accesses other fields (or borrow as needed) so
moving pr_info is valid.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 71d068dd-e9c8-420e-8700-8239ef652288

📥 Commits

Reviewing files that changed from the base of the PR and between 3383601 and 99a4037.

📒 Files selected for processing (16)
  • .github/workflows/ci.yml
  • .github/workflows/report-preview.yml
  • examples/README.md
  • examples/diff-showcase-report.html
  • features/pr_diff_scoping.feature
  • src/adapters/render.rs
  • src/cli/args.rs
  • src/cli/mod.rs
  • src/cli/review.rs
  • src/domain/config.rs
  • src/domain/mod.rs
  • templates/report.html
  • tests/fixtures/MANIFEST.toml
  • tests/fixtures/config-pr-showcase.toml
  • tests/headless_toggle.rs
  • tests/steps/pr_diff_scoping.rs

Comment thread tests/steps/pr_diff_scoping.rs
cmbays and others added 3 commits June 13, 2026 15:55
… review)

gemini security review: PrConfig.url interpolates into the change-context
banner's <a href>. askama escapes the title's HTML metacharacters but NOT a
url's scheme, so a javascript:/data: url from an untrusted [pr] config would
execute on click — an XSS in the otherwise trivially-auditable-safe report.
resolve() now allows only http(s) (case-insensitive); any other scheme (or a
scheme-relative //host) degrades to a link-free banner. +4 unit tests.

Also strengthens the BDD escaped-title step (CodeRabbit) with a positive
assertion that the title is actually rendered (longest non-escapable segment
present), so a dropped {{ pr.title }} can no longer pass it silently.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cmbays
cmbays merged commit 45eda16 into main Jun 13, 2026
37 checks passed
@cmbays
cmbays deleted the render-346-pr-link branch June 13, 2026 20:19
github-actions Bot added a commit that referenced this pull request Jun 13, 2026
@cmbays

cmbays commented Jun 13, 2026

Copy link
Copy Markdown
Contributor Author

🧾 Merge debrief — #363 (#346 PR-link in the change-context banner)

Merged 45eda16 · Closes #346 · feat → patch. Render lane (unblocks the next render slice).

What shipped: the source-PR reference in the change-context banner — PrConfig/PrRef + PrConfig::resolve() (CLI --pr-url/--pr-title/--pr-number over [pr] TOML, review-mode via gh pr view --json url,title), rendered as a linked PR #<n> — <title> clause with the title askama-escaped. New synthetic config-pr-showcase.toml drives the diff-showcase golden.

Bot disposition — 2 threads, BOTH ACCEPTED (probe-first):

  • gemini (security-HIGH) — real, accepted. The url interpolated into the banner <a href>; askama escapes the title's HTML metacharacters but NOT the url scheme, so a javascript:/data: url from an untrusted shared [pr] config would execute on click — an XSS in the otherwise trivially-auditable-safe report. resolve() now allowlists http/https (case-insensitive); any other scheme (or scheme-relative //host) degrades to a link-free banner. +4 unit tests.
  • coderabbit (minor) — accepted. The BDD escaped-title step was one-sided (only asserted the raw title was absent); strengthened with a positive assertion that the title is actually rendered (longest non-escapable segment present), so a dropped {{ pr.title }} can't pass silently.

Gate notes: a real cargo doc failure (redundant explicit intra-doc link) the disk-block had hidden was fixed first. Infra gotcha caught: a git push was silently rejected non-fast-forward while rtk printed a fake "ok" — verified the true remote tip via gh api .../git/refs and reconciled (reset-to-remote + cherry-pick). Full CI matrix green; byte-identity goldens unchanged (showcase url is https → resolve() still yields the banner).

Security note: this hardens the zero-egress/auditability promise — the report can't be turned into an XSS vector via a hostile config.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

render: link the PR number in the report change-context banner to the source PR

1 participant