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 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
@@ -0,0 +1,4 @@
1
+ .DS_Store
2
+ pkg
3
+ .idea/
4
+ .yardoc
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
@@ -0,0 +1,10 @@
1
+ language: ruby
2
+ cache:
3
+ directories:
4
+ - bundle
5
+
6
+ rvm:
7
+ - 2.7
8
+
9
+ script:
10
+ - bundle exec rake spec
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in danger-ai_feedback.gemspec
6
+ gemspec
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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AiFeedback
4
+ VERSION = "0.1.0"
5
+ 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,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ai_feedback/gem_version"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ai_feedback/plugin"
@@ -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
@@ -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