Skip to content

feat: configurable review rules and AI PR description summarize#7

Merged
miqdadyyy merged 10 commits into
mainfrom
feat/advanced-rule
Jun 8, 2026
Merged

feat: configurable review rules and AI PR description summarize#7
miqdadyyy merged 10 commits into
mainfrom
feat/advanced-rule

Conversation

@miqdadyyy

@miqdadyyy miqdadyyy commented Jun 8, 2026

Copy link
Copy Markdown
Owner

AI Summary

Summary

This PR adds structured, per-repository review rules so teams can control which findings are reported, which severities block merges, and which files are ignored during review. It also introduces AI-generated PR description summarization support across Git providers and documents the design/implementation.

Key Changes

  • Added configurable repository review rules in backend settings models and normalization logic:
    • selectable severities to include in reviews
    • configurable merge-gate blocking threshold, including disable support
    • ignored file pattern/type support
  • Updated review pipeline and review method implementations (diff_only, file_by_file, two_pass) to:
    • pass structured rule config through review payloads
    • filter findings by configured severities
    • ignore configured files before analysis
    • evaluate merge-gate failure using severity threshold instead of fixed high
  • Added shared review helpers and constants for severity ranking/defaults, plus tests covering rule normalization and review behavior.
  • Extended Git service interfaces and provider implementations for GitHub, GitLab, and Bitbucket to support PR description summarization.
  • Updated the rules UI to expose the new configuration options, including frontend types, combobox support, and styling changes.
  • Refreshed README and added design/plan docs for:
    • structured review rules
    • AI PR description summarization

Notable Decisions / Risks

  • Review blocking behavior changes from a hardcoded high threshold to a per-repository setting; defaults preserve prior behavior closely but now also treat critical explicitly.
  • Structured rules are stored by extending existing repository-rule JSON rather than introducing new tables, which minimizes schema change risk but increases reliance on normalization/backward compatibility.
  • File ignore behavior and severity filtering now affect what the model sees and what gets reported, which may change review output for existing repositories once rules are configured.

Original Description

Summary

Adds two per-repository configuration features under the Review Rules menu:

