collavre_github 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,207 @@
1
+ module CollavreGithub
2
+ class GithubPrChannel < Collavre::Channel
3
+ self.table_name = "channels"
4
+
5
+ def repo_full_name
6
+ config["repo_full_name"]
7
+ end
8
+
9
+ def pr_number
10
+ config["pr_number"].to_i
11
+ end
12
+
13
+ def pr_url
14
+ "https://github.com/#{repo_full_name}/pull/#{pr_number}"
15
+ end
16
+
17
+ def label
18
+ t("label", number: pr_number)
19
+ end
20
+
21
+ # Chip fallbacks: derived directly from config so the chip can render the
22
+ # full "PR #N" + URL immediately on attach, without waiting for the first
23
+ # webhook event to populate latest_label / latest_link.
24
+ def default_label
25
+ label
26
+ end
27
+
28
+ def default_link
29
+ pr_url
30
+ end
31
+
32
+ def badge_state
33
+ pr_state
34
+ end
35
+
36
+ def badge_title
37
+ I18n.t("collavre_github.channel.pr.badge.#{pr_state}", default: pr_state.to_s)
38
+ end
39
+
40
+ # PR lifecycle state used by the chip badge color. Defaults to "open" so
41
+ # freshly attached channels render the green badge before any close event
42
+ # has been received. Persisted in `config` to avoid a schema change for a
43
+ # channel-subtype-specific concern.
44
+ PR_STATES = %w[open merged closed_without_merge].freeze
45
+
46
+ def pr_state
47
+ state = config["pr_state"].to_s
48
+ PR_STATES.include?(state) ? state : "open"
49
+ end
50
+
51
+ # Symmetric with the reader: refuse to persist values outside PR_STATES
52
+ # rather than silently downgrading to "open" at read time. Without this
53
+ # any caller that mistypes (e.g. "merged_") would corrupt the badge color
54
+ # with no error surfaced.
55
+ def pr_state=(value)
56
+ value = value.to_s
57
+ raise ArgumentError, "Invalid pr_state: #{value.inspect}" unless PR_STATES.include?(value)
58
+ self.config = config.merge("pr_state" => value)
59
+ end
60
+
61
+ def attached_message
62
+ Collavre::Channel::InjectedMessage.new(
63
+ speaker: channel_bot_user,
64
+ message: t("attached_message", label: label, url: pr_url),
65
+ label: label,
66
+ link: pr_url
67
+ )
68
+ end
69
+
70
+ def reopened_message
71
+ Collavre::Channel::InjectedMessage.new(
72
+ speaker: channel_bot_user,
73
+ message: t("reopened_message", label: label, url: pr_url),
74
+ label: label,
75
+ link: pr_url
76
+ )
77
+ end
78
+
79
+ def handle(event:, payload:)
80
+ case event
81
+ when "issue_comment"
82
+ handle_issue_comment(payload)
83
+ when "pull_request_review_comment"
84
+ handle_review_comment(payload)
85
+ when "pull_request_review"
86
+ handle_review_submitted(payload)
87
+ when "pull_request"
88
+ handle_pull_request(payload)
89
+ end
90
+ end
91
+
92
+ private
93
+
94
+ def handle_pull_request(payload)
95
+ return nil unless payload["action"] == "closed"
96
+ pr = payload["pull_request"]
97
+ new_state = pr["merged"] ? "merged" : "closed_without_merge"
98
+ verb = pr["merged"] ? t("state_merged") : t("state_closed")
99
+
100
+ # pr_state is updated atomically with the closing comment: the dispatch
101
+ # path wraps both `handle` and `inject_into_topic!` in a single with_lock
102
+ # transaction, so an inject failure rolls this update back too. That is
103
+ # the intended behavior — we don't want the chip to flash merged/closed
104
+ # without the matching closing message in the timeline.
105
+ self.pr_state = new_state
106
+ save!
107
+
108
+ Collavre::Channel::InjectedMessage.new(
109
+ speaker: channel_bot_user,
110
+ message: t("closed_message", label: label, verb: verb),
111
+ label: label,
112
+ link: pr_url
113
+ )
114
+ # Detach is performed by the webhook controller AFTER injecting this
115
+ # message, so the chip stays visible until the closing comment lands.
116
+ end
117
+
118
+ def handle_review_submitted(payload)
119
+ return nil unless payload["action"] == "submitted"
120
+ review = payload["review"]
121
+ body = review["body"].to_s
122
+ return nil if body.strip.empty?
123
+
124
+ author = review.dig("user", "login")
125
+ return nil if ignored_actor?(author)
126
+ state = review["state"]
127
+ Collavre::Channel::InjectedMessage.new(
128
+ speaker: channel_bot_user,
129
+ message: t("review_submitted_message", author: author, state: state, label: label, body: body),
130
+ label: label,
131
+ link: pr_url
132
+ )
133
+ end
134
+
135
+ def handle_review_comment(payload)
136
+ return nil unless payload["action"] == "created"
137
+ comment = payload["comment"]
138
+ author = comment.dig("user", "login")
139
+ return nil if ignored_actor?(author)
140
+ path = comment["path"]
141
+ line = comment["line"]
142
+ body = comment["body"].to_s
143
+
144
+ location =
145
+ if path && line
146
+ t("review_comment_location_path_line", path: path, line: line)
147
+ elsif path
148
+ t("review_comment_location_path", path: path)
149
+ else
150
+ ""
151
+ end
152
+ Collavre::Channel::InjectedMessage.new(
153
+ speaker: channel_bot_user,
154
+ message: t("review_comment_message", author: author, label: label, location: location, body: body),
155
+ label: label,
156
+ link: pr_url
157
+ )
158
+ end
159
+
160
+ def handle_issue_comment(payload)
161
+ return nil unless payload["action"] == "created"
162
+ return nil unless payload.dig("issue", "pull_request") # PR comments only
163
+
164
+ comment = payload["comment"]
165
+ author = comment.dig("user", "login")
166
+ return nil if ignored_actor?(author)
167
+ body = comment["body"].to_s
168
+
169
+ Collavre::Channel::InjectedMessage.new(
170
+ speaker: channel_bot_user,
171
+ message: t("comment_message", author: author, label: label, body: body),
172
+ label: label,
173
+ link: pr_url
174
+ )
175
+ end
176
+
177
+ def ignored_actor?(login)
178
+ Array(config["ignore_actor_logins"]).include?(login)
179
+ end
180
+
181
+ def t(key, **opts)
182
+ I18n.t("collavre_github.channel.pr.#{key}", **opts)
183
+ end
184
+
185
+ # Find the channel bot user. Falls back to creating the row when missing
186
+ # (e.g. migrations applied but `db:seed` was skipped). Without this
187
+ # fallback every PR webhook would raise RecordNotFound and the event
188
+ # would be silently dropped by the controller's rescue.
189
+ def channel_bot_user
190
+ @channel_bot_user ||=
191
+ Collavre::User.find_by(email: Collavre::Channel::BOT_EMAIL) ||
192
+ ensure_channel_bot_user!
193
+ end
194
+
195
+ def ensure_channel_bot_user!
196
+ email = Collavre::Channel::BOT_EMAIL
197
+ user = Collavre::User.find_or_initialize_by(email: email)
198
+ user.name = Collavre::Channel::BOT_NAME
199
+ user.password = SecureRandom.hex(32) if user.new_record?
200
+ user.email_verified_at ||= Time.current
201
+ user.searchable = false if user.respond_to?(:searchable=)
202
+ user.llm_vendor = nil
203
+ user.save!
204
+ user
205
+ end
206
+ end
207
+ end
@@ -162,12 +162,26 @@ module CollavreGithub
162
162
 
