lex-swarm-github 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/legion/extensions/swarm_github/runners/pr_pipeline.rb +18 -2
- data/lib/legion/extensions/swarm_github/runners/pull_request_reviewer.rb +30 -12
- data/lib/legion/extensions/swarm_github/runners/review_notifier.rb +1 -1
- data/lib/legion/extensions/swarm_github/runners/review_poster.rb +9 -1
- data/lib/legion/extensions/swarm_github/version.rb +1 -1
- data/spec/legion/extensions/swarm_github/runners/pr_pipeline_spec.rb +24 -0
- data/spec/legion/extensions/swarm_github/runners/pull_request_reviewer_spec.rb +20 -0
- data/spec/legion/extensions/swarm_github/runners/review_notifier_spec.rb +20 -0
- data/spec/legion/extensions/swarm_github/runners/review_poster_spec.rb +36 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a5b852399782262ebab25e020d689b147a55ce6f256134a9d53137b223e6ba8e
|
|
4
|
+
data.tar.gz: 8804deca49b8a4bd37a262d90e152ade0e2359fd065f40ea4d32604bb0f885bf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 798c161250c26ba51ca15c0696d7f97dcc794d78b34fc1696b466e0aba16196368b96f2e5fc9351c0b34244efdbf3fe0c35b0a7cca4bc09d684d1fd014cec361
|
|
7
|
+
data.tar.gz: 5ad660b2154048f0974ff1ac444ac0a96f282c9ffa789455bb901a29b3c737cfe103ea0bfb8c177862daec6b11efe2d4606a221edcedb8689005b6c8dea76d38
|
|
@@ -10,8 +10,9 @@ module Legion
|
|
|
10
10
|
REVIEWABLE_ACTIONS = %w[opened synchronize reopened].freeze
|
|
11
11
|
DEFAULT_SLACK_CHANNEL = '#code-reviews'
|
|
12
12
|
|
|
13
|
-
def run_review_pipeline(owner:, repo:, pull_number:, slack_channel: nil, **)
|
|
13
|
+
def run_review_pipeline(owner:, repo:, pull_number:, slack_channel: nil, **opts)
|
|
14
14
|
channel = slack_channel || DEFAULT_SLACK_CHANNEL
|
|
15
|
+
issue_number = opts[:issue_number]
|
|
15
16
|
|
|
16
17
|
review = review_pull_request(owner: owner, repo: repo, pull_number: pull_number)
|
|
17
18
|
return { review: review, post: nil, notify: nil } unless review[:status] == 'reviewed'
|
|
@@ -20,7 +21,10 @@ module Legion
|
|
|
20
21
|
notify = notify_review(channel: channel, pull_ref: "#{owner}/#{repo}##{pull_number}",
|
|
21
22
|
review: review, post_result: post)
|
|
22
23
|
|
|
23
|
-
{ review: review, post: post, notify: notify }
|
|
24
|
+
result = { review: review, post: post, notify: notify }
|
|
25
|
+
bridge_review_to_issue(repo: "#{owner}/#{repo}", issue_number: issue_number,
|
|
26
|
+
review_result: result)
|
|
27
|
+
result
|
|
24
28
|
end
|
|
25
29
|
|
|
26
30
|
def run_review_pipeline_from_webhook(payload:, slack_channel: nil, **)
|
|
@@ -35,6 +39,18 @@ module Legion
|
|
|
35
39
|
slack_channel: slack_channel)
|
|
36
40
|
end
|
|
37
41
|
|
|
42
|
+
def bridge_review_to_issue(repo:, issue_number:, review_result:)
|
|
43
|
+
return unless issue_number
|
|
44
|
+
|
|
45
|
+
review_comments = review_result.dig(:review, :comments) || []
|
|
46
|
+
approved = review_comments.none? { |c| %w[critical high].include?(c[:severity]&.to_s&.downcase) }
|
|
47
|
+
@issue_tracker&.record_validation(
|
|
48
|
+
"#{repo}##{issue_number}",
|
|
49
|
+
validator: :code_review,
|
|
50
|
+
approved: approved
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
38
54
|
def handle_mesh_review_request(payload:, charter_id: nil, **)
|
|
39
55
|
owner = payload[:owner] || payload['owner']
|
|
40
56
|
repo = payload[:repo] || payload['repo']
|
|
@@ -9,8 +9,7 @@ module Legion
|
|
|
9
9
|
files = fetch_pr_files(owner: owner, repo: repo, pull_number: pull_number)
|
|
10
10
|
return { status: 'skipped', reason: 'no files' } if files.empty?
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
review = generate_review(diff_text)
|
|
12
|
+
review = generate_review(files)
|
|
14
13
|
|
|
15
14
|
{
|
|
16
15
|
status: 'reviewed',
|
|
@@ -33,24 +32,43 @@ module Legion
|
|
|
33
32
|
[]
|
|
34
33
|
end
|
|
35
34
|
|
|
36
|
-
def generate_review(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
caller:
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
def generate_review(files)
|
|
36
|
+
chunks = Helpers::DiffChunker.chunk_files(files)
|
|
37
|
+
reviews = chunks.map do |chunk|
|
|
38
|
+
diff_text = chunk.map { |f| "--- #{f[:filename]} ---\n#{f[:patch]}" }.join("\n\n")
|
|
39
|
+
prompt = code_review_prompt(diff_text)
|
|
40
|
+
response = Legion::LLM.chat(message: prompt, caller: { extension: 'lex-swarm-github' })
|
|
41
|
+
parse_review_response(response)
|
|
42
|
+
end
|
|
43
|
+
merge_chunk_reviews(reviews)
|
|
44
44
|
rescue StandardError => e
|
|
45
|
-
|
|
45
|
+
Legion::Logging::Logger.warn "Review generation failed: #{e.message}"
|
|
46
|
+
{ summary: 'Review generation failed', comments: [] }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def merge_chunk_reviews(reviews)
|
|
50
|
+
merged_comments = reviews.flat_map { |r| r[:comments] || [] }
|
|
51
|
+
summaries = reviews.map { |r| r[:summary] }.compact
|
|
52
|
+
{
|
|
53
|
+
summary: summaries.join("\n\n"),
|
|
54
|
+
comments: merged_comments
|
|
55
|
+
}
|
|
46
56
|
end
|
|
47
57
|
|
|
48
|
-
def
|
|
58
|
+
def parse_review_response(response)
|
|
59
|
+
Legion::JSON.load(response)
|
|
60
|
+
rescue StandardError
|
|
61
|
+
{ summary: response.to_s, comments: [] }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def code_review_prompt(diff_text)
|
|
49
65
|
<<~PROMPT
|
|
50
66
|
Review this code diff. Return JSON with:
|
|
51
67
|
- "summary": 1-2 sentence overall assessment
|
|
52
68
|
- "comments": array of {"file", "line", "severity", "message"}
|
|
53
69
|
Severity: info, warning, error. Focus on bugs and security issues.
|
|
70
|
+
|
|
71
|
+
#{diff_text}
|
|
54
72
|
PROMPT
|
|
55
73
|
end
|
|
56
74
|
end
|
|
@@ -11,7 +11,7 @@ module Legion
|
|
|
11
11
|
return { notified: false, reason: 'lex-slack not available' } unless defined?(Legion::Extensions::Slack::Client)
|
|
12
12
|
|
|
13
13
|
message = format_slack_message(pull_ref: pull_ref, review: review, post_result: post_result)
|
|
14
|
-
result = Legion::Extensions::Slack::Client.new
|
|
14
|
+
result = Legion::Extensions::Slack::Client.new.post_message(
|
|
15
15
|
channel: channel, text: message
|
|
16
16
|
)
|
|
17
17
|
|
|
@@ -15,7 +15,8 @@ module Legion
|
|
|
15
15
|
|
|
16
16
|
result = Legion::Extensions::Github::Client.new.create_review(
|
|
17
17
|
owner: owner, repo: repo, pull_number: pull_number,
|
|
18
|
-
body: body, comments: inline_comments
|
|
18
|
+
body: body, comments: inline_comments,
|
|
19
|
+
event: review_event(review[:comments])
|
|
19
20
|
)
|
|
20
21
|
|
|
21
22
|
review_id = result.dig(:result, 'id') || result.dig(:result, :id)
|
|
@@ -26,6 +27,13 @@ module Legion
|
|
|
26
27
|
|
|
27
28
|
private
|
|
28
29
|
|
|
30
|
+
def review_event(comments)
|
|
31
|
+
severities = (comments || []).map { |c| c[:severity]&.to_s&.downcase }
|
|
32
|
+
return 'REQUEST_CHANGES' if severities.any? { |s| %w[critical high].include?(s) }
|
|
33
|
+
|
|
34
|
+
'APPROVE'
|
|
35
|
+
end
|
|
36
|
+
|
|
29
37
|
def format_review_body(review)
|
|
30
38
|
parts = ["**Legion AI Review** (#{review[:files_reviewed]} file#{'s' if review[:files_reviewed] != 1} reviewed)"]
|
|
31
39
|
parts << ''
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'spec_helper'
|
|
4
|
+
require 'legion/extensions/swarm_github/client'
|
|
4
5
|
require 'legion/extensions/swarm_github/helpers/pipeline'
|
|
5
6
|
require 'legion/extensions/swarm_github/helpers/mesh_integration'
|
|
6
7
|
require 'legion/extensions/swarm_github/runners/pull_request_reviewer'
|
|
@@ -75,6 +76,29 @@ RSpec.describe Legion::Extensions::SwarmGithub::Runners::PrPipeline do
|
|
|
75
76
|
end
|
|
76
77
|
end
|
|
77
78
|
|
|
79
|
+
describe '#run_review_pipeline with issue bridge' do
|
|
80
|
+
let(:client) { Legion::Extensions::SwarmGithub::Client.new }
|
|
81
|
+
|
|
82
|
+
before do
|
|
83
|
+
client.ingest_issue(repo: 'owner/repo', issue_number: 5, title: 'Fix bug')
|
|
84
|
+
client.claim_issue(key: 'owner/repo#5')
|
|
85
|
+
client.start_fix(key: 'owner/repo#5')
|
|
86
|
+
allow(client).to receive(:review_pull_request).and_return(
|
|
87
|
+
{ status: 'reviewed', files_reviewed: 1, summary: 'OK', comments: [] }
|
|
88
|
+
)
|
|
89
|
+
allow(client).to receive(:post_review).and_return({ posted: true, review_id: 1, comments_count: 0 })
|
|
90
|
+
allow(client).to receive(:notify_review).and_return({ notified: false, reason: 'lex-slack not available' })
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
context 'when PR is linked to a tracked issue' do
|
|
94
|
+
it 'records review as validation on the linked issue' do
|
|
95
|
+
client.run_review_pipeline(owner: 'owner', repo: 'repo', pull_number: 1, issue_number: 5)
|
|
96
|
+
issue = client.get_issue(key: 'owner/repo#5')[:issue]
|
|
97
|
+
expect(issue[:validations]).not_to be_empty
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
78
102
|
describe '#run_review_pipeline_from_webhook' do
|
|
79
103
|
let(:review_result) do
|
|
80
104
|
{ status: 'reviewed', pr: 'LegionIO/core#7', files_reviewed: 2,
|
|
@@ -45,6 +45,26 @@ RSpec.describe Legion::Extensions::SwarmGithub::Runners::PullRequestReviewer do
|
|
|
45
45
|
end
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
context 'when diff exceeds max_chars' do
|
|
49
|
+
let(:large_files) do
|
|
50
|
+
[
|
|
51
|
+
{ filename: 'a.rb', patch: 'x' * 7000 },
|
|
52
|
+
{ filename: 'b.rb', patch: 'y' * 7000 }
|
|
53
|
+
]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
before do
|
|
57
|
+
stub_const('Legion::LLM', Module.new)
|
|
58
|
+
allow(reviewer).to receive(:fetch_pr_files).and_return(large_files)
|
|
59
|
+
allow(Legion::LLM).to receive(:chat).and_return('{"summary":"ok","comments":[]}')
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it 'calls LLM multiple times for chunked diffs' do
|
|
63
|
+
reviewer.review_pull_request(owner: 'owner', repo: 'repo', pull_number: 1)
|
|
64
|
+
expect(Legion::LLM).to have_received(:chat).at_least(2).times
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
48
68
|
context 'when PR has multiple files with comments' do
|
|
49
69
|
let(:files) do
|
|
50
70
|
[
|
|
@@ -68,6 +68,26 @@ RSpec.describe Legion::Extensions::SwarmGithub::Runners::ReviewNotifier do
|
|
|
68
68
|
end
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
+
context 'when instantiating Slack client' do
|
|
72
|
+
let(:slack_client) { double('Slack::Client') }
|
|
73
|
+
let(:slack_client_class) { Class.new }
|
|
74
|
+
|
|
75
|
+
before do
|
|
76
|
+
stub_const('Legion::Extensions::Slack::Client', slack_client_class)
|
|
77
|
+
allow(slack_client_class).to receive(:new).and_return(slack_client)
|
|
78
|
+
allow(slack_client).to receive(:post_message).and_return({ ok: true, ts: '1' })
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it 'instantiates Slack client with no arguments' do
|
|
82
|
+
notifier.notify_review(
|
|
83
|
+
channel: '#reviews', pull_ref: 'owner/repo#1',
|
|
84
|
+
review: review_result, post_result: post_result,
|
|
85
|
+
extra_kwarg: 'value'
|
|
86
|
+
)
|
|
87
|
+
expect(slack_client_class).to have_received(:new).with(no_args)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
71
91
|
context 'when lex-slack is not available' do
|
|
72
92
|
it 'returns not_available' do
|
|
73
93
|
result = notifier.notify_review(
|
|
@@ -61,6 +61,42 @@ RSpec.describe Legion::Extensions::SwarmGithub::Runners::ReviewPoster do
|
|
|
61
61
|
end
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
+
context 'when review has no critical or high severity comments' do
|
|
65
|
+
let(:github_client) { double('Github::Client') }
|
|
66
|
+
let(:review) { { status: 'reviewed', files_reviewed: 1, summary: 'Looks good', comments: [{ severity: 'info', message: 'nit' }] } }
|
|
67
|
+
|
|
68
|
+
before do
|
|
69
|
+
stub_const('Legion::Extensions::Github::Client', Class.new)
|
|
70
|
+
allow(Legion::Extensions::Github::Client).to receive(:new).and_return(github_client)
|
|
71
|
+
allow(github_client).to receive(:create_review).and_return({ result: { 'id' => 1 } })
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it 'posts with APPROVE event' do
|
|
75
|
+
poster.post_review(owner: 'owner', repo: 'repo', pull_number: 1, review: review)
|
|
76
|
+
expect(github_client).to have_received(:create_review).with(
|
|
77
|
+
hash_including(event: 'APPROVE')
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
context 'when review has critical severity comments' do
|
|
83
|
+
let(:github_client) { double('Github::Client') }
|
|
84
|
+
let(:review) { { status: 'reviewed', files_reviewed: 1, summary: 'Issues found', comments: [{ severity: 'critical', message: 'bug' }] } }
|
|
85
|
+
|
|
86
|
+
before do
|
|
87
|
+
stub_const('Legion::Extensions::Github::Client', Class.new)
|
|
88
|
+
allow(Legion::Extensions::Github::Client).to receive(:new).and_return(github_client)
|
|
89
|
+
allow(github_client).to receive(:create_review).and_return({ result: { 'id' => 2 } })
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'posts with REQUEST_CHANGES event' do
|
|
93
|
+
poster.post_review(owner: 'owner', repo: 'repo', pull_number: 1, review: review)
|
|
94
|
+
expect(github_client).to have_received(:create_review).with(
|
|
95
|
+
hash_including(event: 'REQUEST_CHANGES')
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
64
100
|
context 'when lex-github is not available' do
|
|
65
101
|
it 'returns not_available error' do
|
|
66
102
|
result = poster.post_review(owner: 'org', repo: 'repo', pull_number: 42, review: review_result)
|