1. Structured review rules

  • Severity selection — checkboxes to choose which severities (low/medium/high/critical) get reported. Applied as both an LLM prompt hint and a post-LLM filter.
  • Block checks threshold — a dropdown to configure the minimum severity that fails the GitHub merge-gate check (or "Never block"). Defaults to high, preserving prior behavior. The block check respects the severity filter.
  • Ignore file types — free-form extensions/globs (e.g. .md, *.lock, vendor/**) whose changed files are skipped before review.

2. PR Description Summarize

  • New per-repository toggle. When enabled, Reviewly rewrites the pull request description with an AI-generated summary on every review run.
  • Any existing description is preserved below the AI summary, delimited by markers so the summary is replaced (not duplicated) on subsequent runs.
  • New provider-agnostic UpdatePullRequestDescription implemented for GitHub, GitLab, and Bitbucket.
  • Non-blocking: LLM or provider failures log a warning and the review continues.

Both features extend the existing repository_rule settings JSON (no new tables/migrations) and the existing /web/rules screen. Defaults preserve current behavior for existing repositories.

Test Plan

  • go test ./internal/usecase/setting/... ./internal/services/review/... ./internal/usecase/review/... ./internal/services/git/... — 270 passed
  • make cover — 75.7% (above 60% gate)
  • make lint — 0 issues
  • make build-web — passed
  • make mock — regenerated IGitService mock
  • Manual: enable each control on a repo, open/update a PR, verify severity filtering, block threshold, ignored files, and rewritten PR description

Ultraworked with Sisyphus

miqdadyyy and others added 9 commits June 8, 2026 17:52
Add severity selection, block-check threshold, and ignore-file-types
to the existing repository_rule setting, with prompt+filter severity
enforcement, configurable merge-gate threshold, and pre-review file
filtering.
Add summarize_pr_description toggle to repository_rule settings.
When enabled, Reviewly rewrites PR description with AI summary on
every review run, preserving original text below.
… prompts

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…hing

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…arize

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…ntrols

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown

✅ Coverage Report

Metric Value
Total Coverage 75.7%
Threshold 60.0%
Status Passed

Repository owner deleted a comment from reviewly-2026-06-08-ztpjjy Bot Jun 8, 2026
Repository owner deleted a comment from reviewly-2026-06-08-ztpjjy Bot Jun 8, 2026
Repository owner deleted a comment from reviewly-2026-06-08-ztpjjy Bot Jun 8, 2026
Repository owner deleted a comment from reviewly-2026-06-08-ztpjjy Bot Jun 8, 2026
Repository owner deleted a comment from reviewly-2026-06-08-ztpjjy Bot Jun 8, 2026
Repository owner deleted a comment from reviewly-2026-06-08-ztpjjy Bot Jun 8, 2026
Repository owner deleted a comment from reviewly-2026-06-08-ztpjjy Bot Jun 8, 2026
Repository owner deleted a comment from reviewly-2026-06-08-ztpjjy Bot Jun 8, 2026
Repository owner deleted a comment from reviewly-2026-06-08-ztpjjy Bot Jun 8, 2026
Repository owner deleted a comment from reviewly-2026-06-08-ztpjjy Bot Jun 8, 2026
Repository owner deleted a comment from reviewly-2026-06-08-ztpjjy Bot Jun 8, 2026
Repository owner deleted a comment from reviewly-2026-06-08-ztpjjy Bot Jun 8, 2026
Repository owner deleted a comment from reviewly-2026-06-08-ztpjjy Bot Jun 8, 2026
Repository owner deleted a comment from reviewly-2026-06-08-ztpjjy Bot Jun 8, 2026
Repository owner deleted a comment from reviewly-2026-06-08-ztpjjy Bot Jun 8, 2026
strings.Join(labels, ", ") +
". Ignore findings outside this selected severity set."
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ignore matching is not OS-path-safe and can fail to ignore files on Windows paths

Using path.Clean and checking only for "/" assumes slash-separated paths. If filePath comes from Git or filesystem code with backslashes, base extraction and pattern routing can fail, causing configured ignore patterns to be bypassed and sensitive or excluded files to still be processed.

Suggested change
func shouldIgnoreFile(filePath string, patterns []string) bool {
cleanedPath := strings.TrimPrefix(path.Clean(strings.ReplaceAll(strings.TrimSpace(filePath), "\\", "/")), "./")
if cleanedPath == "." || cleanedPath == "" {
return false
}
base := path.Base(cleanedPath)
for _, rawPattern := range patterns {
pattern := strings.TrimSpace(strings.ReplaceAll(rawPattern, "\\", "/"))
if pattern == "" {
continue
}
target := cleanedPath
if !strings.Contains(pattern, "/") {
target = base
if strings.HasPrefix(pattern, ".") {
pattern = "*" + pattern
}
}
if globMatch(pattern, target) {
return true
}
}
return false
}

return response, errors.New(fiber.StatusBadRequest, errors.ErrRepositoryRuleEmpty)
}

isEnabled := true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Empty repository rules can now be saved, breaking downstream rule processing

The previous guard rejecting blank/whitespace-only rule content was removed. This allows persisting enabled rules with empty content, which can lead to invalid configuration being treated as active and cause failures or bypasses in downstream evaluation that assumes a non-empty rule body.

Suggested change
isEnabled := true
if len(strings.TrimSpace(request.Content)) == 0 {
logger.Errorf("empty repository rule content for repository %d", request.RepositoryID)
return response, errors.New(fiber.StatusBadRequest, errors.ErrRepositoryRuleEmpty)
}
isEnabled := true

@@ -5,7 +5,6 @@ import (
"encoding/json"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Removed import is still required once blank-content validation is restored

The diff drops the strings import even though safe validation of rule content requires trimming whitespace before accepting it. Without reinstating that import, the necessary fix will not compile.

Suggested change
"encoding/json"
import (
"context"
"encoding/json"
stderrors "errors"
"strconv"
"strings"
"github.com/gofiber/fiber/v3"
"github.com/miqdadyyy/reviewly/internal/pkg/repository"
)

await upsertMutation.mutateAsync({
repository_id: selectedRepoId,
content,
is_enabled: isEnabled,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unsaved ignored file type input is dropped when saving

If the user types a pattern and clicks Save without pressing Enter/comma or blurring the field first, the current input is never committed and is silently omitted from the persisted rule.

Suggested change
is_enabled: isEnabled,
const handleSave = async () => {
if (selectedRepoId === null) return
const pendingIgnoredFileTypes = ignoredFileTypeInput
.split(",")
.map((item) => item.trim())
.filter((item) => item.length > 0)
const nextIgnoredFileTypes = Array.from(
new Set([...ignoredFileTypes, ...pendingIgnoredFileTypes])
)
await upsertMutation.mutateAsync({
repository_id: selectedRepoId,
content,
is_enabled: isEnabled,
severities,
block_threshold: blockThreshold,
ignored_file_types: nextIgnoredFileTypes,
summarize_pr_description: summarizePRDescription,
})
setIgnoredFileTypes(nextIgnoredFileTypes)
setIgnoredFileTypeInput("")
setSavedAt(Date.now())
}

is_enabled: isEnabled,
severities,
block_threshold: blockThreshold,
ignored_file_types: ignoredFileTypes,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Block threshold can be saved for severities that are not reviewed

The UI says only reviewed severities can block, but handleSave sends any selected blockThreshold even when that severity is unchecked. This can create an impossible or backend-invalid configuration such as reviewing only critical issues while blocking on medium.

Suggested change
ignored_file_types: ignoredFileTypes,
const handleSave = async () => {
if (selectedRepoId === null) return
const pendingIgnoredFileTypes = ignoredFileTypeInput
.split(",")
.map((item) => item.trim())
.filter((item) => item.length > 0)
const nextIgnoredFileTypes = Array.from(
new Set([...ignoredFileTypes, ...pendingIgnoredFileTypes])
)
const severityRank: Record<ReviewIssueLevel, number> = {
low: 0,
medium: 1,
high: 2,
critical: 3,
}
const normalizedBlockThreshold: ReviewBlockThreshold =
blockThreshold === "none" ||
severities.some(
(level) => severityRank[level] >= severityRank[blockThreshold as ReviewIssueLevel]
)
? blockThreshold
: "none"
await upsertMutation.mutateAsync({
repository_id: selectedRepoId,
content,
is_enabled: isEnabled,
severities,
block_threshold: normalizedBlockThreshold,
ignored_file_types: nextIgnoredFileTypes,
summarize_pr_description: summarizePRDescription,
})
setIgnoredFileTypes(nextIgnoredFileTypes)
setIgnoredFileTypeInput("")
setSavedAt(Date.now())
}

"/repositories/%s/%s/pullrequests/%d",
workspace,
ref.Repo,
ref.Number,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

PUT with a partial body can overwrite pull request fields or fail validation

This sends a full-resource update request with only the description field populated. For Bitbucket pull requests, PUT semantics commonly require the full current representation; using a partial payload risks clearing unspecified fields (e.g. title, destination/source branches, reviewers, close_source_branch) or being rejected by the API depending on server/cloud behavior. Use a partial-update method if supported, or first GET the existing pull request and send back all required fields merged with the new description.

var value settingucm.RepositoryRuleValue
if err := json.Unmarshal([]byte(s.Value), &value); err != nil {
return false
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Test now accepts empty rule content instead of enforcing prior validation

This change removes coverage for rejecting empty/whitespace-only repository rule content and replaces it with a success case. If the use case is still expected to prevent empty rules, this silently drops a critical business validation and can allow invalid settings to be stored. No safe replacement is possible without confirming intended behavior, but the previous failure-path test should be preserved rather than replaced.

RepositoryID int64 `json:"repository_id" validate:"required"`
Content string `json:"content"`
IsEnabled null.Bool `json:"is_enabled"`
Severities []constant.ReviewIssueLevel `json:"severities" validate:"omitempty,dive,oneof=low medium high critical"`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Rule content is no longer required on upsert requests

Removing validation from Content allows creating or overwriting repository rules with an empty string. Because this struct feeds persisted settings, accepting empty content can silently erase an existing rule or store an unusable rule configuration.

Suggested change
Severities []constant.ReviewIssueLevel `json:"severities" validate:"omitempty,dive,oneof=low medium high critical"`
Content string `json:"content" validate:"required"`

}
cfg.Severities = value.Severities
if len(cfg.Severities) == 0 {
cfg.Severities = append([]constant.ReviewIssueLevel(nil), constant.ReviewAllIssueLevels...)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unvalidated block threshold can fail checks for every issue level

BlockThreshold is accepted from repository settings without validation. If the stored value is invalid or lower than the configured severities, ReviewIssueLevelMeetsThreshold may mark all findings as blocking, causing incorrect failed checks across repositories. Clamp it to a known level and ensure it is compatible with configured severities before returning the config.

Suggested change
cfg.Severities = append([]constant.ReviewIssueLevel(nil), constant.ReviewAllIssueLevels...)
cfg.BlockThreshold = value.BlockThreshold
if cfg.BlockThreshold == "" || !constant.ReviewIssueLevelIsValid(cfg.BlockThreshold) {
cfg.BlockThreshold = constant.ReviewIssueLevelHigh
}
if len(cfg.Severities) > 0 {
allowed := false
for _, severity := range cfg.Severities {
if severity == cfg.BlockThreshold {
allowed = true
break
}
}
if !allowed {
cfg.BlockThreshold = constant.ReviewIssueLevelHigh
}
}

&pullRequest,
pullRequestRef,
changedFiles,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LLM summary is written to PR description without sanitization

The generated summary is inserted directly into the PR body and pushed to the Git provider. LLM output can contain arbitrary markdown/HTML, mention spam, or marker strings that break future parsing and can be abused to alter the visible PR description. Sanitize the content and strip reserved markers before composing the new body.

Suggested change
)
sanitizedSummary := strings.TrimSpace(result.Content)
sanitizedSummary = strings.ReplaceAll(sanitizedSummary, constant.ReviewPRSummaryStartMarker, "")
sanitizedSummary = strings.ReplaceAll(sanitizedSummary, constant.ReviewPRSummaryEndMarker, "")
sanitizedSummary = strings.ReplaceAll(sanitizedSummary, "@", "@")
if sanitizedSummary == "" {
logger.Warnf("pr description summarize: empty llm response")
return
}
originalDescription := stripReviewlySummaryBlock(pullRequest.Description)
newDescription := composePRDescription(sanitizedSummary, originalDescription)

@reviewly-2026-06-08-ztpjjy

Copy link
Copy Markdown

Review Summary

Low overall risk: this diff only adds tests and introduces no production-path changes. No high/critical correctness, security, or performance issues are evident in the added test coverage. Recommended next action: run the full test suite and verify these cases exercise the intended production behavior.

Overall risk appears moderate: the change adds severity/file-type filtering but the diff alone shows no clear high/critical correctness or security defect. The main recurring theme is potential behavior drift from new filters. Next action: add/verify tests covering ignored file types and severity-filtered summaries/results end-to-end.

No high-severity issues are evident in this diff. The change only adds configuration fields to a model without introducing logic, so the primary risk is deferred to downstream consumers. Next step: verify all construction, validation, and serialization paths handle these new fields safely.

Moderate-to-high correctness risk in new file-ignore glob handling. The main recurring theme is custom path/glob matching that can misclassify files across platforms and unintentionally skip review coverage. Next action: replace the ad-hoc glob engine with standard path-aware matching and add cross-platform tests for ignore patterns.

High risk: this change removes a key input validation and now persists/returns rule configurations without enforcing basic invariants. The recurring theme is accepting unvalidated user-controlled rule settings. Next step: restore request validation for content and validate enum-like fields before storing them.

Overall risk is low in this constants-only change. No high/critical correctness or security issues are evident from the diff alone. Next action: add tests around the new severity-threshold helper, especially for the new 'none' value and unknown levels.

Overall risk appears low from this isolated change: it adds a straightforward authenticated PATCH wrapper with no obvious high-severity correctness or security flaw. The main recurring theme is lack of visible guardrails around external API mutations, but nothing here clearly meets high/critical severity. Next action: add/update integration tests covering permissions, empty descriptions, and API error propagation for PR body updates.

High risk: the new save path can persist invalid rule configurations that contradict the UI guarantees. The recurring theme is missing normalization/validation before sending state to the API. Next action: enforce payload validation in handleSave so block thresholds and ignore patterns are saved consistently.

Low overall risk: this diff only extends an interface and introduces no executable logic. No high/critical issues are evident from the change itself. Next action: ensure all IGitService implementations and mocks are updated and covered by compilation/tests.

Overall risk is moderate-to-high due to a likely destructive API usage pattern. The main theme is correctness around partial resource updates potentially overwriting pull request fields. Next step: switch to Bitbucket's supported partial-update semantics or fetch-and-merge the existing PR before updating.

Low overall risk: this is generated mock boilerplate and no high/critical correctness, security, or performance issues are evident in the diff. No recurring high-impact theme was found. Next action: run the test suite or regenerate mocks from the interface to confirm signature alignment.

Low overall risk from this diff; no high/critical-impact correctness, security, or performance issues are evident in the shown changes. The main theme is filtering behavior consistency, and the next step is to add tests covering ignored-file and severity-filter interactions.

Overall risk is low; no high/critical correctness, security, or performance issues are evident in this diff. The main theme is minor accessibility/behavior concerns rather than severe defects. Recommended next action: add interaction and accessibility tests for keyboard navigation and focus management.

Low overall risk from this test-only change. The recurring theme is broader behavioral coverage for repository rules and check blocking semantics. Next action: ensure production code has equivalent validation for malformed/unsupported rule values, since these tests currently only cover happy/default paths.

Low overall risk from this diff; the main concern is inconsistent filtering semantics that can silently drop expected review outputs. The recurring theme is applying new filters in multiple phases without preserving invariants. Next step: ensure skipped files and severity-filtered empty results are explicitly handled so summaries and attempted counts stay consistent.

Moderate-to-high risk: the test changes relax previously enforced validation and may let invalid repository rules persist unnoticed. The recurring theme is converting business-rule validation tests into permissive persistence checks. Next action: verify the use case contract for empty/default rule fields and restore explicit rejection tests if validation is still required.

Low overall risk: the change only adds CSS custom variants and introduces no obvious high-severity correctness, security, or performance issues. No recurring high-impact theme was identified. Recommended next action: validate the variants in the build pipeline and component usage with normal testing.

Low overall risk from this diff; no high/critical issues are evident in the added method alone. The main theme is standard API update wiring without obvious correctness or security flaws. Recommended next action: validate behavior with integration tests against GitLab for edge cases like empty descriptions and permissions.

Low overall risk from this diff alone; it only changes frontend type definitions. The main theme is potential runtime/API compatibility breakage if backend responses or callers don't provide the newly required fields. Next action: verify backend contracts and all call sites, or make new fields optional/defaulted if rollout is incremental.

High risk: the request model now accepts empty rule content while adding new fields, which can let callers erase or create invalid repository rules. The recurring theme is weakened validation on persisted configuration inputs. Recommended next action: restore strict validation/defaulting for required fields before these values are stored or enforced.

High risk: this change can silently widen repository policy enforcement and mutate PR descriptions from untrusted content. The main recurring theme is missing validation/sanitization of repository rule and LLM-generated data before applying side effects. Next step: validate rule thresholds against configured severities and sanitize/guard PR description updates.

Finding breakdown: 10 issue(s): 0 critical, 10 high, 0 medium, 0 low.

@miqdadyyy
miqdadyyy merged commit 2b82dfb into main Jun 8, 2026
3 checks passed
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