danger-ai_feedback 0.1.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 +7 -0
- data/.gitignore +4 -0
- data/.rubocop.yml +148 -0
- data/.travis.yml +10 -0
- data/Gemfile +6 -0
- data/Guardfile +21 -0
- data/LICENSE.txt +22 -0
- data/README.md +71 -0
- data/Rakefile +25 -0
- data/danger-ai_feedback.gemspec +50 -0
- data/lib/ai_feedback/gem_version.rb +5 -0
- data/lib/ai_feedback/plugin.rb +144 -0
- data/lib/danger_ai_feedback.rb +3 -0
- data/lib/danger_plugin.rb +3 -0
- data/spec/ai_feedback_spec.rb +79 -0
- data/spec/spec_helper.rb +66 -0
- metadata +201 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9b7b556be6a7aa2b2789240621ba2bbdfc031829e1454ad85758eb8b84097122
|
4
|
+
data.tar.gz: 9315ffa0b6c6530018860302054f51ba7f7b254b3960378b0e3c22967d431c57
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6c84f57d171e81178bb4b5d413bda308a3021a33854d97ee8469b055b029022bea553f656c617744c1641aca1c48ff3a11f1470f37194d556f15637656760bbd
|
7
|
+
data.tar.gz: 28a01d207a6a30847920d730ce7be598d8e1a6a08e8e4065ec208ed60a16e2537c3080e74de21a424274a094c2bb061cbff462543bbf4486683e300647c62f04
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
# Defaults can be found here: https://github.com/bbatsov/rubocop/blob/master/config/default.yml
|
2
|
+
|
3
|
+
# If you don't like these settings, just delete this file :)
|
4
|
+
|
5
|
+
AllCops:
|
6
|
+
TargetRubyVersion: 2.7
|
7
|
+
|
8
|
+
Style/StringLiterals:
|
9
|
+
EnforcedStyle: double_quotes
|
10
|
+
Enabled: true
|
11
|
+
|
12
|
+
# kind_of? is a good way to check a type
|
13
|
+
Style/ClassCheck:
|
14
|
+
EnforcedStyle: kind_of?
|
15
|
+
|
16
|
+
# specs sometimes have useless assignments, which is fine
|
17
|
+
Lint/UselessAssignment:
|
18
|
+
Exclude:
|
19
|
+
- '**/spec/**/*'
|
20
|
+
|
21
|
+
# We could potentially enable the 2 below:
|
22
|
+
Layout/FirstHashElementIndentation:
|
23
|
+
Enabled: false
|
24
|
+
|
25
|
+
Layout/HashAlignment:
|
26
|
+
Enabled: false
|
27
|
+
|
28
|
+
# HoundCI doesn't like this rule
|
29
|
+
Layout/DotPosition:
|
30
|
+
Enabled: false
|
31
|
+
|
32
|
+
# We allow !! as it's an easy way to convert ot boolean
|
33
|
+
Style/DoubleNegation:
|
34
|
+
Enabled: false
|
35
|
+
|
36
|
+
# Cop supports --auto-correct.
|
37
|
+
Lint/UnusedBlockArgument:
|
38
|
+
Enabled: false
|
39
|
+
|
40
|
+
# We want to allow class Fastlane::Class
|
41
|
+
Style/ClassAndModuleChildren:
|
42
|
+
Enabled: false
|
43
|
+
|
44
|
+
Metrics/AbcSize:
|
45
|
+
Max: 60
|
46
|
+
|
47
|
+
# The %w might be confusing for new users
|
48
|
+
Style/WordArray:
|
49
|
+
MinSize: 19
|
50
|
+
|
51
|
+
# raise and fail are both okay
|
52
|
+
Style/SignalException:
|
53
|
+
Enabled: false
|
54
|
+
|
55
|
+
# Better too much 'return' than one missing
|
56
|
+
Style/RedundantReturn:
|
57
|
+
Enabled: false
|
58
|
+
|
59
|
+
# Having if in the same line might not always be good
|
60
|
+
Style/IfUnlessModifier:
|
61
|
+
Enabled: false
|
62
|
+
|
63
|
+
# and and or is okay
|
64
|
+
Style/AndOr:
|
65
|
+
Enabled: false
|
66
|
+
|
67
|
+
# Configuration parameters: CountComments.
|
68
|
+
Metrics/ClassLength:
|
69
|
+
Max: 350
|
70
|
+
|
71
|
+
Metrics/CyclomaticComplexity:
|
72
|
+
Max: 17
|
73
|
+
|
74
|
+
# Configuration parameters: AllowURI, URISchemes.
|
75
|
+
Layout/LineLength:
|
76
|
+
Max: 370
|
77
|
+
|
78
|
+
# Configuration parameters: CountKeywordArgs.
|
79
|
+
Metrics/ParameterLists:
|
80
|
+
Max: 10
|
81
|
+
|
82
|
+
Metrics/PerceivedComplexity:
|
83
|
+
Max: 18
|
84
|
+
|
85
|
+
# Sometimes it's easier to read without guards
|
86
|
+
Style/GuardClause:
|
87
|
+
Enabled: false
|
88
|
+
|
89
|
+
# something = if something_else
|
90
|
+
# that's confusing
|
91
|
+
Style/ConditionalAssignment:
|
92
|
+
Enabled: false
|
93
|
+
|
94
|
+
# Better to have too much self than missing a self
|
95
|
+
Style/RedundantSelf:
|
96
|
+
Enabled: false
|
97
|
+
|
98
|
+
Metrics/MethodLength:
|
99
|
+
Max: 60
|
100
|
+
|
101
|
+
# We're not there yet
|
102
|
+
Style/Documentation:
|
103
|
+
Enabled: false
|
104
|
+
|
105
|
+
# Adds complexity
|
106
|
+
Style/IfInsideElse:
|
107
|
+
Enabled: false
|
108
|
+
|
109
|
+
# danger specific
|
110
|
+
|
111
|
+
Style/BlockComments:
|
112
|
+
Enabled: false
|
113
|
+
|
114
|
+
Layout/MultilineMethodCallIndentation:
|
115
|
+
EnforcedStyle: indented
|
116
|
+
|
117
|
+
# FIXME: 25
|
118
|
+
Metrics/BlockLength:
|
119
|
+
Max: 345
|
120
|
+
Exclude:
|
121
|
+
- "**/*_spec.rb"
|
122
|
+
|
123
|
+
Style/MixinGrouping:
|
124
|
+
Enabled: false
|
125
|
+
|
126
|
+
Naming/FileName:
|
127
|
+
Enabled: false
|
128
|
+
|
129
|
+
Layout/HeredocIndentation:
|
130
|
+
Enabled: false
|
131
|
+
|
132
|
+
Style/SpecialGlobalVars:
|
133
|
+
Enabled: false
|
134
|
+
|
135
|
+
Style/PercentLiteralDelimiters:
|
136
|
+
PreferredDelimiters:
|
137
|
+
"%": ()
|
138
|
+
"%i": ()
|
139
|
+
"%q": ()
|
140
|
+
"%Q": ()
|
141
|
+
"%r": "{}"
|
142
|
+
"%s": ()
|
143
|
+
"%w": ()
|
144
|
+
"%W": ()
|
145
|
+
"%x": ()
|
146
|
+
|
147
|
+
Security/YAMLLoad:
|
148
|
+
Enabled: false
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A guardfile for making Danger Plugins
|
4
|
+
# For more info see https://github.com/guard/guard#readme
|
5
|
+
|
6
|
+
# To run, use `bundle exec guard`.
|
7
|
+
|
8
|
+
guard :rspec, cmd: "bundle exec rspec" do
|
9
|
+
require "guard/rspec/dsl"
|
10
|
+
dsl = Guard::RSpec::Dsl.new(self)
|
11
|
+
|
12
|
+
# RSpec files
|
13
|
+
rspec = dsl.rspec
|
14
|
+
watch(rspec.spec_helper) { rspec.spec_dir }
|
15
|
+
watch(rspec.spec_support) { rspec.spec_dir }
|
16
|
+
watch(rspec.spec_files)
|
17
|
+
|
18
|
+
# Ruby files
|
19
|
+
ruby = dsl.ruby
|
20
|
+
dsl.watch_spec_files_for(ruby.lib_files)
|
21
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2025 Maximilian Lemberg <maximilian@appswithlove.com>
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
# danger-ai_feedback
|
2
|
+
|
3
|
+
A Danger plugin that analyzes GitLab pipelines for failed jobs and provides AI-generated feedback using OpenAI.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
```sh
|
8
|
+
$ gem install danger-ai_feedback
|
9
|
+
```
|
10
|
+
|
11
|
+
## Usage
|
12
|
+
|
13
|
+
Methods and attributes from this plugin are available in your `Dangerfile` under the `ai_feedback` namespace.
|
14
|
+
|
15
|
+
### Example Usage in a `Dangerfile`
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
ai_feedback.analyze_pipeline
|
19
|
+
```
|
20
|
+
|
21
|
+
### Environment Variables
|
22
|
+
To use this plugin, you need to set the following environment variables:
|
23
|
+
|
24
|
+
- `GITLAB_API_TOKEN` – Your GitLab API token.
|
25
|
+
- `CI_API_V4_URL` – The GitLab API base URL.
|
26
|
+
- `CI_PROJECT_ID` – The project ID in GitLab.
|
27
|
+
- `OPENAI_API_KEY` – Your OpenAI API key.
|
28
|
+
|
29
|
+
### How It Works
|
30
|
+
1. The plugin retrieves the latest GitLab pipeline.
|
31
|
+
2. It checks for failed jobs and extracts the last 100 lines of each job’s log.
|
32
|
+
3. It sends the logs to OpenAI for analysis.
|
33
|
+
4. The AI-generated feedback is output as a Danger message or warning.
|
34
|
+
|
35
|
+
### Example Output
|
36
|
+
|
37
|
+
If a pipeline contains failed jobs, Danger will post a message like:
|
38
|
+
|
39
|
+
```
|
40
|
+
🚨 Failing Pipeline detected
|
41
|
+
|
42
|
+
**Job: build-test**
|
43
|
+
❌ Error Details:
|
44
|
+
<error log snippet>
|
45
|
+
|
46
|
+
🔍 Root Cause:
|
47
|
+
- Possible issue with dependency installation.
|
48
|
+
|
49
|
+
🛠 Suggested Fix:
|
50
|
+
```bash
|
51
|
+
bundle install --retry 3
|
52
|
+
```
|
53
|
+
|
54
|
+
_Automatically generated with OpenAI. This is only a suggestion and can be wrong._
|
55
|
+
```
|
56
|
+
|
57
|
+
### Contributing
|
58
|
+
Feel free to open issues or submit PRs to improve the plugin!
|
59
|
+
|
60
|
+
## License
|
61
|
+
|
62
|
+
This project is licensed under the MIT License.
|
63
|
+
|
64
|
+
|
65
|
+
## Development
|
66
|
+
|
67
|
+
1. Clone this repo
|
68
|
+
2. Run `bundle install` to setup dependencies.
|
69
|
+
3. Run `bundle exec rake spec` to run the tests.
|
70
|
+
4. Use `bundle exec guard` to automatically have tests run as you make changes.
|
71
|
+
5. Make your changes.
|
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rspec/core/rake_task"
|
5
|
+
require "rubocop/rake_task"
|
6
|
+
|
7
|
+
RSpec::Core::RakeTask.new(:specs)
|
8
|
+
|
9
|
+
task default: :specs
|
10
|
+
|
11
|
+
task :spec do
|
12
|
+
Rake::Task["specs"].invoke
|
13
|
+
Rake::Task["rubocop"].invoke
|
14
|
+
Rake::Task["spec_docs"].invoke
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "Run RuboCop on the lib/specs directory"
|
18
|
+
RuboCop::RakeTask.new(:rubocop) do |task|
|
19
|
+
task.patterns = ["lib/**/*.rb", "spec/**/*.rb"]
|
20
|
+
end
|
21
|
+
|
22
|
+
desc "Ensure that the plugin passes `danger plugins lint`"
|
23
|
+
task :spec_docs do
|
24
|
+
sh "bundle exec danger plugins lint"
|
25
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path("lib", __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require "ai_feedback/gem_version"
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = "danger-ai_feedback"
|
9
|
+
spec.version = AiFeedback::VERSION
|
10
|
+
spec.authors = ["Maximilian Lemberg"]
|
11
|
+
spec.email = ["maximilian@appswithlove.com"]
|
12
|
+
spec.description = "Analyze GitLab pipelines for failed jobs and provide AI-generated feedback."
|
13
|
+
spec.summary = "A Danger plugin that integrates with GitLab CI/CD and OpenAI to analyze pipeline failures."
|
14
|
+
spec.homepage = "https://github.com/maximilianlemberg-awl/danger-pr-ai_feedback"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files`.split($/)
|
18
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_runtime_dependency "danger-plugin-api", "~> 1.0"
|
23
|
+
|
24
|
+
# General ruby development
|
25
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
26
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
27
|
+
|
28
|
+
# Testing support
|
29
|
+
spec.add_development_dependency "rspec", "~> 3.4"
|
30
|
+
|
31
|
+
# Linting code and docs
|
32
|
+
spec.add_development_dependency "rubocop"
|
33
|
+
spec.add_development_dependency "yard"
|
34
|
+
|
35
|
+
# Makes testing easy via `bundle exec guard`
|
36
|
+
spec.add_development_dependency "guard", "~> 2.14"
|
37
|
+
spec.add_development_dependency "guard-rspec", "~> 4.7"
|
38
|
+
|
39
|
+
# If you want to work on older builds of ruby
|
40
|
+
spec.add_development_dependency "listen", "3.0.7"
|
41
|
+
|
42
|
+
# This gives you the chance to run a REPL inside your tests
|
43
|
+
# via:
|
44
|
+
#
|
45
|
+
# require 'pry'
|
46
|
+
# binding.pry
|
47
|
+
#
|
48
|
+
# This will stop test execution and let you inspect the results
|
49
|
+
spec.add_development_dependency "pry"
|
50
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Danger
|
4
|
+
# This plugin analyzes a GitLab pipeline for failed jobs, retrieves their logs,
|
5
|
+
# sends the last 100 lines of each log to the OpenAI API for analysis,
|
6
|
+
# and then outputs the aggregated feedback as a Danger message.
|
7
|
+
#
|
8
|
+
# Ensure that the following environment variables are set:
|
9
|
+
# - GITLAB_API_TOKEN
|
10
|
+
# - CI_API_V4_URL
|
11
|
+
# - CI_PROJECT_ID
|
12
|
+
# - OPENAI_API_KEY
|
13
|
+
#
|
14
|
+
# @example Run analysis on the current pipeline
|
15
|
+
# ai_feedback.analyze_pipeline
|
16
|
+
#
|
17
|
+
class DangerAiFeedback < Plugin
|
18
|
+
|
19
|
+
require "uri"
|
20
|
+
require "json"
|
21
|
+
|
22
|
+
# Analyzes the current pipeline for failed jobs and uses ChatGPT to generate feedback.
|
23
|
+
# The final analysis is output using Danger's `message` method.
|
24
|
+
def analyze_pipeline
|
25
|
+
required_vars = %w[GITLAB_API_TOKEN CI_API_V4_URL CI_PROJECT_ID OPENAI_API_KEY]
|
26
|
+
missing_vars = required_vars.select { |var| ENV[var].to_s.strip.empty? }
|
27
|
+
unless missing_vars.empty?
|
28
|
+
fail "Missing environment variables: #{missing_vars.join(', ')}"
|
29
|
+
end
|
30
|
+
|
31
|
+
gitlab_api_token = ENV['GITLAB_API_TOKEN']
|
32
|
+
ci_api_v4_url = ENV['CI_API_V4_URL']
|
33
|
+
ci_project_id = ENV['CI_PROJECT_ID']
|
34
|
+
openai_api_key = ENV['OPENAI_API_KEY']
|
35
|
+
|
36
|
+
disclaimer_text = "_Automatically generated with OpenAI. This is only a suggestion and can be wrong._"
|
37
|
+
|
38
|
+
# Retrieve the latest pipeline ID
|
39
|
+
pipelines_url = "#{ci_api_v4_url}/projects/#{ci_project_id}/pipelines?per_page=1"
|
40
|
+
pipelines_response = api_get(pipelines_url, gitlab_api_token)
|
41
|
+
pipelines = JSON.parse(pipelines_response)
|
42
|
+
if pipelines.empty? || pipelines.first["id"].nil?
|
43
|
+
fail "❌ No pipeline found!"
|
44
|
+
end
|
45
|
+
pipeline_id = pipelines.first["id"]
|
46
|
+
|
47
|
+
log "Checking failed jobs in pipeline #{pipeline_id}..."
|
48
|
+
|
49
|
+
# Fetch failed jobs for this pipeline
|
50
|
+
jobs_url = "#{ci_api_v4_url}/projects/#{ci_project_id}/pipelines/#{pipeline_id}/jobs"
|
51
|
+
jobs_response = api_get(jobs_url, gitlab_api_token)
|
52
|
+
jobs = JSON.parse(jobs_response)
|
53
|
+
failed_jobs = jobs.select { |job| job["status"] == "failed" }
|
54
|
+
failed_jobs_count = failed_jobs.count
|
55
|
+
|
56
|
+
if failed_jobs_count.zero?
|
57
|
+
message "✅ No failed jobs found!"
|
58
|
+
return
|
59
|
+
end
|
60
|
+
|
61
|
+
log "🚨 Found #{failed_jobs_count} failed jobs."
|
62
|
+
|
63
|
+
final_analysis = "## 🚨 Failing Pipeline detected\n"
|
64
|
+
|
65
|
+
failed_jobs.each do |job|
|
66
|
+
job_id = job["id"]
|
67
|
+
job_name = job["name"].encode("UTF-8", invalid: :replace, undef: :replace, replace: "?")
|
68
|
+
log "Downloading log for failed job #{job_id} (#{job_name})..."
|
69
|
+
log_url = "#{ci_api_v4_url}/projects/#{ci_project_id}/jobs/#{job_id}/trace"
|
70
|
+
log_response = api_get(log_url, gitlab_api_token)
|
71
|
+
log_response = log_response.force_encoding("UTF-8").encode("UTF-8", invalid: :replace, undef: :replace, replace: "?")
|
72
|
+
|
73
|
+
if log_response.to_s.strip.empty?
|
74
|
+
log "❌ No log found for job #{job_id}!"
|
75
|
+
next
|
76
|
+
end
|
77
|
+
|
78
|
+
# Get the last 100 lines of the log
|
79
|
+
log_lines = log_response.split("\n")
|
80
|
+
last_100_lines = log_lines.last(100).join("\n")
|
81
|
+
|
82
|
+
# Build the OpenAI payload
|
83
|
+
openai_payload = {
|
84
|
+
model: "gpt-4o-mini",
|
85
|
+
messages: [
|
86
|
+
{
|
87
|
+
role: "system",
|
88
|
+
content: "You are a DevOps and CI/CD expert providing concise and actionable feedback as a pull request comment. Format responses in a structured and readable way using Markdown. Focus on helping developers quickly understand the root cause of the failure and suggest a direct fix, without telling them to verify the fix. Use bullet points, code blocks, and **bold text** where necessary to improve readability. Keep responses short, relevant, and to the point—no follow-up steps or generic error messages."
|
89
|
+
},
|
90
|
+
{
|
91
|
+
role: "user",
|
92
|
+
content: "### Job: `#{job_name}`\n\n" \
|
93
|
+
"**❌ Error Details:**\n" \
|
94
|
+
"```plaintext\n#{last_100_lines}\n```\n" \
|
95
|
+
"**🔍 Root Cause:**\n" \
|
96
|
+
"- Identify the most relevant error message.\n\n" \
|
97
|
+
"**🛠️ Suggested Fix:**\n" \
|
98
|
+
"```bash\n# Modify this line in your script\nexit 1 # 🔴 Remove or adjust this as needed\n```\n"
|
99
|
+
}
|
100
|
+
]
|
101
|
+
}
|
102
|
+
|
103
|
+
payload_json = JSON.generate(openai_payload)
|
104
|
+
openai_url = "https://api.openai.com/v1/chat/completions"
|
105
|
+
openai_response = post_request(openai_url, payload_json, openai_api_key)
|
106
|
+
openai_response = openai_response.force_encoding("UTF-8").encode("UTF-8", invalid: :replace, undef: :replace, replace: "?")
|
107
|
+
|
108
|
+
openai_result = JSON.parse(openai_response)
|
109
|
+
response_text = openai_result.dig("choices", 0, "message", "content") || "No response from ChatGPT."
|
110
|
+
log "ChatGPT response for #{job_name} received."
|
111
|
+
final_analysis << "\n#{response_text}\n"
|
112
|
+
end
|
113
|
+
|
114
|
+
final_analysis << "\n#{disclaimer_text}"
|
115
|
+
|
116
|
+
# Instead of posting comments via an API, output the final analysis as a Danger message.
|
117
|
+
fail(final_analysis)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Performs an HTTP GET request to the specified URL using the provided token.
|
121
|
+
def api_get(url, token)
|
122
|
+
response = `curl -s -H "PRIVATE-TOKEN: #{token}" "#{url}"`
|
123
|
+
raise "API request failed!" if response.empty?
|
124
|
+
response
|
125
|
+
end
|
126
|
+
|
127
|
+
# Performs an HTTP POST request to the specified URL with the given JSON data and API key.
|
128
|
+
def post_request(url, data, openai_api_key)
|
129
|
+
data_json = data.is_a?(String) ? data : JSON.generate(data)
|
130
|
+
|
131
|
+
response = `curl -s -X POST "#{url}" \
|
132
|
+
-H "Authorization: Bearer #{openai_api_key}" \
|
133
|
+
-H "Content-Type: application/json" \
|
134
|
+
-d '#{data_json}'`
|
135
|
+
|
136
|
+
raise "POST request to #{url} failed!" if response.empty?
|
137
|
+
response
|
138
|
+
end
|
139
|
+
|
140
|
+
# A helper method for logging messages in Danger's output.
|
141
|
+
def log(msg)
|
142
|
+
UI.message(msg)
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require File.expand_path("spec_helper", __dir__)
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
module Danger
|
7
|
+
describe Danger::DangerAiFeedback do
|
8
|
+
describe "with Dangerfile" do
|
9
|
+
before do
|
10
|
+
@dangerfile = testing_dangerfile
|
11
|
+
@ai_feedback = @dangerfile.ai_feedback
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should be a Danger plugin" do
|
15
|
+
expect(@ai_feedback).to be_a Danger::Plugin
|
16
|
+
end
|
17
|
+
|
18
|
+
context "when required environment variables are missing" do
|
19
|
+
it "fails when any required environment variable is missing" do
|
20
|
+
allow(ENV).to receive(:[]).and_return(nil) # Simulate missing variables
|
21
|
+
|
22
|
+
expect { @ai_feedback.analyze_pipeline }.to raise_error(RuntimeError, /Missing environment variables/)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context "when no failed jobs exist" do
|
27
|
+
before do
|
28
|
+
allow(@ai_feedback).to receive(:api_get).and_return({ "id" => 123 }.to_json)
|
29
|
+
allow(@ai_feedback).to receive(:api_get).and_return([].to_json) # No failed jobs
|
30
|
+
end
|
31
|
+
|
32
|
+
it "outputs a success message when no jobs failed" do
|
33
|
+
expect(@ai_feedback).to receive(:message).with("✅ No failed jobs found!")
|
34
|
+
@ai_feedback.analyze_pipeline
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
context "when failed jobs exist" do
|
39
|
+
let(:failed_jobs) do
|
40
|
+
[
|
41
|
+
{ "id" => 1, "name" => "test-job", "status" => "failed" }
|
42
|
+
]
|
43
|
+
end
|
44
|
+
|
45
|
+
before do
|
46
|
+
allow(@ai_feedback).to receive(:api_get).and_return({ "id" => 123 }.to_json)
|
47
|
+
allow(@ai_feedback).to receive(:api_get).and_return(failed_jobs.to_json)
|
48
|
+
allow(@ai_feedback).to receive(:api_get).and_return("Fake log line\nAnother log line")
|
49
|
+
allow(@ai_feedback).to receive(:post_request).and_return({ "choices" => [{ "message" => { "content" => "Suggested Fix: Do X" } }] }.to_json)
|
50
|
+
end
|
51
|
+
|
52
|
+
it "fetches logs and sends them to OpenAI" do
|
53
|
+
expect(@ai_feedback).to receive(:api_get).at_least(:once)
|
54
|
+
expect(@ai_feedback).to receive(:post_request).at_least(:once)
|
55
|
+
@ai_feedback.analyze_pipeline
|
56
|
+
end
|
57
|
+
|
58
|
+
it "fails with a message when failed jobs are found" do
|
59
|
+
expect(@ai_feedback).to receive(:fail).with(/🚨 Failing Pipeline detected/)
|
60
|
+
@ai_feedback.analyze_pipeline
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context "when OpenAI response is empty" do
|
65
|
+
before do
|
66
|
+
allow(@ai_feedback).to receive(:api_get).and_return({ "id" => 123 }.to_json)
|
67
|
+
allow(@ai_feedback).to receive(:api_get).and_return([{ "id" => 1, "name" => "test-job", "status" => "failed" }].to_json)
|
68
|
+
allow(@ai_feedback).to receive(:api_get).and_return("Fake log line")
|
69
|
+
allow(@ai_feedback).to receive(:post_request).and_return("")
|
70
|
+
end
|
71
|
+
|
72
|
+
it "fails gracefully when OpenAI does not return a response" do
|
73
|
+
expect(@ai_feedback).to receive(:fail).with(/No response from ChatGPT/)
|
74
|
+
@ai_feedback.analyze_pipeline
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pathname"
|
4
|
+
ROOT = Pathname.new(File.expand_path("..", __dir__))
|
5
|
+
$:.unshift("#{ROOT}lib".to_s)
|
6
|
+
$:.unshift("#{ROOT}spec".to_s)
|
7
|
+
|
8
|
+
require "bundler/setup"
|
9
|
+
require "pry"
|
10
|
+
|
11
|
+
require "rspec"
|
12
|
+
require "danger"
|
13
|
+
|
14
|
+
if `git remote -v` == ""
|
15
|
+
puts "You cannot run tests without setting a local git remote on this repo"
|
16
|
+
puts "It's a weird side-effect of Danger's internals."
|
17
|
+
exit(0)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Use coloured output, it's the best.
|
21
|
+
RSpec.configure do |config|
|
22
|
+
config.filter_gems_from_backtrace "bundler"
|
23
|
+
config.color = true
|
24
|
+
config.tty = true
|
25
|
+
end
|
26
|
+
|
27
|
+
require "danger_plugin"
|
28
|
+
|
29
|
+
# These functions are a subset of https://github.com/danger/danger/blob/master/spec/spec_helper.rb
|
30
|
+
# If you are expanding these files, see if it's already been done ^.
|
31
|
+
|
32
|
+
# A silent version of the user interface,
|
33
|
+
# it comes with an extra function `.string` which will
|
34
|
+
# strip all ANSI colours from the string.
|
35
|
+
|
36
|
+
# rubocop:disable Lint/NestedMethodDefinition
|
37
|
+
def testing_ui
|
38
|
+
@output = StringIO.new
|
39
|
+
def @output.winsize
|
40
|
+
[20, 9999]
|
41
|
+
end
|
42
|
+
|
43
|
+
cork = Cork::Board.new(out: @output)
|
44
|
+
def cork.string
|
45
|
+
out.string.gsub(/\e\[([;\d]+)?m/, "")
|
46
|
+
end
|
47
|
+
cork
|
48
|
+
end
|
49
|
+
# rubocop:enable Lint/NestedMethodDefinition
|
50
|
+
|
51
|
+
# Example environment (ENV) that would come from
|
52
|
+
# running a PR on TravisCI
|
53
|
+
def testing_env
|
54
|
+
{
|
55
|
+
"CI_MERGE_REQUEST_IID" => "42",
|
56
|
+
"CI_PROJECT_ID" => "123456",
|
57
|
+
"CI_PIPELINE_ID" => "7890",
|
58
|
+
"GITLAB_API_TOKEN" => "mock_token"
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
62
|
+
# A stubbed out Dangerfile for use in tests
|
63
|
+
def testing_dangerfile
|
64
|
+
env = Danger::EnvironmentManager.new(testing_env)
|
65
|
+
Danger::Dangerfile.new(env, testing_ui)
|
66
|
+
end
|
metadata
ADDED
@@ -0,0 +1,201 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: danger-ai_feedback
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Maximilian Lemberg
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-02-02 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: danger-plugin-api
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.4'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.4'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubocop
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: yard
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: guard
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '2.14'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '2.14'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: guard-rspec
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '4.7'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '4.7'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: listen
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - '='
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 3.0.7
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - '='
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: 3.0.7
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: pry
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
description: Analyze GitLab pipelines for failed jobs and provide AI-generated feedback.
|
154
|
+
email:
|
155
|
+
- maximilian@appswithlove.com
|
156
|
+
executables: []
|
157
|
+
extensions: []
|
158
|
+
extra_rdoc_files: []
|
159
|
+
files:
|
160
|
+
- ".gitignore"
|
161
|
+
- ".rubocop.yml"
|
162
|
+
- ".travis.yml"
|
163
|
+
- Gemfile
|
164
|
+
- Guardfile
|
165
|
+
- LICENSE.txt
|
166
|
+
- README.md
|
167
|
+
- Rakefile
|
168
|
+
- danger-ai_feedback.gemspec
|
169
|
+
- lib/ai_feedback/gem_version.rb
|
170
|
+
- lib/ai_feedback/plugin.rb
|
171
|
+
- lib/danger_ai_feedback.rb
|
172
|
+
- lib/danger_plugin.rb
|
173
|
+
- spec/ai_feedback_spec.rb
|
174
|
+
- spec/spec_helper.rb
|
175
|
+
homepage: https://github.com/maximilianlemberg-awl/danger-pr-ai_feedback
|
176
|
+
licenses:
|
177
|
+
- MIT
|
178
|
+
metadata: {}
|
179
|
+
post_install_message:
|
180
|
+
rdoc_options: []
|
181
|
+
require_paths:
|
182
|
+
- lib
|
183
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - ">="
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: '0'
|
188
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
189
|
+
requirements:
|
190
|
+
- - ">="
|
191
|
+
- !ruby/object:Gem::Version
|
192
|
+
version: '0'
|
193
|
+
requirements: []
|
194
|
+
rubygems_version: 3.2.3
|
195
|
+
signing_key:
|
196
|
+
specification_version: 4
|
197
|
+
summary: A Danger plugin that integrates with GitLab CI/CD and OpenAI to analyze pipeline
|
198
|
+
failures.
|
199
|
+
test_files:
|
200
|
+
- spec/ai_feedback_spec.rb
|
201
|
+
- spec/spec_helper.rb
|