Skip to content

ci: gate Rams reviews behind real UI churn - #111

Merged
jzstern merged 2 commits into
mainfrom
feat/rams-ui-gate
Aug 4, 2026
Merged

ci: gate Rams reviews behind real UI churn#111
jzstern merged 2 commits into
mainfrom
feat/rams-ui-gate

Conversation

@jzstern

@jzstern jzstern commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Description

Rams was configured to review every PR on every push, which spent the free plan's 30-review budget on backend-only changes — this repo's last few PRs (yt-dlp, PO tokens, health probes) touched no UI at all, and the budget is currently 30/30 exhausted.

All of my local repos are now set to "Labeled rams only" in the Rams dashboard, so nothing is reviewed unless something applies the label. This workflow applies it automatically, but only when the diff is genuinely UI:

  • a new .svelte component, or
  • ≥20 changed lines across .svelte / .css / src/app.html / tailwind.config.js

…excluding tests/, e2e/, and *.test.* / *.spec.*.

Two properties worth calling out:

  • It can't burn quota on pushes. Re-adding a label GitHub already has fires no labeled webhook, so only the first qualifying push triggers a review. The workflow also never removes the label — removing and re-adding would cost a second review.
  • It only ever adds. Manual override still works: label a PR yourself to force a review the gate skipped.

It needs issues: write alongside pull-requests: write — the rams label doesn't exist in this repo yet, and creating a repo label is an issues-scoped action, so pull-requests: write alone would fail on the first run.

Future work

The path regex is SvelteKit-specific. Rolling this out to the other repos now in label mode (bcr, quorum, nocturne, …) needs per-stack globs.

How to test

The gate logic was verified against this repo's real PR data plus synthetic diffs:

Case Expected Result
PR #109 (yt-dlp backend, 11 files) skip skip
4-line +page.svelte tweak skip skip
test-only churn, incl. a .svelte under e2e/ skip skip
64-line +page.svelte edit label label
new Toast.svelte (6 lines) label label
40-line app.css change label label

actionlint passes clean (it shellchecks the run: block too).

  • Merge, then open a backend-only PR — it should get no rams label
  • Open a PR touching +page.svelte by >20 lines — it should get the rams label, and the run summary should show the counts that drove the decision

Note: the review budget resets around Aug 7, so until then a labelled PR will get "limit reached" from Rams rather than an actual review.

Author checklist

This PR:

  • Satisfies a goal that is specific & clearly motivated
  • Adds value in isolation (whether user-facing or sustainability-related)
  • Contains a concise & easy-to-understand title + description
  • Adheres to SRP by default
  • Presents the best possible implementation to meet its goal, given constraints at hand

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Chores

    • Added automated checks to identify substantial user-interface changes during development.
    • Pull requests with significant UI updates are now flagged for additional review.
    • Superseded checks are automatically canceled to keep validation results current.
  • User Impact

    • No changes to the application’s functionality or appearance.

Rams was set to review every PR on every push, which burned the free
plan's 30-review budget on backend-only changes. All local repos are now
in "labeled rams only" mode, so nothing gets reviewed unless something
applies the label.

This applies it automatically, but only when the diff is actually UI: a
new .svelte component, or >=20 changed lines across .svelte/.css/
app.html/tailwind config, excluding tests and e2e.

Re-adding a label GitHub already has fires no `labeled` webhook, so
pushes to an already-labelled PR cannot spend a second review. The
workflow never removes the label for the same reason.

Needs issues: write, not just pull-requests: write — the `rams` label
does not exist yet and creating a repo label is an issues-scoped action.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@railway-app

railway-app Bot commented Aug 4, 2026

Copy link
Copy Markdown

🚅 Deployed to the dub-rip-pr-111 environment in dub-rip

Service Status Web Updated (UTC)
dub-rip ✅ Success (View Logs) Web Aug 4, 2026 at 4:03 am

@railway-app
railway-app Bot temporarily deployed to dub-rip / dub-rip-pr-111 August 4, 2026 03:55 Destroyed
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@jzstern, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 52 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

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.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 92e0097b-7d7a-4131-9167-cac4a9bf3e4a

📥 Commits

Reviewing files that changed from the base of the PR and between 041e6b4 and 156f599.

📒 Files selected for processing (1)
  • .github/workflows/rams-ui-gate.yml
📝 Walkthrough

Walkthrough

The PR adds a GitHub Actions workflow that detects meaningful UI changes in eligible pull requests. It counts changed UI lines and new Svelte components, records a summary, and applies the rams label when thresholds are met.

Changes

Rams UI Gate

