├── .ruby-version ├── spec ├── dummy │ ├── lib │ │ └── assets │ │ │ └── .keep │ ├── public │ │ ├── favicon.ico │ │ ├── apple-touch-icon.png │ │ ├── apple-touch-icon-precomposed.png │ │ ├── 500.html │ │ ├── 422.html │ │ └── 404.html │ ├── app │ │ ├── assets │ │ │ ├── images │ │ │ │ └── .keep │ │ │ ├── javascripts │ │ │ │ ├── channels │ │ │ │ │ └── .keep │ │ │ │ ├── cable.js │ │ │ │ └── application.js │ │ │ ├── config │ │ │ │ └── manifest.js │ │ │ └── stylesheets │ │ │ │ └── application.css │ │ ├── models │ │ │ ├── concerns │ │ │ │ └── .keep │ │ │ └── application_record.rb │ │ ├── controllers │ │ │ ├── concerns │ │ │ │ └── .keep │ │ │ └── application_controller.rb │ │ ├── views │ │ │ └── layouts │ │ │ │ ├── mailer.text.erb │ │ │ │ ├── mailer.html.erb │ │ │ │ └── application.html.erb │ │ ├── helpers │ │ │ └── application_helper.rb │ │ ├── jobs │ │ │ └── application_job.rb │ │ ├── channels │ │ │ └── application_cable │ │ │ │ ├── channel.rb │ │ │ │ └── connection.rb │ │ └── mailers │ │ │ └── application_mailer.rb │ ├── config │ │ ├── routes.rb │ │ ├── spring.rb │ │ ├── environment.rb │ │ ├── initializers │ │ │ ├── mime_types.rb │ │ │ ├── application_controller_renderer.rb │ │ │ ├── cookies_serializer.rb │ │ │ ├── filter_parameter_logging.rb │ │ │ ├── permissions_policy.rb │ │ │ ├── wrap_parameters.rb │ │ │ ├── backtrace_silencers.rb │ │ │ ├── assets.rb │ │ │ ├── inflections.rb │ │ │ └── content_security_policy.rb │ │ ├── boot.rb │ │ ├── cable.yml │ │ ├── database.yml │ │ ├── application.rb │ │ ├── locales │ │ │ └── en.yml │ │ ├── storage.yml │ │ ├── secrets.yml │ │ ├── puma.rb │ │ └── environments │ │ │ ├── test.rb │ │ │ ├── development.rb │ │ │ └── production.rb │ ├── bin │ │ ├── rake │ │ ├── bundle │ │ ├── rails │ │ ├── yarn │ │ ├── update │ │ └── setup │ ├── schema │ │ ├── Schemafile │ │ ├── tokite │ │ │ ├── Schemafile │ │ │ ├── tokite_repositories.schema │ │ │ ├── tokite_secure_user_tokens.schema │ │ │ ├── tokite_users.schema │ │ │ └── tokite_rules.schema │ │ ├── tokite_repositories.schema │ │ ├── tokite_secure_user_tokens.schema │ │ ├── tokite_users.schema │ │ └── tokite_rules.schema │ ├── config.ru │ ├── Rakefile │ └── dot.env ├── factories │ └── tokite │ │ ├── rule_factory.rb │ │ └── user_factory.rb ├── rails_helper.rb ├── models │ └── tokite │ │ ├── hook_spec.rb │ │ ├── search_query_spec.rb │ │ └── rule_spec.rb ├── spec_helper.rb ├── requests │ └── hooks_spec.rb └── payloads │ ├── issues.json │ ├── issue_comment.json │ ├── pull_request.review_requested.json │ └── pull_request_review.json ├── app ├── assets │ ├── images │ │ └── tokite │ │ │ └── .keep │ ├── javascripts │ │ └── tokite │ │ │ └── application.js │ ├── config │ │ └── tokite_manifest.js │ └── stylesheets │ │ └── tokite │ │ └── application.scss ├── models │ └── tokite │ │ ├── concerns │ │ └── .keep │ │ ├── secure_user_token.rb │ │ ├── application_record.rb │ │ ├── revision.rb │ │ ├── transfer.rb │ │ ├── hook_event │ │ ├── base_event.rb │ │ ├── issue_comment.rb │ │ ├── pull_request_review_comment.rb │ │ ├── issues.rb │ │ ├── pull_request_review.rb │ │ └── pull_request.rb │ │ ├── user.rb │ │ ├── repository.rb │ │ ├── rule.rb │ │ ├── hook.rb │ │ └── search_query.rb ├── controllers │ └── tokite │ │ ├── concerns │ │ └── .keep │ │ ├── top_controller.rb │ │ ├── sha_controller.rb │ │ ├── sessions_controller.rb │ │ ├── transfers_controller.rb │ │ ├── hooks_controller.rb │ │ ├── application_controller.rb │ │ ├── users_controller.rb │ │ ├── rules_controller.rb │ │ └── repositories_controller.rb ├── views │ ├── layouts │ │ └── tokite │ │ │ ├── mailer.text.erb │ │ │ ├── mailer.html.erb │ │ │ └── application.html.haml │ └── tokite │ │ ├── rules │ │ ├── new.html.haml │ │ ├── edit.html.haml │ │ ├── index.html.haml │ │ ├── _rules.html.haml │ │ └── _form.html.haml │ │ ├── top │ │ └── show.html.haml │ │ ├── sessions │ │ └── new.html.haml │ │ ├── users │ │ ├── show.html.haml │ │ ├── edit.html.haml │ │ ├── _form.html.haml │ │ └── index.html.haml │ │ ├── transfers │ │ └── new.html.haml │ │ └── repositories │ │ ├── index.html.haml │ │ └── new.html.haml ├── jobs │ └── tokite │ │ ├── application_job.rb │ │ └── notify_github_hook_event_job.rb ├── mailers │ └── tokite │ │ └── application_mailer.rb └── helpers │ └── tokite │ └── application_helper.rb ├── .rspec ├── lib ├── tasks │ ├── yarn.rake │ └── tokite.rake ├── tokite │ ├── version.rb │ ├── exception_logger.rb │ └── engine.rb └── tokite.rb ├── schema ├── Schemafile ├── tokite_repositories.schema ├── tokite_secure_user_tokens.schema ├── tokite_users.schema └── tokite_rules.schema ├── package.json ├── .gitignore ├── config ├── initializers │ ├── octokit.rb │ ├── slack_notifier.rb │ └── omniauth.rb └── routes.rb ├── Gemfile ├── yarn.lock ├── bin ├── rake ├── rspec └── rails ├── Rakefile ├── .github └── workflows │ └── test.yml ├── MIT-LICENSE ├── tokite.gemspec ├── CHANGELOG.md └── README.md /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.1.3 2 | -------------------------------------------------------------------------------- /spec/dummy/lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/tokite/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/tokite/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/tokite/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/public/apple-touch-icon.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/javascripts/channels/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/layouts/tokite/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /lib/tasks/yarn.rake: -------------------------------------------------------------------------------- 1 | namespace :tokite do 2 | 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /app/assets/javascripts/tokite/application.js: -------------------------------------------------------------------------------- 1 | //= require rails-ujs 2 | -------------------------------------------------------------------------------- /lib/tokite/version.rb: -------------------------------------------------------------------------------- 1 | module Tokite 2 | VERSION = '0.7.1' 3 | end 4 | -------------------------------------------------------------------------------- /app/views/tokite/rules/new.html.haml: -------------------------------------------------------------------------------- 1 | %h1.title New rule 2 | = render "form" -------------------------------------------------------------------------------- /app/views/tokite/rules/edit.html.haml: -------------------------------------------------------------------------------- 1 | %h1.title Edit rule 2 | = render "form" 3 | -------------------------------------------------------------------------------- /spec/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/views/tokite/top/show.html.haml: -------------------------------------------------------------------------------- 1 | = render "tokite/rules/rules", user: current_user 2 | -------------------------------------------------------------------------------- /spec/dummy/app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | end 3 | -------------------------------------------------------------------------------- /lib/tokite.rb: -------------------------------------------------------------------------------- 1 | require "tokite/engine" 2 | 3 | module Tokite 4 | # Your code goes here... 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | mount Tokite::Engine => "/" 3 | end 4 | -------------------------------------------------------------------------------- /app/jobs/tokite/application_job.rb: -------------------------------------------------------------------------------- 1 | module Tokite 2 | class ApplicationJob < ActiveJob::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /schema/Schemafile: -------------------------------------------------------------------------------- 1 | Dir.glob(File.expand_path("../*.schema", __FILE__)).each do |path| 2 | require path 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /spec/dummy/schema/Schemafile: -------------------------------------------------------------------------------- 1 | Dir.glob(File.expand_path("../*.schema", __FILE__)).each do |path| 2 | require path 3 | end 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tokite", 3 | "private": true, 4 | "dependencies": { 5 | "bulma": "^0.9.4" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /spec/dummy/app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/schema/tokite/Schemafile: -------------------------------------------------------------------------------- 1 | Dir.glob(File.expand_path("../*.schema", __FILE__)).each do |path| 2 | require path 3 | end 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | log/*.log 3 | pkg/ 4 | spec/dummy/log/ 5 | spec/dummy/tmp/ 6 | spec/dummy/.env 7 | node_modules/ 8 | Gemfile.lock 9 | -------------------------------------------------------------------------------- /app/views/tokite/rules/index.html.haml: -------------------------------------------------------------------------------- 1 | - @users.each do |user| 2 | .box 3 | %h2.title= user.name 4 | = render "rules", user: user 5 | -------------------------------------------------------------------------------- /app/models/tokite/secure_user_token.rb: -------------------------------------------------------------------------------- 1 | module Tokite 2 | class SecureUserToken < ApplicationRecord 3 | belongs_to :user 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/tokite/top_controller.rb: -------------------------------------------------------------------------------- 1 | module Tokite 2 | class TopController < ApplicationController 3 | def show 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/views/tokite/sessions/new.html.haml: -------------------------------------------------------------------------------- 1 | = form_with url: "/auth/github", method: :post do |form| 2 | = form.submit "Login with GitHub", class: "button" 3 | -------------------------------------------------------------------------------- /spec/dummy/app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /spec/dummy/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /app/models/tokite/application_record.rb: -------------------------------------------------------------------------------- 1 | module Tokite 2 | class ApplicationRecord < ActiveRecord::Base 3 | self.abstract_class = true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery with: :exception 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'from@example.com' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /spec/dummy/config/spring.rb: -------------------------------------------------------------------------------- 1 | %w( 2 | .ruby-version 3 | .rbenv-vars 4 | tmp/restart.txt 5 | tmp/caching-dev.txt 6 | ).each { |path| Spring.watch(path) } 7 | -------------------------------------------------------------------------------- /app/assets/config/tokite_manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images/tokite 2 | //= link_directory ../javascripts/tokite .js 3 | //= link_directory ../stylesheets/tokite .css 4 | -------------------------------------------------------------------------------- /spec/dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../config/application', __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /spec/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /app/mailers/tokite/application_mailer.rb: -------------------------------------------------------------------------------- 1 | module Tokite 2 | class ApplicationMailer < ActionMailer::Base 3 | default from: 'from@example.com' 4 | layout 'mailer' 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | 2 | //= link_tree ../images 3 | //= link_directory ../javascripts .js 4 | //= link_directory ../stylesheets .css 5 | //= link tokite_manifest.js 6 | -------------------------------------------------------------------------------- /app/views/tokite/users/show.html.haml: -------------------------------------------------------------------------------- 1 | %h2.title 2 | = @user.name 3 | = link_to "Edit", edit_user_path(@user), class: "button is-primary is-small" 4 | 5 | = render "tokite/rules/rules", user: @user 6 | -------------------------------------------------------------------------------- /spec/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative "config/environment" 4 | 5 | run Rails.application 6 | Rails.application.load_server 7 | -------------------------------------------------------------------------------- /config/initializers/octokit.rb: -------------------------------------------------------------------------------- 1 | require "octokit" 2 | 3 | if ENV["GITHUB_HOST"].present? 4 | Octokit.configure do |c| 5 | c.api_endpoint = URI.join(ENV["GITHUB_HOST"], "/api/v3/").to_s 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /app/views/tokite/users/edit.html.haml: -------------------------------------------------------------------------------- 1 | %h1.title Edit user 2 | = render "form" 3 | 4 | - if @user.group_user? 5 | = link_to "Delete user", user_path(@user), method: :delete, data: { confirm: %(Delete "#{@user.name}"?) } 6 | -------------------------------------------------------------------------------- /spec/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require "bundler/setup" # Set up gems listed in the Gemfile. 4 | require "bootsnap/setup" # Speed up boot time by caching expensive operations. 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem 'bootsnap' 6 | gem 'denv' 7 | gem 'byebug' 8 | gem 'listen' 9 | gem 'pry-byebug' 10 | gem 'pry-rails' 11 | gem 'rspec_junit_formatter' 12 | gem 'webrick' 13 | gem 'zeitwerk' 14 | -------------------------------------------------------------------------------- /spec/dummy/config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: test 6 | 7 | production: 8 | adapter: redis 9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> 10 | channel_prefix: dummy_production 11 | -------------------------------------------------------------------------------- /spec/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /spec/factories/tokite/rule_factory.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :rule, class: Tokite::Rule do 3 | association :user, strategy: :create 4 | sequence(:name) {|n| "name_#{n}" } 5 | query { "query" } 6 | channel { "#general" } 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/views/tokite/users/_form.html.haml: -------------------------------------------------------------------------------- 1 | = form_for [@user] do |f| 2 | .field 3 | = f.label :name, class: "label" 4 | %p.control 5 | = f.text_field :name, size: 40, class: "input" 6 | .field 7 | %p.control 8 | = f.submit class: "button is-primary" 9 | -------------------------------------------------------------------------------- /app/views/tokite/rules/_rules.html.haml: -------------------------------------------------------------------------------- 1 | .content 2 | %h3 Rules 3 | %ul 4 | - user.rules.order(:id).each do |rule| 5 | %li= link_to rule.name, edit_rule_path(rule) 6 | 7 | .content 8 | = link_to "New Rule", new_user_rule_path(user), class: "button is-primary" 9 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /app/views/tokite/transfers/new.html.haml: -------------------------------------------------------------------------------- 1 | %h1.title 2 | Transfer 3 | = link_to @rule.name, edit_rule_path(@rule) 4 | 5 | = form_for [@rule, @transfer] do |f| 6 | = f.collection_select :user_id, @users, :id, :name_with_provider 7 | .field.columns 8 | .column.is-8= f.submit "Transfer", class: "button is-primary" 9 | -------------------------------------------------------------------------------- /app/views/layouts/tokite/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/models/tokite/revision.rb: -------------------------------------------------------------------------------- 1 | module Tokite 2 | class Revision 3 | REVISION_PATH = Rails.root.join("REVISION") 4 | 5 | def self.take 6 | return ENV["HEROKU_SLUG_COMMIT"] if ENV["HEROKU_SLUG_COMMIT"] 7 | return File.read(REVISION_PATH).chomp if File.exist?(REVISION_PATH) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [ 5 | :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn 6 | ] 7 | -------------------------------------------------------------------------------- /spec/factories/tokite/user_factory.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :user, class: Tokite::User do 3 | provider { "google_oauth2" } 4 | sequence(:uid) {|n| (n * 100).to_s } 5 | name { "name_#{uid}" } 6 | image_url { "https://lh3.googleusercontent.com/-l5MDH3jtWXc/AAAAAAAAAAI/AAAAAAAAAAA/2wjfVaIkYNY/photo.jpg" } 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%= csrf_meta_tags %> 6 | 7 | <%= stylesheet_link_tag 'application', media: 'all' %> 8 | <%= javascript_include_tag 'application' %> 9 | 10 | 11 | 12 | <%= yield %> 13 | 14 | 15 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | bulma@^0.9.4: 6 | version "0.9.4" 7 | resolved "https://registry.yarnpkg.com/bulma/-/bulma-0.9.4.tgz#0ca8aeb1847a34264768dba26a064c8be72674a1" 8 | integrity sha512-86FlT5+1GrsgKbPLRRY7cGDg8fsJiP/jzTqXXVqiUZZ2aZT8uemEOHlU1CDU+TxklPEZ11HZNNWclRBBecP4CQ== 9 | -------------------------------------------------------------------------------- /lib/tokite/exception_logger.rb: -------------------------------------------------------------------------------- 1 | module Tokite 2 | class ExceptionLogger 3 | def self.callbacks 4 | @callbacks ||= [] 5 | end 6 | 7 | def self.configure(callback) 8 | callbacks << callback 9 | end 10 | 11 | def self.log(e, options = {}) 12 | callbacks.each{|callback| callback.call(e, options) } 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/dummy/schema/tokite_repositories.schema: -------------------------------------------------------------------------------- 1 | create_table "tokite_repositories", force: :cascade do |t| 2 | t.string "name", limit: 200, null: false 3 | t.string "url", limit: 200, null: false 4 | t.datetime "created_at", null: false 5 | end 6 | 7 | add_index "tokite_repositories", ["name"], name: "tokite_repositories_uniq_name", unique: true, using: :btree 8 | -------------------------------------------------------------------------------- /spec/dummy/schema/tokite/tokite_repositories.schema: -------------------------------------------------------------------------------- 1 | create_table "tokite_repositories", force: :cascade do |t| 2 | t.string "name", limit: 200, null: false 3 | t.string "url", limit: 200, null: false 4 | t.datetime "created_at", null: false 5 | end 6 | 7 | add_index "tokite_repositories", ["name"], name: "tokite_repositories_uniq_name", unique: true, using: :btree 8 | -------------------------------------------------------------------------------- /app/jobs/tokite/notify_github_hook_event_job.rb: -------------------------------------------------------------------------------- 1 | require "tokite/exception_logger" 2 | 3 | module Tokite 4 | class NotifyGithubHookEventJob < ApplicationJob 5 | queue_as :default 6 | 7 | def perform(payload) 8 | Rails.application.config.slack_notifier.ping(payload) 9 | rescue Slack::Notifier::APIError => e 10 | ExceptionLogger.log(e, level: :warn) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/views/tokite/users/index.html.haml: -------------------------------------------------------------------------------- 1 | %table.table.is-striped 2 | %thead 3 | %tr 4 | %th ID 5 | %th Name 6 | %tbody 7 | - @users.each do |user| 8 | %tr 9 | %td= user.id 10 | %td 11 | = link_to user.name_with_provider, user_path(user) 12 | 13 | = link_to "Create group", users_path, method: :post, data: { confirm: %(Create Group?) }, class: "button is-primary" 14 | 15 | -------------------------------------------------------------------------------- /lib/tokite/engine.rb: -------------------------------------------------------------------------------- 1 | module Tokite 2 | class Engine < ::Rails::Engine 3 | isolate_namespace Tokite 4 | 5 | config.before_configuration do 6 | require "haml-rails" 7 | require "slack-notifier" 8 | end 9 | 10 | initializer "tokite.config" do 11 | Tokite::Engine.routes.default_url_options = { protocol: "https", host: ENV.fetch("APP_HOST", "example.com") } 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/dummy/dot.env: -------------------------------------------------------------------------------- 1 | GOOGLE_CLIENT_ID=111111111111-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com 2 | GOOGLE_CLIENT_SECRET=xxxxxxxxxxxxxx 3 | GOOGLE_HOSTED_DOMAIN= 4 | SECRET_KEY_BASE=37a0d35d995d13af465c390bc0b013472be45ab344009838e19977bbf50aea1a4a2c706ae7b0d6247f72890885afe8bc850345ff4d00cabd2f61bd349bd35b30 5 | SLACK_WEBHOOK_URL=https://hooks.slack.com/services/XXXXXXXXX/YYYYYYYYY/AAAAAAAAAAAAAAAAAAAAAAAA 6 | SLACK_NAME=tokite 7 | -------------------------------------------------------------------------------- /app/controllers/tokite/sha_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Tokite 4 | class ShaController < ActionController::API 5 | 6 | def show 7 | revision = Revision.take 8 | if revision 9 | status = 200 10 | else 11 | revision = "REVISION_FILE_NOT_FOUND" 12 | status = 404 13 | end 14 | render plain: "#{revision}\n", status: status 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /schema/tokite_repositories.schema: -------------------------------------------------------------------------------- 1 | create_table "tokite_repositories", force: :cascade do |t| 2 | t.string "name", limit: 200, null: false 3 | t.string "url", limit: 200, null: false 4 | t.datetime "created_at", null: false 5 | t.boolean "private", null: false, default: false 6 | end 7 | 8 | add_index "tokite_repositories", ["name"], name: "tokite_repositories_uniq_name", unique: true, using: :btree 9 | -------------------------------------------------------------------------------- /app/models/tokite/transfer.rb: -------------------------------------------------------------------------------- 1 | module Tokite 2 | class Transfer 3 | include ActiveModel::Model 4 | 5 | attr_accessor :rule_id, :user_id 6 | attr_reader :rule 7 | 8 | def self.create!(attributes) 9 | transfer = new(attributes) 10 | transfer.rule.update!(user_id: transfer.user_id) 11 | 12 | return transfer 13 | end 14 | 15 | def rule 16 | @rule ||= Rule.find(rule_id) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /schema/tokite_secure_user_tokens.schema: -------------------------------------------------------------------------------- 1 | create_table "tokite_secure_user_tokens", force: :cascade do |t| 2 | t.integer "user_id", null: false 3 | t.string "token", limit: 40, null: false 4 | t.datetime "created_at", null: false 5 | t.datetime "updated_at", null: false 6 | end 7 | 8 | add_index "tokite_secure_user_tokens", ["user_id"], name: "tokite_secure_user_token_uniq_user_id", unique: true, using: :btree 9 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/javascripts/cable.js: -------------------------------------------------------------------------------- 1 | // Action Cable provides the framework to deal with WebSockets in Rails. 2 | // You can generate new channels where WebSocket features live using the `rails generate channel` command. 3 | // 4 | //= require action_cable 5 | //= require_self 6 | //= require_tree ./channels 7 | 8 | (function() { 9 | this.App || (this.App = {}); 10 | 11 | App.cable = ActionCable.createConsumer(); 12 | 13 | }).call(this); 14 | -------------------------------------------------------------------------------- /spec/dummy/schema/tokite_secure_user_tokens.schema: -------------------------------------------------------------------------------- 1 | create_table "tokite_secure_user_tokens", force: :cascade do |t| 2 | t.integer "user_id", null: false 3 | t.string "token", limit: 40, null: false 4 | t.datetime "created_at", null: false 5 | t.datetime "updated_at", null: false 6 | end 7 | 8 | add_index "tokite_secure_user_tokens", ["user_id"], name: "tokite_secure_user_token_uniq_user_id", unique: true, using: :btree 9 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | # 4 | # This file was generated by Bundler. 5 | # 6 | # The application 'rake' is installed as part of a gem, and 7 | # this file is here to facilitate running it. 8 | # 9 | 10 | require "pathname" 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 12 | Pathname.new(__FILE__).realpath) 13 | 14 | require "rubygems" 15 | require "bundler/setup" 16 | 17 | load Gem.bin_path("rake", "rake") 18 | -------------------------------------------------------------------------------- /spec/dummy/schema/tokite/tokite_secure_user_tokens.schema: -------------------------------------------------------------------------------- 1 | create_table "tokite_secure_user_tokens", force: :cascade do |t| 2 | t.integer "user_id", null: false 3 | t.string "token", limit: 40, null: false 4 | t.datetime "created_at", null: false 5 | t.datetime "updated_at", null: false 6 | end 7 | 8 | add_index "tokite_secure_user_tokens", ["user_id"], name: "tokite_secure_user_token_uniq_user_id", unique: true, using: :btree 9 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/permissions_policy.rb: -------------------------------------------------------------------------------- 1 | # Define an application-wide HTTP permissions policy. For further 2 | # information see https://developers.google.com/web/updates/2018/06/feature-policy 3 | # 4 | # Rails.application.config.permissions_policy do |f| 5 | # f.camera :none 6 | # f.gyroscope :none 7 | # f.microphone :none 8 | # f.usb :none 9 | # f.fullscreen :self 10 | # f.payment :self, "https://secure.example.com" 11 | # end 12 | -------------------------------------------------------------------------------- /app/models/tokite/hook_event/base_event.rb: -------------------------------------------------------------------------------- 1 | module Tokite 2 | module HookEvent 3 | class BaseEvent 4 | attr_reader :hook_params 5 | 6 | def initialize(hook_params) 7 | @hook_params = hook_params 8 | end 9 | 10 | def slack_payload(rule) 11 | { 12 | channel: rule.channel, 13 | text: slack_text, 14 | attachments: slack_attachments(rule), 15 | } 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | # 4 | # This file was generated by Bundler. 5 | # 6 | # The application 'rspec' is installed as part of a gem, and 7 | # this file is here to facilitate running it. 8 | # 9 | 10 | require "pathname" 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 12 | Pathname.new(__FILE__).realpath) 13 | 14 | require "rubygems" 15 | require "bundler/setup" 16 | 17 | load Gem.bin_path("rspec-core", "rspec") 18 | -------------------------------------------------------------------------------- /spec/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | adapter: postgresql 3 | encoding: unicode 4 | host: localhost 5 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 6 | 7 | development: 8 | <<: *default 9 | database: tokite_development 10 | 11 | test: 12 | <<: *default 13 | <% if ENV["GITHUB_ACTIONS"] %> 14 | user: postgres 15 | password: postgres_password 16 | <% end %> 17 | database: tokite_test 18 | 19 | production: 20 | <<: *default 21 | url: <%= ENV['DATABASE_URL'] %> 22 | -------------------------------------------------------------------------------- /app/controllers/tokite/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | module Tokite 2 | class SessionsController < ApplicationController 3 | skip_before_action :require_login 4 | 5 | def new 6 | end 7 | 8 | def create 9 | auth = request.env["omniauth.auth"] 10 | user = User.login!(auth) 11 | session[:user_id] = user.id 12 | redirect_to root_path 13 | end 14 | 15 | def destroy 16 | session[:user_id] = nil 17 | redirect_to root_path 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/controllers/tokite/transfers_controller.rb: -------------------------------------------------------------------------------- 1 | module Tokite 2 | class TransfersController < ApplicationController 3 | def new 4 | @transfer = Transfer.new(rule_id: params[:rule_id]) 5 | @rule = @transfer.rule 6 | @users = User.where.not(id: @rule.user_id) 7 | end 8 | 9 | def create 10 | @transfer = Transfer.create!(rule_id: params[:rule_id], user_id: params[:transfer][:user_id]) 11 | redirect_to user_path(@transfer.rule.user_id) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/controllers/tokite/hooks_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Tokite 4 | class HooksController < ActionController::API 5 | GITHUB_EVENT_HEADER = "X-GitHub-Event" 6 | 7 | def create 8 | logger.debug("Hook triggered: #{github_event}") 9 | Hook.fire!(github_event, request.request_parameters) 10 | render plain: "ok" 11 | end 12 | 13 | private 14 | 15 | def github_event 16 | request.headers[GITHUB_EVENT_HEADER] 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/views/tokite/repositories/index.html.haml: -------------------------------------------------------------------------------- 1 | - if current_user_token 2 | = link_to "Watch new repositories", new_repository_path, class: "button is-primary" 3 | 4 | .my-6 5 | 6 | %h1.title Watching repositories 7 | 8 | %table.table.is-bordered.is-hoverable 9 | - @repositories.each do |repo| 10 | %tr 11 | %td= link_to repo.name, repo.url 12 | - if show_admin_menu? 13 | %td= link_to "Unwatch", repo, method: :delete, class: "button is-danger", data: { confirm: "Unwatch repository?" } 14 | -------------------------------------------------------------------------------- /config/initializers/slack_notifier.rb: -------------------------------------------------------------------------------- 1 | if ENV["SLACK_WEBHOOK_URL"] 2 | webhook_url = ENV["SLACK_WEBHOOK_URL"] 3 | elsif Rails.env.test? 4 | webhook_url = "https://example.com/notify" 5 | end 6 | 7 | if webhook_url 8 | Tokite::Engine.config.slack_notifier = Slack::Notifier.new webhook_url do 9 | if ENV["SLACK_ICON_EMOJI"] 10 | defaults username: ENV.fetch("SLACK_NAME", "tokite"), icon_emoji: ENV["SLACK_ICON_EMOJI"] 11 | else 12 | defaults username: ENV.fetch("SLACK_NAME", "tokite") 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/assets/stylesheets/tokite/application.scss: -------------------------------------------------------------------------------- 1 | @import "bulma/sass/utilities/initial-variables"; 2 | 3 | // Bootstrap-like colors 4 | $white: #fff; 5 | $black: #000; 6 | $red: #d9534f; 7 | $orange: #f0ad4e; 8 | $yellow: #ffd500; 9 | $green: #5cb85c; 10 | $blue: #0275d8; 11 | $teal: #5bc0de; 12 | $pink: #ff5b77; 13 | $purple: #613d7c; 14 | 15 | $primary: $blue; 16 | $info: $teal; 17 | $success: $green; 18 | $warning: $orange; 19 | $danger: $red; 20 | $dark: $grey-darker; 21 | $text: $grey-dark; 22 | 23 | @import "bulma/bulma"; 24 | -------------------------------------------------------------------------------- /config/initializers/omniauth.rb: -------------------------------------------------------------------------------- 1 | require "omniauth-github" 2 | 3 | Tokite::Engine.config.middleware.use OmniAuth::Builder do 4 | options = { scope: "repo,write:repo_hook" } 5 | host = ENV["GITHUB_HOST"] 6 | if host.present? 7 | options.merge!( 8 | client_options: { 9 | site: "#{host}/api/v3", 10 | authorize_url: "#{host}/login/oauth/authorize", 11 | token_url: "#{host}/login/oauth/access_token", 12 | } 13 | ) 14 | end 15 | provider :github, ENV["GITHUB_CLIENT_ID"], ENV["GITHUB_CLIENT_SECRET"], options 16 | end 17 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails gems 3 | # installed from the root of your application. 4 | 5 | ENGINE_ROOT = File.expand_path('..', __dir__) 6 | ENGINE_PATH = File.expand_path('../lib/tokite/engine', __dir__) 7 | APP_PATH = File.expand_path('../spec/dummy/config/application', __dir__) 8 | 9 | # Set up gems listed in the Gemfile. 10 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 11 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 12 | 13 | require 'rails/all' 14 | require 'rails/engine/commands' 15 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code 7 | # by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'". 8 | Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"] 9 | -------------------------------------------------------------------------------- /spec/dummy/bin/yarn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_ROOT = File.expand_path('..', __dir__) 3 | Dir.chdir(APP_ROOT) do 4 | yarn = ENV["PATH"].split(File::PATH_SEPARATOR). 5 | select { |dir| File.expand_path(dir) != __dir__ }. 6 | product(["yarn", "yarn.cmd", "yarn.ps1"]). 7 | map { |dir, file| File.expand_path(file, dir) }. 8 | find { |file| File.executable?(file) } 9 | 10 | if yarn 11 | exec yarn, *ARGV 12 | else 13 | $stderr.puts "Yarn executable was not detected in the system." 14 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" 15 | exit 1 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] ||= 'test' 2 | require File.expand_path('../dummy/config/environment', __FILE__) 3 | 4 | abort("The Rails environment is running in production mode!") if Rails.env.production? 5 | require 'spec_helper' 6 | require 'rspec/rails' 7 | require 'factory_bot_rails' 8 | 9 | Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } 10 | 11 | RSpec.configure do |config| 12 | config.use_transactional_fixtures = true 13 | config.infer_spec_type_from_file_location! 14 | 15 | config.filter_rails_from_backtrace! 16 | 17 | include Tokite::Engine.routes.url_helpers 18 | end 19 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require 'bundler/setup' 3 | rescue LoadError 4 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 5 | end 6 | 7 | require 'rdoc/task' 8 | 9 | RDoc::Task.new(:rdoc) do |rdoc| 10 | rdoc.rdoc_dir = 'rdoc' 11 | rdoc.title = 'Tokite' 12 | rdoc.options << '--line-numbers' 13 | rdoc.rdoc_files.include('README.md') 14 | rdoc.rdoc_files.include('lib/**/*.rb') 15 | end 16 | 17 | APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__) 18 | load 'rails/tasks/engine.rake' 19 | 20 | 21 | load 'rails/tasks/statistics.rake' 22 | 23 | require 'bundler/gem_tasks' 24 | -------------------------------------------------------------------------------- /schema/tokite_users.schema: -------------------------------------------------------------------------------- 1 | create_table "tokite_users", force: :cascade do |t| 2 | t.string "provider", limit: 20, null: false 3 | t.string "uid", limit: 40, null: false 4 | t.string "name", limit: 40, null: false 5 | t.string "image_url", limit: 200, null: false 6 | t.datetime "created_at", null: false 7 | t.datetime "updated_at", null: false 8 | end 9 | 10 | add_index "tokite_users", ["provider", "uid"], name: "tokite_user_uniq_provider_uid", unique: true, using: :btree 11 | -------------------------------------------------------------------------------- /spec/dummy/schema/tokite_users.schema: -------------------------------------------------------------------------------- 1 | create_table "tokite_users", force: :cascade do |t| 2 | t.string "provider", limit: 20, null: false 3 | t.string "uid", limit: 40, null: false 4 | t.string "name", limit: 40, null: false 5 | t.string "image_url", limit: 200, null: false 6 | t.datetime "created_at", null: false 7 | t.datetime "updated_at", null: false 8 | end 9 | 10 | add_index "tokite_users", ["provider", "uid"], name: "tokite_user_uniq_provider_uid", unique: true, using: :btree 11 | -------------------------------------------------------------------------------- /spec/dummy/schema/tokite/tokite_users.schema: -------------------------------------------------------------------------------- 1 | create_table "tokite_users", force: :cascade do |t| 2 | t.string "provider", limit: 20, null: false 3 | t.string "uid", limit: 40, null: false 4 | t.string "name", limit: 40, null: false 5 | t.string "image_url", limit: 200, null: false 6 | t.datetime "created_at", null: false 7 | t.datetime "updated_at", null: false 8 | end 9 | 10 | add_index "tokite_users", ["provider", "uid"], name: "tokite_user_uniq_provider_uid", unique: true, using: :btree 11 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Tokite::Engine.routes.draw do 2 | root to: "top#show" 3 | 4 | resources :hooks, only: %w(create) 5 | resources :users, only: %w(index show create edit update destroy) do 6 | resources :rules, only: %w(new create edit update destroy), shallow: true do 7 | resource :transfers, only: %w(new create) 8 | end 9 | end 10 | resources :rules, only: %w(index) 11 | resources :repositories, only: %w(index new create destroy) 12 | 13 | get "sign_in", to: "sessions#new", as: "sign_in" 14 | get "auth/github/callback", to: "sessions#create" 15 | delete "sign_out", to: "sessions#destroy", as: "sign_out" 16 | 17 | get "site/sha", to: "sha#show" 18 | end 19 | -------------------------------------------------------------------------------- /app/views/tokite/repositories/new.html.haml: -------------------------------------------------------------------------------- 1 | - if @repositories.present? 2 | = form_tag repositories_path do |f| 3 | %ul 4 | - @repositories.each do |repo| 5 | %li.field 6 | .control 7 | %label.checkbox 8 | = check_box_tag "names[]", repo.name, false, id: "names_#{sanitize_to_id(repo.name)}", multiple: true 9 | = repo.name 10 | - if repo.private? 11 | %span.tag.is-danger{style: "height: 1.5em"} private 12 | = link_to "@", repo.url 13 | 14 | .my-6 15 | 16 | .field 17 | .control 18 | = submit_tag "Import repositories", class: "button is-primary" 19 | - else 20 | No importable repositories. 21 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | # Add Yarn node_modules folder to the asset load path. 9 | Rails.application.config.assets.paths << Rails.root.join('../../node_modules') 10 | 11 | # Precompile additional assets. 12 | # application.js, application.css, and all non-JS/CSS in the app/assets 13 | # folder are already added. 14 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 15 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. JavaScript code in this file should be added after the last require_* statement. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require_tree . 14 | -------------------------------------------------------------------------------- /schema/tokite_rules.schema: -------------------------------------------------------------------------------- 1 | create_table "tokite_rules", force: :cascade do |t| 2 | t.integer "user_id", null: false 3 | t.string "name", limit: 50, null: false 4 | t.string "query", limit: 2000, null: false 5 | t.string "channel", limit: 100, null: false 6 | t.string "icon_emoji", limit: 30, null: false, default: "" 7 | t.string "additional_text", limit: 200, null: false, default: "" 8 | t.datetime "created_at", null: false 9 | t.datetime "updated_at", null: false 10 | t.string "display_name", limit: 60, null: false, default: "" 11 | end 12 | 13 | add_index "tokite_rules", ["user_id", "name"], name: "tokite_rule_uniq_name", unique: true, using: :btree 14 | -------------------------------------------------------------------------------- /app/controllers/tokite/application_controller.rb: -------------------------------------------------------------------------------- 1 | module Tokite 2 | class ApplicationController < ActionController::Base 3 | protect_from_forgery with: :exception 4 | 5 | helper_method :current_user, :current_user_token 6 | 7 | before_action :require_login 8 | 9 | private 10 | 11 | def current_user 12 | @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id] 13 | end 14 | 15 | def current_user_token 16 | current_user&.token 17 | end 18 | 19 | def require_login 20 | return if current_user 21 | redirect_to sign_in_path 22 | end 23 | 24 | def octokit_client 25 | @octokit_client ||= Octokit::Client.new(access_token: current_user_token, auto_paginate: true) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/dummy/schema/tokite_rules.schema: -------------------------------------------------------------------------------- 1 | create_table "tokite_rules", force: :cascade do |t| 2 | t.integer "user_id", null: false 3 | t.string "name", limit: 50, null: false 4 | t.string "query", limit: 2000, null: false 5 | t.string "channel", limit: 100, null: false 6 | t.string "icon_emoji", limit: 20, null: false, default: "" 7 | t.string "additional_text", limit: 200, null: false, default: "" 8 | t.datetime "created_at", null: false 9 | t.datetime "updated_at", null: false 10 | t.string "display_name", limit: 60, null: false, default: "" 11 | end 12 | 13 | add_index "tokite_rules", ["user_id", "name"], name: "tokite_rule_uniq_name", unique: true, using: :btree 14 | -------------------------------------------------------------------------------- /spec/dummy/schema/tokite/tokite_rules.schema: -------------------------------------------------------------------------------- 1 | create_table "tokite_rules", force: :cascade do |t| 2 | t.integer "user_id", null: false 3 | t.string "name", limit: 50, null: false 4 | t.string "query", limit: 2000, null: false 5 | t.string "channel", limit: 100, null: false 6 | t.string "icon_emoji", limit: 20, null: false, default: "" 7 | t.string "additional_text", limit: 200, null: false, default: "" 8 | t.datetime "created_at", null: false 9 | t.datetime "updated_at", null: false 10 | t.string "display_name", limit: 60, null: false, default: "" 11 | end 12 | 13 | add_index "tokite_rules", ["user_id", "name"], name: "tokite_rule_uniq_name", unique: true, using: :btree 14 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /spec/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative "boot" 2 | 3 | require "rails/all" 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | require "tokite" 9 | 10 | module Dummy 11 | class Application < Rails::Application 12 | # Initialize configuration defaults for originally generated Rails version. 13 | config.load_defaults 6.1 14 | 15 | # Configuration for the application, engines, and railties goes here. 16 | # 17 | # These settings can be overridden in specific environments using the files 18 | # in config/environments, which are processed later. 19 | # 20 | # config.time_zone = "Central Time (US & Canada)" 21 | # config.eager_load_paths << Rails.root.join("extras") 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/dummy/bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | include FileUtils 4 | 5 | # path to your application root. 6 | APP_ROOT = File.expand_path('..', __dir__) 7 | 8 | def system!(*args) 9 | system(*args) || abort("\n== Command #{args} failed ==") 10 | end 11 | 12 | chdir APP_ROOT do 13 | # This script is a way to update your development environment automatically. 14 | # Add necessary update steps to this file. 15 | 16 | puts '== Installing dependencies ==' 17 | system! 'gem install bundler --conservative' 18 | system('bundle check') || system!('bundle install') 19 | 20 | # Install JavaScript dependencies if using Yarn 21 | # system('bin/yarn') 22 | 23 | puts "\n== Updating database ==" 24 | system! 'bin/rails db:migrate' 25 | 26 | puts "\n== Removing old logs and tempfiles ==" 27 | system! 'bin/rails log:clear tmp:clear' 28 | 29 | puts "\n== Restarting application server ==" 30 | system! 'bin/rails restart' 31 | end 32 | -------------------------------------------------------------------------------- /app/views/layouts/tokite/application.html.haml: -------------------------------------------------------------------------------- 1 | !!! 2 | %html{lang: :en} 3 | %head 4 | %title Tokite 5 | = csrf_meta_tags 6 | 7 | = stylesheet_link_tag 'tokite/application', media: 'all' 8 | = javascript_include_tag 'tokite/application' 9 | 10 | %body 11 | - if current_user 12 | %nav.navbar.has-shadow 13 | .navbar-menu.is-active 14 | .navbar-start 15 | = nav_list_item "Top", root_path, ["top"] 16 | = nav_list_item "Users", users_path, ["users"] 17 | = nav_list_item "Rules", rules_path, ["rules"] 18 | = nav_list_item "Repositories", repositories_path, ["repositories"] 19 | .navbar-end 20 | %span.navbar-item= current_user.name_with_provider 21 | 22 | %section.section 23 | - if flash[:error] 24 | .notification.is-danger= flash[:error] 25 | - if flash[:info] 26 | .notification.is-success= flash[:info] 27 | 28 | = yield 29 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | services: 13 | postgres: 14 | image: postgres 15 | ports: 16 | - 5432:5432 17 | env: 18 | POSTGRES_DB: tokite_test 19 | POSTGRES_PASSWORD: postgres_password 20 | options: >- 21 | --health-cmd pg_isready 22 | --health-interval 10s 23 | --health-timeout 5s 24 | --health-retries 5 25 | steps: 26 | - uses: actions/checkout@v3 27 | - uses: ruby/setup-ruby@v1 28 | with: 29 | ruby-version: .ruby-version 30 | bundler-cache: true 31 | - name: Setup DB 32 | run: | 33 | RAILS_ENV=test bundle exec rails app:tokite:ridgepole:install app:tokite:ridgepole:apply 34 | - name: Run tests 35 | run: bundle exec rspec --profile 10 36 | -------------------------------------------------------------------------------- /spec/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at https://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /app/models/tokite/hook_event/issue_comment.rb: -------------------------------------------------------------------------------- 1 | module Tokite 2 | module HookEvent 3 | class IssueComment < BaseEvent 4 | def fields 5 | { 6 | event: "issue_comment", 7 | repo: hook_params[:repository][:full_name], 8 | body: hook_params[:comment][:body], 9 | user: hook_params[:comment][:user][:login], 10 | label: hook_params[:issue][:labels].map { |label| label[:name] }, 11 | } 12 | end 13 | 14 | def notify? 15 | %w(created).include?(hook_params[:action]) 16 | end 17 | 18 | def slack_text 19 | "[#{hook_params[:repository][:full_name]}] New comment by #{hook_params[:comment][:user][:login]} on issue <#{hook_params[:comment][:html_url]}|##{hook_params[:issue][:number]}: #{hook_params[:issue][:title]}>" 20 | end 21 | 22 | def slack_attachment 23 | { 24 | fallback: hook_params[:comment][:body], 25 | text: hook_params[:comment][:body], 26 | color: "good", 27 | } 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/controllers/tokite/users_controller.rb: -------------------------------------------------------------------------------- 1 | module Tokite 2 | class UsersController < ApplicationController 3 | def index 4 | @users = User.all.order(:id) 5 | end 6 | 7 | def show 8 | @user = User.find(params[:id]) 9 | end 10 | 11 | def create 12 | @user = User.create_group_user!("group name") 13 | flash[:info] = "User created." 14 | redirect_to edit_user_path(@user) 15 | end 16 | 17 | def edit 18 | @user = User.find(params[:id]) 19 | end 20 | 21 | def update 22 | @user = User.find(params[:id]) 23 | @user.update!(user_params) 24 | flash[:info] = "User updated." 25 | redirect_to user_path(@user) 26 | end 27 | 28 | def destroy 29 | @user = User.find(params[:id]) 30 | if @user.group_user? 31 | @user.destroy! 32 | flash[:info] = "User deleted." 33 | redirect_to users_path 34 | else 35 | head 400 36 | end 37 | end 38 | 39 | private 40 | 41 | def user_params 42 | params.require(:user).permit(:name) 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 hogelog 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /app/models/tokite/hook_event/pull_request_review_comment.rb: -------------------------------------------------------------------------------- 1 | module Tokite 2 | module HookEvent 3 | class PullRequestReviewComment < BaseEvent 4 | def fields 5 | { 6 | event: "pull_request_review_comment", 7 | repo: hook_params[:repository][:full_name], 8 | body: hook_params[:comment][:body], 9 | user: hook_params[:comment][:user][:login], 10 | } 11 | end 12 | 13 | def notify? 14 | hook_params[:action] == "created" 15 | end 16 | 17 | def slack_text 18 | nil 19 | end 20 | 21 | def slack_attachment 22 | user = hook_params[:comment][:user][:login] 23 | line = hook_params[:comment][:position] 24 | path = hook_params[:comment][:path] 25 | footer_url = hook_params[:comment][:html_url] 26 | footer_text = "Comment by #{user} on line #{line} of #{path}" 27 | { 28 | fallback: "#{hook_params[:comment][:body]}\n#{footer_text}", 29 | text: hook_params[:comment][:body], 30 | footer: "<#{footer_url}|#{footer_text}>" 31 | } 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/helpers/tokite/application_helper.rb: -------------------------------------------------------------------------------- 1 | module Tokite 2 | module ApplicationHelper 3 | def nav_list_item(name, path, controllers) 4 | if controllers.include?(params[:controller]) 5 | link_to(name, path, class: "navbar-item is-tab is-active") 6 | else 7 | link_to(name, path, class: "navbar-item is-tab") 8 | end 9 | end 10 | 11 | def form_text_field(form, name, options) 12 | html_class = options[:class].dup 13 | object = form.object 14 | content_tag("div", class: "field") do 15 | form.label(name, class: "label") + 16 | if object.errors[name].present? 17 | errors = object.errors[name] 18 | content_tag("p", class: "control") do 19 | form.text_field name, options.merge(class: "#{html_class} is-danger") 20 | end + 21 | content_tag("p", errors.join("\n"), class: "help is-danger") 22 | else 23 | content_tag("p", class: "control") do 24 | form.text_field name, size: 400, class: html_class 25 | end 26 | end 27 | end 28 | end 29 | 30 | def show_admin_menu? 31 | params[:admin] 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/models/tokite/hook_event/issues.rb: -------------------------------------------------------------------------------- 1 | module Tokite 2 | module HookEvent 3 | class Issues < BaseEvent 4 | def fields 5 | { 6 | event: "issues", 7 | repo: hook_params[:repository][:full_name], 8 | title: hook_params[:issue][:title], 9 | body: hook_params[:issue][:body] || "", 10 | user: hook_params[:issue][:user][:login], 11 | label: hook_params[:issue][:labels].map { |label| label[:name] }, 12 | } 13 | end 14 | 15 | def notify? 16 | %w(opened).include?(hook_params[:action]) 17 | end 18 | 19 | def slack_text 20 | "[#{hook_params[:repository][:full_name]}] Issue created by <#{hook_params[:issue][:user][:html_url]}|#{hook_params[:issue][:user][:login]}>" 21 | end 22 | 23 | def slack_attachment 24 | { 25 | title: "##{hook_params[:issue][:number]} #{hook_params[:issue][:title]}", 26 | title_link: hook_params[:issue][:html_url], 27 | fallback: "#{hook_params[:issue][:title]}\n#{hook_params[:issue][:body]}", 28 | text: hook_params[:issue][:body] || "", 29 | color: "good", 30 | } 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/dummy/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | # path to your application root. 5 | APP_ROOT = File.expand_path('..', __dir__) 6 | 7 | def system!(*args) 8 | system(*args) || abort("\n== Command #{args} failed ==") 9 | end 10 | 11 | FileUtils.chdir APP_ROOT do 12 | # This script is a way to set up or update your development environment automatically. 13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 14 | # Add necessary setup steps to this file. 15 | 16 | puts '== Installing dependencies ==' 17 | system! 'gem install bundler --conservative' 18 | system('bundle check') || system!('bundle install') 19 | 20 | # Install JavaScript dependencies 21 | # system! 'bin/yarn' 22 | 23 | # puts "\n== Copying sample files ==" 24 | # unless File.exist?('config/database.yml') 25 | # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' 26 | # end 27 | 28 | puts "\n== Preparing database ==" 29 | system! 'bin/rails db:prepare' 30 | 31 | puts "\n== Removing old logs and tempfiles ==" 32 | system! 'bin/rails log:clear tmp:clear' 33 | 34 | puts "\n== Restarting application server ==" 35 | system! 'bin/rails restart' 36 | end 37 | -------------------------------------------------------------------------------- /spec/dummy/config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket 23 | 24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /app/models/tokite/user.rb: -------------------------------------------------------------------------------- 1 | module Tokite 2 | class User < ApplicationRecord 3 | has_many :rules, dependent: :destroy 4 | 5 | has_one :secure_user_token 6 | 7 | delegate :token, to: :secure_user_token, allow_nil: true 8 | 9 | def self.login!(auth) 10 | user = find_or_initialize_by( 11 | provider: auth[:provider], 12 | uid: auth[:uid], 13 | ) 14 | if user.persisted? 15 | user.secure_user_token.update!(token: auth[:credentials][:token]) 16 | else 17 | user.assign_attributes( 18 | name: auth[:info][:name], 19 | image_url: auth[:info][:image], 20 | ) 21 | user.build_secure_user_token(token: auth[:credentials][:token]) 22 | transaction do 23 | user.save! 24 | end 25 | end 26 | user 27 | end 28 | 29 | def self.create_group_user!(name) 30 | uuid = SecureRandom.uuid.tr("-", "") 31 | create!( 32 | provider: "GROUP", 33 | uid: uuid, 34 | image_url: "", 35 | name: name 36 | ) 37 | end 38 | 39 | def group_user? 40 | provider == "GROUP" 41 | end 42 | 43 | def name_with_provider 44 | "#{name} (#{provider})" 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /app/models/tokite/repository.rb: -------------------------------------------------------------------------------- 1 | module Tokite 2 | class Repository < ApplicationRecord 3 | def self.hook!(octokit_client, github_repo) 4 | repo_name = github_repo.full_name 5 | existing_hook = find_hook(octokit_client, repo_name) 6 | if existing_hook 7 | octokit_client.edit_hook(repo_name, existing_hook.id, "web", hook_config, hook_options) 8 | else 9 | octokit_client.create_hook(repo_name, "web", hook_config, hook_options) 10 | end 11 | create!(name: repo_name, url: github_repo.html_url) 12 | end 13 | 14 | def self.find_hook(octokit_client, repo_name) 15 | octokit_client.hooks(repo_name).find {|hook| hook.config.url == hooks_url } 16 | end 17 | 18 | def self.hook_config 19 | { 20 | content_type: "json", 21 | url: hooks_url, 22 | } 23 | end 24 | 25 | def self.hook_options 26 | { 27 | events: ["*"] 28 | } 29 | end 30 | 31 | def self.hooks_url 32 | Tokite::Engine.routes.url_helpers.hooks_url 33 | end 34 | 35 | def unhook!(octokit_client) 36 | hook = self.class.find_hook(octokit_client, name) 37 | octokit_client.remove_hook(name, hook.id) if hook 38 | destroy! 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /tokite.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | 3 | # Maintain your gem's version: 4 | require "tokite/version" 5 | 6 | # Describe your gem and declare its dependencies: 7 | Gem::Specification.new do |s| 8 | s.name = "tokite" 9 | s.version = Tokite::VERSION 10 | s.authors = ["hogelog"] 11 | s.email = ["konbu.komuro@gmail.com"] 12 | s.homepage = "https://github.com/cookpad/tokite/" 13 | s.summary = "Customizable Slack notification from GitHub" 14 | s.description = "Customizable Slack notification from GitHub" 15 | s.license = "MIT" 16 | 17 | s.files = Dir["{app,config,db,lib,schema}/**/*", "MIT-LICENSE", "Rakefile", "README.md"] 18 | 19 | s.add_dependency "rails", "~> 6.1.0" 20 | s.add_dependency "pg" 21 | s.add_dependency 'sass-rails', '~> 6.0' 22 | s.add_dependency "haml" 23 | s.add_dependency "haml-rails" 24 | s.add_dependency "omniauth-github", ">= 2.0.0" 25 | s.add_dependency "omniauth-rails_csrf_protection" 26 | s.add_dependency "octokit" 27 | s.add_dependency "slack-notifier" 28 | s.add_dependency "ridgepole" 29 | s.add_dependency "parslet" 30 | 31 | s.add_development_dependency "rspec-rails", '>= 3.9.0' 32 | s.add_development_dependency "factory_bot_rails" 33 | end 34 | -------------------------------------------------------------------------------- /spec/models/tokite/hook_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Tokite::Hook, type: :model do 4 | xdescribe "fire!" do 5 | context "for debug" do 6 | before do 7 | FactoryBot.create(:rule, query: "/./", channel: "#test-private", additional_text: "@hogelog Hi!", display_name: "tokite-spec") 8 | end 9 | let(:params) { 10 | JSON.parse(payload_json("#{event}.json")).with_indifferent_access 11 | } 12 | 13 | context "with issue comment" do 14 | let(:event) { "issue_comment" } 15 | it { Tokite::Hook.fire!(event, params) } 16 | end 17 | 18 | context "with issues" do 19 | let(:event) { "issues" } 20 | it { Tokite::Hook.fire!(event, params) } 21 | end 22 | 23 | context "with pull request" do 24 | let(:event) { "pull_request" } 25 | it { Tokite::Hook.fire!(event, params) } 26 | end 27 | 28 | context "with pull request review" do 29 | let(:event) { "pull_request_review" } 30 | it { Tokite::Hook.fire!(event, params) } 31 | end 32 | 33 | context "with pull request review comment" do 34 | let(:event) { "pull_request_review_comment" } 35 | it { Tokite::Hook.fire!(event, params) } 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/dummy/config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rails secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | # Shared secrets are available across all environments. 14 | 15 | # shared: 16 | # api_key: a1B2c3D4e5F6 17 | 18 | # Environmental secrets are only available for that specific environment. 19 | 20 | development: 21 | secret_key_base: 223270007dfd9884790a106821142eac09c2a56c9e0ba1b82b053ea317438b8ac352e65c9c529737e9cdd4ffb33d5c7acbd6b52245fc64c99a0259ff2a50fc45 22 | 23 | test: 24 | secret_key_base: 8bd1a9cc6ab0bcdb6a6cebb74461859f85249b543de518489e88f82f1e0eccb54ddc4d61c67a67a9a7f467125ec5cd76f04e540883da238cc0f4e12084e82b99 25 | 26 | # Do not keep production secrets in the unencrypted secrets file. 27 | # Instead, either read values from the environment. 28 | # Or, use `bin/rails secrets:setup` to configure encrypted secrets 29 | # and move the `production:` environment over there. 30 | 31 | production: 32 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 33 | -------------------------------------------------------------------------------- /app/controllers/tokite/rules_controller.rb: -------------------------------------------------------------------------------- 1 | module Tokite 2 | class RulesController < ApplicationController 3 | def index 4 | @users = User.all.includes(:rules) 5 | end 6 | 7 | def new 8 | @rule_user = User.find(params[:user_id]) 9 | @rule = @rule_user.rules.build 10 | end 11 | 12 | def create 13 | @rule_user = User.find(params[:user_id]) 14 | @rule = @rule_user.rules.new(rule_params) 15 | if @rule.save 16 | flash[:info] = "Rule created." 17 | redirect_to user_path(@rule.user_id) 18 | else 19 | render "new" 20 | end 21 | end 22 | 23 | def edit 24 | @rule_user = nil 25 | @rule = Rule.find(params[:id]) 26 | end 27 | 28 | def update 29 | @rule = Rule.find(params[:id]) 30 | @rule.assign_attributes(rule_params) 31 | if @rule.save 32 | flash[:info] = "Rule updated." 33 | redirect_to user_path(@rule.user_id) 34 | else 35 | render "edit" 36 | end 37 | end 38 | 39 | def destroy 40 | @rule = Rule.find(params[:id]) 41 | @rule.destroy! 42 | flash[:info] = "Rule deleted." 43 | redirect_to user_path(@rule.user_id) 44 | end 45 | 46 | private 47 | 48 | def rule_params 49 | params.require(:rule).permit(:user_id, :name, :query, :channel, :icon_emoji, :additional_text, :display_name) 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy 4 | # For further information see the following documentation 5 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 6 | 7 | # Rails.application.config.content_security_policy do |policy| 8 | # policy.default_src :self, :https 9 | # policy.font_src :self, :https, :data 10 | # policy.img_src :self, :https, :data 11 | # policy.object_src :none 12 | # policy.script_src :self, :https 13 | # policy.style_src :self, :https 14 | # # If you are using webpack-dev-server then specify webpack-dev-server host 15 | # policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development? 16 | 17 | # # Specify URI for violation reports 18 | # # policy.report_uri "/csp-violation-report-endpoint" 19 | # end 20 | 21 | # If you are using UJS then enable automatic nonce generation 22 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } 23 | 24 | # Set the nonce only to specific directives 25 | # Rails.application.config.content_security_policy_nonce_directives = %w(script-src) 26 | 27 | # Report CSP violations to a specified URI 28 | # For further information see the following documentation: 29 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only 30 | # Rails.application.config.content_security_policy_report_only = true 31 | -------------------------------------------------------------------------------- /lib/tasks/tokite.rake: -------------------------------------------------------------------------------- 1 | namespace :tokite do 2 | namespace :ridgepole do 3 | def engine_path(file) 4 | Tokite::Engine.root.join(file).to_s 5 | end 6 | 7 | def app_path(file) 8 | Rails.root.join(file).to_s 9 | end 10 | 11 | def ridgepole_exec(*args) 12 | yml = Rails.root.join("config/database.yml").to_s 13 | sh "bundle", "exec", "ridgepole", "-c", yml, "-E", Rails.env, *args 14 | end 15 | 16 | desc "Apply Schemafile" 17 | task :apply do 18 | ridgepole_exec("--file", app_path("schema/Schemafile"), "-a") 19 | end 20 | 21 | desc "Apply Schemafile (dry-run)" 22 | task :"dry-run" do 23 | ridgepole_exec("--file", app_path("schema/Schemafile"), "-a", "--dry-run") 24 | end 25 | 26 | desc "Install schema" 27 | task :install do 28 | schema_dir = app_path("schema") 29 | tokite_schema_dir = app_path("schema/tokite") 30 | mkdir_p(tokite_schema_dir) unless Dir.exist?(tokite_schema_dir) 31 | 32 | Dir.glob("#{engine_path("schema")}/**/*.schema").each do |src_path| 33 | basename = File.basename(src_path) 34 | if File.exist?(File.join(tokite_schema_dir, basename)) 35 | puts "Skip install schema #{src_path}" 36 | else 37 | puts "Install schema #{src_path}" 38 | cp src_path, tokite_schema_dir 39 | cp src_path, schema_dir 40 | end 41 | end 42 | end 43 | end 44 | 45 | namespace :yarn do 46 | desc "Install yarn packages" 47 | task :install do 48 | sh "yarn", "add", "bulma" 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | # rspec-expectations config goes here. You can use an alternate 3 | # assertion/expectation library such as wrong or the stdlib/minitest 4 | # assertions if you prefer. 5 | config.expect_with :rspec do |expectations| 6 | # This option will default to `true` in RSpec 4. It makes the `description` 7 | # and `failure_message` of custom matchers include text for helper methods 8 | # defined using `chain`, e.g.: 9 | # be_bigger_than(2).and_smaller_than(4).description 10 | # # => "be bigger than 2 and smaller than 4" 11 | # ...rather than: 12 | # # => "be bigger than 2" 13 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 14 | end 15 | 16 | # rspec-mocks config goes here. You can use an alternate test double 17 | # library (such as bogus or mocha) by changing the `mock_with` option here. 18 | config.mock_with :rspec do |mocks| 19 | # Prevents you from mocking or stubbing a method that does not exist on 20 | # a real object. This is generally recommended, and will default to 21 | # `true` in RSpec 4. 22 | mocks.verify_partial_doubles = true 23 | end 24 | 25 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 26 | # have no way to turn it off -- the option exists only for backwards 27 | # compatibility in RSpec 3). It causes shared context metadata to be 28 | # inherited by the metadata hash of host groups and examples, rather than 29 | # triggering implicit auto-inclusion in groups with matching metadata. 30 | config.shared_context_metadata_behavior = :apply_to_host_groups 31 | 32 | config.filter_run_when_matching :focus 33 | config.disable_monkey_patching! 34 | 35 | def payload_json(path) 36 | File.read(File.join(__dir__, "payloads", path)) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /app/controllers/tokite/repositories_controller.rb: -------------------------------------------------------------------------------- 1 | module Tokite 2 | class RepositoriesController < ApplicationController 3 | before_action :require_github_token, only: [:new, :create, :destroy] 4 | 5 | def index 6 | @repositories = Repository.all 7 | end 8 | 9 | def new 10 | github_repos = octokit_client.repositories. 11 | select{|r| r.permissions.admin }. 12 | delete_if(&:fork). 13 | delete_if(&:archived) 14 | @repositories = github_repos.map do |repo| 15 | Repository.new(name: repo.full_name, url: repo.html_url, private: repo.private) 16 | end 17 | Repository.all.pluck(:name).each do |existing_name| 18 | @repositories.delete_if {|repo| repo.name == existing_name } 19 | end 20 | end 21 | 22 | def create 23 | if params[:names].nil? 24 | flash[:error] = "Error: No repository was selected" 25 | else 26 | github_repos = params[:names].map do |name| 27 | octokit_client.repository(name) 28 | end 29 | errors = github_repos.select(&:archived).map(&:full_name) 30 | if errors != [] 31 | flash[:error] = %(Error: The following repositories have been archived: #{errors.join(", ")}) 32 | else 33 | github_repos.each do |repo| 34 | Repository.hook!(octokit_client, repo) 35 | end 36 | flash[:info] = "Import repositories." 37 | end 38 | end 39 | redirect_to repositories_path 40 | end 41 | 42 | def destroy 43 | repo = Repository.find(params[:id]) 44 | repo.unhook!(octokit_client) 45 | flash[:info] = "Unhook repository #{repo.name}" 46 | redirect_to repositories_path 47 | end 48 | 49 | private 50 | 51 | def require_github_token 52 | redirect_to repositories_path unless current_user_token 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /app/models/tokite/hook_event/pull_request_review.rb: -------------------------------------------------------------------------------- 1 | module Tokite 2 | module HookEvent 3 | class PullRequestReview < BaseEvent 4 | def fields 5 | { 6 | event: "pull_request_review", 7 | repo: hook_params[:repository][:full_name], 8 | body: hook_params[:review][:body] || "", 9 | user: hook_params[:review][:user][:login], 10 | review_state: hook_params[:review][:state], 11 | } 12 | end 13 | 14 | def notify? 15 | if hook_params[:action] != "submitted" 16 | false 17 | elsif hook_params[:review][:state] == "commented" 18 | hook_params[:review][:body] != nil 19 | else 20 | true 21 | end 22 | end 23 | 24 | def slack_text 25 | repo = "<#{hook_params[:repository][:html_url]}|[#{hook_params[:repository][:full_name]}]>" 26 | user = "<#{hook_params[:review][:user][:html_url]}|#{hook_params[:review][:user][:login]}>" 27 | title = "<#{hook_params[:pull_request][:html_url]}|##{hook_params[:pull_request][:number]} #{hook_params[:pull_request][:title]}>" 28 | case hook_params[:review][:state] 29 | when "commented" 30 | "#{repo} New comment by #{user} on pull request #{title}" 31 | when "approved" 32 | "#{repo} #{user} approved #{title}" 33 | when "changes_requested" 34 | "#{repo} #{user} requested changes #{title}" 35 | end 36 | end 37 | 38 | def slack_attachment 39 | case hook_params[:review][:state] 40 | when "commented" 41 | when "approved" 42 | color = "good" 43 | when "changes_requested" 44 | color = "warning" 45 | end 46 | { 47 | fallback: hook_params[:review][:body] || "", 48 | text: hook_params[:review][:body] || "", 49 | color: color, 50 | } 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /app/models/tokite/rule.rb: -------------------------------------------------------------------------------- 1 | module Tokite 2 | class Rule < ApplicationRecord 3 | attr_reader :search_query 4 | 5 | belongs_to :user 6 | 7 | validates :name, presence: true 8 | validate :validate_query 9 | validate :validate_user_id 10 | validate :validate_channel 11 | 12 | before_validation :normalize_channel 13 | 14 | INVALID_CHANNEL_CHARS = [" ", ","] 15 | 16 | # TODO: Performance 17 | def self.matched_rules(event) 18 | Rule.all.to_a.select do |rule| 19 | rule.match?(event) 20 | end 21 | end 22 | 23 | def search_query 24 | @search_query ||= SearchQuery.new(query) 25 | end 26 | 27 | def match?(event) 28 | search_query.match?(event.fields) 29 | end 30 | 31 | def slack_attachment_fallback 32 | "#{name} by #{user.name}" 33 | end 34 | 35 | def slack_attachment_text 36 | "#{rule_name_link} (#{user_link}) " 37 | end 38 | 39 | def rule_name_link 40 | "<#{Tokite::Engine.routes.url_helpers.edit_rule_url(self)}|#{name}>" 41 | end 42 | 43 | def user_link 44 | "<#{Tokite::Engine.routes.url_helpers.user_url(user)}|#{user.name}>" 45 | end 46 | 47 | private 48 | 49 | def validate_query 50 | SearchQuery.validate(query) 51 | rescue SearchQuery::QueryError => e 52 | errors.add(:query, e.message) 53 | end 54 | 55 | def validate_user_id 56 | unless User.find_by(id: user_id) 57 | errors.add(:user_id, "Unknown user_id: #{user_id}") 58 | end 59 | end 60 | 61 | def validate_channel 62 | INVALID_CHANNEL_CHARS.each do |invalid_char| 63 | errors.add(:channel, %(Invalid character: "#{invalid_char}")) if channel.index(invalid_char) 64 | end 65 | end 66 | 67 | def normalize_channel 68 | if channel.start_with?("#") 69 | self.channel = channel.strip 70 | else 71 | self.channel = "##{channel.strip}" 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /spec/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /spec/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /app/models/tokite/hook_event/pull_request.rb: -------------------------------------------------------------------------------- 1 | module Tokite 2 | module HookEvent 3 | class PullRequest < BaseEvent 4 | def fields 5 | { 6 | event: "pull_request", 7 | repo: hook_params[:repository][:full_name], 8 | title: hook_params[:pull_request][:title], 9 | body: hook_params[:pull_request][:body], 10 | user: hook_params[:pull_request][:user][:login], 11 | label: hook_params[:pull_request][:labels].map { |label| label[:name] }, 12 | requested_reviewer: hook_params[:requested_reviewer]&.[](:login) || hook_params[:pull_request][:requested_reviewers].map { |reviewer| reviewer[:login] }, 13 | requested_team: hook_params[:pull_request][:requested_teams].map { |team| parse_team_name(team) }, 14 | } 15 | end 16 | 17 | def parse_team_name(team) 18 | html_url = team[:html_url] 19 | if /\/orgs\/(?[^\s\/]+)\/teams\/(?[^\s\/]+)\z/ =~ html_url 20 | org_name + "/" + team_name 21 | else 22 | team[:slug] || team[:name] 23 | end 24 | end 25 | 26 | def notify? 27 | %w(opened review_requested).include?(hook_params[:action]) 28 | end 29 | 30 | def slack_text 31 | case hook_params[:action] 32 | when 'opened' 33 | "[#{hook_params[:repository][:full_name]}] Pull request submitted by <#{hook_params[:pull_request][:user][:html_url]}|#{hook_params[:pull_request][:user][:login]}>" 34 | when 'review_requested' 35 | "[#{hook_params[:repository][:full_name]}] Pull request review requested by <#{hook_params[:pull_request][:user][:html_url]}|#{hook_params[:pull_request][:user][:login]}>" 36 | end 37 | end 38 | 39 | def slack_attachment 40 | { 41 | title: "##{hook_params[:pull_request][:number]} #{hook_params[:pull_request][:title]}", 42 | title_link: hook_params[:pull_request][:html_url], 43 | fallback: "#{hook_params[:pull_request][:title]}\n#{hook_params[:pull_request][:body]}", 44 | text: hook_params[:pull_request][:body] || "No description provided.", 45 | color: "good", 46 | } 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /app/models/tokite/hook.rb: -------------------------------------------------------------------------------- 1 | module Tokite 2 | class Hook 3 | attr_reader :event 4 | 5 | HOOK_EVENTS = { 6 | "pull_request" => HookEvent::PullRequest, 7 | "issues" => HookEvent::Issues, 8 | "issue_comment" => HookEvent::IssueComment, 9 | "pull_request_review" => HookEvent::PullRequestReview, 10 | "pull_request_review_comment" => HookEvent::PullRequestReviewComment, 11 | }.freeze 12 | 13 | def self.fire!(github_event, hook_params) 14 | event_class = HOOK_EVENTS[github_event] 15 | Hook.new(event_class.new(hook_params)).fire! if event_class 16 | end 17 | 18 | def initialize(event) 19 | @event = event 20 | end 21 | 22 | def fire! 23 | return unless event.notify? 24 | payloads = [] 25 | Rule.matched_rules(event).each do |rule| 26 | attachment = event.slack_attachment 27 | attachment[:fallback] += "\n\n#{rule.slack_attachment_fallback}" 28 | attachment[:text] += "\n\n#{rule.slack_attachment_text}" 29 | emoji = rule.icon_emoji.chomp.presence 30 | additional_text = rule.additional_text 31 | 32 | if payloads.none? {|payload| payload[:channel] == rule.channel && payload[:emoji] == emoji && payload[:additional_text] == additional_text } 33 | payloads << { 34 | channel: rule.channel, 35 | username: rule.display_name.presence || "tokite", 36 | text: event.slack_text, 37 | emoji: emoji, 38 | additional_text: additional_text, 39 | attachments: [attachment], 40 | } 41 | end 42 | end 43 | payloads.each do |payload| 44 | notify!( 45 | channel: payload[:channel], 46 | text: payload[:text], 47 | icon_emoji: payload[:emoji], 48 | attachments: payload[:attachments], 49 | username: payload[:username] 50 | ) 51 | if payload[:additional_text].present? 52 | notify!( 53 | channel: payload[:channel], 54 | text: payload[:additional_text], 55 | icon_emoji: payload[:emoji], 56 | parse: "full", 57 | username: payload[:username] 58 | ) 59 | end 60 | end 61 | end 62 | 63 | def notify!(payload) 64 | NotifyGithubHookEventJob.perform_now(payload.compact) 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/dummy/config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers: a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum; this matches the default thread size of Active Record. 6 | # 7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 8 | threads threads_count, threads_count 9 | 10 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 11 | # 12 | port ENV.fetch("PORT") { 3000 } 13 | 14 | # Specifies the `environment` that Puma will run in. 15 | # 16 | environment ENV.fetch("RAILS_ENV") { "development" } 17 | 18 | # Specifies the number of `workers` to boot in clustered mode. 19 | # Workers are forked webserver processes. If using threads and workers together 20 | # the concurrency of the application would be max `threads` * `workers`. 21 | # Workers do not work on JRuby or Windows (both of which do not support 22 | # processes). 23 | # 24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 25 | 26 | # Use the `preload_app!` method when specifying a `workers` number. 27 | # This directive tells Puma to first boot the application and load code 28 | # before forking the application. This takes advantage of Copy On Write 29 | # process behavior so workers use less memory. If you use this option 30 | # you need to make sure to reconnect any threads in the `on_worker_boot` 31 | # block. 32 | # 33 | # preload_app! 34 | 35 | # If you are preloading your application and using Active Record, it's 36 | # recommended that you close any connections to the database before workers 37 | # are forked to prevent connection leakage. 38 | # 39 | # before_fork do 40 | # ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord) 41 | # end 42 | 43 | # The code in the `on_worker_boot` will be called if you are using 44 | # clustered mode by specifying a number of `workers`. After each worker 45 | # process is booted, this block will be run. If you are using the `preload_app!` 46 | # option, you will want to use this block to reconnect to any threads 47 | # or connections that may have been created at application boot, as Ruby 48 | # cannot share connections between processes. 49 | # 50 | # on_worker_boot do 51 | # ActiveRecord::Base.establish_connection if defined?(ActiveRecord) 52 | # end 53 | # 54 | 55 | # Allow puma to be restarted by `rails restart` command. 56 | plugin :tmp_restart 57 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | # The test environment is used exclusively to run your application's 4 | # test suite. You never need to work with it otherwise. Remember that 5 | # your test database is "scratch space" for the test suite and is wiped 6 | # and recreated between test runs. Don't rely on the data there! 7 | 8 | Rails.application.configure do 9 | # Settings specified here will take precedence over those in config/application.rb. 10 | 11 | config.cache_classes = true 12 | 13 | # Do not eager load code on boot. This avoids loading your whole application 14 | # just for the purpose of running a single test. If you are using a tool that 15 | # preloads Rails for running tests, you may have to set it to true. 16 | config.eager_load = false 17 | 18 | # Configure public file server for tests with Cache-Control for performance. 19 | config.public_file_server.enabled = true 20 | config.public_file_server.headers = { 21 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}" 22 | } 23 | 24 | # Show full error reports and disable caching. 25 | config.consider_all_requests_local = true 26 | config.action_controller.perform_caching = false 27 | config.cache_store = :null_store 28 | 29 | # Raise exceptions instead of rendering exception templates. 30 | config.action_dispatch.show_exceptions = false 31 | 32 | # Disable request forgery protection in test environment. 33 | config.action_controller.allow_forgery_protection = false 34 | 35 | # Store uploaded files on the local file system in a temporary directory. 36 | config.active_storage.service = :test 37 | 38 | config.action_mailer.perform_caching = false 39 | 40 | # Tell Action Mailer not to deliver emails to the real world. 41 | # The :test delivery method accumulates sent emails in the 42 | # ActionMailer::Base.deliveries array. 43 | config.action_mailer.delivery_method = :test 44 | 45 | # Print deprecation notices to the stderr. 46 | config.active_support.deprecation = :stderr 47 | 48 | # Raise exceptions for disallowed deprecations. 49 | config.active_support.disallowed_deprecation = :raise 50 | 51 | # Tell Active Support which deprecation messages to disallow. 52 | config.active_support.disallowed_deprecation_warnings = [] 53 | 54 | # Raises error for missing translations. 55 | # config.i18n.raise_on_missing_translations = true 56 | 57 | # Annotate rendered view with file names. 58 | # config.action_view.annotate_rendered_view_with_filenames = true 59 | end 60 | -------------------------------------------------------------------------------- /app/views/tokite/rules/_form.html.haml: -------------------------------------------------------------------------------- 1 | = form_for [@rule_user, @rule] do |f| 2 | = form_text_field f, :name, size: 40, class: "input" 3 | = form_text_field f, :query, size: 400, class: "input" 4 | = form_text_field f, :channel, size: 40, class: "input" 5 | = form_text_field f, :icon_emoji, size: 40, class: "input" 6 | = form_text_field f, :additional_text, size: 40, class: "input" 7 | = form_text_field f, :display_name, size: 40, class: "input" 8 | .field.columns 9 | .column.is-8= f.submit "Update", class: "button is-primary" 10 | - if @rule.persisted? 11 | .column.is-2= link_to "Transfer", new_rule_transfers_path(@rule), class: "button is-primary" 12 | .column.is-2= link_to "Delete", rule_path(@rule), method: :delete, data: { confirm: %(Delete "#{@rule.name}"?) }, class: "button is-danger" 13 | 14 | .message.my-6 15 | .message-header 16 | Query description 17 | .message-body 18 | %h2.title Supported query type 19 | %table.table.is-bordered 20 | %tr 21 | %th Name 22 | %th Example 23 | %tr 24 | %td Plain word 25 | %td hoge fuga moge 26 | %tr 27 | %td Quoted word 28 | %td "hoge fuga moge" 29 | %tr 30 | %td Regular expression word 31 | %td /(hoge|fuga|moge)/ 32 | %tr 33 | %td Exclude word 34 | %td -/(hoge|fuga|moge)/ -user:hogelog 35 | 36 | %h2 Supported query field 37 | %table.table.is-bordered 38 | %tr 39 | %th Field 40 | %th Description 41 | %th Example 42 | %tr 43 | %td repo: 44 | %td Match repository name. 45 | %td repo:cookpad/tokite 46 | %tr 47 | %td title: 48 | %td Match pull_request or issues title. 49 | %td title:Bug 50 | %tr 51 | %td event: 52 | %td Match event type pull_request, issues, issue_comment, pull_request_review, pull_request_review_comment. 53 | %td event:/pull_request|issues|pull_request_review|pull_request_review_comment/ 54 | %tr 55 | %td body: 56 | %td Match body text. 57 | %td body:"review please" 58 | %tr 59 | %td user: 60 | %td Match user name. 61 | %td user:hogelog 62 | %tr 63 | %td label: 64 | %td Match pull_request or issue label. 65 | %td label:Feature 66 | %tr 67 | %td review_state: 68 | %td Match pull_request_review state. 69 | %td review_state:/commented|approved|changes_requested/ 70 | %tr 71 | %td requested_reviewer: 72 | %td Match user name of review requested reviewer. 73 | %td requested_reviewer:hogelog 74 | %tr 75 | %td requested_team: 76 | %td Match team name of review requested team. 77 | %td requested_team:cookpad/chef 78 | %tr 79 | %td unspecified 80 | %td Match title or body field. 81 | %td review please 82 | -------------------------------------------------------------------------------- /app/models/tokite/search_query.rb: -------------------------------------------------------------------------------- 1 | require "parslet" 2 | 3 | module Tokite 4 | class SearchQuery 5 | attr_reader :query, :tree 6 | 7 | DEFAULT_FIELDS = %i(title body) 8 | 9 | class QueryError < StandardError 10 | end 11 | class QueryParseError < QueryError 12 | end 13 | class QueryRegexpError < QueryError 14 | end 15 | 16 | class Parser < Parslet::Parser 17 | rule(:space) { match('\s').repeat(1) } 18 | rule(:space?) { space.maybe } 19 | 20 | rule(:quot) { str('"') } 21 | rule(:quoted_char) { (str('\\').ignore >> any) | match('[^"]') } 22 | rule(:char) { match('[^\s]') } 23 | rule(:slash) { str('/') } 24 | rule(:regexp_char) { (str('\\').ignore >> slash) | match('[^/]') } 25 | 26 | rule(:regexp_word) { slash >> regexp_char.repeat(1).as(:regexp_word) >> slash } 27 | rule(:quot_word) { quot >> quoted_char.repeat(1).as(:word) >> quot } 28 | rule(:plain_word) { (match('[^\s/"]') >> match('[^\s]').repeat).as(:word) } 29 | rule(:exclude) { match('-').as(:exclude) } 30 | rule(:field) { match('\w').repeat(1).as(:field) } 31 | rule(:word) { exclude.maybe >> (field >> str(':') >> space?).maybe >> (regexp_word | quot_word | plain_word) } 32 | 33 | rule(:query) { space? >> word >> (space >> word).repeat >> space? } 34 | root :query 35 | end 36 | 37 | def self.parser 38 | @parser ||= Tokite::SearchQuery::Parser.new 39 | end 40 | 41 | def self.parse(query) 42 | Array.wrap(parser.parse(query)) 43 | rescue Parslet::ParseFailed => e 44 | raise QueryParseError, e 45 | end 46 | 47 | def self.validate(query) 48 | tree = SearchQuery.parse(query) 49 | tree.each do |word| 50 | Regexp.compile(word[:regexp_word].to_s, Regexp::IGNORECASE) if word[:regexp_word] 51 | end 52 | rescue RegexpError => e 53 | raise QueryRegexpError, e 54 | end 55 | 56 | def initialize(query) 57 | @query = query 58 | @tree = Array.wrap(self.class.parse(query)) 59 | end 60 | 61 | def match?(doc) 62 | tree.all? do |word| 63 | field = word[:field] 64 | if field 65 | targets = 66 | case doc[field.to_sym] 67 | when Array 68 | doc[field.to_sym].map(&:downcase) 69 | when nil 70 | [] 71 | else 72 | [doc[field.to_sym].downcase] 73 | end 74 | else 75 | targets = DEFAULT_FIELDS.map{|field| doc[field]&.downcase }.compact 76 | end 77 | if word[:regexp_word] 78 | begin 79 | regexp = Regexp.compile(word[:regexp_word].to_s, Regexp::IGNORECASE) 80 | matched = targets.any?{|text| regexp.match?(text) } 81 | rescue RegexpError 82 | matched = false 83 | end 84 | else 85 | value = word[:word].to_s.downcase 86 | matched = targets.any?{|text| text.index(value) } 87 | end 88 | word[:exclude].present? ? !matched : matched 89 | end 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # In the development environment your application's code is reloaded any time 7 | # it changes. This slows down response time but is perfect for development 8 | # since you don't have to restart the web server when you make code changes. 9 | config.cache_classes = false 10 | 11 | # Do not eager load code on boot. 12 | config.eager_load = false 13 | 14 | # Show full error reports. 15 | config.consider_all_requests_local = true 16 | 17 | # Enable/disable caching. By default caching is disabled. 18 | # Run rails dev:cache to toggle caching. 19 | if Rails.root.join('tmp', 'caching-dev.txt').exist? 20 | config.action_controller.perform_caching = true 21 | config.action_controller.enable_fragment_cache_logging = true 22 | 23 | config.cache_store = :memory_store 24 | config.public_file_server.headers = { 25 | 'Cache-Control' => "public, max-age=#{2.days.to_i}" 26 | } 27 | else 28 | config.action_controller.perform_caching = false 29 | 30 | config.cache_store = :null_store 31 | end 32 | 33 | # Store uploaded files on the local file system (see config/storage.yml for options). 34 | config.active_storage.service = :local 35 | 36 | # Don't care if the mailer can't send. 37 | config.action_mailer.raise_delivery_errors = false 38 | 39 | config.action_mailer.perform_caching = false 40 | 41 | # Print deprecation notices to the Rails logger. 42 | config.active_support.deprecation = :log 43 | 44 | # Raise exceptions for disallowed deprecations. 45 | config.active_support.disallowed_deprecation = :raise 46 | 47 | # Tell Active Support which deprecation messages to disallow. 48 | config.active_support.disallowed_deprecation_warnings = [] 49 | 50 | # Raise an error on page load if there are pending migrations. 51 | config.active_record.migration_error = :page_load 52 | 53 | # Highlight code that triggered database queries in logs. 54 | config.active_record.verbose_query_logs = true 55 | 56 | # Debug mode disables concatenation and preprocessing of assets. 57 | # This option may cause significant delays in view rendering with a large 58 | # number of complex assets. 59 | config.assets.debug = true 60 | 61 | # Suppress logger output for asset requests. 62 | config.assets.quiet = true 63 | 64 | # Raises error for missing translations. 65 | # config.i18n.raise_on_missing_translations = true 66 | 67 | # Annotate rendered view with file names. 68 | # config.action_view.annotate_rendered_view_with_filenames = true 69 | 70 | # Use an evented file watcher to asynchronously detect changes in source code, 71 | # routes, locales, etc. This feature depends on the listen gem. 72 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 73 | 74 | # Uncomment if you wish to allow Action Cable access from any origin. 75 | # config.action_cable.disable_request_forgery_protection = true 76 | end 77 | -------------------------------------------------------------------------------- /spec/models/tokite/search_query_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Tokite::SearchQuery, type: :model do 4 | describe ".parse" do 5 | subject { Tokite::SearchQuery.parse(query) } 6 | 7 | context "spaced words" do 8 | let(:query) { 'foo bar' } 9 | 10 | it do 11 | expect(subject.size).to eq(2) 12 | expect(subject[0][:word].to_s).to eq('foo') 13 | expect(subject[1][:word].to_s).to eq('bar') 14 | end 15 | end 16 | 17 | context "quoted text" do 18 | let(:query) { '"foo bar"' } 19 | 20 | it do 21 | expect(subject.size).to eq(1) 22 | expect(subject.first[:word].to_s).to eq('foo bar') 23 | end 24 | end 25 | 26 | context "quoted text with escaped quot" do 27 | let(:query) { '"foo\ \"bar\""' } 28 | 29 | it do 30 | expect(subject.size).to eq(1) 31 | expect(subject.first[:word].to_s).to eq('foo "bar"') 32 | end 33 | end 34 | 35 | context "regular expression" do 36 | let(:query) { '/foo bar/' } 37 | 38 | it do 39 | expect(subject.size).to eq(1) 40 | expect(subject.first[:regexp_word].to_s).to eq('foo bar') 41 | end 42 | end 43 | 44 | context "regular expression with escaped character" do 45 | let(:query) { '/\Afoo \/bar\/\z/' } 46 | 47 | it do 48 | expect(subject.size).to eq(1) 49 | expect(subject.first[:regexp_word].to_s).to eq('\Afoo /bar/\z') 50 | end 51 | end 52 | 53 | context "with field" do 54 | let(:query) { 'foo: bar' } 55 | 56 | it do 57 | expect(subject.size).to eq(1) 58 | expect(subject.first[:field].to_s).to eq('foo') 59 | expect(subject.first[:word].to_s).to eq('bar') 60 | end 61 | end 62 | 63 | context "with -word" do 64 | let(:query) { '-foo:bar' } 65 | 66 | it do 67 | expect(subject.size).to eq(1) 68 | expect(subject.first[:field].to_s).to eq('foo') 69 | expect(subject.first[:word].to_s).to eq('bar') 70 | expect(subject.first[:exclude]).to be_present 71 | end 72 | end 73 | 74 | context "with prefix and suffix space" do 75 | let(:query) { ' bar ' } 76 | 77 | it do 78 | expect(subject.size).to eq(1) 79 | expect(subject.first[:word].to_s).to eq('bar') 80 | end 81 | end 82 | 83 | context "with some words" do 84 | let(:query) { ' /./ -foo:"hoge fuga" ' } 85 | 86 | it do 87 | expect(subject.size).to eq(2) 88 | expect(subject[0][:field]).not_to be_present 89 | expect(subject[0][:regexp_word].to_s).to eq('.') 90 | expect(subject[0][:word]).not_to be_present 91 | expect(subject[0][:exclude]).not_to be_present 92 | expect(subject[1][:field].to_s).to eq("foo") 93 | expect(subject[1][:regexp_word]).not_to be_present 94 | expect(subject[1][:word].to_s).to eq("hoge fuga") 95 | expect(subject[1][:exclude]).to be_present 96 | end 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.7.1] - 2023-06-15 11 | 12 | ### Changed 13 | 14 | - Upgrade omniauth-github to v2 (https://github.com/cookpad/tokite/pull/66). 15 | - Please add omniauth-rails_csrf_protection gem to your Gemfile. 16 | - Use Ruby 3.1 for testing (https://github.com/cookpad/tokite/pull/67). 17 | 18 | ## [0.7.0] - 2023-06-14 19 | 20 | ### Changed 21 | 22 | - **BREAKING**: Upgrade to Rails 6.1 (https://github.com/cookpad/tokite/pull/64). 23 | 24 | ## [0.6.0] - 2023-06-14 25 | 26 | ### Changed 27 | 28 | - **BREAKING**: Upgrade to Rails 6.0 (https://github.com/cookpad/tokite/pull/61). 29 | 30 | ## [0.5.1] - 2023-06-14 31 | 32 | ### Changed 33 | 34 | - Upgrade bulma to 0.9.4 (https://github.com/cookpad/tokite/pull/59). 35 | 36 | ## [0.5.0] - 2023-06-14 37 | 38 | We have not released a new version of tokite gem for a long time. Be careful when upgrading to this version from previous versions. 39 | 40 | ### Added 41 | 42 | - **BREAKING**: Add requested_reviewer and requested_team to query field names (https://github.com/cookpad/tokite/pull/53). 43 | - If you feel these notification are too noisy, please report an issue. 44 | - Add badges for private repositories (https://github.com/cookpad/tokite/pull/41). 45 | - Add input form for editting display name in Slack channel (https://github.com/cookpad/tokite/pull/40). 46 | - Support label query (https://github.com/cookpad/tokite/pull/51). 47 | - Use issue labels info also for issue_comment event (https://github.com/cookpad/tokite/pull/56). 48 | 49 | ### Changed 50 | 51 | - **BREAKING**: Upgrade to Rails 5.2 (https://github.com/cookpad/tokite/pull/57). 52 | - Loose length limit of icon_emoji (https://github.com/cookpad/tokite/pull/46). 53 | - Upgrade rspec version for development (https://github.com/cookpad/tokite/pull/47). 54 | - Use Ruby 2.7 (https://github.com/cookpad/tokite/pull/52). 55 | - Use GitHub Actions for CI (https://github.com/cookpad/tokite/pull/54). 56 | 57 | ### Fixed 58 | 59 | - Fix to treat nil text body in pull_request_review hook (https://github.com/cookpad/tokite/pull/38). 60 | - Fix to check if a repository is archived or not (https://github.com/cookpad/tokite/pull/39). 61 | - Fix to show error message when no repository is selected in `/repositories/new` page (https://github.com/cookpad/tokite/pull/42). 62 | - Fix an error occurred when a query contains wrong regular expressions (https://github.com/cookpad/tokite/pull/43). 63 | 64 | 65 | [Unreleased]: https://github.com/cookpad/tokite/compare/v0.7.1...HEAD 66 | [0.7.1]: https://github.com/cookpad/tokite/compare/v0.7.0...v0.7.1 67 | [0.7.0]: https://github.com/cookpad/tokite/compare/v0.6.0...v0.7.0 68 | [0.6.0]: https://github.com/cookpad/tokite/compare/v0.5.1...v0.6.0 69 | [0.5.1]: https://github.com/cookpad/tokite/compare/v0.5.0...v0.5.1 70 | [0.5.0]: https://github.com/cookpad/tokite/compare/v0.4.1...v0.5.0 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tokite [![Gem Version](https://badge.fury.io/rb/tokite.svg)](https://badge.fury.io/rb/tokite) 2 | 3 | Tokite send GitHub event (pull-request, issue and comment) to Slack. 4 | 5 | Notification setting are personalized and customizable by query. 6 | 7 | ## Installation 8 | Tokite works as rails mountable engine. 9 | 10 | Add this line to your rails application's Gemfile: 11 | ```ruby 12 | gem "tokite" 13 | ``` 14 | 15 | And mount engine. 16 | 17 | ```ruby 18 | Rails.application.routes.draw do 19 | mount Tokite::Engine => "/" 20 | end 21 | ``` 22 | 23 | ### Setup database 24 | ```console 25 | $ ./bin/rails db:create 26 | $ ./bin/rails app:tokite:ridgepole:install 27 | $ ./bin/rails app:tokite:ridgepole:apply 28 | $ RAILS_ENV=test ./bin/rails app:tokite:ridgepole:apply 29 | ``` 30 | 31 | ### Setup yarn pkg 32 | ```console 33 | $ ./bin/rails tokite:yarn:install 34 | ``` 35 | 36 | ## Configuration 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
GITHUB_CLIENT_IDGitHub OAuth2 client ID
GITHUB_CLIENT_SECRETGitHub OAuth2 client secret
GITHUB_HOST (optional)GitHub Enterprise host
SECRET_KEY_BASErails secret key
SLACK_WEBHOOK_URLSlack incoming webhook url
SLACK_NAME (optional)Slack notification user name
SLACK_ICON_EMOJI (optional)Slack notification icon
APP_HOST (optional)Application host url
47 | 48 | ## Usage 49 | ### Supported Event 50 | 51 | Tokite support only below events now. 52 | 53 | - pull_request 54 | - issues 55 | - issue_comment 56 | 57 | ### Supported query type 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
NameExample
Plain wordhoge fuga moge
Quoted word"hoge fuga moge"
Regular expression word/hoge|fuga|moge/
Exclude word -/(hoge|fuga|moge)/ -user:hogelog
66 | 67 | ### Supported query field 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 |
NameDescriptionExample
repo:Match repository name.repo:cookpad/tokite
title:Match pull_request or issues title.title:Bug
event:Match event type pull_request, issues, issue_comment, pull_request_review, pull_request_review_comment.event:/pull_request|issues|pull_request_review|pull_request_review_comment/
body:Match body text.body:"review please"
user:Match user name.user:hogelog
label:Match pull_request or issue label.label:Feature
review_state:Match pull_request_review state.review_state:/commented|approved|changes_requested/
requested_reviewer:Match user name of review requested reviewerrequested_reviewer:hogelog
requested_team:Match team name of review requested teamrequested_team:cookpad/chef
unspecifiedMatch title or body field.review please
82 | -------------------------------------------------------------------------------- /spec/models/tokite/rule_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Tokite::Rule, type: :model do 4 | let(:hook_payload) { JSON.parse(payload_json("pull_request.json")).with_indifferent_access } 5 | let(:hook_event) { Tokite::HookEvent::PullRequest.new(hook_payload) } 6 | 7 | describe ".matched_rules" do 8 | before do 9 | FactoryBot.create(:rule, query: "foo") 10 | FactoryBot.create(:rule, query: "(?:foo|bar)") 11 | FactoryBot.create(:rule, query: "/(?:foo|bar)/") 12 | FactoryBot.create(:rule, query: "bar") 13 | hook_event.hook_params[:pull_request][:title] = "This is foo." 14 | end 15 | 16 | it "returns only matched rules" do 17 | rules = Tokite::Rule.matched_rules(hook_event) 18 | expect(rules.size).to eq(2) 19 | end 20 | end 21 | 22 | describe "#match?" do 23 | let(:title) { "title" } 24 | let(:body) { "body" } 25 | let(:user_login) { "hogelog" } 26 | let(:rule) { FactoryBot.create(:rule, query: query) } 27 | subject { rule.match?(hook_event) } 28 | before do 29 | hook_event.hook_params[:pull_request][:title] = title 30 | hook_event.hook_params[:pull_request][:body] = body 31 | hook_event.hook_params[:pull_request][:user][:login] = user_login 32 | end 33 | 34 | context "with matched single word" do 35 | let(:query) { "title" } 36 | it { is_expected.to eq(true) } 37 | end 38 | 39 | context "with unmatched single word" do 40 | let(:query) { "foobar" } 41 | it { is_expected.to eq(false) } 42 | end 43 | 44 | context "with matched multiple words" do 45 | let(:query) { "title body" } 46 | it { is_expected.to eq(true) } 47 | end 48 | 49 | context "with unmatched multiple words" do 50 | let(:query) { "title body foobar" } 51 | it { is_expected.to eq(false) } 52 | end 53 | 54 | context "with unlabeled word" do 55 | let(:query) { "hogelog" } 56 | it { is_expected.not_to eq(true) } 57 | end 58 | 59 | context "with matched labeled word" do 60 | let(:query) { "title:title body:body user:hogelog" } 61 | it { is_expected.to eq(true) } 62 | end 63 | 64 | context "with unmatched labeled word" do 65 | let(:query) { "title:body" } 66 | it { is_expected.to eq(false) } 67 | end 68 | 69 | context "with matched quoted word" do 70 | let(:query) { %("title title") } 71 | before do 72 | hook_event.hook_params[:pull_request][:title] = %(title title) 73 | end 74 | it { is_expected.to eq(true) } 75 | end 76 | 77 | context "with unmatched quoted word" do 78 | let(:query) { %("title title") } 79 | it { is_expected.to eq(false) } 80 | end 81 | 82 | context "with case unmatched word" do 83 | let(:query) { "TITLE" } 84 | it { is_expected.to eq(true) } 85 | end 86 | 87 | context "with case unmatched regular expression" do 88 | let(:query) { '/\ATITLE\z/' } 89 | it { is_expected.to eq(true) } 90 | end 91 | 92 | context "with backslash regular expression" do 93 | let(:query) { 'body:/\AThis is \w+.\z/' } 94 | let(:body) { "This is body." } 95 | it { is_expected.to eq(true) } 96 | end 97 | 98 | context "with matched exclude word" do 99 | let(:query) { 'body:/\AThis is \w+.\z/ -user:/hogelog/' } 100 | let(:body) { "This is body." } 101 | it { is_expected.to eq(false) } 102 | end 103 | 104 | context "with unmatched exclude word" do 105 | let(:query) { 'body:/\AThis is \w+.\z/ -user:fugalog' } 106 | let(:body) { "This is body." } 107 | it { is_expected.to eq(true) } 108 | end 109 | end 110 | 111 | describe "validates :query" do 112 | let(:query) { "foo bar" } 113 | let(:rule) { FactoryBot.build(:rule, query: query) } 114 | 115 | it { expect(rule).to be_valid } 116 | 117 | context "with unclosed quoted text" do 118 | let(:query) { 'foobar "foo bar' } 119 | 120 | it { expect(rule).not_to be_valid } 121 | end 122 | 123 | context "with unclosed regular expression" do 124 | let(:query) { 'foobar /foo bar' } 125 | 126 | it { expect(rule).not_to be_valid } 127 | end 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # Code is not reloaded between requests. 7 | config.cache_classes = true 8 | 9 | # Eager load code on boot. This eager loads most of Rails and 10 | # your application in memory, allowing both threaded web servers 11 | # and those relying on copy on write to perform better. 12 | # Rake tasks automatically ignore this option for performance. 13 | config.eager_load = true 14 | 15 | # Full error reports are disabled and caching is turned on. 16 | config.consider_all_requests_local = false 17 | config.action_controller.perform_caching = true 18 | 19 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] 20 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). 21 | # config.require_master_key = true 22 | 23 | # Disable serving static files from the `/public` folder by default since 24 | # Apache or NGINX already handles this. 25 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 26 | 27 | # Compress CSS using a preprocessor. 28 | # config.assets.css_compressor = :sass 29 | 30 | # Do not fallback to assets pipeline if a precompiled asset is missed. 31 | config.assets.compile = false 32 | 33 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 34 | # config.asset_host = 'http://assets.example.com' 35 | 36 | # Specifies the header that your server uses for sending files. 37 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 38 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 39 | 40 | # Store uploaded files on the local file system (see config/storage.yml for options). 41 | config.active_storage.service = :local 42 | 43 | # Mount Action Cable outside main process or domain. 44 | # config.action_cable.mount_path = nil 45 | # config.action_cable.url = 'wss://example.com/cable' 46 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] 47 | 48 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 49 | # config.force_ssl = true 50 | 51 | # Include generic and useful information about system operation, but avoid logging too much 52 | # information to avoid inadvertent exposure of personally identifiable information (PII). 53 | config.log_level = :info 54 | 55 | # Prepend all log lines with the following tags. 56 | config.log_tags = [ :request_id ] 57 | 58 | # Use a different cache store in production. 59 | # config.cache_store = :mem_cache_store 60 | 61 | # Use a real queuing backend for Active Job (and separate queues per environment). 62 | # config.active_job.queue_adapter = :resque 63 | # config.active_job.queue_name_prefix = "dummy_production" 64 | 65 | config.action_mailer.perform_caching = false 66 | 67 | # Ignore bad email addresses and do not raise email delivery errors. 68 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 69 | # config.action_mailer.raise_delivery_errors = false 70 | 71 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 72 | # the I18n.default_locale when a translation cannot be found). 73 | config.i18n.fallbacks = true 74 | 75 | # Send deprecation notices to registered listeners. 76 | config.active_support.deprecation = :notify 77 | 78 | # Log disallowed deprecations. 79 | config.active_support.disallowed_deprecation = :log 80 | 81 | # Tell Active Support which deprecation messages to disallow. 82 | config.active_support.disallowed_deprecation_warnings = [] 83 | 84 | # Use default logging formatter so that PID and timestamp are not suppressed. 85 | config.log_formatter = ::Logger::Formatter.new 86 | 87 | # Use a different logger for distributed setups. 88 | # require "syslog/logger" 89 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 90 | 91 | if ENV["RAILS_LOG_TO_STDOUT"].present? 92 | logger = ActiveSupport::Logger.new(STDOUT) 93 | logger.formatter = config.log_formatter 94 | config.logger = ActiveSupport::TaggedLogging.new(logger) 95 | end 96 | 97 | # Do not dump schema after migrations. 98 | config.active_record.dump_schema_after_migration = false 99 | 100 | # Inserts middleware to perform automatic connection switching. 101 | # The `database_selector` hash is used to pass options to the DatabaseSelector 102 | # middleware. The `delay` is used to determine how long to wait after a write 103 | # to send a subsequent read to the primary. 104 | # 105 | # The `database_resolver` class is used by the middleware to determine which 106 | # database is appropriate to use based on the time delay. 107 | # 108 | # The `database_resolver_context` class is used by the middleware to set 109 | # timestamps for the last write to the primary. The resolver uses the context 110 | # class timestamps to determine how long to wait before reading from the 111 | # replica. 112 | # 113 | # By default Rails will store a last write timestamp in the session. The 114 | # DatabaseSelector middleware is designed as such you can define your own 115 | # strategy for connection switching and pass that into the middleware through 116 | # these configuration options. 117 | # config.active_record.database_selector = { delay: 2.seconds } 118 | # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver 119 | # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session 120 | end 121 | -------------------------------------------------------------------------------- /spec/requests/hooks_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | require 'json' 3 | 4 | RSpec.describe "Hook", type: :request do 5 | describe "#create" do 6 | let(:headers) { 7 | { Tokite::HooksController::GITHUB_EVENT_HEADER => event } 8 | } 9 | let(:params) { 10 | JSON.parse(payload_json("#{event}.json")) 11 | } 12 | let!(:rule) { FactoryBot.create(:rule, query: query, icon_emoji: ":snail:") } 13 | 14 | context "with pull_request" do 15 | let(:event) { "pull_request" } 16 | let(:query) { %(event:pull_request repo:hogelog/test-repo user:hogelog title:/./ body:/./ /./ -"unmatched word") } 17 | 18 | it "fires a hook" do 19 | expect_any_instance_of(Tokite::Hook).to receive(:fire!).and_call_original 20 | expect_any_instance_of(Tokite::NotifyGithubHookEventJob).to receive(:perform) 21 | post hooks_path, params: params, headers: headers, as: :json 22 | end 23 | 24 | context "without body comment" do 25 | let(:query) { %(event:pull_request user:hogelog) } 26 | before do 27 | params["pull_request"].delete("body") 28 | end 29 | 30 | it "fires a hook" do 31 | expect_any_instance_of(Tokite::Hook).to receive(:fire!).and_call_original 32 | expect_any_instance_of(Tokite::NotifyGithubHookEventJob).to receive(:perform) 33 | post hooks_path, params: params, headers: headers, as: :json 34 | end 35 | end 36 | end 37 | 38 | context "with issues" do 39 | let(:event) { "issues" } 40 | let(:query) { %(event:issues repo:hogelog/test-repo user:hogelog title:/./ body:/./ /./ -"unmatched word") } 41 | 42 | it "fires a hook" do 43 | expect_any_instance_of(Tokite::Hook).to receive(:fire!).and_call_original 44 | expect_any_instance_of(Tokite::NotifyGithubHookEventJob).to receive(:perform) 45 | post hooks_path, params: params, headers: headers, as: :json 46 | end 47 | 48 | context "without body comment" do 49 | let(:query) { %(event:issues user:hogelog) } 50 | before do 51 | params["issue"].delete("body") 52 | end 53 | 54 | it "fires a hook" do 55 | expect_any_instance_of(Tokite::Hook).to receive(:fire!).and_call_original 56 | expect_any_instance_of(Tokite::NotifyGithubHookEventJob).to receive(:perform) 57 | post hooks_path, params: params, headers: headers, as: :json 58 | end 59 | end 60 | end 61 | 62 | context "with issue_comment" do 63 | let(:event) { "issue_comment" } 64 | let(:query) { %(event:issue_comment repo:hogelog/test-repo user:hogelog body:/./ /./ -"unmatched word") } 65 | 66 | it "fires a hook" do 67 | expect_any_instance_of(Tokite::Hook).to receive(:fire!).and_call_original 68 | expect_any_instance_of(Tokite::NotifyGithubHookEventJob).to receive(:perform) 69 | post hooks_path, params: params, headers: headers, as: :json 70 | end 71 | end 72 | 73 | context "with pull_request_review" do 74 | let(:event) { "pull_request_review" } 75 | let(:query) { %(event:pull_request_review repo:hogelog/test-repo user:hogelog review_state:commented body:/./ /./ -"unmatched word") } 76 | 77 | it "fires a hook" do 78 | expect_any_instance_of(Tokite::Hook).to receive(:fire!).and_call_original 79 | expect_any_instance_of(Tokite::NotifyGithubHookEventJob).to receive(:perform) 80 | post hooks_path, params: params, headers: headers, as: :json 81 | end 82 | 83 | context "without body comment" do 84 | let(:query) { %(event:pull_request_review user:hogelog) } 85 | before do 86 | params["review"].delete("body") 87 | end 88 | 89 | it "fires a hook" do 90 | expect_any_instance_of(Tokite::Hook).to receive(:fire!).and_call_original 91 | expect_any_instance_of(Tokite::NotifyGithubHookEventJob).not_to receive(:perform) 92 | post hooks_path, params: params, headers: headers, as: :json 93 | end 94 | end 95 | end 96 | 97 | context "with pull_request_review_comment" do 98 | let(:event) { "pull_request_review_comment" } 99 | let(:query) { %(event:pull_request_review_comment repo:hogelog/test-repo user:hogelog body:/./ /./ -"unmatched word") } 100 | 101 | it "fires a hook" do 102 | expect_any_instance_of(Tokite::Hook).to receive(:fire!).and_call_original 103 | expect_any_instance_of(Tokite::NotifyGithubHookEventJob).to receive(:perform) 104 | post hooks_path, params: params, headers: headers, as: :json 105 | end 106 | end 107 | 108 | context "with duplicated rules" do 109 | let(:event) { "issue_comment" } 110 | let(:query) { %(event:issue_comment repo:hogelog/test-repo user:hogelog body:/./ /./ -"unmatched word") } 111 | let!(:duplicated_rule) { FactoryBot.create(:rule, query: query, icon_emoji: ":snail:") } 112 | 113 | it "preserves duplicated notification" do 114 | expect(Tokite::NotifyGithubHookEventJob).to receive(:perform_now).once 115 | post hooks_path, params: params, headers: headers, as: :json 116 | end 117 | end 118 | 119 | context "when slack returns error" do 120 | let(:event) { "pull_request" } 121 | let(:query) { %(event:pull_request repo:hogelog/test-repo user:hogelog title:/./ body:/./ /./ -"unmatched word") } 122 | 123 | it "captures exception" do 124 | expect_any_instance_of(Tokite::Hook).to receive(:fire!).and_call_original 125 | expect_any_instance_of(Slack::Notifier).to receive(:ping).and_raise(Slack::Notifier::APIError) 126 | expect(Tokite::ExceptionLogger).to receive(:log).once 127 | post hooks_path, params: params, headers: headers, as: :json 128 | end 129 | end 130 | 131 | context "with label rule" do 132 | let(:event) { "issues" } 133 | let(:query) { %(label:/bug|foobar/) } 134 | 135 | it "fires a hook" do 136 | expect_any_instance_of(Tokite::Hook).to receive(:fire!).and_call_original 137 | expect_any_instance_of(Tokite::NotifyGithubHookEventJob).to receive(:perform) 138 | post hooks_path, params: params, headers: headers, as: :json 139 | end 140 | end 141 | 142 | context "with unmatched label rule" do 143 | let(:event) { "issues" } 144 | let(:query) { %(label:/foo|bar/) } 145 | 146 | it "doesn't notify to slack" do 147 | expect_any_instance_of(Tokite::Hook).to receive(:fire!).and_call_original 148 | expect_any_instance_of(Tokite::NotifyGithubHookEventJob).not_to receive(:perform) 149 | post hooks_path, params: params, headers: headers, as: :json 150 | end 151 | end 152 | 153 | context "with requested_reviewer rule" do 154 | let(:event) { "pull_request" } 155 | let(:query) { %(requested_reviewer:other_user) } 156 | 157 | it "fires a hook" do 158 | expect_any_instance_of(Tokite::Hook).to receive(:fire!).and_call_original 159 | expect_any_instance_of(Tokite::NotifyGithubHookEventJob).to receive(:perform) 160 | post hooks_path, params: params, headers: headers, as: :json 161 | end 162 | 163 | context 'review_requested action' do 164 | let(:params) { 165 | JSON.parse(payload_json("#{event}.review_requested.json")) 166 | } 167 | 168 | it "fires a hook" do 169 | expect_any_instance_of(Tokite::Hook).to receive(:fire!).and_call_original 170 | expect_any_instance_of(Tokite::NotifyGithubHookEventJob).to receive(:perform) 171 | post hooks_path, params: params, headers: headers, as: :json 172 | end 173 | end 174 | end 175 | 176 | context "with requested_team rule" do 177 | let(:event) { "pull_request" } 178 | let(:query) { %(requested_team:github/justice-league) } 179 | 180 | it "fires a hook" do 181 | expect_any_instance_of(Tokite::Hook).to receive(:fire!).and_call_original 182 | expect_any_instance_of(Tokite::NotifyGithubHookEventJob).to receive(:perform) 183 | post hooks_path, params: params, headers: headers, as: :json 184 | end 185 | end 186 | end 187 | end 188 | -------------------------------------------------------------------------------- /spec/payloads/issues.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "opened", 3 | "issue": { 4 | "url": "https://api.github.com/repos/hogelog/test-repo/issues/3", 5 | "repository_url": "https://api.github.com/repos/hogelog/test-repo", 6 | "labels_url": "https://api.github.com/repos/hogelog/test-repo/issues/3/labels{/name}", 7 | "comments_url": "https://api.github.com/repos/hogelog/test-repo/issues/3/comments", 8 | "events_url": "https://api.github.com/repos/hogelog/test-repo/issues/3/events", 9 | "html_url": "https://github.com/hogelog/test-repo/issues/3", 10 | "id": 233433516, 11 | "number": 3, 12 | "title": "Test", 13 | "user": { 14 | "login": "hogelog", 15 | "id": 50920, 16 | "avatar_url": "https://avatars2.githubusercontent.com/u/50920?v=3", 17 | "gravatar_id": "", 18 | "url": "https://api.github.com/users/hogelog", 19 | "html_url": "https://github.com/hogelog", 20 | "followers_url": "https://api.github.com/users/hogelog/followers", 21 | "following_url": "https://api.github.com/users/hogelog/following{/other_user}", 22 | "gists_url": "https://api.github.com/users/hogelog/gists{/gist_id}", 23 | "starred_url": "https://api.github.com/users/hogelog/starred{/owner}{/repo}", 24 | "subscriptions_url": "https://api.github.com/users/hogelog/subscriptions", 25 | "organizations_url": "https://api.github.com/users/hogelog/orgs", 26 | "repos_url": "https://api.github.com/users/hogelog/repos", 27 | "events_url": "https://api.github.com/users/hogelog/events{/privacy}", 28 | "received_events_url": "https://api.github.com/users/hogelog/received_events", 29 | "type": "User", 30 | "site_admin": false 31 | }, 32 | "labels": [ 33 | { 34 | "id": 1362934389, 35 | "node_id": "MDU6TGFiZWwxMzYyOTM0Mzg5", 36 | "url": "https://api.github.com/repos/Codertocat/Hello-World/labels/bug", 37 | "name": "bug", 38 | "color": "d73a4a", 39 | "default": true 40 | }, 41 | { 42 | "id": 1362934390, 43 | "node_id": "MDU6TGFiZWwxMzYyOTM0Mzg6", 44 | "url": "https://api.github.com/repos/Codertocat/Hello-World/labels/feature", 45 | "name": "feature", 46 | "color": "d73a4b", 47 | "default": true 48 | } 49 | ], 50 | "state": "open", 51 | "locked": false, 52 | "assignee": null, 53 | "assignees": [ 54 | 55 | ], 56 | "milestone": null, 57 | "comments": 0, 58 | "created_at": "2017-06-04T13:26:46Z", 59 | "updated_at": "2017-06-04T13:26:46Z", 60 | "closed_at": null, 61 | "body": "aaaaaaa\r\nbbbbbb\r\n\r\n\r\nccccccc" 62 | }, 63 | "repository": { 64 | "id": 93059937, 65 | "name": "test-repo", 66 | "full_name": "hogelog/test-repo", 67 | "owner": { 68 | "login": "hogelog", 69 | "id": 50920, 70 | "avatar_url": "https://avatars2.githubusercontent.com/u/50920?v=3", 71 | "gravatar_id": "", 72 | "url": "https://api.github.com/users/hogelog", 73 | "html_url": "https://github.com/hogelog", 74 | "followers_url": "https://api.github.com/users/hogelog/followers", 75 | "following_url": "https://api.github.com/users/hogelog/following{/other_user}", 76 | "gists_url": "https://api.github.com/users/hogelog/gists{/gist_id}", 77 | "starred_url": "https://api.github.com/users/hogelog/starred{/owner}{/repo}", 78 | "subscriptions_url": "https://api.github.com/users/hogelog/subscriptions", 79 | "organizations_url": "https://api.github.com/users/hogelog/orgs", 80 | "repos_url": "https://api.github.com/users/hogelog/repos", 81 | "events_url": "https://api.github.com/users/hogelog/events{/privacy}", 82 | "received_events_url": "https://api.github.com/users/hogelog/received_events", 83 | "type": "User", 84 | "site_admin": false 85 | }, 86 | "private": false, 87 | "html_url": "https://github.com/hogelog/test-repo", 88 | "description": null, 89 | "fork": false, 90 | "url": "https://api.github.com/repos/hogelog/test-repo", 91 | "forks_url": "https://api.github.com/repos/hogelog/test-repo/forks", 92 | "keys_url": "https://api.github.com/repos/hogelog/test-repo/keys{/key_id}", 93 | "collaborators_url": "https://api.github.com/repos/hogelog/test-repo/collaborators{/collaborator}", 94 | "teams_url": "https://api.github.com/repos/hogelog/test-repo/teams", 95 | "hooks_url": "https://api.github.com/repos/hogelog/test-repo/hooks", 96 | "issue_events_url": "https://api.github.com/repos/hogelog/test-repo/issues/events{/number}", 97 | "events_url": "https://api.github.com/repos/hogelog/test-repo/events", 98 | "assignees_url": "https://api.github.com/repos/hogelog/test-repo/assignees{/user}", 99 | "branches_url": "https://api.github.com/repos/hogelog/test-repo/branches{/branch}", 100 | "tags_url": "https://api.github.com/repos/hogelog/test-repo/tags", 101 | "blobs_url": "https://api.github.com/repos/hogelog/test-repo/git/blobs{/sha}", 102 | "git_tags_url": "https://api.github.com/repos/hogelog/test-repo/git/tags{/sha}", 103 | "git_refs_url": "https://api.github.com/repos/hogelog/test-repo/git/refs{/sha}", 104 | "trees_url": "https://api.github.com/repos/hogelog/test-repo/git/trees{/sha}", 105 | "statuses_url": "https://api.github.com/repos/hogelog/test-repo/statuses/{sha}", 106 | "languages_url": "https://api.github.com/repos/hogelog/test-repo/languages", 107 | "stargazers_url": "https://api.github.com/repos/hogelog/test-repo/stargazers", 108 | "contributors_url": "https://api.github.com/repos/hogelog/test-repo/contributors", 109 | "subscribers_url": "https://api.github.com/repos/hogelog/test-repo/subscribers", 110 | "subscription_url": "https://api.github.com/repos/hogelog/test-repo/subscription", 111 | "commits_url": "https://api.github.com/repos/hogelog/test-repo/commits{/sha}", 112 | "git_commits_url": "https://api.github.com/repos/hogelog/test-repo/git/commits{/sha}", 113 | "comments_url": "https://api.github.com/repos/hogelog/test-repo/comments{/number}", 114 | "issue_comment_url": "https://api.github.com/repos/hogelog/test-repo/issues/comments{/number}", 115 | "contents_url": "https://api.github.com/repos/hogelog/test-repo/contents/{+path}", 116 | "compare_url": "https://api.github.com/repos/hogelog/test-repo/compare/{base}...{head}", 117 | "merges_url": "https://api.github.com/repos/hogelog/test-repo/merges", 118 | "archive_url": "https://api.github.com/repos/hogelog/test-repo/{archive_format}{/ref}", 119 | "downloads_url": "https://api.github.com/repos/hogelog/test-repo/downloads", 120 | "issues_url": "https://api.github.com/repos/hogelog/test-repo/issues{/number}", 121 | "pulls_url": "https://api.github.com/repos/hogelog/test-repo/pulls{/number}", 122 | "milestones_url": "https://api.github.com/repos/hogelog/test-repo/milestones{/number}", 123 | "notifications_url": "https://api.github.com/repos/hogelog/test-repo/notifications{?since,all,participating}", 124 | "labels_url": "https://api.github.com/repos/hogelog/test-repo/labels{/name}", 125 | "releases_url": "https://api.github.com/repos/hogelog/test-repo/releases{/id}", 126 | "deployments_url": "https://api.github.com/repos/hogelog/test-repo/deployments", 127 | "created_at": "2017-06-01T13:26:00Z", 128 | "updated_at": "2017-06-01T14:03:40Z", 129 | "pushed_at": "2017-06-01T15:26:59Z", 130 | "git_url": "git://github.com/hogelog/test-repo.git", 131 | "ssh_url": "git@github.com:hogelog/test-repo.git", 132 | "clone_url": "https://github.com/hogelog/test-repo.git", 133 | "svn_url": "https://github.com/hogelog/test-repo", 134 | "homepage": null, 135 | "size": 1, 136 | "stargazers_count": 0, 137 | "watchers_count": 0, 138 | "language": "Ruby", 139 | "has_issues": true, 140 | "has_projects": true, 141 | "has_downloads": true, 142 | "has_wiki": true, 143 | "has_pages": false, 144 | "forks_count": 0, 145 | "mirror_url": null, 146 | "open_issues_count": 2, 147 | "forks": 0, 148 | "open_issues": 2, 149 | "watchers": 0, 150 | "default_branch": "master" 151 | }, 152 | "sender": { 153 | "login": "hogelog", 154 | "id": 50920, 155 | "avatar_url": "https://avatars2.githubusercontent.com/u/50920?v=3", 156 | "gravatar_id": "", 157 | "url": "https://api.github.com/users/hogelog", 158 | "html_url": "https://github.com/hogelog", 159 | "followers_url": "https://api.github.com/users/hogelog/followers", 160 | "following_url": "https://api.github.com/users/hogelog/following{/other_user}", 161 | "gists_url": "https://api.github.com/users/hogelog/gists{/gist_id}", 162 | "starred_url": "https://api.github.com/users/hogelog/starred{/owner}{/repo}", 163 | "subscriptions_url": "https://api.github.com/users/hogelog/subscriptions", 164 | "organizations_url": "https://api.github.com/users/hogelog/orgs", 165 | "repos_url": "https://api.github.com/users/hogelog/repos", 166 | "events_url": "https://api.github.com/users/hogelog/events{/privacy}", 167 | "received_events_url": "https://api.github.com/users/hogelog/received_events", 168 | "type": "User", 169 | "site_admin": false 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /spec/payloads/issue_comment.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "action": "created", 4 | "issue": { 5 | "url": "https://api.github.com/repos/hogelog/test-repo/issues/2", 6 | "repository_url": "https://api.github.com/repos/hogelog/test-repo", 7 | "labels_url": "https://api.github.com/repos/hogelog/test-repo/issues/2/labels{/name}", 8 | "comments_url": "https://api.github.com/repos/hogelog/test-repo/issues/2/comments", 9 | "events_url": "https://api.github.com/repos/hogelog/test-repo/issues/2/events", 10 | "html_url": "https://github.com/hogelog/test-repo/pull/2", 11 | "id": 232918062, 12 | "number": 2, 13 | "title": "fuga", 14 | "user": { 15 | "login": "hogelog", 16 | "id": 50920, 17 | "avatar_url": "https://avatars2.githubusercontent.com/u/50920?v=3", 18 | "gravatar_id": "", 19 | "url": "https://api.github.com/users/hogelog", 20 | "html_url": "https://github.com/hogelog", 21 | "followers_url": "https://api.github.com/users/hogelog/followers", 22 | "following_url": "https://api.github.com/users/hogelog/following{/other_user}", 23 | "gists_url": "https://api.github.com/users/hogelog/gists{/gist_id}", 24 | "starred_url": "https://api.github.com/users/hogelog/starred{/owner}{/repo}", 25 | "subscriptions_url": "https://api.github.com/users/hogelog/subscriptions", 26 | "organizations_url": "https://api.github.com/users/hogelog/orgs", 27 | "repos_url": "https://api.github.com/users/hogelog/repos", 28 | "events_url": "https://api.github.com/users/hogelog/events{/privacy}", 29 | "received_events_url": "https://api.github.com/users/hogelog/received_events", 30 | "type": "User", 31 | "site_admin": false 32 | }, 33 | "labels": [ 34 | 35 | ], 36 | "state": "open", 37 | "locked": false, 38 | "assignee": null, 39 | "assignees": [ 40 | 41 | ], 42 | "milestone": null, 43 | "comments": 0, 44 | "created_at": "2017-06-01T15:26:58Z", 45 | "updated_at": "2017-06-02T14:15:12Z", 46 | "closed_at": null, 47 | "pull_request": { 48 | "url": "https://api.github.com/repos/hogelog/test-repo/pulls/2", 49 | "html_url": "https://github.com/hogelog/test-repo/pull/2", 50 | "diff_url": "https://github.com/hogelog/test-repo/pull/2.diff", 51 | "patch_url": "https://github.com/hogelog/test-repo/pull/2.patch" 52 | }, 53 | "body": "abjiefaaaaaa" 54 | }, 55 | "comment": { 56 | "url": "https://api.github.com/repos/hogelog/test-repo/issues/comments/305800928", 57 | "html_url": "https://github.com/hogelog/test-repo/pull/2#issuecomment-305800928", 58 | "issue_url": "https://api.github.com/repos/hogelog/test-repo/issues/2", 59 | "id": 305800928, 60 | "user": { 61 | "login": "hogelog", 62 | "id": 50920, 63 | "avatar_url": "https://avatars2.githubusercontent.com/u/50920?v=3", 64 | "gravatar_id": "", 65 | "url": "https://api.github.com/users/hogelog", 66 | "html_url": "https://github.com/hogelog", 67 | "followers_url": "https://api.github.com/users/hogelog/followers", 68 | "following_url": "https://api.github.com/users/hogelog/following{/other_user}", 69 | "gists_url": "https://api.github.com/users/hogelog/gists{/gist_id}", 70 | "starred_url": "https://api.github.com/users/hogelog/starred{/owner}{/repo}", 71 | "subscriptions_url": "https://api.github.com/users/hogelog/subscriptions", 72 | "organizations_url": "https://api.github.com/users/hogelog/orgs", 73 | "repos_url": "https://api.github.com/users/hogelog/repos", 74 | "events_url": "https://api.github.com/users/hogelog/events{/privacy}", 75 | "received_events_url": "https://api.github.com/users/hogelog/received_events", 76 | "type": "User", 77 | "site_admin": false 78 | }, 79 | "created_at": "2017-06-02T14:15:12Z", 80 | "updated_at": "2017-06-02T14:15:12Z", 81 | "body": "こんにちはこんにちは\r\n\r\n## タイトル\r\n\r\naaaaaa\r\nbbbbbb\r\nccccc\r\nddddd" 82 | }, 83 | "repository": { 84 | "id": 93059937, 85 | "name": "test-repo", 86 | "full_name": "hogelog/test-repo", 87 | "owner": { 88 | "login": "hogelog", 89 | "id": 50920, 90 | "avatar_url": "https://avatars2.githubusercontent.com/u/50920?v=3", 91 | "gravatar_id": "", 92 | "url": "https://api.github.com/users/hogelog", 93 | "html_url": "https://github.com/hogelog", 94 | "followers_url": "https://api.github.com/users/hogelog/followers", 95 | "following_url": "https://api.github.com/users/hogelog/following{/other_user}", 96 | "gists_url": "https://api.github.com/users/hogelog/gists{/gist_id}", 97 | "starred_url": "https://api.github.com/users/hogelog/starred{/owner}{/repo}", 98 | "subscriptions_url": "https://api.github.com/users/hogelog/subscriptions", 99 | "organizations_url": "https://api.github.com/users/hogelog/orgs", 100 | "repos_url": "https://api.github.com/users/hogelog/repos", 101 | "events_url": "https://api.github.com/users/hogelog/events{/privacy}", 102 | "received_events_url": "https://api.github.com/users/hogelog/received_events", 103 | "type": "User", 104 | "site_admin": false 105 | }, 106 | "private": false, 107 | "html_url": "https://github.com/hogelog/test-repo", 108 | "description": null, 109 | "fork": false, 110 | "url": "https://api.github.com/repos/hogelog/test-repo", 111 | "forks_url": "https://api.github.com/repos/hogelog/test-repo/forks", 112 | "keys_url": "https://api.github.com/repos/hogelog/test-repo/keys{/key_id}", 113 | "collaborators_url": "https://api.github.com/repos/hogelog/test-repo/collaborators{/collaborator}", 114 | "teams_url": "https://api.github.com/repos/hogelog/test-repo/teams", 115 | "hooks_url": "https://api.github.com/repos/hogelog/test-repo/hooks", 116 | "issue_events_url": "https://api.github.com/repos/hogelog/test-repo/issues/events{/number}", 117 | "events_url": "https://api.github.com/repos/hogelog/test-repo/events", 118 | "assignees_url": "https://api.github.com/repos/hogelog/test-repo/assignees{/user}", 119 | "branches_url": "https://api.github.com/repos/hogelog/test-repo/branches{/branch}", 120 | "tags_url": "https://api.github.com/repos/hogelog/test-repo/tags", 121 | "blobs_url": "https://api.github.com/repos/hogelog/test-repo/git/blobs{/sha}", 122 | "git_tags_url": "https://api.github.com/repos/hogelog/test-repo/git/tags{/sha}", 123 | "git_refs_url": "https://api.github.com/repos/hogelog/test-repo/git/refs{/sha}", 124 | "trees_url": "https://api.github.com/repos/hogelog/test-repo/git/trees{/sha}", 125 | "statuses_url": "https://api.github.com/repos/hogelog/test-repo/statuses/{sha}", 126 | "languages_url": "https://api.github.com/repos/hogelog/test-repo/languages", 127 | "stargazers_url": "https://api.github.com/repos/hogelog/test-repo/stargazers", 128 | "contributors_url": "https://api.github.com/repos/hogelog/test-repo/contributors", 129 | "subscribers_url": "https://api.github.com/repos/hogelog/test-repo/subscribers", 130 | "subscription_url": "https://api.github.com/repos/hogelog/test-repo/subscription", 131 | "commits_url": "https://api.github.com/repos/hogelog/test-repo/commits{/sha}", 132 | "git_commits_url": "https://api.github.com/repos/hogelog/test-repo/git/commits{/sha}", 133 | "comments_url": "https://api.github.com/repos/hogelog/test-repo/comments{/number}", 134 | "issue_comment_url": "https://api.github.com/repos/hogelog/test-repo/issues/comments{/number}", 135 | "contents_url": "https://api.github.com/repos/hogelog/test-repo/contents/{+path}", 136 | "compare_url": "https://api.github.com/repos/hogelog/test-repo/compare/{base}...{head}", 137 | "merges_url": "https://api.github.com/repos/hogelog/test-repo/merges", 138 | "archive_url": "https://api.github.com/repos/hogelog/test-repo/{archive_format}{/ref}", 139 | "downloads_url": "https://api.github.com/repos/hogelog/test-repo/downloads", 140 | "issues_url": "https://api.github.com/repos/hogelog/test-repo/issues{/number}", 141 | "pulls_url": "https://api.github.com/repos/hogelog/test-repo/pulls{/number}", 142 | "milestones_url": "https://api.github.com/repos/hogelog/test-repo/milestones{/number}", 143 | "notifications_url": "https://api.github.com/repos/hogelog/test-repo/notifications{?since,all,participating}", 144 | "labels_url": "https://api.github.com/repos/hogelog/test-repo/labels{/name}", 145 | "releases_url": "https://api.github.com/repos/hogelog/test-repo/releases{/id}", 146 | "deployments_url": "https://api.github.com/repos/hogelog/test-repo/deployments", 147 | "created_at": "2017-06-01T13:26:00Z", 148 | "updated_at": "2017-06-01T14:03:40Z", 149 | "pushed_at": "2017-06-01T15:26:59Z", 150 | "git_url": "git://github.com/hogelog/test-repo.git", 151 | "ssh_url": "git@github.com:hogelog/test-repo.git", 152 | "clone_url": "https://github.com/hogelog/test-repo.git", 153 | "svn_url": "https://github.com/hogelog/test-repo", 154 | "homepage": null, 155 | "size": 1, 156 | "stargazers_count": 0, 157 | "watchers_count": 0, 158 | "language": "Ruby", 159 | "has_issues": true, 160 | "has_projects": true, 161 | "has_downloads": true, 162 | "has_wiki": true, 163 | "has_pages": false, 164 | "forks_count": 0, 165 | "mirror_url": null, 166 | "open_issues_count": 1, 167 | "forks": 0, 168 | "open_issues": 1, 169 | "watchers": 0, 170 | "default_branch": "master" 171 | }, 172 | "sender": { 173 | "login": "hogelog", 174 | "id": 50920, 175 | "avatar_url": "https://avatars2.githubusercontent.com/u/50920?v=3", 176 | "gravatar_id": "", 177 | "url": "https://api.github.com/users/hogelog", 178 | "html_url": "https://github.com/hogelog", 179 | "followers_url": "https://api.github.com/users/hogelog/followers", 180 | "following_url": "https://api.github.com/users/hogelog/following{/other_user}", 181 | "gists_url": "https://api.github.com/users/hogelog/gists{/gist_id}", 182 | "starred_url": "https://api.github.com/users/hogelog/starred{/owner}{/repo}", 183 | "subscriptions_url": "https://api.github.com/users/hogelog/subscriptions", 184 | "organizations_url": "https://api.github.com/users/hogelog/orgs", 185 | "repos_url": "https://api.github.com/users/hogelog/repos", 186 | "events_url": "https://api.github.com/users/hogelog/events{/privacy}", 187 | "received_events_url": "https://api.github.com/users/hogelog/received_events", 188 | "type": "User", 189 | "site_admin": false 190 | } 191 | } -------------------------------------------------------------------------------- /spec/payloads/pull_request.review_requested.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "review_requested", 3 | "number": 2, 4 | "pull_request": { 5 | "url": "https://api.github.com/repos/hogelog/test-repo/pulls/2", 6 | "id": 123527521, 7 | "html_url": "https://github.com/hogelog/test-repo/pull/2", 8 | "diff_url": "https://github.com/hogelog/test-repo/pull/2.diff", 9 | "patch_url": "https://github.com/hogelog/test-repo/pull/2.patch", 10 | "issue_url": "https://api.github.com/repos/hogelog/test-repo/issues/2", 11 | "number": 2, 12 | "state": "open", 13 | "locked": false, 14 | "title": "Super great feature", 15 | "user": { 16 | "login": "hogelog", 17 | "id": 50920, 18 | "avatar_url": "https://avatars2.githubusercontent.com/u/50920?v=3", 19 | "gravatar_id": "", 20 | "url": "https://api.github.com/users/hogelog", 21 | "html_url": "https://github.com/hogelog", 22 | "followers_url": "https://api.github.com/users/hogelog/followers", 23 | "following_url": "https://api.github.com/users/hogelog/following{/other_user}", 24 | "gists_url": "https://api.github.com/users/hogelog/gists{/gist_id}", 25 | "starred_url": "https://api.github.com/users/hogelog/starred{/owner}{/repo}", 26 | "subscriptions_url": "https://api.github.com/users/hogelog/subscriptions", 27 | "organizations_url": "https://api.github.com/users/hogelog/orgs", 28 | "repos_url": "https://api.github.com/users/hogelog/repos", 29 | "events_url": "https://api.github.com/users/hogelog/events{/privacy}", 30 | "received_events_url": "https://api.github.com/users/hogelog/received_events", 31 | "type": "User", 32 | "site_admin": false 33 | }, 34 | "body": "I implemented super great feature.", 35 | "created_at": "2017-06-01T15:26:58Z", 36 | "updated_at": "2017-06-01T15:26:58Z", 37 | "closed_at": null, 38 | "merged_at": null, 39 | "merge_commit_sha": null, 40 | "assignee": null, 41 | "assignees": [ 42 | 43 | ], 44 | "requested_reviewers": [ 45 | 46 | ], 47 | "requested_teams": [ 48 | 49 | ], 50 | "labels": [ 51 | ], 52 | "milestone": null, 53 | "commits_url": "https://api.github.com/repos/hogelog/test-repo/pulls/2/commits", 54 | "review_comments_url": "https://api.github.com/repos/hogelog/test-repo/pulls/2/comments", 55 | "review_comment_url": "https://api.github.com/repos/hogelog/test-repo/pulls/comments{/number}", 56 | "comments_url": "https://api.github.com/repos/hogelog/test-repo/issues/2/comments", 57 | "statuses_url": "https://api.github.com/repos/hogelog/test-repo/statuses/887720a39cd6a5348898429a5d110cd90f26ca10", 58 | "head": { 59 | "label": "hogelog:fuga", 60 | "ref": "fuga", 61 | "sha": "887720a39cd6a5348898429a5d110cd90f26ca10", 62 | "user": { 63 | "login": "hogelog", 64 | "id": 50920, 65 | "avatar_url": "https://avatars2.githubusercontent.com/u/50920?v=3", 66 | "gravatar_id": "", 67 | "url": "https://api.github.com/users/hogelog", 68 | "html_url": "https://github.com/hogelog", 69 | "followers_url": "https://api.github.com/users/hogelog/followers", 70 | "following_url": "https://api.github.com/users/hogelog/following{/other_user}", 71 | "gists_url": "https://api.github.com/users/hogelog/gists{/gist_id}", 72 | "starred_url": "https://api.github.com/users/hogelog/starred{/owner}{/repo}", 73 | "subscriptions_url": "https://api.github.com/users/hogelog/subscriptions", 74 | "organizations_url": "https://api.github.com/users/hogelog/orgs", 75 | "repos_url": "https://api.github.com/users/hogelog/repos", 76 | "events_url": "https://api.github.com/users/hogelog/events{/privacy}", 77 | "received_events_url": "https://api.github.com/users/hogelog/received_events", 78 | "type": "User", 79 | "site_admin": false 80 | }, 81 | "repo": { 82 | "id": 93059937, 83 | "name": "test-repo", 84 | "full_name": "hogelog/test-repo", 85 | "owner": { 86 | "login": "hogelog", 87 | "id": 50920, 88 | "avatar_url": "https://avatars2.githubusercontent.com/u/50920?v=3", 89 | "gravatar_id": "", 90 | "url": "https://api.github.com/users/hogelog", 91 | "html_url": "https://github.com/hogelog", 92 | "followers_url": "https://api.github.com/users/hogelog/followers", 93 | "following_url": "https://api.github.com/users/hogelog/following{/other_user}", 94 | "gists_url": "https://api.github.com/users/hogelog/gists{/gist_id}", 95 | "starred_url": "https://api.github.com/users/hogelog/starred{/owner}{/repo}", 96 | "subscriptions_url": "https://api.github.com/users/hogelog/subscriptions", 97 | "organizations_url": "https://api.github.com/users/hogelog/orgs", 98 | "repos_url": "https://api.github.com/users/hogelog/repos", 99 | "events_url": "https://api.github.com/users/hogelog/events{/privacy}", 100 | "received_events_url": "https://api.github.com/users/hogelog/received_events", 101 | "type": "User", 102 | "site_admin": false 103 | }, 104 | "private": false, 105 | "html_url": "https://github.com/hogelog/test-repo", 106 | "description": null, 107 | "fork": false, 108 | "url": "https://api.github.com/repos/hogelog/test-repo", 109 | "forks_url": "https://api.github.com/repos/hogelog/test-repo/forks", 110 | "keys_url": "https://api.github.com/repos/hogelog/test-repo/keys{/key_id}", 111 | "collaborators_url": "https://api.github.com/repos/hogelog/test-repo/collaborators{/collaborator}", 112 | "teams_url": "https://api.github.com/repos/hogelog/test-repo/teams", 113 | "hooks_url": "https://api.github.com/repos/hogelog/test-repo/hooks", 114 | "issue_events_url": "https://api.github.com/repos/hogelog/test-repo/issues/events{/number}", 115 | "events_url": "https://api.github.com/repos/hogelog/test-repo/events", 116 | "assignees_url": "https://api.github.com/repos/hogelog/test-repo/assignees{/user}", 117 | "branches_url": "https://api.github.com/repos/hogelog/test-repo/branches{/branch}", 118 | "tags_url": "https://api.github.com/repos/hogelog/test-repo/tags", 119 | "blobs_url": "https://api.github.com/repos/hogelog/test-repo/git/blobs{/sha}", 120 | "git_tags_url": "https://api.github.com/repos/hogelog/test-repo/git/tags{/sha}", 121 | "git_refs_url": "https://api.github.com/repos/hogelog/test-repo/git/refs{/sha}", 122 | "trees_url": "https://api.github.com/repos/hogelog/test-repo/git/trees{/sha}", 123 | "statuses_url": "https://api.github.com/repos/hogelog/test-repo/statuses/{sha}", 124 | "languages_url": "https://api.github.com/repos/hogelog/test-repo/languages", 125 | "stargazers_url": "https://api.github.com/repos/hogelog/test-repo/stargazers", 126 | "contributors_url": "https://api.github.com/repos/hogelog/test-repo/contributors", 127 | "subscribers_url": "https://api.github.com/repos/hogelog/test-repo/subscribers", 128 | "subscription_url": "https://api.github.com/repos/hogelog/test-repo/subscription", 129 | "commits_url": "https://api.github.com/repos/hogelog/test-repo/commits{/sha}", 130 | "git_commits_url": "https://api.github.com/repos/hogelog/test-repo/git/commits{/sha}", 131 | "comments_url": "https://api.github.com/repos/hogelog/test-repo/comments{/number}", 132 | "issue_comment_url": "https://api.github.com/repos/hogelog/test-repo/issues/comments{/number}", 133 | "contents_url": "https://api.github.com/repos/hogelog/test-repo/contents/{+path}", 134 | "compare_url": "https://api.github.com/repos/hogelog/test-repo/compare/{base}...{head}", 135 | "merges_url": "https://api.github.com/repos/hogelog/test-repo/merges", 136 | "archive_url": "https://api.github.com/repos/hogelog/test-repo/{archive_format}{/ref}", 137 | "downloads_url": "https://api.github.com/repos/hogelog/test-repo/downloads", 138 | "issues_url": "https://api.github.com/repos/hogelog/test-repo/issues{/number}", 139 | "pulls_url": "https://api.github.com/repos/hogelog/test-repo/pulls{/number}", 140 | "milestones_url": "https://api.github.com/repos/hogelog/test-repo/milestones{/number}", 141 | "notifications_url": "https://api.github.com/repos/hogelog/test-repo/notifications{?since,all,participating}", 142 | "labels_url": "https://api.github.com/repos/hogelog/test-repo/labels{/name}", 143 | "releases_url": "https://api.github.com/repos/hogelog/test-repo/releases{/id}", 144 | "deployments_url": "https://api.github.com/repos/hogelog/test-repo/deployments", 145 | "created_at": "2017-06-01T13:26:00Z", 146 | "updated_at": "2017-06-01T14:03:40Z", 147 | "pushed_at": "2017-06-01T15:26:35Z", 148 | "git_url": "git://github.com/hogelog/test-repo.git", 149 | "ssh_url": "git@github.com:hogelog/test-repo.git", 150 | "clone_url": "https://github.com/hogelog/test-repo.git", 151 | "svn_url": "https://github.com/hogelog/test-repo", 152 | "homepage": null, 153 | "size": 0, 154 | "stargazers_count": 0, 155 | "watchers_count": 0, 156 | "language": "Ruby", 157 | "has_issues": true, 158 | "has_projects": true, 159 | "has_downloads": true, 160 | "has_wiki": true, 161 | "has_pages": false, 162 | "forks_count": 0, 163 | "mirror_url": null, 164 | "open_issues_count": 1, 165 | "forks": 0, 166 | "open_issues": 1, 167 | "watchers": 0, 168 | "default_branch": "master" 169 | } 170 | }, 171 | "base": { 172 | "label": "hogelog:master", 173 | "ref": "master", 174 | "sha": "e75b5b1c499aebaa17f125089aef5e50e6f3a1a3", 175 | "user": { 176 | "login": "hogelog", 177 | "id": 50920, 178 | "avatar_url": "https://avatars2.githubusercontent.com/u/50920?v=3", 179 | "gravatar_id": "", 180 | "url": "https://api.github.com/users/hogelog", 181 | "html_url": "https://github.com/hogelog", 182 | "followers_url": "https://api.github.com/users/hogelog/followers", 183 | "following_url": "https://api.github.com/users/hogelog/following{/other_user}", 184 | "gists_url": "https://api.github.com/users/hogelog/gists{/gist_id}", 185 | "starred_url": "https://api.github.com/users/hogelog/starred{/owner}{/repo}", 186 | "subscriptions_url": "https://api.github.com/users/hogelog/subscriptions", 187 | "organizations_url": "https://api.github.com/users/hogelog/orgs", 188 | "repos_url": "https://api.github.com/users/hogelog/repos", 189 | "events_url": "https://api.github.com/users/hogelog/events{/privacy}", 190 | "received_events_url": "https://api.github.com/users/hogelog/received_events", 191 | "type": "User", 192 | "site_admin": false 193 | }, 194 | "repo": { 195 | "id": 93059937, 196 | "name": "test-repo", 197 | "full_name": "hogelog/test-repo", 198 | "owner": { 199 | "login": "hogelog", 200 | "id": 50920, 201 | "avatar_url": "https://avatars2.githubusercontent.com/u/50920?v=3", 202 | "gravatar_id": "", 203 | "url": "https://api.github.com/users/hogelog", 204 | "html_url": "https://github.com/hogelog", 205 | "followers_url": "https://api.github.com/users/hogelog/followers", 206 | "following_url": "https://api.github.com/users/hogelog/following{/other_user}", 207 | "gists_url": "https://api.github.com/users/hogelog/gists{/gist_id}", 208 | "starred_url": "https://api.github.com/users/hogelog/starred{/owner}{/repo}", 209 | "subscriptions_url": "https://api.github.com/users/hogelog/subscriptions", 210 | "organizations_url": "https://api.github.com/users/hogelog/orgs", 211 | "repos_url": "https://api.github.com/users/hogelog/repos", 212 | "events_url": "https://api.github.com/users/hogelog/events{/privacy}", 213 | "received_events_url": "https://api.github.com/users/hogelog/received_events", 214 | "type": "User", 215 | "site_admin": false 216 | }, 217 | "private": false, 218 | "html_url": "https://github.com/hogelog/test-repo", 219 | "description": null, 220 | "fork": false, 221 | "url": "https://api.github.com/repos/hogelog/test-repo", 222 | "forks_url": "https://api.github.com/repos/hogelog/test-repo/forks", 223 | "keys_url": "https://api.github.com/repos/hogelog/test-repo/keys{/key_id}", 224 | "collaborators_url": "https://api.github.com/repos/hogelog/test-repo/collaborators{/collaborator}", 225 | "teams_url": "https://api.github.com/repos/hogelog/test-repo/teams", 226 | "hooks_url": "https://api.github.com/repos/hogelog/test-repo/hooks", 227 | "issue_events_url": "https://api.github.com/repos/hogelog/test-repo/issues/events{/number}", 228 | "events_url": "https://api.github.com/repos/hogelog/test-repo/events", 229 | "assignees_url": "https://api.github.com/repos/hogelog/test-repo/assignees{/user}", 230 | "branches_url": "https://api.github.com/repos/hogelog/test-repo/branches{/branch}", 231 | "tags_url": "https://api.github.com/repos/hogelog/test-repo/tags", 232 | "blobs_url": "https://api.github.com/repos/hogelog/test-repo/git/blobs{/sha}", 233 | "git_tags_url": "https://api.github.com/repos/hogelog/test-repo/git/tags{/sha}", 234 | "git_refs_url": "https://api.github.com/repos/hogelog/test-repo/git/refs{/sha}", 235 | "trees_url": "https://api.github.com/repos/hogelog/test-repo/git/trees{/sha}", 236 | "statuses_url": "https://api.github.com/repos/hogelog/test-repo/statuses/{sha}", 237 | "languages_url": "https://api.github.com/repos/hogelog/test-repo/languages", 238 | "stargazers_url": "https://api.github.com/repos/hogelog/test-repo/stargazers", 239 | "contributors_url": "https://api.github.com/repos/hogelog/test-repo/contributors", 240 | "subscribers_url": "https://api.github.com/repos/hogelog/test-repo/subscribers", 241 | "subscription_url": "https://api.github.com/repos/hogelog/test-repo/subscription", 242 | "commits_url": "https://api.github.com/repos/hogelog/test-repo/commits{/sha}", 243 | "git_commits_url": "https://api.github.com/repos/hogelog/test-repo/git/commits{/sha}", 244 | "comments_url": "https://api.github.com/repos/hogelog/test-repo/comments{/number}", 245 | "issue_comment_url": "https://api.github.com/repos/hogelog/test-repo/issues/comments{/number}", 246 | "contents_url": "https://api.github.com/repos/hogelog/test-repo/contents/{+path}", 247 | "compare_url": "https://api.github.com/repos/hogelog/test-repo/compare/{base}...{head}", 248 | "merges_url": "https://api.github.com/repos/hogelog/test-repo/merges", 249 | "archive_url": "https://api.github.com/repos/hogelog/test-repo/{archive_format}{/ref}", 250 | "downloads_url": "https://api.github.com/repos/hogelog/test-repo/downloads", 251 | "issues_url": "https://api.github.com/repos/hogelog/test-repo/issues{/number}", 252 | "pulls_url": "https://api.github.com/repos/hogelog/test-repo/pulls{/number}", 253 | "milestones_url": "https://api.github.com/repos/hogelog/test-repo/milestones{/number}", 254 | "notifications_url": "https://api.github.com/repos/hogelog/test-repo/notifications{?since,all,participating}", 255 | "labels_url": "https://api.github.com/repos/hogelog/test-repo/labels{/name}", 256 | "releases_url": "https://api.github.com/repos/hogelog/test-repo/releases{/id}", 257 | "deployments_url": "https://api.github.com/repos/hogelog/test-repo/deployments", 258 | "created_at": "2017-06-01T13:26:00Z", 259 | "updated_at": "2017-06-01T14:03:40Z", 260 | "pushed_at": "2017-06-01T15:26:35Z", 261 | "git_url": "git://github.com/hogelog/test-repo.git", 262 | "ssh_url": "git@github.com:hogelog/test-repo.git", 263 | "clone_url": "https://github.com/hogelog/test-repo.git", 264 | "svn_url": "https://github.com/hogelog/test-repo", 265 | "homepage": null, 266 | "size": 0, 267 | "stargazers_count": 0, 268 | "watchers_count": 0, 269 | "language": "Ruby", 270 | "has_issues": true, 271 | "has_projects": true, 272 | "has_downloads": true, 273 | "has_wiki": true, 274 | "has_pages": false, 275 | "forks_count": 0, 276 | "mirror_url": null, 277 | "open_issues_count": 1, 278 | "forks": 0, 279 | "open_issues": 1, 280 | "watchers": 0, 281 | "default_branch": "master" 282 | } 283 | }, 284 | "_links": { 285 | "self": { 286 | "href": "https://api.github.com/repos/hogelog/test-repo/pulls/2" 287 | }, 288 | "html": { 289 | "href": "https://github.com/hogelog/test-repo/pull/2" 290 | }, 291 | "issue": { 292 | "href": "https://api.github.com/repos/hogelog/test-repo/issues/2" 293 | }, 294 | "comments": { 295 | "href": "https://api.github.com/repos/hogelog/test-repo/issues/2/comments" 296 | }, 297 | "review_comments": { 298 | "href": "https://api.github.com/repos/hogelog/test-repo/pulls/2/comments" 299 | }, 300 | "review_comment": { 301 | "href": "https://api.github.com/repos/hogelog/test-repo/pulls/comments{/number}" 302 | }, 303 | "commits": { 304 | "href": "https://api.github.com/repos/hogelog/test-repo/pulls/2/commits" 305 | }, 306 | "statuses": { 307 | "href": "https://api.github.com/repos/hogelog/test-repo/statuses/887720a39cd6a5348898429a5d110cd90f26ca10" 308 | } 309 | }, 310 | "merged": false, 311 | "mergeable": null, 312 | "rebaseable": null, 313 | "mergeable_state": "unknown", 314 | "merged_by": null, 315 | "comments": 0, 316 | "review_comments": 0, 317 | "maintainer_can_modify": false, 318 | "commits": 1, 319 | "additions": 1, 320 | "deletions": 0, 321 | "changed_files": 1 322 | }, 323 | "requested_reviewer": { 324 | "login": "other_user", 325 | "id": 1, 326 | "node_id": "MDQ6VXNlcjE=", 327 | "avatar_url": "https://github.com/images/error/other_user_happy.gif", 328 | "gravatar_id": "", 329 | "url": "https://api.github.com/users/other_user", 330 | "html_url": "https://github.com/other_user", 331 | "followers_url": "https://api.github.com/users/other_user/followers", 332 | "following_url": "https://api.github.com/users/other_user/following{/other_user}", 333 | "gists_url": "https://api.github.com/users/other_user/gists{/gist_id}", 334 | "starred_url": "https://api.github.com/users/other_user/starred{/owner}{/repo}", 335 | "subscriptions_url": "https://api.github.com/users/other_user/subscriptions", 336 | "organizations_url": "https://api.github.com/users/other_user/orgs", 337 | "repos_url": "https://api.github.com/users/other_user/repos", 338 | "events_url": "https://api.github.com/users/other_user/events{/privacy}", 339 | "received_events_url": "https://api.github.com/users/other_user/received_events", 340 | "type": "User", 341 | "site_admin": false 342 | }, 343 | "repository": { 344 | "id": 93059937, 345 | "name": "test-repo", 346 | "full_name": "hogelog/test-repo", 347 | "owner": { 348 | "login": "hogelog", 349 | "id": 50920, 350 | "avatar_url": "https://avatars2.githubusercontent.com/u/50920?v=3", 351 | "gravatar_id": "", 352 | "url": "https://api.github.com/users/hogelog", 353 | "html_url": "https://github.com/hogelog", 354 | "followers_url": "https://api.github.com/users/hogelog/followers", 355 | "following_url": "https://api.github.com/users/hogelog/following{/other_user}", 356 | "gists_url": "https://api.github.com/users/hogelog/gists{/gist_id}", 357 | "starred_url": "https://api.github.com/users/hogelog/starred{/owner}{/repo}", 358 | "subscriptions_url": "https://api.github.com/users/hogelog/subscriptions", 359 | "organizations_url": "https://api.github.com/users/hogelog/orgs", 360 | "repos_url": "https://api.github.com/users/hogelog/repos", 361 | "events_url": "https://api.github.com/users/hogelog/events{/privacy}", 362 | "received_events_url": "https://api.github.com/users/hogelog/received_events", 363 | "type": "User", 364 | "site_admin": false 365 | }, 366 | "private": false, 367 | "html_url": "https://github.com/hogelog/test-repo", 368 | "description": null, 369 | "fork": false, 370 | "url": "https://api.github.com/repos/hogelog/test-repo", 371 | "forks_url": "https://api.github.com/repos/hogelog/test-repo/forks", 372 | "keys_url": "https://api.github.com/repos/hogelog/test-repo/keys{/key_id}", 373 | "collaborators_url": "https://api.github.com/repos/hogelog/test-repo/collaborators{/collaborator}", 374 | "teams_url": "https://api.github.com/repos/hogelog/test-repo/teams", 375 | "hooks_url": "https://api.github.com/repos/hogelog/test-repo/hooks", 376 | "issue_events_url": "https://api.github.com/repos/hogelog/test-repo/issues/events{/number}", 377 | "events_url": "https://api.github.com/repos/hogelog/test-repo/events", 378 | "assignees_url": "https://api.github.com/repos/hogelog/test-repo/assignees{/user}", 379 | "branches_url": "https://api.github.com/repos/hogelog/test-repo/branches{/branch}", 380 | "tags_url": "https://api.github.com/repos/hogelog/test-repo/tags", 381 | "blobs_url": "https://api.github.com/repos/hogelog/test-repo/git/blobs{/sha}", 382 | "git_tags_url": "https://api.github.com/repos/hogelog/test-repo/git/tags{/sha}", 383 | "git_refs_url": "https://api.github.com/repos/hogelog/test-repo/git/refs{/sha}", 384 | "trees_url": "https://api.github.com/repos/hogelog/test-repo/git/trees{/sha}", 385 | "statuses_url": "https://api.github.com/repos/hogelog/test-repo/statuses/{sha}", 386 | "languages_url": "https://api.github.com/repos/hogelog/test-repo/languages", 387 | "stargazers_url": "https://api.github.com/repos/hogelog/test-repo/stargazers", 388 | "contributors_url": "https://api.github.com/repos/hogelog/test-repo/contributors", 389 | "subscribers_url": "https://api.github.com/repos/hogelog/test-repo/subscribers", 390 | "subscription_url": "https://api.github.com/repos/hogelog/test-repo/subscription", 391 | "commits_url": "https://api.github.com/repos/hogelog/test-repo/commits{/sha}", 392 | "git_commits_url": "https://api.github.com/repos/hogelog/test-repo/git/commits{/sha}", 393 | "comments_url": "https://api.github.com/repos/hogelog/test-repo/comments{/number}", 394 | "issue_comment_url": "https://api.github.com/repos/hogelog/test-repo/issues/comments{/number}", 395 | "contents_url": "https://api.github.com/repos/hogelog/test-repo/contents/{+path}", 396 | "compare_url": "https://api.github.com/repos/hogelog/test-repo/compare/{base}...{head}", 397 | "merges_url": "https://api.github.com/repos/hogelog/test-repo/merges", 398 | "archive_url": "https://api.github.com/repos/hogelog/test-repo/{archive_format}{/ref}", 399 | "downloads_url": "https://api.github.com/repos/hogelog/test-repo/downloads", 400 | "issues_url": "https://api.github.com/repos/hogelog/test-repo/issues{/number}", 401 | "pulls_url": "https://api.github.com/repos/hogelog/test-repo/pulls{/number}", 402 | "milestones_url": "https://api.github.com/repos/hogelog/test-repo/milestones{/number}", 403 | "notifications_url": "https://api.github.com/repos/hogelog/test-repo/notifications{?since,all,participating}", 404 | "labels_url": "https://api.github.com/repos/hogelog/test-repo/labels{/name}", 405 | "releases_url": "https://api.github.com/repos/hogelog/test-repo/releases{/id}", 406 | "deployments_url": "https://api.github.com/repos/hogelog/test-repo/deployments", 407 | "created_at": "2017-06-01T13:26:00Z", 408 | "updated_at": "2017-06-01T14:03:40Z", 409 | "pushed_at": "2017-06-01T15:26:35Z", 410 | "git_url": "git://github.com/hogelog/test-repo.git", 411 | "ssh_url": "git@github.com:hogelog/test-repo.git", 412 | "clone_url": "https://github.com/hogelog/test-repo.git", 413 | "svn_url": "https://github.com/hogelog/test-repo", 414 | "homepage": null, 415 | "size": 0, 416 | "stargazers_count": 0, 417 | "watchers_count": 0, 418 | "language": "Ruby", 419 | "has_issues": true, 420 | "has_projects": true, 421 | "has_downloads": true, 422 | "has_wiki": true, 423 | "has_pages": false, 424 | "forks_count": 0, 425 | "mirror_url": null, 426 | "open_issues_count": 1, 427 | "forks": 0, 428 | "open_issues": 1, 429 | "watchers": 0, 430 | "default_branch": "master" 431 | }, 432 | "sender": { 433 | "login": "hogelog", 434 | "id": 50920, 435 | "avatar_url": "https://avatars2.githubusercontent.com/u/50920?v=3", 436 | "gravatar_id": "", 437 | "url": "https://api.github.com/users/hogelog", 438 | "html_url": "https://github.com/hogelog", 439 | "followers_url": "https://api.github.com/users/hogelog/followers", 440 | "following_url": "https://api.github.com/users/hogelog/following{/other_user}", 441 | "gists_url": "https://api.github.com/users/hogelog/gists{/gist_id}", 442 | "starred_url": "https://api.github.com/users/hogelog/starred{/owner}{/repo}", 443 | "subscriptions_url": "https://api.github.com/users/hogelog/subscriptions", 444 | "organizations_url": "https://api.github.com/users/hogelog/orgs", 445 | "repos_url": "https://api.github.com/users/hogelog/repos", 446 | "events_url": "https://api.github.com/users/hogelog/events{/privacy}", 447 | "received_events_url": "https://api.github.com/users/hogelog/received_events", 448 | "type": "User", 449 | "site_admin": false 450 | } 451 | } 452 | -------------------------------------------------------------------------------- /spec/payloads/pull_request_review.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "submitted", 3 | "review": { 4 | "id": 57343613, 5 | "user": { 6 | "login": "hogelog", 7 | "id": 50920, 8 | "avatar_url": "https://avatars1.githubusercontent.com/u/50920?v=4", 9 | "gravatar_id": "", 10 | "url": "https://api.github.com/users/hogelog", 11 | "html_url": "https://github.com/hogelog", 12 | "followers_url": "https://api.github.com/users/hogelog/followers", 13 | "following_url": "https://api.github.com/users/hogelog/following{/other_user}", 14 | "gists_url": "https://api.github.com/users/hogelog/gists{/gist_id}", 15 | "starred_url": "https://api.github.com/users/hogelog/starred{/owner}{/repo}", 16 | "subscriptions_url": "https://api.github.com/users/hogelog/subscriptions", 17 | "organizations_url": "https://api.github.com/users/hogelog/orgs", 18 | "repos_url": "https://api.github.com/users/hogelog/repos", 19 | "events_url": "https://api.github.com/users/hogelog/events{/privacy}", 20 | "received_events_url": "https://api.github.com/users/hogelog/received_events", 21 | "type": "User", 22 | "site_admin": false 23 | }, 24 | "body": "Hi", 25 | "commit_id": "5ed03c5fb86d88994fda89c656cc4c9c45d0d317", 26 | "submitted_at": "2017-08-19T06:45:02Z", 27 | "state": "commented", 28 | "html_url": "https://github.com/hogelog/test-repo/pull/6#pullrequestreview-57343613", 29 | "pull_request_url": "https://api.github.com/repos/hogelog/test-repo/pulls/6", 30 | "_links": { 31 | "html": { 32 | "href": "https://github.com/hogelog/test-repo/pull/6#pullrequestreview-57343613" 33 | }, 34 | "pull_request": { 35 | "href": "https://api.github.com/repos/hogelog/test-repo/pulls/6" 36 | } 37 | } 38 | }, 39 | "pull_request": { 40 | "url": "https://api.github.com/repos/hogelog/test-repo/pulls/6", 41 | "id": 123948075, 42 | "html_url": "https://github.com/hogelog/test-repo/pull/6", 43 | "diff_url": "https://github.com/hogelog/test-repo/pull/6.diff", 44 | "patch_url": "https://github.com/hogelog/test-repo/pull/6.patch", 45 | "issue_url": "https://api.github.com/repos/hogelog/test-repo/issues/6", 46 | "number": 6, 47 | "state": "open", 48 | "locked": false, 49 | "title": "Moge", 50 | "user": { 51 | "login": "hogelog", 52 | "id": 50920, 53 | "avatar_url": "https://avatars1.githubusercontent.com/u/50920?v=4", 54 | "gravatar_id": "", 55 | "url": "https://api.github.com/users/hogelog", 56 | "html_url": "https://github.com/hogelog", 57 | "followers_url": "https://api.github.com/users/hogelog/followers", 58 | "following_url": "https://api.github.com/users/hogelog/following{/other_user}", 59 | "gists_url": "https://api.github.com/users/hogelog/gists{/gist_id}", 60 | "starred_url": "https://api.github.com/users/hogelog/starred{/owner}{/repo}", 61 | "subscriptions_url": "https://api.github.com/users/hogelog/subscriptions", 62 | "organizations_url": "https://api.github.com/users/hogelog/orgs", 63 | "repos_url": "https://api.github.com/users/hogelog/repos", 64 | "events_url": "https://api.github.com/users/hogelog/events{/privacy}", 65 | "received_events_url": "https://api.github.com/users/hogelog/received_events", 66 | "type": "User", 67 | "site_admin": false 68 | }, 69 | "body": "aaaaaaaa\r\nbbb", 70 | "created_at": "2017-06-05T10:03:09Z", 71 | "updated_at": "2017-08-19T06:45:02Z", 72 | "closed_at": null, 73 | "merged_at": null, 74 | "merge_commit_sha": "064210440b09c7d4cfb5bd7a301fe50127a86e6d", 75 | "assignee": null, 76 | "assignees": [ 77 | 78 | ], 79 | "requested_reviewers": [ 80 | 81 | ], 82 | "requested_teams": [ 83 | 84 | ], 85 | "milestone": null, 86 | "commits_url": "https://api.github.com/repos/hogelog/test-repo/pulls/6/commits", 87 | "review_comments_url": "https://api.github.com/repos/hogelog/test-repo/pulls/6/comments", 88 | "review_comment_url": "https://api.github.com/repos/hogelog/test-repo/pulls/comments{/number}", 89 | "comments_url": "https://api.github.com/repos/hogelog/test-repo/issues/6/comments", 90 | "statuses_url": "https://api.github.com/repos/hogelog/test-repo/statuses/5ed03c5fb86d88994fda89c656cc4c9c45d0d317", 91 | "head": { 92 | "label": "hogelog:moge", 93 | "ref": "moge", 94 | "sha": "5ed03c5fb86d88994fda89c656cc4c9c45d0d317", 95 | "user": { 96 | "login": "hogelog", 97 | "id": 50920, 98 | "avatar_url": "https://avatars1.githubusercontent.com/u/50920?v=4", 99 | "gravatar_id": "", 100 | "url": "https://api.github.com/users/hogelog", 101 | "html_url": "https://github.com/hogelog", 102 | "followers_url": "https://api.github.com/users/hogelog/followers", 103 | "following_url": "https://api.github.com/users/hogelog/following{/other_user}", 104 | "gists_url": "https://api.github.com/users/hogelog/gists{/gist_id}", 105 | "starred_url": "https://api.github.com/users/hogelog/starred{/owner}{/repo}", 106 | "subscriptions_url": "https://api.github.com/users/hogelog/subscriptions", 107 | "organizations_url": "https://api.github.com/users/hogelog/orgs", 108 | "repos_url": "https://api.github.com/users/hogelog/repos", 109 | "events_url": "https://api.github.com/users/hogelog/events{/privacy}", 110 | "received_events_url": "https://api.github.com/users/hogelog/received_events", 111 | "type": "User", 112 | "site_admin": false 113 | }, 114 | "repo": { 115 | "id": 93059937, 116 | "name": "test-repo", 117 | "full_name": "hogelog/test-repo", 118 | "owner": { 119 | "login": "hogelog", 120 | "id": 50920, 121 | "avatar_url": "https://avatars1.githubusercontent.com/u/50920?v=4", 122 | "gravatar_id": "", 123 | "url": "https://api.github.com/users/hogelog", 124 | "html_url": "https://github.com/hogelog", 125 | "followers_url": "https://api.github.com/users/hogelog/followers", 126 | "following_url": "https://api.github.com/users/hogelog/following{/other_user}", 127 | "gists_url": "https://api.github.com/users/hogelog/gists{/gist_id}", 128 | "starred_url": "https://api.github.com/users/hogelog/starred{/owner}{/repo}", 129 | "subscriptions_url": "https://api.github.com/users/hogelog/subscriptions", 130 | "organizations_url": "https://api.github.com/users/hogelog/orgs", 131 | "repos_url": "https://api.github.com/users/hogelog/repos", 132 | "events_url": "https://api.github.com/users/hogelog/events{/privacy}", 133 | "received_events_url": "https://api.github.com/users/hogelog/received_events", 134 | "type": "User", 135 | "site_admin": false 136 | }, 137 | "private": false, 138 | "html_url": "https://github.com/hogelog/test-repo", 139 | "description": null, 140 | "fork": false, 141 | "url": "https://api.github.com/repos/hogelog/test-repo", 142 | "forks_url": "https://api.github.com/repos/hogelog/test-repo/forks", 143 | "keys_url": "https://api.github.com/repos/hogelog/test-repo/keys{/key_id}", 144 | "collaborators_url": "https://api.github.com/repos/hogelog/test-repo/collaborators{/collaborator}", 145 | "teams_url": "https://api.github.com/repos/hogelog/test-repo/teams", 146 | "hooks_url": "https://api.github.com/repos/hogelog/test-repo/hooks", 147 | "issue_events_url": "https://api.github.com/repos/hogelog/test-repo/issues/events{/number}", 148 | "events_url": "https://api.github.com/repos/hogelog/test-repo/events", 149 | "assignees_url": "https://api.github.com/repos/hogelog/test-repo/assignees{/user}", 150 | "branches_url": "https://api.github.com/repos/hogelog/test-repo/branches{/branch}", 151 | "tags_url": "https://api.github.com/repos/hogelog/test-repo/tags", 152 | "blobs_url": "https://api.github.com/repos/hogelog/test-repo/git/blobs{/sha}", 153 | "git_tags_url": "https://api.github.com/repos/hogelog/test-repo/git/tags{/sha}", 154 | "git_refs_url": "https://api.github.com/repos/hogelog/test-repo/git/refs{/sha}", 155 | "trees_url": "https://api.github.com/repos/hogelog/test-repo/git/trees{/sha}", 156 | "statuses_url": "https://api.github.com/repos/hogelog/test-repo/statuses/{sha}", 157 | "languages_url": "https://api.github.com/repos/hogelog/test-repo/languages", 158 | "stargazers_url": "https://api.github.com/repos/hogelog/test-repo/stargazers", 159 | "contributors_url": "https://api.github.com/repos/hogelog/test-repo/contributors", 160 | "subscribers_url": "https://api.github.com/repos/hogelog/test-repo/subscribers", 161 | "subscription_url": "https://api.github.com/repos/hogelog/test-repo/subscription", 162 | "commits_url": "https://api.github.com/repos/hogelog/test-repo/commits{/sha}", 163 | "git_commits_url": "https://api.github.com/repos/hogelog/test-repo/git/commits{/sha}", 164 | "comments_url": "https://api.github.com/repos/hogelog/test-repo/comments{/number}", 165 | "issue_comment_url": "https://api.github.com/repos/hogelog/test-repo/issues/comments{/number}", 166 | "contents_url": "https://api.github.com/repos/hogelog/test-repo/contents/{+path}", 167 | "compare_url": "https://api.github.com/repos/hogelog/test-repo/compare/{base}...{head}", 168 | "merges_url": "https://api.github.com/repos/hogelog/test-repo/merges", 169 | "archive_url": "https://api.github.com/repos/hogelog/test-repo/{archive_format}{/ref}", 170 | "downloads_url": "https://api.github.com/repos/hogelog/test-repo/downloads", 171 | "issues_url": "https://api.github.com/repos/hogelog/test-repo/issues{/number}", 172 | "pulls_url": "https://api.github.com/repos/hogelog/test-repo/pulls{/number}", 173 | "milestones_url": "https://api.github.com/repos/hogelog/test-repo/milestones{/number}", 174 | "notifications_url": "https://api.github.com/repos/hogelog/test-repo/notifications{?since,all,participating}", 175 | "labels_url": "https://api.github.com/repos/hogelog/test-repo/labels{/name}", 176 | "releases_url": "https://api.github.com/repos/hogelog/test-repo/releases{/id}", 177 | "deployments_url": "https://api.github.com/repos/hogelog/test-repo/deployments", 178 | "created_at": "2017-06-01T13:26:00Z", 179 | "updated_at": "2017-06-01T14:03:40Z", 180 | "pushed_at": "2017-06-05T14:35:20Z", 181 | "git_url": "git://github.com/hogelog/test-repo.git", 182 | "ssh_url": "git@github.com:hogelog/test-repo.git", 183 | "clone_url": "https://github.com/hogelog/test-repo.git", 184 | "svn_url": "https://github.com/hogelog/test-repo", 185 | "homepage": null, 186 | "size": 1, 187 | "stargazers_count": 0, 188 | "watchers_count": 0, 189 | "language": "Ruby", 190 | "has_issues": true, 191 | "has_projects": true, 192 | "has_downloads": true, 193 | "has_wiki": true, 194 | "has_pages": false, 195 | "forks_count": 0, 196 | "mirror_url": null, 197 | "open_issues_count": 4, 198 | "forks": 0, 199 | "open_issues": 4, 200 | "watchers": 0, 201 | "default_branch": "master" 202 | } 203 | }, 204 | "base": { 205 | "label": "hogelog:master", 206 | "ref": "master", 207 | "sha": "e75b5b1c499aebaa17f125089aef5e50e6f3a1a3", 208 | "user": { 209 | "login": "hogelog", 210 | "id": 50920, 211 | "avatar_url": "https://avatars1.githubusercontent.com/u/50920?v=4", 212 | "gravatar_id": "", 213 | "url": "https://api.github.com/users/hogelog", 214 | "html_url": "https://github.com/hogelog", 215 | "followers_url": "https://api.github.com/users/hogelog/followers", 216 | "following_url": "https://api.github.com/users/hogelog/following{/other_user}", 217 | "gists_url": "https://api.github.com/users/hogelog/gists{/gist_id}", 218 | "starred_url": "https://api.github.com/users/hogelog/starred{/owner}{/repo}", 219 | "subscriptions_url": "https://api.github.com/users/hogelog/subscriptions", 220 | "organizations_url": "https://api.github.com/users/hogelog/orgs", 221 | "repos_url": "https://api.github.com/users/hogelog/repos", 222 | "events_url": "https://api.github.com/users/hogelog/events{/privacy}", 223 | "received_events_url": "https://api.github.com/users/hogelog/received_events", 224 | "type": "User", 225 | "site_admin": false 226 | }, 227 | "repo": { 228 | "id": 93059937, 229 | "name": "test-repo", 230 | "full_name": "hogelog/test-repo", 231 | "owner": { 232 | "login": "hogelog", 233 | "id": 50920, 234 | "avatar_url": "https://avatars1.githubusercontent.com/u/50920?v=4", 235 | "gravatar_id": "", 236 | "url": "https://api.github.com/users/hogelog", 237 | "html_url": "https://github.com/hogelog", 238 | "followers_url": "https://api.github.com/users/hogelog/followers", 239 | "following_url": "https://api.github.com/users/hogelog/following{/other_user}", 240 | "gists_url": "https://api.github.com/users/hogelog/gists{/gist_id}", 241 | "starred_url": "https://api.github.com/users/hogelog/starred{/owner}{/repo}", 242 | "subscriptions_url": "https://api.github.com/users/hogelog/subscriptions", 243 | "organizations_url": "https://api.github.com/users/hogelog/orgs", 244 | "repos_url": "https://api.github.com/users/hogelog/repos", 245 | "events_url": "https://api.github.com/users/hogelog/events{/privacy}", 246 | "received_events_url": "https://api.github.com/users/hogelog/received_events", 247 | "type": "User", 248 | "site_admin": false 249 | }, 250 | "private": false, 251 | "html_url": "https://github.com/hogelog/test-repo", 252 | "description": null, 253 | "fork": false, 254 | "url": "https://api.github.com/repos/hogelog/test-repo", 255 | "forks_url": "https://api.github.com/repos/hogelog/test-repo/forks", 256 | "keys_url": "https://api.github.com/repos/hogelog/test-repo/keys{/key_id}", 257 | "collaborators_url": "https://api.github.com/repos/hogelog/test-repo/collaborators{/collaborator}", 258 | "teams_url": "https://api.github.com/repos/hogelog/test-repo/teams", 259 | "hooks_url": "https://api.github.com/repos/hogelog/test-repo/hooks", 260 | "issue_events_url": "https://api.github.com/repos/hogelog/test-repo/issues/events{/number}", 261 | "events_url": "https://api.github.com/repos/hogelog/test-repo/events", 262 | "assignees_url": "https://api.github.com/repos/hogelog/test-repo/assignees{/user}", 263 | "branches_url": "https://api.github.com/repos/hogelog/test-repo/branches{/branch}", 264 | "tags_url": "https://api.github.com/repos/hogelog/test-repo/tags", 265 | "blobs_url": "https://api.github.com/repos/hogelog/test-repo/git/blobs{/sha}", 266 | "git_tags_url": "https://api.github.com/repos/hogelog/test-repo/git/tags{/sha}", 267 | "git_refs_url": "https://api.github.com/repos/hogelog/test-repo/git/refs{/sha}", 268 | "trees_url": "https://api.github.com/repos/hogelog/test-repo/git/trees{/sha}", 269 | "statuses_url": "https://api.github.com/repos/hogelog/test-repo/statuses/{sha}", 270 | "languages_url": "https://api.github.com/repos/hogelog/test-repo/languages", 271 | "stargazers_url": "https://api.github.com/repos/hogelog/test-repo/stargazers", 272 | "contributors_url": "https://api.github.com/repos/hogelog/test-repo/contributors", 273 | "subscribers_url": "https://api.github.com/repos/hogelog/test-repo/subscribers", 274 | "subscription_url": "https://api.github.com/repos/hogelog/test-repo/subscription", 275 | "commits_url": "https://api.github.com/repos/hogelog/test-repo/commits{/sha}", 276 | "git_commits_url": "https://api.github.com/repos/hogelog/test-repo/git/commits{/sha}", 277 | "comments_url": "https://api.github.com/repos/hogelog/test-repo/comments{/number}", 278 | "issue_comment_url": "https://api.github.com/repos/hogelog/test-repo/issues/comments{/number}", 279 | "contents_url": "https://api.github.com/repos/hogelog/test-repo/contents/{+path}", 280 | "compare_url": "https://api.github.com/repos/hogelog/test-repo/compare/{base}...{head}", 281 | "merges_url": "https://api.github.com/repos/hogelog/test-repo/merges", 282 | "archive_url": "https://api.github.com/repos/hogelog/test-repo/{archive_format}{/ref}", 283 | "downloads_url": "https://api.github.com/repos/hogelog/test-repo/downloads", 284 | "issues_url": "https://api.github.com/repos/hogelog/test-repo/issues{/number}", 285 | "pulls_url": "https://api.github.com/repos/hogelog/test-repo/pulls{/number}", 286 | "milestones_url": "https://api.github.com/repos/hogelog/test-repo/milestones{/number}", 287 | "notifications_url": "https://api.github.com/repos/hogelog/test-repo/notifications{?since,all,participating}", 288 | "labels_url": "https://api.github.com/repos/hogelog/test-repo/labels{/name}", 289 | "releases_url": "https://api.github.com/repos/hogelog/test-repo/releases{/id}", 290 | "deployments_url": "https://api.github.com/repos/hogelog/test-repo/deployments", 291 | "created_at": "2017-06-01T13:26:00Z", 292 | "updated_at": "2017-06-01T14:03:40Z", 293 | "pushed_at": "2017-06-05T14:35:20Z", 294 | "git_url": "git://github.com/hogelog/test-repo.git", 295 | "ssh_url": "git@github.com:hogelog/test-repo.git", 296 | "clone_url": "https://github.com/hogelog/test-repo.git", 297 | "svn_url": "https://github.com/hogelog/test-repo", 298 | "homepage": null, 299 | "size": 1, 300 | "stargazers_count": 0, 301 | "watchers_count": 0, 302 | "language": "Ruby", 303 | "has_issues": true, 304 | "has_projects": true, 305 | "has_downloads": true, 306 | "has_wiki": true, 307 | "has_pages": false, 308 | "forks_count": 0, 309 | "mirror_url": null, 310 | "open_issues_count": 4, 311 | "forks": 0, 312 | "open_issues": 4, 313 | "watchers": 0, 314 | "default_branch": "master" 315 | } 316 | }, 317 | "_links": { 318 | "self": { 319 | "href": "https://api.github.com/repos/hogelog/test-repo/pulls/6" 320 | }, 321 | "html": { 322 | "href": "https://github.com/hogelog/test-repo/pull/6" 323 | }, 324 | "issue": { 325 | "href": "https://api.github.com/repos/hogelog/test-repo/issues/6" 326 | }, 327 | "comments": { 328 | "href": "https://api.github.com/repos/hogelog/test-repo/issues/6/comments" 329 | }, 330 | "review_comments": { 331 | "href": "https://api.github.com/repos/hogelog/test-repo/pulls/6/comments" 332 | }, 333 | "review_comment": { 334 | "href": "https://api.github.com/repos/hogelog/test-repo/pulls/comments{/number}" 335 | }, 336 | "commits": { 337 | "href": "https://api.github.com/repos/hogelog/test-repo/pulls/6/commits" 338 | }, 339 | "statuses": { 340 | "href": "https://api.github.com/repos/hogelog/test-repo/statuses/5ed03c5fb86d88994fda89c656cc4c9c45d0d317" 341 | } 342 | } 343 | }, 344 | "repository": { 345 | "id": 93059937, 346 | "name": "test-repo", 347 | "full_name": "hogelog/test-repo", 348 | "owner": { 349 | "login": "hogelog", 350 | "id": 50920, 351 | "avatar_url": "https://avatars1.githubusercontent.com/u/50920?v=4", 352 | "gravatar_id": "", 353 | "url": "https://api.github.com/users/hogelog", 354 | "html_url": "https://github.com/hogelog", 355 | "followers_url": "https://api.github.com/users/hogelog/followers", 356 | "following_url": "https://api.github.com/users/hogelog/following{/other_user}", 357 | "gists_url": "https://api.github.com/users/hogelog/gists{/gist_id}", 358 | "starred_url": "https://api.github.com/users/hogelog/starred{/owner}{/repo}", 359 | "subscriptions_url": "https://api.github.com/users/hogelog/subscriptions", 360 | "organizations_url": "https://api.github.com/users/hogelog/orgs", 361 | "repos_url": "https://api.github.com/users/hogelog/repos", 362 | "events_url": "https://api.github.com/users/hogelog/events{/privacy}", 363 | "received_events_url": "https://api.github.com/users/hogelog/received_events", 364 | "type": "User", 365 | "site_admin": false 366 | }, 367 | "private": false, 368 | "html_url": "https://github.com/hogelog/test-repo", 369 | "description": null, 370 | "fork": false, 371 | "url": "https://api.github.com/repos/hogelog/test-repo", 372 | "forks_url": "https://api.github.com/repos/hogelog/test-repo/forks", 373 | "keys_url": "https://api.github.com/repos/hogelog/test-repo/keys{/key_id}", 374 | "collaborators_url": "https://api.github.com/repos/hogelog/test-repo/collaborators{/collaborator}", 375 | "teams_url": "https://api.github.com/repos/hogelog/test-repo/teams", 376 | "hooks_url": "https://api.github.com/repos/hogelog/test-repo/hooks", 377 | "issue_events_url": "https://api.github.com/repos/hogelog/test-repo/issues/events{/number}", 378 | "events_url": "https://api.github.com/repos/hogelog/test-repo/events", 379 | "assignees_url": "https://api.github.com/repos/hogelog/test-repo/assignees{/user}", 380 | "branches_url": "https://api.github.com/repos/hogelog/test-repo/branches{/branch}", 381 | "tags_url": "https://api.github.com/repos/hogelog/test-repo/tags", 382 | "blobs_url": "https://api.github.com/repos/hogelog/test-repo/git/blobs{/sha}", 383 | "git_tags_url": "https://api.github.com/repos/hogelog/test-repo/git/tags{/sha}", 384 | "git_refs_url": "https://api.github.com/repos/hogelog/test-repo/git/refs{/sha}", 385 | "trees_url": "https://api.github.com/repos/hogelog/test-repo/git/trees{/sha}", 386 | "statuses_url": "https://api.github.com/repos/hogelog/test-repo/statuses/{sha}", 387 | "languages_url": "https://api.github.com/repos/hogelog/test-repo/languages", 388 | "stargazers_url": "https://api.github.com/repos/hogelog/test-repo/stargazers", 389 | "contributors_url": "https://api.github.com/repos/hogelog/test-repo/contributors", 390 | "subscribers_url": "https://api.github.com/repos/hogelog/test-repo/subscribers", 391 | "subscription_url": "https://api.github.com/repos/hogelog/test-repo/subscription", 392 | "commits_url": "https://api.github.com/repos/hogelog/test-repo/commits{/sha}", 393 | "git_commits_url": "https://api.github.com/repos/hogelog/test-repo/git/commits{/sha}", 394 | "comments_url": "https://api.github.com/repos/hogelog/test-repo/comments{/number}", 395 | "issue_comment_url": "https://api.github.com/repos/hogelog/test-repo/issues/comments{/number}", 396 | "contents_url": "https://api.github.com/repos/hogelog/test-repo/contents/{+path}", 397 | "compare_url": "https://api.github.com/repos/hogelog/test-repo/compare/{base}...{head}", 398 | "merges_url": "https://api.github.com/repos/hogelog/test-repo/merges", 399 | "archive_url": "https://api.github.com/repos/hogelog/test-repo/{archive_format}{/ref}", 400 | "downloads_url": "https://api.github.com/repos/hogelog/test-repo/downloads", 401 | "issues_url": "https://api.github.com/repos/hogelog/test-repo/issues{/number}", 402 | "pulls_url": "https://api.github.com/repos/hogelog/test-repo/pulls{/number}", 403 | "milestones_url": "https://api.github.com/repos/hogelog/test-repo/milestones{/number}", 404 | "notifications_url": "https://api.github.com/repos/hogelog/test-repo/notifications{?since,all,participating}", 405 | "labels_url": "https://api.github.com/repos/hogelog/test-repo/labels{/name}", 406 | "releases_url": "https://api.github.com/repos/hogelog/test-repo/releases{/id}", 407 | "deployments_url": "https://api.github.com/repos/hogelog/test-repo/deployments", 408 | "created_at": "2017-06-01T13:26:00Z", 409 | "updated_at": "2017-06-01T14:03:40Z", 410 | "pushed_at": "2017-06-05T14:35:20Z", 411 | "git_url": "git://github.com/hogelog/test-repo.git", 412 | "ssh_url": "git@github.com:hogelog/test-repo.git", 413 | "clone_url": "https://github.com/hogelog/test-repo.git", 414 | "svn_url": "https://github.com/hogelog/test-repo", 415 | "homepage": null, 416 | "size": 1, 417 | "stargazers_count": 0, 418 | "watchers_count": 0, 419 | "language": "Ruby", 420 | "has_issues": true, 421 | "has_projects": true, 422 | "has_downloads": true, 423 | "has_wiki": true, 424 | "has_pages": false, 425 | "forks_count": 0, 426 | "mirror_url": null, 427 | "open_issues_count": 4, 428 | "forks": 0, 429 | "open_issues": 4, 430 | "watchers": 0, 431 | "default_branch": "master" 432 | }, 433 | "sender": { 434 | "login": "hogelog", 435 | "id": 50920, 436 | "avatar_url": "https://avatars1.githubusercontent.com/u/50920?v=4", 437 | "gravatar_id": "", 438 | "url": "https://api.github.com/users/hogelog", 439 | "html_url": "https://github.com/hogelog", 440 | "followers_url": "https://api.github.com/users/hogelog/followers", 441 | "following_url": "https://api.github.com/users/hogelog/following{/other_user}", 442 | "gists_url": "https://api.github.com/users/hogelog/gists{/gist_id}", 443 | "starred_url": "https://api.github.com/users/hogelog/starred{/owner}{/repo}", 444 | "subscriptions_url": "https://api.github.com/users/hogelog/subscriptions", 445 | "organizations_url": "https://api.github.com/users/hogelog/orgs", 446 | "repos_url": "https://api.github.com/users/hogelog/repos", 447 | "events_url": "https://api.github.com/users/hogelog/events{/privacy}", 448 | "received_events_url": "https://api.github.com/users/hogelog/received_events", 449 | "type": "User", 450 | "site_admin": false 451 | } 452 | } 453 | --------------------------------------------------------------------------------