├── .gitignore ├── Gemfile ├── Gemfile.lock ├── README.md ├── app ├── controllers │ └── webhook_settings_controller.rb ├── models │ └── webhook.rb └── views │ └── webhook_settings │ └── _show.html.erb ├── config ├── locales │ ├── en.yml │ └── ja.yml └── routes.rb ├── db └── migrate │ └── 001_create_webhooks.rb ├── init.rb ├── lib ├── redmine_webhook.rb └── redmine_webhook │ ├── author_wrapper.rb │ ├── custom_field_value_wrapper.rb │ ├── issue_wrapper.rb │ ├── journal_detail_wrapper.rb │ ├── journal_wrapper.rb │ ├── priority_wrapper.rb │ ├── project_wrapper.rb │ ├── projects_helper_patch.rb │ ├── status_wrapper.rb │ ├── tracker_wrapper.rb │ └── webhook_listener.rb └── test ├── test_helper.rb └── unit └── webhook_test.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'faraday' 4 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | specs: 3 | faraday (0.8.8) 4 | multipart-post (~> 1.2.0) 5 | multipart-post (1.2.0) 6 | 7 | PLATFORMS 8 | ruby 9 | 10 | DEPENDENCIES 11 | faraday 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Redmine WebHook Plugin 2 | ====================== 3 | 4 | A Redmine plugin posts webhook on creating and updating tickets. 5 | 6 | Author 7 | ------------------------------ 8 | * @suer 9 | 10 | Install 11 | ------------------------------ 12 | Type below commands: 13 | 14 | $ cd $RAILS_ROOT/plugins 15 | $ git clone https://github.com/suer/redmine_webhook.git 16 | $ rake redmine:plugins:migrate RAILS_ENV=production 17 | 18 | Then, restart your redmine. 19 | 20 | Post Data Example 21 | ------------------------------ 22 | 23 | ### Issue opened 24 | 25 | { 26 | "payload": { 27 | "issue": { 28 | "author": { 29 | "icon_url": "http://www.gravatar.com/avatar/example", 30 | "identity_url": null, 31 | "lastname": "user", 32 | "firstname": "test", 33 | "mail": "test@example.com", 34 | "login": "test", 35 | "id": 3 36 | }, 37 | "assignee": { 38 | "icon_url": "http://www.gravatar.com/avatar/example", 39 | "identity_url": null, 40 | "lastname": "user", 41 | "firstname": "test", 42 | "mail": "test@example.com", 43 | "login": "test", 44 | "id": 3 45 | }, 46 | "priority": { 47 | "name": "normal", 48 | "id": 2 49 | }, 50 | "tracker": { 51 | "name": "bug", 52 | "id": 1 53 | }, 54 | "parent_id": null, 55 | "root_id": 191, 56 | "closed_on": null, 57 | "updated_on": "2014-03-01T15:17:48Z", 58 | "created_on": "2014-03-01T15:17:48Z", 59 | "description": "I'm having a problem with this.", 60 | "subject": "Found a bug", 61 | "id": 191, 62 | "done_ratio": 0, 63 | "start_date": "2014-03-02", 64 | "due_date": null, 65 | "estimated_hours": null, 66 | "is_private": false, 67 | "lock_version": 0, 68 | "project": { 69 | "homepage": "", 70 | "created_on": "2013-01-12T11:50:26Z", 71 | "description": "", 72 | "name": "Test Project", 73 | "identifier": "test", 74 | "id": 4 75 | }, 76 | "status": { 77 | "name": "new", 78 | "id": 1 79 | } 80 | }, 81 | "action": "opened", 82 | "url": "https://example.com" 83 | } 84 | } 85 | 86 | ### Issue updated 87 | 88 | { 89 | "payload": { 90 | "url": "https://example.com", 91 | "journal": { 92 | "details": [], 93 | "author": { 94 | "icon_url": "http://www.gravatar.com/avatar/example", 95 | "identity_url": null, 96 | "lastname": "user", 97 | "firstname": "test", 98 | "mail": "test@example.com", 99 | "login": "test", 100 | "id": 3 101 | }, 102 | "assignee": { 103 | "icon_url": "http://www.gravatar.com/avatar/example", 104 | "identity_url": null, 105 | "lastname": "user", 106 | "firstname": "test", 107 | "mail": "test@example.com", 108 | "login": "test", 109 | "id": 3 110 | }, 111 | "private_notes": false, 112 | "created_on": "2014-03-01T16:22:46Z", 113 | "notes": "Fixed", 114 | "id": 195 115 | }, 116 | "issue": { 117 | "author": { 118 | "icon_url": "http://www.gravatar.com/avatar/example", 119 | "identity_url": null, 120 | "lastname": "user", 121 | "firstname": "test", 122 | "mail": "test@example.com", 123 | "login": "test", 124 | "id": 3 125 | }, 126 | "priority": { 127 | "name": "normal", 128 | "id": 2 129 | }, 130 | "tracker": { 131 | "name": "bug", 132 | "id": 1 133 | }, 134 | "parent_id": null, 135 | "root_id": 196, 136 | "closed_on": null, 137 | "updated_on": "2014-03-01T16:22:46Z", 138 | "created_on": "2014-03-01T15:44:22Z", 139 | "description": "test", 140 | "subject": "Found a bug", 141 | "id": 196, 142 | "done_ratio": 0, 143 | "start_date": "2014-03-02", 144 | "due_date": null, 145 | "estimated_hours": null, 146 | "is_private": false, 147 | "lock_version": 2, 148 | "project": { 149 | "homepage": "", 150 | "created_on": "2013-01-12T11:50:26Z", 151 | "description": "", 152 | "name": "Test Project", 153 | "identifier": "test", 154 | "id": 4 155 | }, 156 | "status": { 157 | "name": "normal", 158 | "id": 1 159 | } 160 | }, 161 | "action": "updated" 162 | } 163 | } 164 | 165 | Requirements 166 | ------------------------------ 167 | * Redmine 4.0 or later 168 | 169 | 170 | Skipping webhooks 171 | ------------------------------ 172 | When a webhook triggers a change via REST API, this would trigger another webhook. 173 | If you need to prevent this, the API request can include the `X-Skip-Webhooks` header, which will prevent webhooks being triggered by that request. 174 | 175 | 176 | Known Limitations 177 | ------------------------------ 178 | 179 | An update from context menu doesn't call a webhook event. 180 | It is caused by a lack of functionality hooking in Redmine. 181 | Please see https://github.com/suer/redmine_webhook/issues/4 for details. 182 | 183 | This limitation has been affected on all Redmine versions includes 2.4, 2.6, 184 | and 3.0. It is not fixed in end of April, 2015. 185 | 186 | 187 | License 188 | ------------------------------ 189 | The MIT License (MIT) 190 | Copyright (c) suer 191 | 192 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 193 | 194 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 195 | 196 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 197 | -------------------------------------------------------------------------------- /app/controllers/webhook_settings_controller.rb: -------------------------------------------------------------------------------- 1 | class WebhookSettingsController < ApplicationController 2 | before_action :find_project, :authorize 3 | accept_api_auth :index, :create, :update, :destroy 4 | 5 | def index 6 | render :json => Webhook.where(:project_id => @project.id).as_json(:only => [:id, :url]) 7 | end 8 | 9 | def create 10 | webhook = Webhook.new(:project_id => @project.id) 11 | webhook.url = params[:url] 12 | if webhook.save 13 | flash[:notice] = l(:notice_successful_create_webhook) 14 | else 15 | flash[:error] = l(:notice_fail_create_webhook) 16 | end 17 | redirect_to settings_project_path(@project, :tab => 'webhook') 18 | end 19 | def update 20 | id = params[:webhook_id] 21 | webhook = Webhook.where(:project_id => @project.id).where(:id => id).first 22 | webhook.url = params[:url] 23 | if webhook.url.blank? ? webhook.destroy : webhook.save 24 | flash[:notice] = l(:notice_successful_update_webhook) 25 | else 26 | flash[:error] = l(:notice_fail_update_webhook) 27 | end 28 | redirect_to settings_project_path(@project, :tab => 'webhook') 29 | end 30 | def destroy 31 | id = params[:webhook_id] 32 | webhook = Webhook.where(:project_id => @project.id).where(:id => id).first 33 | if webhook.destroy 34 | flash[:notice] = l(:notice_successful_delete_webhook) 35 | else 36 | flash[:error] = l(:notice_fail_delete_webhook) 37 | end 38 | redirect_to settings_project_path(@project, :tab => 'webhook') 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /app/models/webhook.rb: -------------------------------------------------------------------------------- 1 | class Webhook < ActiveRecord::Base 2 | unloadable 3 | belongs_to :project 4 | end 5 | -------------------------------------------------------------------------------- /app/views/webhook_settings/_show.html.erb: -------------------------------------------------------------------------------- 1 | <% if Webhook.where(:project_id => @project.id).first%> 2 |
3 | <% Webhook.where(:project_id => @project.id).each do |webhook|%> 4 | <%= form_tag(update_webhook_path(@project, webhook.id), :method => :put, :class => "tabular") do %> 5 | 6 | URL 7 | <%= text_field_tag :url, webhook.url, :size => 80 %> 8 | <%= submit_tag l(:button_update) %> 9 | 10 | <% end %> 11 | <%= link_to l(:button_delete), delete_webhook_path(@project, webhook.id), :class => "icon icon-del", :method => :delete, :confirm => l(:text_are_you_sure) %> 12 |
13 | <% end %> 14 |
15 | <% end %> 16 | 17 | <%= form_tag(create_webhook_path(@project), :method => :post, :class => "tabular") do %> 18 |
19 | 20 | URL 21 | <%= text_field_tag :url, '', :size => 80 %> 22 | <%= submit_tag l(:button_add) %> 23 | 24 |
25 | <% end %> 26 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # English strings go here for Rails i18n 2 | en: 3 | webhook: WebHook 4 | notice_fail_create_webhook: Webhook creation failed. 5 | notice_successful_create_webhook: Webhook created successfully. 6 | notice_fail_update_webhook: Webhook update failed. 7 | notice_successful_update_webhook : Webhook updated successfully. 8 | notice_fail_delete_webhook: Webhook delete failed. 9 | notice_successful_delete_webhook : Webhook deleted successfully. 10 | 11 | -------------------------------------------------------------------------------- /config/locales/ja.yml: -------------------------------------------------------------------------------- 1 | ja: 2 | webhook: WebHook 3 | notice_fail_create_webhook: Webhook の作成に失敗しました 4 | notice_successful_create_webhook: Webhook の作成に成功しました 5 | notice_fail_update_webhook: Webhook の更新に失敗しました 6 | notice_successful_update_webhook : Webhook の更新に成功しました 7 | notice_fail_delete_webhook: Webhook の削除に失敗しました 8 | notice_successful_delete_webhook : Webhook の削除に成功しました 9 | 10 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | get 'projects/:id/webhook_settings', :controller => 'webhook_settings', :action => 'index', :as => :list_webhook_settings 2 | get 'projects/:id/webhook_settings/show', :controller => 'webhook_settings', :action => 'show', :as => :show_webhook_settings 3 | post 'projects/:id/webhook_settings/create', :controller => 'webhook_settings', :action => 'create', :as => :create_webhook 4 | put 'projects/:id/webhook_settings/:webhook_id', :controller => 'webhook_settings', :action => 'update', :as => :update_webhook 5 | delete 'projects/:id/webhook_settings/:webhook_id', :controller => 'webhook_settings', :action => 'destroy', :as => :delete_webhook 6 | -------------------------------------------------------------------------------- /db/migrate/001_create_webhooks.rb: -------------------------------------------------------------------------------- 1 | class CreateWebhooks < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :webhooks do |t| 4 | t.string :url 5 | t.integer :project_id 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | if Rails.try(:autoloaders).try(:zeitwerk_enabled?) 2 | Rails.autoloaders.main.push_dir File.dirname(__FILE__) + '/lib/redmine_webhook' 3 | RedmineWebhook::ProjectsHelperPatch 4 | RedmineWebhook::WebhookListener 5 | else 6 | require "redmine_webhook" 7 | end 8 | 9 | Redmine::Plugin.register :redmine_webhook do 10 | name 'Redmine Webhook plugin' 11 | author 'suer' 12 | description 'A Redmine plugin posts webhook on creating and updating tickets' 13 | version '0.0.5' 14 | url 'https://github.com/suer/redmine_webhook' 15 | author_url 'http://d.hatena.ne.jp/suer' 16 | project_module :webhooks do 17 | permission :manage_hook, {:webhook_settings => [:index, :show, :update, :create, :destroy]}, :require => :member 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/redmine_webhook.rb: -------------------------------------------------------------------------------- 1 | unless Rails.try(:autoloaders).try(:zeitwerk_enabled?) 2 | require 'redmine_webhook/projects_helper_patch' 3 | require 'redmine_webhook/issue_wrapper' 4 | require 'redmine_webhook/webhook_listener' 5 | end 6 | 7 | module RedmineWebhook 8 | end 9 | -------------------------------------------------------------------------------- /lib/redmine_webhook/author_wrapper.rb: -------------------------------------------------------------------------------- 1 | module RedmineWebhook 2 | class AuthorWrapper 3 | include GravatarHelper::PublicMethods 4 | include ERB::Util 5 | 6 | def initialize(author) 7 | @author = author 8 | end 9 | 10 | def to_hash 11 | return nil unless @author 12 | { 13 | :id => @author.id, 14 | :login => @author.login, 15 | :mail => @author.mail, 16 | :firstname => @author.firstname, 17 | :lastname => @author.lastname, 18 | :identity_url => @author.try(:identity_url), 19 | :icon_url => icon_url 20 | } 21 | end 22 | 23 | def icon_url 24 | if @author.mail.blank? 25 | icon_url = nil 26 | else 27 | icon_url = gravatar_url(@author.mail) 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/redmine_webhook/custom_field_value_wrapper.rb: -------------------------------------------------------------------------------- 1 | class RedmineWebhook::CustomFieldValueWrapper 2 | def initialize(custom_field_value) 3 | @custom_field_value = custom_field_value 4 | end 5 | 6 | def to_hash 7 | { 8 | custom_field_id: @custom_field_value.custom_field_id, 9 | custom_field_name: @custom_field_value.custom_field.name, 10 | value: @custom_field_value.value 11 | } 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/redmine_webhook/issue_wrapper.rb: -------------------------------------------------------------------------------- 1 | module RedmineWebhook 2 | class IssueWrapper 3 | def initialize(issue) 4 | @issue = issue 5 | end 6 | 7 | def to_hash 8 | { 9 | :id => @issue.id, 10 | :subject => @issue.subject, 11 | :description => @issue.description, 12 | :created_on => @issue.created_on, 13 | :updated_on => @issue.updated_on, 14 | :closed_on => @issue.closed_on, 15 | :root_id => @issue.root_id, 16 | :parent_id => @issue.parent_id, 17 | :done_ratio => @issue.done_ratio, 18 | :start_date => @issue.start_date, 19 | :due_date => @issue.due_date, 20 | :estimated_hours => @issue.estimated_hours, 21 | :is_private => @issue.is_private, 22 | :lock_version => @issue.lock_version, 23 | :custom_field_values => @issue.custom_field_values.collect { |value| RedmineWebhook::CustomFieldValueWrapper.new(value).to_hash }, 24 | :project => RedmineWebhook::ProjectWrapper.new(@issue.project).to_hash, 25 | :status => RedmineWebhook::StatusWrapper.new(@issue.status).to_hash, 26 | :tracker => RedmineWebhook::TrackerWrapper.new(@issue.tracker).to_hash, 27 | :priority => RedmineWebhook::PriorityWrapper.new(@issue.priority).to_hash, 28 | :author => RedmineWebhook::AuthorWrapper.new(@issue.author).to_hash, 29 | :assignee => RedmineWebhook::AuthorWrapper.new(@issue.assigned_to).to_hash, 30 | :watchers => @issue.watcher_users.collect{|u| RedmineWebhook::AuthorWrapper.new(u).to_hash} 31 | } 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/redmine_webhook/journal_detail_wrapper.rb: -------------------------------------------------------------------------------- 1 | module RedmineWebhook 2 | class JournalDetailWrapper 3 | def initialize(journal_detail) 4 | @journal_detail = journal_detail 5 | end 6 | 7 | def to_hash 8 | { 9 | :id => @journal_detail.id, 10 | :value => @journal_detail.value, 11 | :old_value => @journal_detail.old_value, 12 | :prop_key => @journal_detail.prop_key, 13 | :property => @journal_detail.property 14 | } 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/redmine_webhook/journal_wrapper.rb: -------------------------------------------------------------------------------- 1 | module RedmineWebhook 2 | class JournalWrapper 3 | def initialize(journal) 4 | @journal = journal 5 | end 6 | 7 | def to_hash 8 | { 9 | :id => @journal.id, 10 | :notes => @journal.notes, 11 | :created_on => @journal.created_on, 12 | :private_notes => @journal.private_notes, 13 | :author => RedmineWebhook::AuthorWrapper.new(@journal.user).to_hash, 14 | :details => @journal.details.map {|detail| RedmineWebhook::JournalDetailWrapper.new(detail).to_hash } 15 | } 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/redmine_webhook/priority_wrapper.rb: -------------------------------------------------------------------------------- 1 | module RedmineWebhook 2 | class PriorityWrapper 3 | def initialize(priority) 4 | @priority = priority 5 | end 6 | 7 | def to_hash 8 | { 9 | :id => @priority.id, 10 | :name => @priority.name 11 | } 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/redmine_webhook/project_wrapper.rb: -------------------------------------------------------------------------------- 1 | class RedmineWebhook::ProjectWrapper 2 | def initialize(project) 3 | @project = project 4 | end 5 | 6 | def to_hash 7 | { 8 | :id => @project.id, 9 | :identifier => @project.identifier, 10 | :name => @project.name, 11 | :description => @project.description, 12 | :created_on => @project.created_on, 13 | :homepage => @project.homepage, 14 | } 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/redmine_webhook/projects_helper_patch.rb: -------------------------------------------------------------------------------- 1 | module RedmineWebhook 2 | module ProjectsHelperPatch 3 | def project_settings_tabs 4 | tabs = super 5 | webhook_tab = { 6 | :name => 'webhook', 7 | :controller => 'webhook_settings', 8 | :action => :show, 9 | :partial => 'webhook_settings/show', 10 | :label => :webhook 11 | } 12 | tabs << webhook_tab if User.current.allowed_to?(:manage_hook, @project) 13 | tabs 14 | end 15 | end 16 | end 17 | 18 | ProjectsController.send(:helper, RedmineWebhook::ProjectsHelperPatch) 19 | -------------------------------------------------------------------------------- /lib/redmine_webhook/status_wrapper.rb: -------------------------------------------------------------------------------- 1 | module RedmineWebhook 2 | class StatusWrapper 3 | def initialize(status) 4 | @status = status 5 | end 6 | 7 | def to_hash 8 | { 9 | :id => @status.id, 10 | :name => @status.name 11 | } 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/redmine_webhook/tracker_wrapper.rb: -------------------------------------------------------------------------------- 1 | module RedmineWebhook 2 | class TrackerWrapper 3 | def initialize(tracker) 4 | @tracker = tracker 5 | end 6 | 7 | def to_hash 8 | { 9 | :id => @tracker.id, 10 | :name => @tracker.name 11 | } 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/redmine_webhook/webhook_listener.rb: -------------------------------------------------------------------------------- 1 | module RedmineWebhook 2 | class WebhookListener < Redmine::Hook::Listener 3 | 4 | def skip_webhooks(context) 5 | return true unless context[:request] 6 | return true if context[:request].headers['X-Skip-Webhooks'] 7 | 8 | false 9 | end 10 | 11 | def controller_issues_new_after_save(context = {}) 12 | return if skip_webhooks(context) 13 | issue = context[:issue] 14 | controller = context[:controller] 15 | project = issue.project 16 | webhooks = Webhook.where(:project_id => project.project.id) 17 | webhooks = Webhook.where(:project_id => 0) unless webhooks && webhooks.length > 0 18 | return unless webhooks 19 | post(webhooks, issue_to_json(issue, controller)) 20 | end 21 | 22 | def controller_issues_edit_after_save(context = {}) 23 | return if skip_webhooks(context) 24 | journal = context[:journal] 25 | controller = context[:controller] 26 | issue = context[:issue] 27 | project = issue.project 28 | webhooks = Webhook.where(:project_id => project.project.id) 29 | webhooks = Webhook.where(:project_id => 0) unless webhooks && webhooks.length > 0 30 | return unless webhooks 31 | post(webhooks, journal_to_json(issue, journal, controller)) 32 | end 33 | 34 | def controller_issues_bulk_edit_after_save(context = {}) 35 | return if skip_webhooks(context) 36 | journal = context[:journal] 37 | controller = context[:controller] 38 | issue = context[:issue] 39 | project = issue.project 40 | webhooks = Webhook.where(:project_id => project.project.id) 41 | webhooks = Webhook.where(:project_id => 0) unless webhooks && webhooks.length > 0 42 | return unless webhooks 43 | post(webhooks, journal_to_json(issue, journal, controller)) 44 | end 45 | 46 | def model_changeset_scan_commit_for_issue_ids_pre_issue_update(context = {}) 47 | issue = context[:issue] 48 | journal = issue.current_journal 49 | webhooks = Webhook.where(:project_id => issue.project.project.id) 50 | webhooks = Webhook.where(:project_id => 0) unless webhooks && webhooks.length > 0 51 | return unless webhooks 52 | post(webhooks, journal_to_json(issue, journal, nil)) 53 | end 54 | 55 | private 56 | def issue_to_json(issue, controller) 57 | { 58 | :payload => { 59 | :action => 'opened', 60 | :issue => RedmineWebhook::IssueWrapper.new(issue).to_hash, 61 | :url => controller.issue_url(issue) 62 | } 63 | }.to_json 64 | end 65 | 66 | def journal_to_json(issue, journal, controller) 67 | { 68 | :payload => { 69 | :action => 'updated', 70 | :issue => RedmineWebhook::IssueWrapper.new(issue).to_hash, 71 | :journal => RedmineWebhook::JournalWrapper.new(journal).to_hash, 72 | :url => controller.nil? ? 'not yet implemented' : controller.issue_url(issue) 73 | } 74 | }.to_json 75 | end 76 | 77 | def post(webhooks, request_body) 78 | Thread.start do 79 | webhooks.each do |webhook| 80 | begin 81 | Faraday.post do |req| 82 | req.url webhook.url 83 | req.headers['Content-Type'] = 'application/json' 84 | req.body = request_body 85 | end 86 | rescue => e 87 | Rails.logger.error e 88 | end 89 | end 90 | end 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # Load the Redmine helper 2 | require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper') 3 | -------------------------------------------------------------------------------- /test/unit/webhook_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../test_helper', __FILE__) 2 | 3 | class WebhookTest < ActiveSupport::TestCase 4 | 5 | # Replace this with your real tests. 6 | def test_truth 7 | assert true 8 | end 9 | end 10 | --------------------------------------------------------------------------------