163
163
  attr_reader :client
164
164
 
165
- # Use GITHUB_API_ENDPOINT env var if set, otherwise fall back to mock server
166
- # in development when no real GitHub credentials are configured.
165
+ # Resolve the API endpoint via Resolver (DB > ENV), otherwise fall back to
166
+ # the mock server in development when no real GitHub credentials are
167
+ # configured.
167
168
  def resolve_api_endpoint
168
- return ENV["GITHUB_API_ENDPOINT"] if ENV["GITHUB_API_ENDPOINT"].present?
169
-
170
- if Rails.env.development? && ENV["GITHUB_CLIENT_ID"].blank?
169
+ endpoint =
170
+ if defined?(Collavre::IntegrationSettings::Resolver)
171
+ Collavre::IntegrationSettings::Resolver.get(:github_api_endpoint).presence
172
+ else
173
+ ENV["GITHUB_API_ENDPOINT"].presence
174
+ end
175
+ return endpoint if endpoint.present?
176
+
177
+ github_client_id =
178
+ if defined?(Collavre::IntegrationSettings::Resolver)
179
+ Collavre::IntegrationSettings::Resolver.get(:github_client_id).presence
180
+ else
181
+ ENV["GITHUB_CLIENT_ID"].presence
182
+ end
183
+
184
+ if Rails.env.development? && github_client_id.blank?
171
185
  MOCK_SERVER_DEFAULT
