From 96e1f7332b859a7a23603a41b8e4a587439f1b6a Mon Sep 17 00:00:00 2001 From: Anes Hodza Date: Tue, 20 Jun 2023 14:30:47 +0200 Subject: [PATCH 01/10] Update readme --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 4f281eb..dcad534 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ ### CI Status + |develop|main| |:------:|:---:| |[![Build Status](https://renuo.semaphoreci.com/badges/gnosis/branches/develop.svg?style=shields)](https://renuo.semaphoreci.com/projects/gnosis)|[![Build Status](https://renuo.semaphoreci.com/badges/gnosis/branches/main.svg?style=shields)](https://renuo.semaphoreci.com/projects/gnosis)| @@ -9,11 +10,13 @@ Welcome to gnosis! This is a Redmine Plugin with the goal of making your development process easier to keep an overview of. ## What does this plugin do? + Gnosis is able to show you GitHub Pull Requests and Deployments that belong to a Redmine issue, all in its "details" page. To make this magic work there are only a few steps you have to follow and certain conventions to keep in mind. ### GitHub Webhooks configuration + To configure the GitHub Webhooks, go to github.com/your_org/your_repo/settings/hooks. There you click on "Add Webhook". The "Payload URL" is your Redmine URL + /github_webhook. The secret is up for you to decide. It's important that this string is complex and secure. Then, you get into your `config/application.yml`, where you add the @@ -21,6 +24,7 @@ key as your `GITHUB_WEBHOOK_SECRET`. Click "Let me select individual events." an You should now be good to go! ### SemaphoreCI Webhooks configuration + **Important:** The GitHub notifications work without the SemaphoreCI setup. If you don't use a CI or just a different you can skip this step. To set up the SemaphoreCI Webhook go to your_org.semaphoreci.com/notifications and click on "New Notification". @@ -34,6 +38,7 @@ secret. This should match the `SEMAPHORE_WEBHOOK_SECRET` in your `config/applica This should now also work just fine! ### GitHub Access Token + This app needs to query your repository every now and then, so its able to know which deployment belongs to which pull request. For that you need to set the `GITHUB_ACCESS_TOKEN` in your `config/application.yml`. To create it, go to github.com/settings/tokens and click on "Generate new token". There click the "(classic)" option. It only needs "repo" @@ -41,6 +46,7 @@ access to work. If you make a deploy, it should correctly show now. ### Conventions you need to keep + The plugin needs information on which Pull Requests belong to which issues, which is why there are set conventions for branch naming. When you create a branch that is connected to a certain Issue it should be named as follows: something/issue_number-description. The only important part of this is the /issue_number as that's how From 6ad98c6b635127db081990cf989f12b145140423 Mon Sep 17 00:00:00 2001 From: Anes Hodza Date: Tue, 20 Jun 2023 14:48:30 +0200 Subject: [PATCH 02/10] Remove nil from env fetch --- app/controllers/webhooks_controller.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/webhooks_controller.rb b/app/controllers/webhooks_controller.rb index f90e088..ae82552 100644 --- a/app/controllers/webhooks_controller.rb +++ b/app/controllers/webhooks_controller.rb @@ -4,13 +4,13 @@ class WebhooksController < ApplicationController protect_from_forgery except: %i[github_webhook_catcher semaphore_webhook_catcher] Octokit.configure do |config| - config.access_token = ENV.fetch('GITHUB_ACCESS_TOKEN', nil) + config.access_token = ENV.fetch('GITHUB_ACCESS_TOKEN') end CLIENT = Octokit::Client.new def github_webhook_catcher unless verify_signature(request.body.read, request.env['HTTP_X_HUB_SIGNATURE_256'], - ENV.fetch('GITHUB_WEBHOOK_SECRET', nil)) + ENV.fetch('GITHUB_WEBHOOK_SECRET')) return render json: {status: 403}, status: :forbidden end @@ -21,7 +21,7 @@ def github_webhook_catcher def semaphore_webhook_catcher unless verify_signature(request.body.read, "sha256=#{request.headers['X-Semaphore-Signature-256']}", - ENV.fetch('SEMAPHORE_WEBHOOK_SECRET', nil)) + ENV.fetch('SEMAPHORE_WEBHOOK_SECRET')) return render json: {status: 403}, status: :forbidden end From 0d7c4e68b38278566440cdc51b4edea30401e31d Mon Sep 17 00:00:00 2001 From: Anes Hodza Date: Tue, 20 Jun 2023 14:50:21 +0200 Subject: [PATCH 03/10] Move const to init.rb --- app/controllers/webhooks_controller.rb | 5 ----- init.rb | 5 +++++ 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/controllers/webhooks_controller.rb b/app/controllers/webhooks_controller.rb index ae82552..4706958 100644 --- a/app/controllers/webhooks_controller.rb +++ b/app/controllers/webhooks_controller.rb @@ -3,11 +3,6 @@ class WebhooksController < ApplicationController protect_from_forgery except: %i[github_webhook_catcher semaphore_webhook_catcher] - Octokit.configure do |config| - config.access_token = ENV.fetch('GITHUB_ACCESS_TOKEN') - end - CLIENT = Octokit::Client.new - def github_webhook_catcher unless verify_signature(request.body.read, request.env['HTTP_X_HUB_SIGNATURE_256'], ENV.fetch('GITHUB_WEBHOOK_SECRET')) diff --git a/init.rb b/init.rb index 89b6699..13552a6 100644 --- a/init.rb +++ b/init.rb @@ -12,6 +12,11 @@ Rails.logger.warn 'GITHUB_ACCESS_TOKEN is default value' end +Octokit.configure do |config| + config.access_token = ENV.fetch('GITHUB_ACCESS_TOKEN') +end +CLIENT = Octokit::Client.new + Redmine::Plugin.register :gnosis do name 'Gnosis plugin' author 'Anes Hodza' From fff56ed3a8c0513a1d1f77d4c4a88d6f2b7af8f0 Mon Sep 17 00:00:00 2001 From: Anes Hodza Date: Tue, 20 Jun 2023 15:19:02 +0200 Subject: [PATCH 04/10] DRY up signature method --- app/controllers/webhooks_controller.rb | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/app/controllers/webhooks_controller.rb b/app/controllers/webhooks_controller.rb index 4706958..8644e5b 100644 --- a/app/controllers/webhooks_controller.rb +++ b/app/controllers/webhooks_controller.rb @@ -2,26 +2,15 @@ class WebhooksController < ApplicationController protect_from_forgery except: %i[github_webhook_catcher semaphore_webhook_catcher] + before_action :verify_signature, only: %i[github_webhook_catcher semaphore_webhook_catcher] def github_webhook_catcher - unless verify_signature(request.body.read, request.env['HTTP_X_HUB_SIGNATURE_256'], - ENV.fetch('GITHUB_WEBHOOK_SECRET')) - return render json: {status: 403}, status: :forbidden - end - github_webhook_handler(params) - render json: {status: :ok} end def semaphore_webhook_catcher - unless verify_signature(request.body.read, "sha256=#{request.headers['X-Semaphore-Signature-256']}", - ENV.fetch('SEMAPHORE_WEBHOOK_SECRET')) - return render json: {status: 403}, status: :forbidden - end - semaphore_webhook_handler(params) - render json: {status: :ok} end @@ -49,9 +38,15 @@ def semaphore_webhook_handler(params) create_deploys_for_pull_requests(sha_between, branch, passed, time) end - def verify_signature(payload_body, recieved_signature, secret) - signature = "sha256=#{OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), secret, payload_body)}" - Rack::Utils.secure_compare(signature, recieved_signature) + def verify_signature + recieved_signature = request.headers['HTTP_X_HUB_SIGNATURE_256'] || + request.headers['X-Semaphore-Signature-256'] + signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), + ENV.fetch('GITHUB_WEBHOOK_SECRET'), + request.body.read).to_s + return if Rack::Utils.secure_compare(signature, recieved_signature.gsub('sha256=', '')) + + render json: {status: 403}, status: :forbidden end def fetch_commit_history(repo, branch, first_commit, last_commit) From 5ddff6793afe685ebdb46209b4270149f3041ff4 Mon Sep 17 00:00:00 2001 From: Anes Hodza Date: Tue, 20 Jun 2023 16:25:49 +0200 Subject: [PATCH 05/10] Change commit history fetch method --- app/controllers/webhooks_controller.rb | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/app/controllers/webhooks_controller.rb b/app/controllers/webhooks_controller.rb index 8644e5b..2833ff4 100644 --- a/app/controllers/webhooks_controller.rb +++ b/app/controllers/webhooks_controller.rb @@ -34,7 +34,7 @@ def semaphore_webhook_handler(params) first_sha = range.split('...').first last_sha = range.split('...').last - sha_between = fetch_commit_history(repo, branch, first_sha, last_sha) + sha_between = fetch_commit_history(repo, first_sha, last_sha) create_deploys_for_pull_requests(sha_between, branch, passed, time) end @@ -49,13 +49,9 @@ def verify_signature render json: {status: 403}, status: :forbidden end - def fetch_commit_history(repo, branch, first_commit, last_commit) - commit_sha_list = CLIENT.commits(repo, branch).pluck(:sha) - - first_commit_index = commit_sha_list.index(first_commit) - last_commit_index = commit_sha_list.index(last_commit) - - commit_sha_list[last_commit_index..first_commit_index] + def fetch_commit_history(repo, first_commit, last_commit) + comparison = CLIENT.compare(repo, first_commit, last_commit) + comparison.commits.pluck(:sha) end def create_deploys_for_pull_requests(sha_between, branch, passed, time) From e999f6ea55cc785935be626e6a2a1a0e4122e6f7 Mon Sep 17 00:00:00 2001 From: Anes Hodza Date: Tue, 20 Jun 2023 16:37:20 +0200 Subject: [PATCH 06/10] Change stub --- test/functional/webhooks_controller_test.rb | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/test/functional/webhooks_controller_test.rb b/test/functional/webhooks_controller_test.rb index 70aea9b..99a1b78 100644 --- a/test/functional/webhooks_controller_test.rb +++ b/test/functional/webhooks_controller_test.rb @@ -6,11 +6,16 @@ class WebhookCatchControllerControllerTest < ActionController::TestCase def setup @controller = WebhooksController.new - Octokit::Client.any_instance.stubs(:commits).returns([ - { sha: 'another_hash' }, - { sha: 'in_between_hash' }, - { sha: 'one_hash' } - ]) + commit = Struct.new(:sha) + comparison_result_struct = Struct.new(:commits) + + comparison_result = comparison_result_struct.new([ + commit.new('another_hash'), + commit.new('in_between_hash'), + commit.new('one_hash') + ]) + + Octokit::Client.any_instance.stubs(:compare).returns(comparison_result) @github_webhook_hash = { pull_request: { From 8fc5f00964e756351eca020f6756fd27d2742898 Mon Sep 17 00:00:00 2001 From: Anes Hodza Date: Tue, 20 Jun 2023 16:40:18 +0200 Subject: [PATCH 07/10] Remove unused helper --- app/helpers/webhooks_helper.rb | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 app/helpers/webhooks_helper.rb diff --git a/app/helpers/webhooks_helper.rb b/app/helpers/webhooks_helper.rb deleted file mode 100644 index 4ed9a94..0000000 --- a/app/helpers/webhooks_helper.rb +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true - -module WebhooksHelper -end From ab7e78f52c532a9ab8e02e62abfbd8468c597949 Mon Sep 17 00:00:00 2001 From: Anes Hodza Date: Tue, 20 Jun 2023 17:07:03 +0200 Subject: [PATCH 08/10] Move state into method --- app/models/pull_request.rb | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/models/pull_request.rb b/app/models/pull_request.rb index b17b462..477761e 100644 --- a/app/models/pull_request.rb +++ b/app/models/pull_request.rb @@ -5,12 +5,17 @@ class PullRequest < GnosisApplicationRecord has_many :pull_request_deployments, dependent: :destroy def self.auto_create_or_update(params) - state = params[:pull_request][:merged] ? 'merged' : params[:pull_request][:state] - state = 'draft' if params[:pull_request][:draft] pr = PullRequest.find_or_initialize_by(url: params[:pull_request][:html_url]) - pr.update!(state: state, url: params[:pull_request][:html_url], + pr.update!(state: state(params), url: params[:pull_request][:html_url], title: params[:pull_request][:title], source_branch: params[:pull_request][:head][:ref], target_branch: params[:pull_request][:base][:ref], was_merged: params[:pull_request][:merged], merge_commit_sha: params[:pull_request][:merge_commit_sha], issue_id: params[:issue_id]) end + + def self.state(params) + return 'merged' if params[:pull_request][:merged] + return 'draft' if params[:pull_request][:draft] + + params[:pull_request][:state] + end end From 48ade1d1d9a6296721092ab8c46eec3ae15b4a81 Mon Sep 17 00:00:00 2001 From: Anes Hodza Date: Tue, 20 Jun 2023 17:20:32 +0200 Subject: [PATCH 09/10] DRY up pr stuff --- app/models/pull_request.rb | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/app/models/pull_request.rb b/app/models/pull_request.rb index 477761e..47f5910 100644 --- a/app/models/pull_request.rb +++ b/app/models/pull_request.rb @@ -5,17 +5,18 @@ class PullRequest < GnosisApplicationRecord has_many :pull_request_deployments, dependent: :destroy def self.auto_create_or_update(params) - pr = PullRequest.find_or_initialize_by(url: params[:pull_request][:html_url]) - pr.update!(state: state(params), url: params[:pull_request][:html_url], - title: params[:pull_request][:title], source_branch: params[:pull_request][:head][:ref], - target_branch: params[:pull_request][:base][:ref], was_merged: params[:pull_request][:merged], - merge_commit_sha: params[:pull_request][:merge_commit_sha], issue_id: params[:issue_id]) + pr = params[:pull_request] + pull_request = PullRequest.find_or_initialize_by(url: pr[:html_url]) + pull_request.update!(state: state(params, pr), url: pr[:html_url], + title: pr[:title], source_branch: pr[:head][:ref], + target_branch: pr[:base][:ref], was_merged: pr[:merged], + merge_commit_sha: pr[:merge_commit_sha], issue_id: params[:issue_id]) end - def self.state(params) - return 'merged' if params[:pull_request][:merged] - return 'draft' if params[:pull_request][:draft] + def self.state(params, pr) + return 'merged' if pr[:merged] + return 'draft' if pr[:draft] - params[:pull_request][:state] + pr[:state] end end From 173e2cb9ed80d51716ffb27498f42367f3b71a62 Mon Sep 17 00:00:00 2001 From: Anes Hodza Date: Tue, 20 Jun 2023 17:29:46 +0200 Subject: [PATCH 10/10] Fix style --- app/models/pull_request.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/models/pull_request.rb b/app/models/pull_request.rb index 47f5910..f480e53 100644 --- a/app/models/pull_request.rb +++ b/app/models/pull_request.rb @@ -7,16 +7,16 @@ class PullRequest < GnosisApplicationRecord def self.auto_create_or_update(params) pr = params[:pull_request] pull_request = PullRequest.find_or_initialize_by(url: pr[:html_url]) - pull_request.update!(state: state(params, pr), url: pr[:html_url], - title: pr[:title], source_branch: pr[:head][:ref], - target_branch: pr[:base][:ref], was_merged: pr[:merged], - merge_commit_sha: pr[:merge_commit_sha], issue_id: params[:issue_id]) + pull_request.update!(state: state(pr), url: pr[:html_url], + title: pr[:title], source_branch: pr[:head][:ref], + target_branch: pr[:base][:ref], was_merged: pr[:merged], + merge_commit_sha: pr[:merge_commit_sha], issue_id: params[:issue_id]) end - def self.state(params, pr) - return 'merged' if pr[:merged] - return 'draft' if pr[:draft] + def self.state(pr_param) + return 'merged' if pr_param[:merged] + return 'draft' if pr_param[:draft] - pr[:state] + pr_param[:state] end end