Layer / File(s) Summary
Workflow eligibility
.github/workflows/rams-ui-gate.yml
The workflow adds pull request triggers, concurrency controls, permissions, and conditions for non-draft pull requests from the same repository.
UI detection and labeling
.github/workflows/rams-ui-gate.yml
The workflow filters UI files, calculates change metrics, writes a summary, and creates or applies the rams label when thresholds are met.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PullRequest
  participant GitHubActions
  participant GitHubAPI
  PullRequest->>GitHubActions: Trigger eligible pull request workflow
  GitHubActions->>GitHubAPI: Retrieve pull request files
  GitHubAPI-->>GitHubActions: Return changed UI files and line counts
  GitHubActions->>GitHubActions: Calculate UI metrics and thresholds
  GitHubActions->>GitHubAPI: Create or apply rams label
  GitHubActions-->>PullRequest: Publish workflow summary
Loading
🚥 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 title clearly summarizes the workflow that gates Rams reviews based on meaningful UI changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/rams-ui-gate

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.

@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: 3

🤖 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 @.github/workflows/rams-ui-gate.yml:
- Around line 60-63: Update the label setup command in the workflow so it
creates the shared rams label only when absent and does not overwrite existing
color or description metadata. Remove the --force behavior from the gh label
create invocation, or otherwise guard creation with an existence check while
preserving the current metadata for existing labels.
- Around line 34-43: Update the gh api invocation assigned to files to use
--slurp, then flatten the paginated response into a single file array before the
existing UI filtering in the ui jq pipeline. Preserve the current filename
filters and scalar new_components and ui_lines calculations so the summary
receives single numeric values.
- Around line 60-67: Update the mutating gh commands in the workflow step to
explicitly target "$REPO": add the repository option to both gh label create and
gh pr edit, while preserving their existing arguments and behavior.
🪄 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 Plus

Run ID: 829e2c57-71b1-44f8-8026-f832b2ec4670

📥 Commits

Reviewing files that changed from the base of the PR and between 0ff94df and 041e6b4.

📒 Files selected for processing (1)
  • .github/workflows/rams-ui-gate.yml

Comment on lines +34 to +43
files=$(gh api "repos/$REPO/pulls/$PR/files" --paginate)