172
186
  end
173
187
  end
@@ -0,0 +1,11 @@
1
+ module CollavreGithub
2
+ class PrTopicLinkParser
3
+ TOPIC_RE = %r{/creatives/\d+/topics/(\d+)}.freeze
4
+
5
+ def self.call(body)
6
+ return nil if body.blank?
7
+ m = body.match(TOPIC_RE)
8
+ m && m[1].to_i
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CollavreGithub
4
+ module Tools
5
+ # Kept as an alias of the core engine's shared error so existing rescue
6
+ # blocks and tests in collavre_github continue to match. New code should
7
+ # reference `Collavre::Tools::PermissionDeniedError` directly.
8
+ PermissionDeniedError = ::Collavre::Tools::PermissionDeniedError
9
+ end
10
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sorbet-runtime"
4
+ require "rails_mcp_engine"
5
+
6
+ module CollavreGithub
7
+ module Tools
8
+ class PrMonitorService
9
+ extend T::Sig
10
+ extend ToolMeta
11
+
12
+ PR_URL_RE = %r{\Ahttps?://github\.com/([^/]+/[^/]+)/pull/(\d+)\z}.freeze
13
+
14
+ tool_name "pr_monitor"
15
+ tool_description <<~DESC.strip
16
+ Attach a GitHub PR monitor to a Collavre topic. After attachment,
17
+ PR comments, review comments, and review submissions are injected
18
+ into the topic as chat messages. Idempotent.
19
+ DESC
20
+
21
+ tool_param :topic_id, description: "The Collavre topic id to attach the PR channel to."
22
+ tool_param :pr_url, description: "Full GitHub PR URL, e.g. https://github.com/owner/repo/pull/123"
23
+
24
+ sig { params(topic_id: Integer, pr_url: String).returns(T::Hash[Symbol, T.untyped]) }
25
+ def call(topic_id:, pr_url:)
26
+ m = pr_url.match(PR_URL_RE)
27
+ raise ArgumentError, "Invalid PR URL: #{pr_url}" unless m
28
+ # GitHub owner/repo identifiers are case-insensitive but webhook payloads
29
+ # always carry the canonical case. Normalize on store so user input
30
+ # like "Owner/Repo" still matches incoming events.
31
+ repo = m[1].downcase
32
+ pr_number = m[2].to_i
33
+
34
+ topic = Collavre::Topic.find(topic_id)
35
+ Collavre::Tools::TopicAuthorizer.authorize_write!(topic)
36
+ channel, attach_status = find_or_attach_channel(topic, repo, pr_number)
37
+ # Re-seed announcement on fresh attach AND on detached->active so the
38
+ # chip label/link cache is repopulated after the channel was previously
39
+ # auto-detached (PR closed) and the user reattached it.
40
+ if attach_status == :created || attach_status == :reactivated
41
+ channel.inject_into_topic!(channel.attached_message)
42
+ end
43
+
44
+ result = { ok: true, channel_id: channel.id, repo: repo, pr_number: pr_number }
45
+ warning = ensure_webhook_events(topic, repo)
46
+ result[:webhook_warning] = warning if warning
47
+ result
48
+ end
49
+
50
+ private
51
+
52
+ # Wraps Collavre::ChannelAttacher with PR-specific create attrs and a
53
+ # PR-specific reactivation reset (pr_state back to "open"). The shared
54
+ # attacher handles the create/reactivate/noop lifecycle, including the
55
+ # concurrent insert race and dismissed_at clearing — Mirror
56
+ # WebhooksController#maybe_auto_attach_channel reactivation, so the
57
+ # chip resurfaces under the not_dismissed render scope.
58
+ sig { params(topic: Collavre::Topic, repo: String, pr_number: Integer).returns([ CollavreGithub::GithubPrChannel, Symbol ]) }
59
+ def find_or_attach_channel(topic, repo, pr_number)
60
+ Collavre::ChannelAttacher.call(
61
+ channel_class: CollavreGithub::GithubPrChannel,
62
+ lookup: -> { lookup_channel(topic, repo, pr_number) },
63
+ create_attrs: {
64
+ topic_id: topic.id,
65
+ config: { "repo_full_name" => repo, "pr_number" => pr_number, "pr_state" => "open" }
66
+ },
67
+ on_reactivate: ->(channel) {
68
+ channel.pr_state = "open" if channel.pr_state != "open"
69
+ }
70
+ )
71
+ end
72
+
73
+ sig { params(topic: Collavre::Topic, repo: String, pr_number: Integer).returns(T.nilable(CollavreGithub::GithubPrChannel)) }
74
+ def lookup_channel(topic, repo, pr_number)
75
+ CollavreGithub::GithubPrChannel.where(topic_id: topic.id).find do |c|
76
+ c.repo_full_name.to_s.downcase == repo.downcase && c.pr_number == pr_number
77
+ end
78
+ end
79
+
80
+ # Make sure the repo's webhook subscribes to the PR-channel events
81
+ # (issue_comment / pull_request_review / pull_request_review_comment).
82
+ # Without these, GitHub never delivers comment payloads and the channel
83
+ # silently misses them — exactly the bug that motivated this method.
84
+ # Returns a warning string when provisioning cannot run or fails; nil on
85
+ # success so the MCP response stays clean.
86
+ def ensure_webhook_events(topic, repo)
87
+ scoped_link = scoped_repository_link_for(topic, repo)
88
+ return "no RepositoryLink found for #{repo} in topic creative scope; webhook events not auto-provisioned" unless scoped_link
89
+
90
+ # Provision through the *global* primary link (lowest id across all
91
+ # creatives), not the scoped link. WebhookProvisioner only patches hook
92
+ # events when the link IS the primary; non-primary links short-circuit
93
+ # to secret alignment and skip the GitHub edit_hook call. So if we
94
+ # provisioned the scoped link and it was not the global primary, the
95
+ # existing hook would keep its old event list. The scoped link is only
96
+ # used as an authorization gate above.
97
+ provisioning_link = global_primary_repository_link_for(repo) || scoped_link
98
+
99
+ account = provisioning_link.github_account
100
+ return "RepositoryLink for #{repo} has no GitHub account; webhook events not auto-provisioned" unless account
101
+
102
+ results = CollavreGithub::WebhookProvisioner.ensure_for_links(
103
+ account: account,
104
+ links: [ provisioning_link ],
105
+ webhook_url: github_webhook_url
106
+ )
107
+ status = results.first&.last
108
+ # :failed means Client returned nil (Octokit/Faraday error rescued in
109
+ # CollavreGithub::Client). Surface that to the MCP caller so they know
110
+ # webhook events were not actually patched.
111
+ return "webhook provisioning failed: GitHub API rejected the hook request (see logs)" if status == :failed
112
+ nil
113
+ rescue => e
114
+ Rails.logger.warn("[pr_monitor] webhook provisioning failed for #{repo}: #{e.class}: #{e.message}")
115
+ "webhook provisioning failed: #{e.message}"
116
+ end
117
+
118
+ # Authorization gate: a RepositoryLink for `repo` must live in this
119
+ # topic's creative subtree (the topic creative itself or one of its
120
+ # ancestors). Without one, the dispatch path in WebhooksController would
121
+ # silently drop events for this topic anyway.
122
+ def scoped_repository_link_for(topic, repo)
123
+ creative = topic.creative
124
+ return nil unless creative
125
+
126
+ candidate_ids = [ creative.id ] + creative.ancestors.pluck(:id)
127
+ CollavreGithub::RepositoryLink
128
+ .where("LOWER(repository_full_name) = ?", repo.downcase)
129
+ .where(creative_id: candidate_ids)
130
+ .order(:id)
131
+ .first
132
+ end
133
+
134
+ # Mirrors WebhookProvisioner#primary_link_for: the lowest-id link for the
135
+ # repo across ALL creatives. This is the link whose secret + events the
136
+ # hook is aligned to, so we must provision through it to trigger an
137
+ # actual edit_hook call.
138
+ def global_primary_repository_link_for(repo)
139
+ CollavreGithub::RepositoryLink
140
+ .where("LOWER(repository_full_name) = ?", repo.downcase)
141
+ .order(:id)
142
+ .first
143
+ end
144
+
145
+ def github_webhook_url
146
+ CollavreGithub::Engine.routes.url_helpers.webhooks_url(
147
+ Rails.application.config.action_mailer.default_url_options
148
+ )
149
+ end
150
+ end
151
+ end
152
+ end
@@ -1,7 +1,13 @@
1
1
  module CollavreGithub
2
2
  class WebhookProvisioner
3
- EVENTS = %w[pull_request].freeze
4
- EVENTS_WITH_PUSH = %w[pull_request push].freeze
3
+ # PR channel webhooks need every event GithubPrChannel handles. Without
4
+ # `issue_comment` / `pull_request_review` / `pull_request_review_comment`
5
+ # GitHub never delivers the relevant deliveries, so pr_monitor would attach
6
+ # a channel that silently misses comments. `pull_request` is required by
7
+ # the auto-attach + close detection paths.
8
+ CHANNEL_EVENTS = %w[issue_comment pull_request_review pull_request_review_comment].freeze
9
+ EVENTS = (%w[pull_request] + CHANNEL_EVENTS).freeze
10
+ EVENTS_WITH_PUSH = (%w[pull_request push] + CHANNEL_EVENTS).freeze
5
11
  CONTENT_TYPE = "json".freeze
6
12
 
7
13
  def self.ensure_for_links(account:, links:, webhook_url:)
@@ -17,10 +23,15 @@ module CollavreGithub
17
23
  @webhook_url = webhook_url
18
24
  end
19
25
 
26
+ # Returns [[link, status], ...] so callers can detect silent GitHub
27
+ # rejections. status is one of:
28
+ # :created - new hook created
29
+ # :updated - existing hook patched (events/url/secret)
30
+ # :secret_aligned - non-primary link with existing hook; only the local
31
+ # RepositoryLink secret was aligned. No GitHub call.
32
+ # :failed - Octokit/Faraday error OR Client returned nil
20
33
  def ensure_for_links(links)
21
- links.each do |link|
22
- ensure_webhook(link)
23
- end
34
+ links.map { |link| [ link, ensure_webhook(link) ] }
24
35
  end
25
36
 
26
37
  def remove_for_repositories(repositories)
@@ -50,8 +61,9 @@ module CollavreGithub
50
61
  if hook
51
62
  if primary_link && primary_link != link
52
63
  align_link_secret(link, primary_link.webhook_secret)
64
+ :secret_aligned
53
65
  else
54
- update_webhook(repository_full_name, hook.id, link.webhook_secret)
66
+ update_webhook(repository_full_name, hook.id, link.webhook_secret) ? :updated : :failed
55
67
  end
56
68
  else
57
69
  secret = link.webhook_secret
@@ -61,12 +73,13 @@ module CollavreGithub
61
73
  align_link_secret(link, secret)
62
74
  end
63
75
 
64
- create_webhook(repository_full_name, secret)
76
+ create_webhook(repository_full_name, secret) ? :created : :failed
65
77
  end
66
78
  rescue Octokit::Error => e
67
79
  Rails.logger.warn(
68
80
  "GitHub webhook provisioning failed for #{repository_full_name}: #{e.message}"
69
81
  )
82
+ :failed
70
83
  end
71
84
 
72
85
  def remove_webhook(repository_full_name)
@@ -0,0 +1,11 @@
1
+ # Register collavre_github integration setting keys with the central registry.
2
+ # Registered eagerly (not in `to_prepare`) because `github_mock` is read at
3
+ # boot by `config/initializers/omniauth.rb`. The other keys (`webhook_secret`,
4
+ # `api_endpoint`) are resolved per request and don't strictly need eager
5
+ # registration, but we keep them together for clarity.
6
+ if defined?(Collavre::IntegrationSettings::Registry)
7
+ registry = Collavre::IntegrationSettings::Registry.instance
8
+ registry.register(:github_webhook_secret, category: "github", sensitive: true, requires_restart: false)
9
+ registry.register(:github_api_endpoint, category: "github", sensitive: false, requires_restart: false)
10
+ registry.register(:github_mock, category: "github", sensitive: false, requires_restart: true)
11
+ end
@@ -93,3 +93,21 @@ en:
93
93
  updated: "Updated"
94
94
  ready_for_review: "Ready for Review"
95
95
  event: "Event"
96
+ channel:
97
+ pr:
98
+ label: "PR #%{number}"
99
+ label_with_state: "%{label} (%{state})"
100
+ state_merged: "merged"
101
+ state_closed: "closed"
102
+ comment_message: "**@%{author}** commented on %{label}:\n\n%{body}"
103
+ review_comment_message: "**@%{author}** reviewed %{label}%{location}:\n\n%{body}"
104
+ review_comment_location_path: " on `%{path}`"
105
+ review_comment_location_path_line: " on `%{path}`:%{line}"
106
+ review_submitted_message: "**@%{author}** submitted a review (`%{state}`) on %{label}:\n\n%{body}"
107
+ closed_message: "%{label} was **%{verb}**. Detaching channel."
108
+ attached_message: "%{label} monitoring started.\n\n%{url}"
109
+ reopened_message: "%{label} was reopened. Monitoring resumed.\n\n%{url}"
110
+ badge:
111
+ open: "Open"
112
+ merged: "Merged"
113
+ closed_without_merge: "Closed without merging"
@@ -93,3 +93,21 @@ ko:
93
93
  updated: "업데이트됨"
94
94
  ready_for_review: "리뷰 준비됨"
95
95
  event: "이벤트"
96
+ channel:
97
+ pr:
98
+ label: "PR #%{number}"
99
+ label_with_state: "%{label} (%{state})"
100
+ state_merged: "머지됨"
101
+ state_closed: "닫힘"
102
+ comment_message: "**@%{author}**님이 %{label}에 댓글을 남겼습니다:\n\n%{body}"
103
+ review_comment_message: "**@%{author}**님이 %{label}%{location}을(를) 리뷰했습니다:\n\n%{body}"
104
+ review_comment_location_path: " (`%{path}`)"
105
+ review_comment_location_path_line: " (`%{path}`:%{line})"
106
+ review_submitted_message: "**@%{author}**님이 %{label}에 리뷰를 제출했습니다 (`%{state}`):\n\n%{body}"
107
+ closed_message: "%{label}이(가) **%{verb}** 되었습니다. 채널을 해제합니다."
108
+ attached_message: "%{label} 모니터링이 시작되었습니다.\n\n%{url}"
109
+ reopened_message: "%{label}이(가) 재오픈되어 모니터링이 재개되었습니다.\n\n%{url}"
110
+ badge:
111
+ open: "열림"
112
+ merged: "머지됨"
113
+ closed_without_merge: "머지 없이 닫힘"
data/db/seeds.rb CHANGED
@@ -76,6 +76,7 @@ module CollavreGithub
76
76
  github_pr_commits
77
77
  creative_retrieval_service
78
78
  creative_batch_service
79
+ pr_monitor
79
80
  ].freeze
