feat(integrations): add per-repo and per-board title filters - #508
feat(integrations): add per-repo and per-board title filters#508mezotv wants to merge 4 commits into
Conversation
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
|
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. |
|
React Doctor found no new issues. 🎉 Reviewed by React Doctor for commit |
There was a problem hiding this comment.
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 unchangeddata.headCommit, so downstream logic may process or display a commit that should have been blocked—recompute or clearheadCommitafter commit filtering before merging. - In
packages/ai/src/tools/github.ts, pagination can terminate early when SHA and title filters are both enabled becausehasNextPagedepends on post-filter commit count; this can silently miss valid commits and produce incomplete results—base pagination continuation on source page/pageInforather than filtered length. - In
apps/dashboard/src/lib/orpc/routers/integrations.tsandapps/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 activerepositoryIdin the client flow before merge. - In
packages/db/src/schema.tsandpackages/db/migrations/0048_normal_blue_marvel.sql, weak runtime constraints allow invalidmatch_typevalues and case-variant duplicate rules (wip/WIP), which can cause unexpected matching behavior and consume rule quota—enforce DB-levelmatch_typeconstraints 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
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
|
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
|
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
|
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. |
| pattern: titleFilterPatternSchema.optional(), | ||
| }) | ||
| .superRefine((value, ctx) => { | ||
| if (value.enabled === undefined && value.pattern === undefined) { |
There was a problem hiding this comment.
[🟡 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", |
There was a problem hiding this comment.
[🟡 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",
},There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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>
| pattern: "update.*lock", | |
| pattern: "^update .*\\b(?:[\\w.-]*\\.lock|lockfile)\\b", |
| ) { | ||
| const [updated] = await db | ||
| .update(githubTitleFilters) | ||
| .set(toTitleFilterUpdateSet(params)) |
There was a problem hiding this comment.
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"], |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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>
| if (value.enabled === undefined && value.pattern === undefined) { | |
| if (value.enabled === undefined && value.pattern === undefined && value.matchType === undefined) { |
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.
github_title_filtersandlinear_title_filterstables (one row per rule, no jsonb) withcontains/regexmatch types, an enabled flag, and a unique index per integration to prevent duplicates. Migration0048_normal_blue_marvel.sql.integrations.repositories.titleFilters.*andintegrations.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 --noEmitclean in packages/ai, packages/db, apps/dashboard, apps/apiultracite checkclean on all changed filesbun run db:migratehttps://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
integrations.repositories.titleFilters.*andintegrations.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.github_title_filtersandlinear_title_filtersusing a pg enum formatch_typeand case-insensitive unique indexes on pattern.Migration
0048_hesitant_kate_bishop.sql.Written for commit 81fac61. Summary will update on new commits.