lex-swarm-github 0.3.0 → 0.3.1
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/actors/lifecycle_subscriber.rb +53 -0
- data/lib/legion/extensions/swarm_github/runners/extension_lifecycle.rb +177 -0
- data/lib/legion/extensions/swarm_github/runners/pull_request_reviewer.rb +6 -4
- data/lib/legion/extensions/swarm_github/runners/review_notifier.rb +1 -0
- data/lib/legion/extensions/swarm_github/runners/review_poster.rb +1 -0
- data/lib/legion/extensions/swarm_github/version.rb +1 -1
- data/lib/legion/extensions/swarm_github.rb +2 -0
- data/spec/legion/extensions/swarm_github/actors/lifecycle_subscriber_spec.rb +108 -0
- data/spec/legion/extensions/swarm_github/runners/extension_lifecycle_spec.rb +179 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a1c767cd01e97e707ce4079c0a3313c723e984143e1e29cfcd580bfd7b391735
|
|
4
|
+
data.tar.gz: abf61ad73ed7875421cef31b0ff713031f5fc99bc61ece84943fdba22cf34169
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 99ae16ec047585531e0c41eedb26e55d4a4e4b2940562078bbc1c36f4f9eb734c08c1df8aef46aa35032ea68c2199e90d39cef50b7535eb8c179f16f90a689c9
|
|
7
|
+
data.tar.gz: f61a356f212fa8f7b7b250fa6d4fd9ef10446233842cf65410ec3239e76bb689793ef3694978b2e6ee67c00ea09f137b9bcc36d7ec0ad438b626ec5da9fa3875
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
return unless defined?(Legion::Extensions::Actors::Subscription)
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module SwarmGithub
|
|
8
|
+
module Actor
|
|
9
|
+
class LifecycleSubscriber < Legion::Extensions::Actors::Subscription
|
|
10
|
+
def runner_class = self.class
|
|
11
|
+
def runner_function = 'action'
|
|
12
|
+
def check_subtask? = false
|
|
13
|
+
def generate_task? = false
|
|
14
|
+
|
|
15
|
+
def action(payload)
|
|
16
|
+
verdict = (payload[:verdict] || payload['verdict']).to_s
|
|
17
|
+
|
|
18
|
+
unless verdict == 'approve' && github_lifecycle_enabled?
|
|
19
|
+
return { skipped: true, reason: verdict == 'approve' ? :github_disabled : :not_approved }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
generation = payload[:generation] || {}
|
|
23
|
+
review = payload.except(:generation)
|
|
24
|
+
|
|
25
|
+
Runners::ExtensionLifecycle.run_lifecycle(generation: generation, review: review)
|
|
26
|
+
rescue StandardError => e
|
|
27
|
+
log.warn("LifecycleSubscriber failed: #{e.message}")
|
|
28
|
+
{ success: false, error: e.message }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def github_lifecycle_enabled?
|
|
34
|
+
return false unless defined?(Legion::Settings)
|
|
35
|
+
|
|
36
|
+
Legion::Settings.dig(:codegen, :self_generate, :github, :enabled) == true
|
|
37
|
+
rescue StandardError => e
|
|
38
|
+
log.warn(e.message)
|
|
39
|
+
false
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def log
|
|
43
|
+
return Legion::Logging if defined?(Legion::Logging)
|
|
44
|
+
|
|
45
|
+
@log ||= Object.new.tap do |nl|
|
|
46
|
+
%i[debug info warn error fatal].each { |m| nl.define_singleton_method(m) { |*| nil } }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module SwarmGithub
|
|
6
|
+
module Runners
|
|
7
|
+
module ExtensionLifecycle
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
def run_lifecycle(generation:, review:)
|
|
11
|
+
config = github_config
|
|
12
|
+
return { success: false, error: :github_not_enabled } unless config[:enabled]
|
|
13
|
+
return { success: false, error: :target_repo_missing } unless config[:target_repo]
|
|
14
|
+
|
|
15
|
+
owner, repo = config[:target_repo].split('/')
|
|
16
|
+
name = generation[:name] || generation[:generation_id]
|
|
17
|
+
branch_name = "#{config[:branch_prefix]}-#{name}-#{Time.now.strftime('%Y%m%d%H%M%S')}"
|
|
18
|
+
|
|
19
|
+
branch = create_lifecycle_branch(owner: owner, repo: repo,
|
|
20
|
+
branch: branch_name, from_ref: config[:target_branch])
|
|
21
|
+
return branch unless branch[:success]
|
|
22
|
+
|
|
23
|
+
commit = commit_generated_files(owner: owner, repo: repo, branch: branch_name,
|
|
24
|
+
generation: generation)
|
|
25
|
+
return commit unless commit[:success]
|
|
26
|
+
|
|
27
|
+
pr = open_pull_request(owner: owner, repo: repo, branch: branch_name,
|
|
28
|
+
base: config[:target_branch], generation: generation, review: review)
|
|
29
|
+
return pr unless pr[:success]
|
|
30
|
+
|
|
31
|
+
label_pull_request(owner: owner, repo: repo, pull_number: pr[:pull_number],
|
|
32
|
+
labels: config[:pr_labels])
|
|
33
|
+
|
|
34
|
+
handle_auto_merge(owner: owner, repo: repo, pull_number: pr[:pull_number],
|
|
35
|
+
config: config, review: review)
|
|
36
|
+
|
|
37
|
+
{ success: true, pull_number: pr[:pull_number], html_url: pr[:html_url],
|
|
38
|
+
branch: branch_name, generation_id: generation[:generation_id] }
|
|
39
|
+
rescue StandardError => e
|
|
40
|
+
log.warn("ExtensionLifecycle failed: #{e.message}")
|
|
41
|
+
{ success: false, error: e.message }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def create_lifecycle_branch(owner:, repo:, branch:, from_ref:)
|
|
47
|
+
return { success: false, error: :github_runner_unavailable } unless github_runner_available?
|
|
48
|
+
|
|
49
|
+
github_client.create_branch(owner: owner, repo: repo, branch: branch, from_ref: from_ref)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def commit_generated_files(owner:, repo:, branch:, generation:)
|
|
53
|
+
return { success: false, error: :github_runner_unavailable } unless github_runner_available?
|
|
54
|
+
|
|
55
|
+
files = build_file_list(generation)
|
|
56
|
+
message = "add auto-generated #{generation[:name]} from gap #{generation[:gap_id]}"
|
|
57
|
+
|
|
58
|
+
github_client.commit_files(owner: owner, repo: repo, branch: branch, files: files, message: message)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def open_pull_request(owner:, repo:, branch:, base:, generation:, review:) # rubocop:disable Metrics/ParameterLists
|
|
62
|
+
return { success: false, error: :github_runner_unavailable } unless github_runner_available?
|
|
63
|
+
|
|
64
|
+
body = build_pr_body(generation: generation, review: review)
|
|
65
|
+
result = github_client.create_pull_request(
|
|
66
|
+
owner: owner, repo: repo, title: "auto-generated: #{generation[:name]}",
|
|
67
|
+
head: branch, base: base, body: body
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
pr = result[:result] || {}
|
|
71
|
+
{ success: true, pull_number: pr['number'], html_url: pr['html_url'] }
|
|
72
|
+
rescue StandardError => e
|
|
73
|
+
log.warn(e.message)
|
|
74
|
+
{ success: false, error: e.message }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def label_pull_request(owner:, repo:, pull_number:, labels:)
|
|
78
|
+
return { success: true } unless pull_number && labels&.any?
|
|
79
|
+
return { success: false, error: :github_runner_unavailable } unless github_runner_available?
|
|
80
|
+
|
|
81
|
+
github_client.add_labels(owner: owner, repo: repo, issue_number: pull_number, labels: labels)
|
|
82
|
+
rescue StandardError => e
|
|
83
|
+
log.warn("Label failed: #{e.message}")
|
|
84
|
+
{ success: false, error: e.message }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def handle_auto_merge(owner:, repo:, pull_number:, config:, review:)
|
|
88
|
+
return unless config[:auto_merge] && review[:verdict]&.to_sym == :approve
|
|
89
|
+
|
|
90
|
+
github_client.merge_pull_request(
|
|
91
|
+
owner: owner, repo: repo, pull_number: pull_number,
|
|
92
|
+
commit_title: 'auto-merge: generated extension'
|
|
93
|
+
)
|
|
94
|
+
rescue StandardError => e
|
|
95
|
+
log.warn("Auto-merge failed: #{e.message}")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def build_file_list(generation)
|
|
99
|
+
files = []
|
|
100
|
+
files << { path: generation[:file_path], content: generation[:code] } if generation[:code]
|
|
101
|
+
files << { path: generation[:spec_path], content: generation[:spec_code] } if generation[:spec_code]
|
|
102
|
+
files
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def build_pr_body(generation:, review:)
|
|
106
|
+
stages = (review[:stages] || {}).map do |name, stage|
|
|
107
|
+
passed = stage.is_a?(Hash) && stage[:passed] ? 'pass' : 'fail'
|
|
108
|
+
"| #{name} | #{passed} | |"
|
|
109
|
+
end.join("\n")
|
|
110
|
+
|
|
111
|
+
<<~BODY
|
|
112
|
+
## Auto-Generated Extension
|
|
113
|
+
|
|
114
|
+
**Gap**: #{generation[:gap_type]} - "#{generation[:name]}"
|
|
115
|
+
**Tier**: #{generation[:tier]}
|
|
116
|
+
**Generation ID**: #{generation[:generation_id]}
|
|
117
|
+
|
|
118
|
+
### Validation Results
|
|
119
|
+
|
|
120
|
+
| Stage | Result | Details |
|
|
121
|
+
|-------|--------|---------|
|
|
122
|
+
#{stages}
|
|
123
|
+
|
|
124
|
+
### Files
|
|
125
|
+
|
|
126
|
+
- `#{generation[:file_path]}` - Runner implementation
|
|
127
|
+
- `#{generation[:spec_path]}` - Specs
|
|
128
|
+
BODY
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def github_config
|
|
132
|
+
return default_config unless defined?(Legion::Settings)
|
|
133
|
+
|
|
134
|
+
settings = Legion::Settings.dig(:codegen, :self_generate, :github) || {}
|
|
135
|
+
default_config.merge(settings)
|
|
136
|
+
rescue StandardError => e
|
|
137
|
+
log.warn(e.message)
|
|
138
|
+
default_config
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def default_config
|
|
142
|
+
{
|
|
143
|
+
enabled: false, target_repo: nil, target_branch: 'main',
|
|
144
|
+
auto_merge: false, pr_labels: %w[auto-generated needs-review],
|
|
145
|
+
branch_prefix: 'feature/auto-generated'
|
|
146
|
+
}
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def github_runner_available?
|
|
150
|
+
defined?(Legion::Extensions::Github::Client)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def github_client
|
|
154
|
+
@github_client ||= Legion::Extensions::Github::Client.new(**github_connection_opts)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def github_connection_opts
|
|
158
|
+
return {} unless defined?(Legion::Settings)
|
|
159
|
+
|
|
160
|
+
{ token: Legion::Settings.dig(:github, :token) }
|
|
161
|
+
rescue StandardError => e
|
|
162
|
+
log.warn(e.message)
|
|
163
|
+
{}
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def log
|
|
167
|
+
return Legion::Logging if defined?(Legion::Logging)
|
|
168
|
+
|
|
169
|
+
@log ||= Object.new.tap do |nl|
|
|
170
|
+
%i[debug info warn error fatal].each { |m| nl.define_singleton_method(m) { |*| nil } }
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
@@ -28,7 +28,8 @@ module Legion
|
|
|
28
28
|
Legion::Extensions::Github::Client.new.list_pull_request_files(
|
|
29
29
|
owner: owner, repo: repo, pull_number: pull_number
|
|
30
30
|
)[:result] || []
|
|
31
|
-
rescue StandardError
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
log.warn(e.message) if respond_to?(:log, true)
|
|
32
33
|
[]
|
|
33
34
|
end
|
|
34
35
|
|
|
@@ -42,7 +43,7 @@ module Legion
|
|
|
42
43
|
end
|
|
43
44
|
merge_chunk_reviews(reviews)
|
|
44
45
|
rescue StandardError => e
|
|
45
|
-
|
|
46
|
+
log.warn("Review generation failed: #{e.message}") if respond_to?(:log, true)
|
|
46
47
|
{ summary: 'Review generation failed', comments: [] }
|
|
47
48
|
end
|
|
48
49
|
|
|
@@ -56,8 +57,9 @@ module Legion
|
|
|
56
57
|
end
|
|
57
58
|
|
|
58
59
|
def parse_review_response(response)
|
|
59
|
-
Legion::JSON.
|
|
60
|
-
rescue StandardError
|
|
60
|
+
Legion::JSON.parse(response)
|
|
61
|
+
rescue StandardError => e
|
|
62
|
+
log.warn(e.message) if respond_to?(:log, true)
|
|
61
63
|
{ summary: response.to_s, comments: [] }
|
|
62
64
|
end
|
|
63
65
|
|
|
@@ -22,6 +22,7 @@ module Legion
|
|
|
22
22
|
review_id = result.dig(:result, 'id') || result.dig(:result, :id)
|
|
23
23
|
{ posted: true, review_id: review_id, comments_count: inline_comments.size }
|
|
24
24
|
rescue StandardError => e
|
|
25
|
+
log.warn(e.message) if respond_to?(:log, true)
|
|
25
26
|
{ posted: false, reason: "post failed: #{e.message}" }
|
|
26
27
|
end
|
|
27
28
|
|
|
@@ -10,6 +10,8 @@ require 'legion/extensions/swarm_github/helpers/diff_chunker'
|
|
|
10
10
|
require 'legion/extensions/swarm_github/runners/review_poster'
|
|
11
11
|
require 'legion/extensions/swarm_github/runners/review_notifier'
|
|
12
12
|
require 'legion/extensions/swarm_github/runners/pr_pipeline'
|
|
13
|
+
require 'legion/extensions/swarm_github/runners/extension_lifecycle'
|
|
14
|
+
require 'legion/extensions/swarm_github/actors/lifecycle_subscriber' if defined?(Legion::Extensions::Actors::Subscription)
|
|
13
15
|
|
|
14
16
|
module Legion
|
|
15
17
|
module Extensions
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Actors
|
|
6
|
+
class Subscription # rubocop:disable Lint/EmptyClass
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
$LOADED_FEATURES << 'legion/extensions/actors/subscription'
|
|
13
|
+
|
|
14
|
+
require_relative '../../../../../lib/legion/extensions/swarm_github/runners/extension_lifecycle'
|
|
15
|
+
require_relative '../../../../../lib/legion/extensions/swarm_github/actors/lifecycle_subscriber'
|
|
16
|
+
|
|
17
|
+
RSpec.describe Legion::Extensions::SwarmGithub::Actor::LifecycleSubscriber do
|
|
18
|
+
subject(:actor) { described_class.new }
|
|
19
|
+
|
|
20
|
+
describe '#runner_function' do
|
|
21
|
+
it { expect(actor.runner_function).to eq('action') }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe '#check_subtask?' do
|
|
25
|
+
it { expect(actor.check_subtask?).to be false }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
describe '#generate_task?' do
|
|
29
|
+
it { expect(actor.generate_task?).to be false }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe '#action' do
|
|
33
|
+
let(:generation) do
|
|
34
|
+
{ name: 'lex-foo', generation_id: 'gen-1', file_path: 'lib/foo.rb', spec_path: 'spec/foo_spec.rb' }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
context 'when verdict is not approve' do
|
|
38
|
+
it 'skips with :not_approved reason' do
|
|
39
|
+
result = actor.action({ verdict: 'reject', generation: generation })
|
|
40
|
+
expect(result[:skipped]).to be true
|
|
41
|
+
expect(result[:reason]).to eq(:not_approved)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'handles string verdict' do
|
|
45
|
+
result = actor.action({ 'verdict' => 'pending', 'generation' => {} })
|
|
46
|
+
expect(result[:skipped]).to be true
|
|
47
|
+
expect(result[:reason]).to eq(:not_approved)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
context 'when verdict is approve but github lifecycle is disabled' do
|
|
52
|
+
before do
|
|
53
|
+
allow(actor).to receive(:github_lifecycle_enabled?).and_return(false)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'skips with :github_disabled reason' do
|
|
57
|
+
result = actor.action({ verdict: 'approve', generation: generation })
|
|
58
|
+
expect(result[:skipped]).to be true
|
|
59
|
+
expect(result[:reason]).to eq(:github_disabled)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
context 'when verdict is approve and github lifecycle is enabled' do
|
|
64
|
+
let(:lifecycle_result) { { success: true, pull_number: 99, html_url: 'https://github.com/org/repo/pull/99' } }
|
|
65
|
+
|
|
66
|
+
before do
|
|
67
|
+
allow(actor).to receive(:github_lifecycle_enabled?).and_return(true)
|
|
68
|
+
allow(Legion::Extensions::SwarmGithub::Runners::ExtensionLifecycle)
|
|
69
|
+
.to receive(:run_lifecycle).and_return(lifecycle_result)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'calls ExtensionLifecycle.run_lifecycle' do
|
|
73
|
+
actor.action({ verdict: 'approve', generation: generation })
|
|
74
|
+
expect(Legion::Extensions::SwarmGithub::Runners::ExtensionLifecycle)
|
|
75
|
+
.to have_received(:run_lifecycle)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'passes generation and review payload correctly' do
|
|
79
|
+
payload = { verdict: 'approve', generation: generation, extra_key: 'value' }
|
|
80
|
+
actor.action(payload)
|
|
81
|
+
expect(Legion::Extensions::SwarmGithub::Runners::ExtensionLifecycle)
|
|
82
|
+
.to have_received(:run_lifecycle).with(
|
|
83
|
+
generation: generation,
|
|
84
|
+
review: hash_including(verdict: 'approve')
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'returns the lifecycle result' do
|
|
89
|
+
result = actor.action({ verdict: 'approve', generation: generation })
|
|
90
|
+
expect(result).to eq(lifecycle_result)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
context 'when a StandardError is raised' do
|
|
95
|
+
before do
|
|
96
|
+
allow(actor).to receive(:github_lifecycle_enabled?).and_return(true)
|
|
97
|
+
allow(Legion::Extensions::SwarmGithub::Runners::ExtensionLifecycle)
|
|
98
|
+
.to receive(:run_lifecycle).and_raise(RuntimeError, 'boom')
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'returns success false with error message' do
|
|
102
|
+
result = actor.action({ verdict: 'approve', generation: {} })
|
|
103
|
+
expect(result[:success]).to be false
|
|
104
|
+
expect(result[:error]).to eq('boom')
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'legion/extensions/swarm_github/runners/extension_lifecycle'
|
|
5
|
+
|
|
6
|
+
RSpec.describe Legion::Extensions::SwarmGithub::Runners::ExtensionLifecycle do
|
|
7
|
+
subject(:runner) { described_class }
|
|
8
|
+
|
|
9
|
+
after { described_class.instance_variable_set(:@github_client, nil) }
|
|
10
|
+
|
|
11
|
+
let(:generation) do
|
|
12
|
+
{
|
|
13
|
+
name: 'lex-foo',
|
|
14
|
+
generation_id: 'gen-abc123',
|
|
15
|
+
gap_id: 'gap-001',
|
|
16
|
+
gap_type: 'missing_runner',
|
|
17
|
+
tier: 'utility',
|
|
18
|
+
file_path: 'lib/legion/extensions/foo.rb',
|
|
19
|
+
spec_path: 'spec/legion/extensions/foo_spec.rb',
|
|
20
|
+
code: '# runner code',
|
|
21
|
+
spec_code: '# spec code'
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
let(:review) { { verdict: 'approve', stages: { syntax: { passed: true }, quality: { passed: true } } } }
|
|
26
|
+
|
|
27
|
+
describe '#run_lifecycle' do
|
|
28
|
+
context 'when github is not enabled' do
|
|
29
|
+
it 'returns github_not_enabled error' do
|
|
30
|
+
result = runner.run_lifecycle(generation: generation, review: review)
|
|
31
|
+
expect(result).to eq({ success: false, error: :github_not_enabled })
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
context 'when github is enabled but target_repo is missing' do
|
|
36
|
+
before do
|
|
37
|
+
allow(runner).to receive(:github_config).and_return(
|
|
38
|
+
{ enabled: true, target_repo: nil, target_branch: 'main',
|
|
39
|
+
auto_merge: false, pr_labels: [], branch_prefix: 'feature/auto-generated' }
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'returns target_repo_missing error' do
|
|
44
|
+
result = runner.run_lifecycle(generation: generation, review: review)
|
|
45
|
+
expect(result).to eq({ success: false, error: :target_repo_missing })
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
context 'when github runner is unavailable' do
|
|
50
|
+
before do
|
|
51
|
+
allow(runner).to receive(:github_config).and_return(
|
|
52
|
+
{ enabled: true, target_repo: 'org/repo', target_branch: 'main',
|
|
53
|
+
auto_merge: false, pr_labels: [], branch_prefix: 'feature/auto-generated' }
|
|
54
|
+
)
|
|
55
|
+
allow(runner).to receive(:github_runner_available?).and_return(false)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'returns github_runner_unavailable on branch creation' do
|
|
59
|
+
result = runner.run_lifecycle(generation: generation, review: review)
|
|
60
|
+
expect(result).to eq({ success: false, error: :github_runner_unavailable })
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
context 'when all pipeline steps succeed' do
|
|
65
|
+
let(:github_client) { double('Github::Client') }
|
|
66
|
+
|
|
67
|
+
before do
|
|
68
|
+
runner.instance_variable_set(:@github_client, nil)
|
|
69
|
+
allow(runner).to receive(:github_config).and_return(
|
|
70
|
+
{ enabled: true, target_repo: 'org/repo', target_branch: 'main',
|
|
71
|
+
auto_merge: false, pr_labels: %w[auto-generated needs-review],
|
|
72
|
+
branch_prefix: 'feature/auto-generated' }
|
|
73
|
+
)
|
|
74
|
+
stub_const('Legion::Extensions::Github::Client', Class.new)
|
|
75
|
+
allow(Legion::Extensions::Github::Client).to receive(:new).and_return(github_client)
|
|
76
|
+
allow(github_client).to receive(:create_branch).and_return({ success: true })
|
|
77
|
+
allow(github_client).to receive(:commit_files).and_return({ success: true })
|
|
78
|
+
allow(github_client).to receive(:create_pull_request).and_return(
|
|
79
|
+
{ result: { 'number' => 42, 'html_url' => 'https://github.com/org/repo/pull/42' } }
|
|
80
|
+
)
|
|
81
|
+
allow(github_client).to receive(:add_labels).and_return({ success: true })
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'returns success with PR details' do
|
|
85
|
+
result = runner.run_lifecycle(generation: generation, review: review)
|
|
86
|
+
expect(result[:success]).to be true
|
|
87
|
+
expect(result[:pull_number]).to eq(42)
|
|
88
|
+
expect(result[:html_url]).to eq('https://github.com/org/repo/pull/42')
|
|
89
|
+
expect(result[:generation_id]).to eq('gen-abc123')
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'calls create_branch' do
|
|
93
|
+
runner.run_lifecycle(generation: generation, review: review)
|
|
94
|
+
expect(github_client).to have_received(:create_branch)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'calls commit_files with file list' do
|
|
98
|
+
runner.run_lifecycle(generation: generation, review: review)
|
|
99
|
+
expect(github_client).to have_received(:commit_files).with(
|
|
100
|
+
hash_including(files: array_including(hash_including(path: 'lib/legion/extensions/foo.rb')))
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it 'calls create_pull_request' do
|
|
105
|
+
runner.run_lifecycle(generation: generation, review: review)
|
|
106
|
+
expect(github_client).to have_received(:create_pull_request).with(
|
|
107
|
+
hash_including(title: 'auto-generated: lex-foo')
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it 'calls add_labels' do
|
|
112
|
+
runner.run_lifecycle(generation: generation, review: review)
|
|
113
|
+
expect(github_client).to have_received(:add_labels).with(
|
|
114
|
+
hash_including(labels: %w[auto-generated needs-review])
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
context 'when branch creation fails' do
|
|
120
|
+
before do
|
|
121
|
+
runner.instance_variable_set(:@github_client, nil)
|
|
122
|
+
allow(runner).to receive(:github_config).and_return(
|
|
123
|
+
{ enabled: true, target_repo: 'org/repo', target_branch: 'main',
|
|
124
|
+
auto_merge: false, pr_labels: [], branch_prefix: 'feature/auto-generated' }
|
|
125
|
+
)
|
|
126
|
+
stub_const('Legion::Extensions::Github::Client', Class.new)
|
|
127
|
+
allow(Legion::Extensions::Github::Client).to receive(:new).and_return(double(
|
|
128
|
+
create_branch: { success: false, error: 'branch exists' }
|
|
129
|
+
))
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it 'propagates the branch error' do
|
|
133
|
+
result = runner.run_lifecycle(generation: generation, review: review)
|
|
134
|
+
expect(result[:success]).to be false
|
|
135
|
+
expect(result[:error]).to eq('branch exists')
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
context 'when auto_merge is enabled and verdict is approve' do
|
|
140
|
+
let(:github_client) { double('Github::Client') }
|
|
141
|
+
let(:review_approve) { { verdict: 'approve', stages: {} } }
|
|
142
|
+
|
|
143
|
+
before do
|
|
144
|
+
runner.instance_variable_set(:@github_client, nil)
|
|
145
|
+
allow(runner).to receive(:github_config).and_return(
|
|
146
|
+
{ enabled: true, target_repo: 'org/repo', target_branch: 'main',
|
|
147
|
+
auto_merge: true, pr_labels: [], branch_prefix: 'feature/auto-generated' }
|
|
148
|
+
)
|
|
149
|
+
stub_const('Legion::Extensions::Github::Client', Class.new)
|
|
150
|
+
allow(Legion::Extensions::Github::Client).to receive(:new).and_return(github_client)
|
|
151
|
+
allow(github_client).to receive(:create_branch).and_return({ success: true })
|
|
152
|
+
allow(github_client).to receive(:commit_files).and_return({ success: true })
|
|
153
|
+
allow(github_client).to receive(:create_pull_request).and_return(
|
|
154
|
+
{ result: { 'number' => 7, 'html_url' => 'https://github.com/org/repo/pull/7' } }
|
|
155
|
+
)
|
|
156
|
+
allow(github_client).to receive(:merge_pull_request).and_return({ success: true })
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it 'calls merge_pull_request' do
|
|
160
|
+
runner.run_lifecycle(generation: generation, review: review_approve)
|
|
161
|
+
expect(github_client).to have_received(:merge_pull_request).with(
|
|
162
|
+
hash_including(pull_number: 7)
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
context 'when a StandardError is raised' do
|
|
168
|
+
before do
|
|
169
|
+
allow(runner).to receive(:github_config).and_raise(RuntimeError, 'unexpected failure')
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
it 'returns success false with error message' do
|
|
173
|
+
result = runner.run_lifecycle(generation: generation, review: review)
|
|
174
|
+
expect(result[:success]).to be false
|
|
175
|
+
expect(result[:error]).to eq('unexpected failure')
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lex-swarm-github
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -118,6 +118,7 @@ files:
|
|
|
118
118
|
- Gemfile
|
|
119
119
|
- lex-swarm-github.gemspec
|
|
120
120
|
- lib/legion/extensions/swarm_github.rb
|
|
121
|
+
- lib/legion/extensions/swarm_github/actors/lifecycle_subscriber.rb
|
|
121
122
|
- lib/legion/extensions/swarm_github/actors/pr_webhook.rb
|
|
122
123
|
- lib/legion/extensions/swarm_github/actors/stale_issues.rb
|
|
123
124
|
- lib/legion/extensions/swarm_github/client.rb
|
|
@@ -125,12 +126,14 @@ files:
|
|
|
125
126
|
- lib/legion/extensions/swarm_github/helpers/issue_tracker.rb
|
|
126
127
|
- lib/legion/extensions/swarm_github/helpers/mesh_integration.rb
|
|
127
128
|
- lib/legion/extensions/swarm_github/helpers/pipeline.rb
|
|
129
|
+
- lib/legion/extensions/swarm_github/runners/extension_lifecycle.rb
|
|
128
130
|
- lib/legion/extensions/swarm_github/runners/github_swarm.rb
|
|
129
131
|
- lib/legion/extensions/swarm_github/runners/pr_pipeline.rb
|
|
130
132
|
- lib/legion/extensions/swarm_github/runners/pull_request_reviewer.rb
|
|
131
133
|
- lib/legion/extensions/swarm_github/runners/review_notifier.rb
|
|
132
134
|
- lib/legion/extensions/swarm_github/runners/review_poster.rb
|
|
133
135
|
- lib/legion/extensions/swarm_github/version.rb
|
|
136
|
+
- spec/legion/extensions/swarm_github/actors/lifecycle_subscriber_spec.rb
|
|
134
137
|
- spec/legion/extensions/swarm_github/actors/pr_webhook_spec.rb
|
|
135
138
|
- spec/legion/extensions/swarm_github/actors/stale_issues_spec.rb
|
|
136
139
|
- spec/legion/extensions/swarm_github/client_spec.rb
|
|
@@ -138,6 +141,7 @@ files:
|
|
|
138
141
|
- spec/legion/extensions/swarm_github/helpers/issue_tracker_spec.rb
|
|
139
142
|
- spec/legion/extensions/swarm_github/helpers/mesh_integration_spec.rb
|
|
140
143
|
- spec/legion/extensions/swarm_github/helpers/pipeline_spec.rb
|
|
144
|
+
- spec/legion/extensions/swarm_github/runners/extension_lifecycle_spec.rb
|
|
141
145
|
- spec/legion/extensions/swarm_github/runners/github_swarm_spec.rb
|
|
142
146
|
- spec/legion/extensions/swarm_github/runners/pr_pipeline_spec.rb
|
|
143
147
|
- spec/legion/extensions/swarm_github/runners/pull_request_reviewer_spec.rb
|