80
81
 
81
82
  def self.call
@@ -1,3 +1,3 @@
1
1
  module CollavreGithub
2
- VERSION = "0.5.0"
2
+ VERSION = "0.6.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: collavre_github
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Collavre
@@ -66,22 +66,28 @@ files:
66
66
  - app/controllers/collavre_github/auth_controller.rb
67
67
  - app/controllers/collavre_github/creatives/integrations_controller.rb
68
68
  - app/controllers/collavre_github/webhooks_controller.rb
69
+ - app/javascript/collavre_github.js
69
70
  - app/jobs/collavre_github/initial_markdown_sync_job.rb
70
71
  - app/jobs/collavre_github/markdown_sync_job.rb
71
72
  - app/models/collavre_github/account.rb
72
73
  - app/models/collavre_github/application_record.rb
74
+ - app/models/collavre_github/github_pr_channel.rb
73
75
  - app/models/collavre_github/repository_link.rb
74
76
  - app/services/collavre_github/client.rb
75
77
  - app/services/collavre_github/markdown_sync/content_processor.rb
76
78
  - app/services/collavre_github/markdown_sync/incremental_sync_service.rb
77
79
  - app/services/collavre_github/markdown_sync/initial_import_service.rb
80
+ - app/services/collavre_github/pr_topic_link_parser.rb
78
81
  - app/services/collavre_github/tools/concerns/github_client_finder.rb
79
82
  - app/services/collavre_github/tools/github_pr_commits_service.rb
80
83
  - app/services/collavre_github/tools/github_pr_details_service.rb
81
84
  - app/services/collavre_github/tools/github_pr_diff_service.rb
85
+ - app/services/collavre_github/tools/permission_denied_error.rb
86
+ - app/services/collavre_github/tools/pr_monitor_service.rb
82
87
  - app/services/collavre_github/webhook_provisioner.rb
83
88
  - app/views/collavre_github/auth/setup.html.erb
84
89
  - app/views/collavre_github/integrations/_modal.html.erb
90
+ - config/initializers/integration_settings.rb
85
91
  - config/locales/en.yml
86
92
  - config/locales/ko.yml
87
93
  - config/routes.rb