Skip to content

feat(integrations): add per-repo and per-board title filters - #508

Open
mezotv wants to merge 4 commits into
mainfrom
emdash/empty-dodos-cheat-4fx39
Open

feat(integrations): add per-repo and per-board title filters#508
mezotv wants to merge 4 commits into
mainfrom
emdash/empty-dodos-cheat-4fx39

Conversation

@mezotv

@mezotv mezotv commented Jul 3, 2026

Copy link
Copy Markdown
Member

Summary

Adds a title filter feature for GitHub repositories and Linear integrations: exclude items whose titles match a rule from content generation and webhook-triggered automations.

  • New github_title_filters and linear_title_filters tables (one row per rule, no jsonb) with contains / regex match types, an enabled flag, and a unique index per integration to prevent duplicates. Migration 0048_normal_blue_marvel.sql.
  • Enabled rules are loaded into the GitHub and Linear tool contexts, so pull requests, commits, and Linear issues with matching titles are skipped in every workflow (schedules, events, chat, on-demand) with no per-route changes.
  • The GitHub webhook path filters push commits and drops release events whose title matches before dispatching event triggers.
  • New "Title filters" section on both integration detail pages: one-click presets with explanatory tooltips (docs, chore, ci/build, tests, dependency bumps, reverts, merge commits, wip; Linear gets the relevant subset), plus custom text or regex rules with client- and server-side regex validation, pause/resume switches, and delete.
  • New oRPC routes integrations.repositories.titleFilters.* and integrations.linear.titleFilters.* with org-access and subscription guards, a 50-rule cap, and 409 on duplicates.

Matching is case-insensitive; regex rules that fail to compile are ignored at runtime and rejected at write time. Note: tool results are cached for 2-10 minutes, so new filters can take a few minutes to affect in-flight generation.

Testing

  • tsc --noEmit clean in packages/ai, packages/db, apps/dashboard, apps/api
  • ultracite check clean on all changed files
  • Matcher unit-tested ad hoc (conventional-commit regexes, contains, invalid regex ignored, commit first-line extraction)
  • Migration not applied; run bun run db:migrate

https://claude.ai/code/session_01BzhAqsadq9kcj1vA52r6Ko


Summary by cubic

Add per-repo (GitHub) and per-board (Linear) title filters to exclude items from generated content and automations. GitHub webhooks also drop matching commits/releases and rebuild headCommit so filtered titles never reach triggers.

  • New Features

    • Dashboard: Title filters per GitHub repo (and per Linear integration) with presets, custom text/regex, enable/disable, delete, validation, and inline pattern editing (saves on blur/Enter). Suggestions now include lockfile updates and typo fixes; layout renamed to “Filters” with an inline type label.
    • API: integrations.repositories.titleFilters.* and integrations.linear.titleFilters.* with org access, subscription checks, 50-rule cap enforced atomically, 409 on case-insensitive duplicates, and an update route that supports pattern and match type changes.
    • DB: github_title_filters and linear_title_filters using a pg enum for match_type and case-insensitive unique indexes on pattern.
    • Runtime: case-insensitive matching; rules load into GitHub/Linear tool contexts to skip PRs, commits, and issues; GitHub webhooks drop filtered push commits, rebuild headCommit, and drop release events; commit pagination no longer ends early when filters apply; changes may take 2–10 minutes to apply due to caching.
  • Migration

    • Run DB migration 0048_hesitant_kate_bishop.sql.

Written for commit 81fac61. Summary will update on new commits.

Review in cubic

Adds github_title_filters and linear_title_filters tables (one row per
rule, no jsonb) with contains/regex match types. Enabled rules are
loaded into the GitHub and Linear tool contexts so pull requests,
commits, and issues with matching titles are skipped during content
generation, and the GitHub webhook path drops filtered commits and
release events before dispatching triggers.

The integration detail pages get a Title filters section with one-click
presets (docs, chore, ci/build, tests, dependency bumps, reverts, merge
commits, wip) plus custom text or regex rules, with pause/resume and
delete per rule.

Claude-Session: https://claude.ai/code/session_01BzhAqsadq9kcj1vA52r6Ko
@cursor