ui=$(jq -c '
[ .[]
| select(.filename | test("\\.svelte$|\\.css$|^src/app\\.html$|^tailwind\\.config\\.[cm]?[jt]s$"))
| select(.filename | test("(^|/)(tests?|e2e)/|\\.(test|spec)\\.") | not)
]' <<<"$files")

new_components=$(jq '[.[] | select(.status == "added" and (.filename | endswith(".svelte")))] | length' <<<"$ui")
ui_lines=$(jq '[.[].changes] | add // 0' <<<"$ui")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- workflow excerpt ---\n'
sed -n '1,120p' .github/workflows/rams-ui-gate.yml 2>/dev/null || true

printf '\n--- local version/tools ---\n'
(git --version 2>/dev/null | head -n1) || true
(gh version 2>/dev/null | head -n1) || true
(jq --version 2>/dev/null) || true
(node --version 2>/dev/null) || true

printf '\n--- inspect changed files if available ---\n'
git diff --name-only HEAD~1..HEAD 2>/dev/null | sed -n '1,120p' || true

Repository: jzstern/dub-rip

Length of output: 244


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- workflow excerpt ---'
sed -n '1,120p' .github/workflows/rams-ui-gate.yml 2>/dev/null || true

echo
echo '--- local version/tools ---'
git --version 2>/dev/null | head -n1 || true
gh version 2>/dev/null | head -n1 || true
jq --version 2>/dev/null || true
node --version 2>/dev/null || true

echo
echo '--- changed files if available ---'
git diff --name-only HEAD~1..HEAD 2>/dev/null | sed -n '1,120p' || true

Repository: jzstern/dub-rip

Length of output: 2866


🌐 Web query:

GitHub CLI gh api --slurp pagination each page output separate JSON value

💡 Result:

When using the GitHub CLI (gh api) with both --paginate and --slurp, the tool does not output each page as a separate, individual JSON value [1][2]. Instead, the --slurp flag instructs the CLI to wrap all individual pages (which are returned as JSON arrays or objects) into a single, outer JSON array [1][2]. In this combined mode: - Each individual page retrieved from the API is collected and placed as an element within the resulting master array [1][2]. - This effectively results in a single, valid JSON document containing all pages as items in that list [1][3][2]. If your workflow requires processing each page as a separate JSON value (for example, to pipe each page into a separate process or to handle them as discrete streams), you should avoid using --slurp [1]. Without --slurp, the GitHub CLI outputs each page sequentially as its own JSON document [1][2]. If you need to manipulate or flatten these pages while maintaining their individual integrity, you can use standard command-line tools like jq to process the stream without the wrapping array [3]. For further context on why this behavior exists, see the official gh api documentation [1] and related discussions in the GitHub CLI repository regarding how pagination and JSON formatting are handled [3][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- jq behavior with newline-delimited arrays versus slurped array ---'
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT

page1='[ {"filename":"src/add.css","status":"changed","changes":5}, {"filename":"src/foo.svelte","status":"changed","changes":3} ]'
page2='[ {"filename":"src/bar.css","status":"added","changes":7} ]'

echo 'without --slurp (default jq behavior)'
jq '[.[] | select(.filename | test("\\.svelte$|\\.css$|^src/app\\.html$|^tailwind\\.config\\.[cm]?[jt]s$"))]' <<< "$page1"
jq '[.[] | select(.filename | test("\\.svelte$|\\.css$|^src/app\\.html$|^tailwind\\.config\\.[cm]?[jt]s$"))]' <<< "$page2"
ui="$(jq '[.[] | select(.filename | test("\\.svelte$|\\.css$|^src/app\\.html$|^tailwind\\.config\\.[cm]?[jt]s$"))]' <<< "$page1"; jq '[.[] | select(.filename | test("\\.svelte$|\\.css$|^src/app\\.html$|^tailwind\\.config\\.[cm]?[jt]s$"))]' <<< "$page2")"
echo "ui=$(cat <<< "$ui")"
ui_lines="$(jq '[.[].changes] | add // 0' <<< "$ui")"
printf 'ui_lines=%s\n' "$ui_lines"
new_components="$(jq '[.[] | select(.status == "added" and (.filename | endswith(".svelte")))] | length' <<< "$ui")"
printf 'new_components=%s\n' "$new_components"

echo
echo 'with slurped wrapper array'
slurped='[{"filename":"src/add.css","status":"changed","changes":5}, {"filename":"src/foo.svelte","status":"changed","changes":3},{"filename":"src/bar.css","status":"added","changes":7}]'
ui_slurped="$(jq -c '[ .[][] | select(.filename | test("\\.svelte$|\\.css$|^src/app\\.html$|^tailwind\\.config\\.[cm]?[jt]s$")) ]' <<< "$slurped")"
ui_lines_slurped="$(jq '[.[].changes] | add // 0' <<< "$ui_slurped")"
new_components_slurped="$(jq '[.[] | select(.status == "added" and (.filename | endswith(".svelte")))] | length' <<< "$ui_slurped")"
printf 'ui_lines=%s\n' "$ui_lines_slurped"
printf 'new_components=%s\n' "$new_components_slurped"

Repository: jzstern/dub-rip

Length of output: 900


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- correct slurped filter path ---'
slurped='[
  [{"filename":"src/add.css","status":"changed","changes":5},{"filename":"src/foo.svelte","status":"changed","changes":3}],
  [{"filename":"src/bar.css","status":"added","changes":7}]
]'
ui="$(jq -c '[ .[][] | select(.filename | test("\\.svelte$|\\.css$|^src/app\\.html$|^tailwind\\.config\\.[cm]?[jt]s$")) ]' <<< "$slurped")"
printf '%s\n' "$ui"
ui_lines="$(jq '[.[].changes] | add // 0' <<< "$ui")"
printf 'ui_lines=%s\n' "$ui_lines"
new_components="$(jq '[.[] | select(.status == "added" and (.filename | endswith(".svelte")))] | length' <<< "$ui")"
printf 'new_components=%s\n' "$new_components"

Repository: jzstern/dub-rip

Length of output: 395


Flatten the paginated gh api --paginate response before comparing the counts.

Without --slurp, ui_lines and new_components receive newline-separated numbers from each page and the summary labels the PR from a non-scalar value, skipping the label-only path. Add --slurp and flatten the page arrays before UI filtering.

🤖 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 @.github/workflows/rams-ui-gate.yml around lines 34 - 43, Update the gh api
invocation assigned to files to use --slurp, then flatten the paginated response
into a single file array before the existing UI filtering in the ui jq pipeline.
Preserve the current filename filters and scalar new_components and ui_lines
calculations so the summary receives single numeric values.

Comment thread .github/workflows/rams-ui-gate.yml Outdated
Comment thread .github/workflows/rams-ui-gate.yml Outdated
Two fixes from review:

The job never checks out, so `gh label create` and `gh pr edit` had no
git remote to infer the repository from and would have failed on the
first qualifying UI PR. The passing run on #111 proves nothing here — it
exits before both commands. Set GH_REPO so every gh call is scoped.

`gh label create --force` rewrites an existing label's colour and
description, so a qualifying PR could silently revert repo-managed
metadata on the shared `rams` label. Create it only when absent.

A third suggestion — that `gh api --paginate` needs `--slurp` or jq sees
one document per page — was not applied. On gh 2.96.0 `--paginate`
merges array responses: fetching an 11-file PR at per_page=2 yields a
single 11-element array, which is what the jq filters already assume.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@railway-app
railway-app Bot temporarily deployed to dub-rip / dub-rip-pr-111 August 4, 2026 04:02 Destroyed
@jzstern
jzstern merged commit 70aba4f into main Aug 4, 2026
7 checks passed
@jzstern
jzstern deleted the feat/rams-ui-gate branch August 4, 2026 04:04
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.

1 participant