cursor Bot commented Jul 3, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@vercel

vercel Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
notra Ready Ready Preview, Comment Jul 4, 2026 4:15pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
notra-web Skipped Skipped Jul 4, 2026 4:15pm

Request Review

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

React Doctor found no new issues. 🎉

Reviewed by React Doctor for commit 81fac61.

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Added 1 comment

Comment thread apps/dashboard/src/lib/orpc/routers/integrations.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

6 issues found across 20 files

Confidence score: 3/5

  • In apps/dashboard/src/lib/webhooks/title-filters.ts (filterPushEvent), filtered push payloads can still expose an excluded title via unchanged data.headCommit, so downstream logic may process or display a commit that should have been blocked—recompute or clear headCommit after commit filtering before merging.
  • In packages/ai/src/tools/github.ts, pagination can terminate early when SHA and title filters are both enabled because hasNextPage depends on post-filter commit count; this can silently miss valid commits and produce incomplete results—base pagination continuation on source page/pageInfo rather than filtered length.
  • In apps/dashboard/src/lib/orpc/routers/integrations.ts and apps/dashboard/src/app/(dashboard)/[slug]/integrations/github/[id]/page-client.tsx, filter management can become inconsistent: concurrent creates can exceed the 50-rule cap, and multi-repo integrations only surface filters for the primary repo in the UI—add transactional/locking enforcement for creation and pass/select the active repositoryId in the client flow before merge.
  • In packages/db/src/schema.ts and packages/db/migrations/0048_normal_blue_marvel.sql, weak runtime constraints allow invalid match_type values and case-variant duplicate rules (wip/WIP), which can cause unexpected matching behavior and consume rule quota—enforce DB-level match_type constraints and case-normalized uniqueness/deduplication.

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

Comment thread apps/dashboard/src/lib/orpc/routers/integrations.ts Outdated
Comment thread packages/db/src/schema.ts Outdated
Comment thread apps/dashboard/src/lib/webhooks/title-filters.ts
Comment thread packages/ai/src/tools/github.ts
Comment thread packages/db/migrations/0048_hesitant_kate_bishop.sql
Enforce the 50-filter cap atomically with a pg advisory lock inside the
create transaction instead of a racy check-then-insert. Store match_type
as a pg enum and dedupe patterns case-insensitively via a lower(pattern)
unique index (migration 0048 regenerated, previously unapplied). Rebuild
headCommit when push-event commits are filtered so excluded titles no
longer leak through webhook payloads. Base commit pagination on the
SHA-filtered count so title filters cannot end paging early. Render a
title filter section per repository instead of only the primary one.
Split the title filters component into smaller pieces with inline query
invalidation, and give react-doctor full git history for baseline
comparison.

Claude-Session: https://claude.ai/code/session_01BzhAqsadq9kcj1vA52r6Ko
@cursor

cursor Bot commented Jul 4, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

The regenerated-in-place 0048 diverged from the copy already applied to
the staging database, breaking the drizzle migration ledger on deploy.
Regenerate a single 0048 with the final shape (match_type enum and
case-insensitive unique pattern indexes); staging will be reset so it
applies fresh.

Claude-Session: https://claude.ai/code/session_01BzhAqsadq9kcj1vA52r6Ko
@cursor

cursor Bot commented Jul 4, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

…polish

Filter patterns are now inline-editable inputs that save on blur or
Enter, backed by an update endpoint that accepts pattern and match type
changes with duplicate detection. Restructure the section so rules list
first and the composer sits in a muted band below, drop the per-row
badge in favor of an inline label, add lockfile and typo-fix presets
under a Suggestions group, rename the heading to Filters, tighten the
tooltip copy, and capitalize the match type select value.

Claude-Session: https://claude.ai/code/session_01BzhAqsadq9kcj1vA52r6Ko
@vercel
vercel Bot temporarily deployed to Preview – notra-web July 4, 2026 16:12 Inactive
@cursor

cursor Bot commented Jul 4, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Added 2 comments

pattern: titleFilterPatternSchema.optional(),
})
.superRefine((value, ctx) => {
if (value.enabled === undefined && value.pattern === undefined) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

updateTitleFilterBodySchema does not treat matchType as a first-class update field and only runs regex validation when a new pattern is provided, which creates inconsistent behavior: matchType-only updates are rejected, but requests that include enabled can still switch an existing filter to regex without validating the persisted pattern. That can leave a filter marked as regex but silently non-matching at runtime when the old pattern is not a valid regex. Update the refine logic so matchType is included in update-field checks and ensure the effective pattern is validated whenever matchType is set to regex.

// apps/dashboard/src/schemas/title-filters.ts
if (value.enabled === undefined && value.pattern === undefined) {
  ctx.addIssue({
    code: "custom",
    message: "At least one field must be provided",
    path: ["enabled"],

label: "Lockfile updates",
description: 'Excludes lockfile bumps like "update bun.lock"',
matchType: "regex",
pattern: "update.*lock",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[🟡 Medium] [🔵 Bug]

The new lockfile preset uses update.*lock, which matches many non-lockfile titles (for example, update lock timeout) because matching is case-insensitive and applied directly to item titles. Users who apply this preset can unintentionally suppress legitimate PRs/commits/issues from generation and automations. Tighten this preset to lockfile-specific tokens (for example lockfile, .lock, package-lock.json, pnpm-lock.yaml, bun.lockb) instead of any update ... lock phrasing.

// apps/dashboard/src/constants/title-filters.ts
{
  id: "lockfiles",
  label: "Lockfile updates",
  description: 'Excludes lockfile bumps like "update bun.lock"',
  matchType: "regex",
  pattern: "update.*lock",
},

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

4 issues found across 6 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/dashboard/src/schemas/title-filters.ts">

<violation number="1" location="apps/dashboard/src/schemas/title-filters.ts:50">
P3: The 'At least one field must be provided' check only considers `enabled` and `pattern`, but `matchType` is also an optional updatable field. Passing only `matchType` (e.g., `{ matchType: "contains" }`) would fail validation with a misleading error even though a valid field was supplied. Consider adding `&& value.matchType === undefined` to the condition, or decide explicitly whether updating matchType without a pattern should be allowed.</violation>

<violation number="2" location="apps/dashboard/src/schemas/title-filters.ts:54">
P3: When the 'at least one field' check fires on a payload that doesn't touch `enabled` at all (e.g. only `matchType` or only non-matching fields), the error path `["enabled"]` points to the wrong field. A more error-path-neutral approach would be to set `path: []` (form-level error) or use a descriptive code instead of anchoring on a field the caller didn't provide.</violation>
</file>

<file name="apps/dashboard/src/constants/title-filters.ts">

<violation number="1" location="apps/dashboard/src/constants/title-filters.ts:54">
P2: The new **Lockfile updates** preset can match unrelated titles, so users may accidentally filter out valid items. The cause is the broad regex `update.*lock`, which also matches words like `deadlock` after `update`. A narrower lockfile-focused pattern would better match the preset’s intent and avoid false positives.</violation>
</file>

<file name="packages/ai/src/integrations/title-filters.ts">

<violation number="1" location="packages/ai/src/integrations/title-filters.ts:102">
P2: These update methods now accept an all-optional params object, so calling `updateGithubTitleFilter`/`updateLinearTitleFilter` with `{}` is valid at the type level and flows into `.set({})`. That creates a fragile API surface where non-route callers can trigger runtime query failures instead of getting an explicit input error. It would be safer to enforce “at least one updatable field” in this package as well (type-level and/or runtime guard) before executing the update.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

label: "Lockfile updates",
description: 'Excludes lockfile bumps like "update bun.lock"',
matchType: "regex",
pattern: "update.*lock",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: The new Lockfile updates preset can match unrelated titles, so users may accidentally filter out valid items. The cause is the broad regex update.*lock, which also matches words like deadlock after update. A narrower lockfile-focused pattern would better match the preset’s intent and avoid false positives.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/dashboard/src/constants/title-filters.ts, line 54:

<comment>The new **Lockfile updates** preset can match unrelated titles, so users may accidentally filter out valid items. The cause is the broad regex `update.*lock`, which also matches words like `deadlock` after `update`. A narrower lockfile-focused pattern would better match the preset’s intent and avoid false positives.</comment>

<file context>
@@ -46,6 +46,20 @@ const TITLE_FILTER_PRESETS: TitleFilterPreset[] = [
+    label: "Lockfile updates",
+    description: 'Excludes lockfile bumps like "update bun.lock"',
+    matchType: "regex",
+    pattern: "update.*lock",
+  },
+  {
</file context>
Suggested change
pattern: "update.*lock",
pattern: "^update .*\\b(?:[\\w.-]*\\.lock|lockfile)\\b",

) {
const [updated] = await db
.update(githubTitleFilters)
.set(toTitleFilterUpdateSet(params))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: These update methods now accept an all-optional params object, so calling updateGithubTitleFilter/updateLinearTitleFilter with {} is valid at the type level and flows into .set({}). That creates a fragile API surface where non-route callers can trigger runtime query failures instead of getting an explicit input error. It would be safer to enforce “at least one updatable field” in this package as well (type-level and/or runtime guard) before executing the update.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/ai/src/integrations/title-filters.ts, line 102:

<comment>These update methods now accept an all-optional params object, so calling `updateGithubTitleFilter`/`updateLinearTitleFilter` with `{}` is valid at the type level and flows into `.set({})`. That creates a fragile API surface where non-route callers can trigger runtime query failures instead of getting an explicit input error. It would be safer to enforce “at least one updatable field” in this package as well (type-level and/or runtime guard) before executing the update.</comment>

<file context>
@@ -78,14 +92,14 @@ export async function createGithubTitleFilter(
   const [updated] = await db
     .update(githubTitleFilters)
-    .set({ enabled })
+    .set(toTitleFilterUpdateSet(params))
     .where(
       and(
</file context>

ctx.addIssue({
code: "custom",
message: "At least one field must be provided",
path: ["enabled"],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: When the 'at least one field' check fires on a payload that doesn't touch enabled at all (e.g. only matchType or only non-matching fields), the error path ["enabled"] points to the wrong field. A more error-path-neutral approach would be to set path: [] (form-level error) or use a descriptive code instead of anchoring on a field the caller didn't provide.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/dashboard/src/schemas/title-filters.ts, line 54:

<comment>When the 'at least one field' check fires on a payload that doesn't touch `enabled` at all (e.g. only `matchType` or only non-matching fields), the error path `["enabled"]` points to the wrong field. A more error-path-neutral approach would be to set `path: []` (form-level error) or use a descriptive code instead of anchoring on a field the caller didn't provide.</comment>

<file context>
@@ -40,7 +40,39 @@ export const titleFilterIdSchema = z.object({
+      ctx.addIssue({
+        code: "custom",
+        message: "At least one field must be provided",
+        path: ["enabled"],
+      });
+    }
</file context>

pattern: titleFilterPatternSchema.optional(),
})
.superRefine((value, ctx) => {
if (value.enabled === undefined && value.pattern === undefined) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: The 'At least one field must be provided' check only considers enabled and pattern, but matchType is also an optional updatable field. Passing only matchType (e.g., { matchType: "contains" }) would fail validation with a misleading error even though a valid field was supplied. Consider adding && value.matchType === undefined to the condition, or decide explicitly whether updating matchType without a pattern should be allowed.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/dashboard/src/schemas/title-filters.ts, line 50:

<comment>The 'At least one field must be provided' check only considers `enabled` and `pattern`, but `matchType` is also an optional updatable field. Passing only `matchType` (e.g., `{ matchType: "contains" }`) would fail validation with a misleading error even though a valid field was supplied. Consider adding `&& value.matchType === undefined` to the condition, or decide explicitly whether updating matchType without a pattern should be allowed.</comment>

<file context>
@@ -40,7 +40,39 @@ export const titleFilterIdSchema = z.object({
+    pattern: titleFilterPatternSchema.optional(),
+  })
+  .superRefine((value, ctx) => {
+    if (value.enabled === undefined && value.pattern === undefined) {
+      ctx.addIssue({
+        code: "custom",
</file context>
Suggested change
if (value.enabled === undefined && value.pattern === undefined) {
if (value.enabled === undefined && value.pattern === undefined && value.matchType === undefined) {

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