collavre_github 0.6.3 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/collavre_github/creatives/integrations_controller.rb +253 -36
- data/app/controllers/collavre_github/webhooks_controller.rb +562 -37
- data/app/jobs/collavre_github/webhook_delivery_prune_job.rb +14 -0
- data/app/models/collavre_github/github_pr_channel.rb +72 -7
- data/app/models/collavre_github/repository_link.rb +61 -0
- data/app/models/collavre_github/webhook_delivery.rb +159 -0
- data/app/services/collavre_github/client.rb +14 -0
- data/app/services/collavre_github/pr_channel_state_updater.rb +83 -0
- data/app/services/collavre_github/repository_identity_synchronizer.rb +145 -0
- data/app/services/collavre_github/repository_provisioning_lock.rb +95 -0
- data/app/services/collavre_github/tools/concerns/pr_channel_locator.rb +50 -0
- data/app/services/collavre_github/tools/pr_monitor_service.rb +80 -61
- data/app/services/collavre_github/tools/pr_state_set_service.rb +170 -0
- data/app/services/collavre_github/webhook_provisioner.rb +600 -41
- data/app/services/collavre_github/webhook_reprovisioner.rb +118 -0
- data/config/locales/en.yml +1 -0
- data/config/locales/ko.yml +1 -0
- data/config/routes.rb +13 -3
- data/db/migrate/20260727000001_add_webhook_hook_id_to_github_repository_links.rb +17 -0
- data/db/migrate/20260727000002_create_github_webhook_deliveries.rb +30 -0
- data/db/migrate/20260729000000_add_repository_id_index_to_github_repository_links.rb +13 -0
- data/db/seeds.rb +1 -0
- data/lib/collavre_github/version.rb +1 -1
- metadata +12 -1
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
module CollavreGithub
|
|
2
|
+
# Reconciles every ID-backed repository hook with WebhookProvisioner's current
|
|
3
|
+
# event list. New event subscriptions in code do not change hooks already
|
|
4
|
+
# stored at GitHub, so deploys run this once after the new app is live.
|
|
5
|
+
#
|
|
6
|
+
# Name-only legacy rows are never mutated automatically. Older provisioners
|
|
7
|
+
# copied a hook registration and secret to every same-name row, so neither
|
|
8
|
+
# value is independent proof that a legacy row belongs to the repository
|
|
9
|
+
# currently owning that name. Their presence is reported as requiring manual
|
|
10
|
+
# verification so deploys cannot silently leave them unreconciled.
|
|
11
|
+
class WebhookReprovisioner
|
|
12
|
+
def self.call(webhook_url: default_webhook_url)
|
|
13
|
+
new(webhook_url: webhook_url).call
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.default_webhook_url
|
|
17
|
+
CollavreGithub::Engine.routes.url_helpers.webhooks_url(
|
|
18
|
+
Rails.application.config.action_mailer.default_url_options
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(webhook_url:)
|
|
23
|
+
@webhook_url = webhook_url
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def call
|
|
27
|
+
repository_names.map do |repository_name|
|
|
28
|
+
[ repository_name, reprovision(repository_name) ]
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
attr_reader :webhook_url
|
|
35
|
+
|
|
36
|
+
def repository_names
|
|
37
|
+
CollavreGithub::RepositoryLink
|
|
38
|
+
.order(:id)
|
|
39
|
+
.pluck(:repository_full_name)
|
|
40
|
+
.filter_map { |name| name.to_s.downcase.presence }
|
|
41
|
+
.uniq
|
|
42
|
+
.sort
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def reprovision(repository_name)
|
|
46
|
+
links = CollavreGithub::RepositoryLink
|
|
47
|
+
.where("LOWER(repository_full_name) = ?", repository_name)
|
|
48
|
+
.order(:id)
|
|
49
|
+
.to_a
|
|
50
|
+
manual_verification_required = links.any? { |link| link.repository_id.blank? }
|
|
51
|
+
failed = false
|
|
52
|
+
|
|
53
|
+
links.select { |link| link.repository_id.present? }.each do |candidate|
|
|
54
|
+
repository_id = candidate.repository_id
|
|
55
|
+
status = CollavreGithub::RepositoryProvisioningLock.with_lock(repository_id) do
|
|
56
|
+
locked_candidate = CollavreGithub::RepositoryLink.find_by(
|
|
57
|
+
id: candidate.id,
|
|
58
|
+
repository_id: repository_id
|
|
59
|
+
)
|
|
60
|
+
next :skipped_unverified unless locked_candidate
|
|
61
|
+
next :failed unless locked_candidate.github_account
|
|
62
|
+
|
|
63
|
+
link = establish_repository_identity(locked_candidate)
|
|
64
|
+
next :skipped_unverified unless link
|
|
65
|
+
|
|
66
|
+
result = CollavreGithub::WebhookProvisioner.ensure_for_links(
|
|
67
|
+
account: link.github_account,
|
|
68
|
+
links: [ link ],
|
|
69
|
+
webhook_url: webhook_url,
|
|
70
|
+
force_hook_refresh: true
|
|
71
|
+
)
|
|
72
|
+
result.first&.last || :failed
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
if status == :failed
|
|
76
|
+
failed = true
|
|
77
|
+
next
|
|
78
|
+
end
|
|
79
|
+
next if status == :skipped_unverified
|
|
80
|
+
|
|
81
|
+
return :manual_verification_required if manual_verification_required
|
|
82
|
+
|
|
83
|
+
return status
|
|
84
|
+
rescue => e
|
|
85
|
+
failed = true
|
|
86
|
+
log_failure(repository_name, e)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
return :failed if failed
|
|
90
|
+
return :manual_verification_required if manual_verification_required
|
|
91
|
+
|
|
92
|
+
:skipped_unverified
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def log_failure(repository_name, error)
|
|
96
|
+
Rails.logger.warn(
|
|
97
|
+
"[CollavreGithub::WebhookReprovisioner] #{repository_name} failed: " \
|
|
98
|
+
"#{error.class}: #{error.message}"
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def establish_repository_identity(link)
|
|
103
|
+
return if link.repository_id.blank?
|
|
104
|
+
|
|
105
|
+
client = CollavreGithub::Client.new(link.github_account)
|
|
106
|
+
identity = client.repository_identity(link.repository_full_name)
|
|
107
|
+
return if identity&.id.blank? || identity&.full_name.blank?
|
|
108
|
+
return if link.repository_id.to_s != identity.id.to_s
|
|
109
|
+
|
|
110
|
+
synchronized = CollavreGithub::RepositoryIdentitySynchronizer.call(
|
|
111
|
+
anchor: link,
|
|
112
|
+
repository_id: identity.id,
|
|
113
|
+
full_name: identity.full_name
|
|
114
|
+
)
|
|
115
|
+
synchronized.find { |candidate| candidate.creative_id == link.creative_id }
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
data/config/locales/en.yml
CHANGED
|
@@ -107,6 +107,7 @@ en:
|
|
|
107
107
|
closed_message: "%{label} was **%{verb}**. Detaching channel."
|
|
108
108
|
attached_message: "%{label} monitoring started.\n\n%{url}"
|
|
109
109
|
reopened_message: "%{label} was reopened. Monitoring resumed.\n\n%{url}"
|
|
110
|
+
auth_failure_message: "⚠️ A GitHub webhook delivery for `%{repo}` could not be verified, so events for %{label} are **not being received**.\n\nThis usually means the repository was renamed or its webhook secret was changed. Re-run the GitHub integration setup for this repository to restore delivery.\n\n%{url}"
|
|
110
111
|
badge:
|
|
111
112
|
open: "Open"
|
|
112
113
|
merged: "Merged"
|
data/config/locales/ko.yml
CHANGED
|
@@ -107,6 +107,7 @@ ko:
|
|
|
107
107
|
closed_message: "%{label}이(가) **%{verb}** 되었습니다. 채널을 해제합니다."
|
|
108
108
|
attached_message: "%{label} 모니터링이 시작되었습니다.\n\n%{url}"
|
|
109
109
|
reopened_message: "%{label}이(가) 재오픈되어 모니터링이 재개되었습니다.\n\n%{url}"
|
|
110
|
+
auth_failure_message: "⚠️ `%{repo}` 저장소의 GitHub 웹훅 전달을 검증하지 못해 %{label}의 이벤트를 **수신하지 못하고 있습니다**.\n\n저장소 이름이 변경되었거나 웹훅 시크릿이 바뀐 경우입니다. 해당 저장소의 GitHub 연동 설정을 다시 실행하면 복구됩니다.\n\n%{url}"
|
|
110
111
|
badge:
|
|
111
112
|
open: "열림"
|
|
112
113
|
merged: "머지됨"
|
data/config/routes.rb
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
CollavreGithub::Engine.routes.draw do
|
|
2
|
-
# Webhook endpoint (public, no auth)
|
|
3
|
-
# Both singular and plural for backward compatibility
|
|
4
|
-
post "webhook", to: "webhooks#create", as: :webhook
|
|
2
|
+
# Webhook endpoint (public, no auth).
|
|
5
3
|
post "webhooks", to: "webhooks#create"
|
|
6
4
|
|
|
5
|
+
# Deprecated singular alias, kept only until no repository points a hook at
|
|
6
|
+
# it. Repos provisioned before the plural path existed still target this URL,
|
|
7
|
+
# and nothing migrates them at deploy time — `WebhookProvisioner` deletes such
|
|
8
|
+
# hooks, but only for repositories it happens to touch when an integration is
|
|
9
|
+
# saved or `pr_monitor` runs. Dropping the route outright would 404 every
|
|
10
|
+
# untouched repository's hook and silently stop its events.
|
|
11
|
+
#
|
|
12
|
+
# Serving both spellings no longer duplicates anything: a repo carrying a hook
|
|
13
|
+
# on each receives the same X-GitHub-Delivery GUID twice and the second is
|
|
14
|
+
# discarded by the idempotency ledger.
|
|
15
|
+
post "webhook", to: "webhooks#create", as: :webhook
|
|
16
|
+
|
|
7
17
|
# OAuth setup flow (popup wizard after callback)
|
|
8
18
|
get "auth/setup", to: "auth#setup", as: :setup_auth
|
|
9
19
|
post "auth/store_creative", to: "auth#store_creative", as: :store_creative_auth
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class AddWebhookHookIdToGithubRepositoryLinks < ActiveRecord::Migration[8.1]
|
|
2
|
+
# The GitHub hook id that an instance of this app created for the repository.
|
|
3
|
+
#
|
|
4
|
+
# It is the only positive evidence that a hook found on GitHub belongs to a
|
|
5
|
+
# deployment sharing THIS database: the id was written here by whichever
|
|
6
|
+
# instance created it. Host or path similarity proves nothing — an unrelated
|
|
7
|
+
# deployment can serve the very same path — and deferring to a hook that
|
|
8
|
+
# feeds someone else's database would leave this instance receiving nothing.
|
|
9
|
+
#
|
|
10
|
+
# Stored on every link for the repository rather than only the primary one so
|
|
11
|
+
# that deleting a link does not lose the registration and let the next
|
|
12
|
+
# provisioning run create a second hook. This mirrors how `webhook_secret` is
|
|
13
|
+
# already aligned across a repository's links.
|
|
14
|
+
def change
|
|
15
|
+
add_column :github_repository_links, :webhook_hook_id, :bigint
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
class CreateGithubWebhookDeliveries < ActiveRecord::Migration[8.1]
|
|
2
|
+
# Durable idempotency key for GitHub webhook deliveries. GitHub stamps the
|
|
3
|
+
# `X-GitHub-Delivery` GUID per EVENT, not per delivery, so one GUID covers
|
|
4
|
+
# redeliveries AND the fan-out to every hook configured on the repo. A
|
|
5
|
+
# UNIQUE index therefore collapses both duplicate sources into one processed
|
|
6
|
+
# request. It must live in the DB, not in a process-local cache: multiple app
|
|
7
|
+
# instances share this database and each one receives its own copy.
|
|
8
|
+
#
|
|
9
|
+
# `processed_at` is what actually suppresses a redelivery. The row alone only
|
|
10
|
+
# records that some request claimed the GUID; if that request then died, the
|
|
11
|
+
# claim must not outlive it, or the redelivery would be answered 200 and the
|
|
12
|
+
# event dropped for good.
|
|
13
|
+
#
|
|
14
|
+
# `claim_token` identifies WHICH run holds the claim. A run that outlives
|
|
15
|
+
# STALE_CLAIM_AFTER has its claim taken over by the next delivery of the same
|
|
16
|
+
# GUID, and without a token the original run's cleanup would act on the GUID
|
|
17
|
+
# alone and delete the replacement's claim.
|
|
18
|
+
def change
|
|
19
|
+
create_table :github_webhook_deliveries do |t|
|
|
20
|
+
t.string :delivery_guid, null: false
|
|
21
|
+
t.string :event
|
|
22
|
+
t.string :claim_token
|
|
23
|
+
t.datetime :created_at, null: false
|
|
24
|
+
t.datetime :processed_at
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
add_index :github_webhook_deliveries, :delivery_guid, unique: true
|
|
28
|
+
add_index :github_webhook_deliveries, :created_at
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class AddRepositoryIdIndexToGithubRepositoryLinks < ActiveRecord::Migration[8.1]
|
|
2
|
+
# The column has existed since the engine's first migration but was never
|
|
3
|
+
# written or read. Webhook routing now matches on it (it is the only
|
|
4
|
+
# repository identifier that survives a rename), so it needs an index.
|
|
5
|
+
# `webhook_hook_id` is also used as a request-time fallback for links that
|
|
6
|
+
# have not received a repository id yet, and must not require a table scan.
|
|
7
|
+
#
|
|
8
|
+
# Not unique: one repository is deliberately linked to many creatives.
|
|
9
|
+
def change
|
|
10
|
+
add_index :github_repository_links, :repository_id
|
|
11
|
+
add_index :github_repository_links, :webhook_hook_id
|
|
12
|
+
end
|
|
13
|
+
end
|
data/db/seeds.rb
CHANGED
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.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Collavre
|
|
@@ -71,22 +71,30 @@ files:
|
|
|
71
71
|
- app/javascript/github_wizard_state.js
|
|
72
72
|
- app/jobs/collavre_github/initial_markdown_sync_job.rb
|
|
73
73
|
- app/jobs/collavre_github/markdown_sync_job.rb
|
|
74
|
+
- app/jobs/collavre_github/webhook_delivery_prune_job.rb
|
|
74
75
|
- app/models/collavre_github/account.rb
|
|
75
76
|
- app/models/collavre_github/application_record.rb
|
|
76
77
|
- app/models/collavre_github/github_pr_channel.rb
|
|
77
78
|
- app/models/collavre_github/repository_link.rb
|
|
79
|
+
- app/models/collavre_github/webhook_delivery.rb
|
|
78
80
|
- app/services/collavre_github/client.rb
|
|
79
81
|
- app/services/collavre_github/markdown_sync/content_processor.rb
|
|
80
82
|
- app/services/collavre_github/markdown_sync/incremental_sync_service.rb
|
|
81
83
|
- app/services/collavre_github/markdown_sync/initial_import_service.rb
|
|
84
|
+
- app/services/collavre_github/pr_channel_state_updater.rb
|
|
82
85
|
- app/services/collavre_github/pr_topic_link_parser.rb
|
|
86
|
+
- app/services/collavre_github/repository_identity_synchronizer.rb
|
|
87
|
+
- app/services/collavre_github/repository_provisioning_lock.rb
|
|
83
88
|
- app/services/collavre_github/tools/concerns/github_client_finder.rb
|
|
89
|
+
- app/services/collavre_github/tools/concerns/pr_channel_locator.rb
|
|
84
90
|
- app/services/collavre_github/tools/github_pr_commits_service.rb
|
|
85
91
|
- app/services/collavre_github/tools/github_pr_details_service.rb
|
|
86
92
|
- app/services/collavre_github/tools/github_pr_diff_service.rb
|
|
87
93
|
- app/services/collavre_github/tools/permission_denied_error.rb
|
|
88
94
|
- app/services/collavre_github/tools/pr_monitor_service.rb
|
|
95
|
+
- app/services/collavre_github/tools/pr_state_set_service.rb
|
|
89
96
|
- app/services/collavre_github/webhook_provisioner.rb
|
|
97
|
+
- app/services/collavre_github/webhook_reprovisioner.rb
|
|
90
98
|
- app/views/collavre_github/auth/setup.html.erb
|
|
91
99
|
- app/views/collavre_github/integrations/_modal.html.erb
|
|
92
100
|
- config/initializers/integration_settings.rb
|
|
@@ -101,6 +109,9 @@ files:
|
|
|
101
109
|
- db/migrate/20260412000000_add_markdown_sync_to_repository_links.rb
|
|
102
110
|
- db/migrate/20260416000000_add_markdown_root_creative_index.rb
|
|
103
111
|
- db/migrate/20260711000000_encrypt_github_repository_link_webhook_secrets.rb
|
|
112
|
+
- db/migrate/20260727000001_add_webhook_hook_id_to_github_repository_links.rb
|
|
113
|
+
- db/migrate/20260727000002_create_github_webhook_deliveries.rb
|
|
114
|
+
- db/migrate/20260729000000_add_repository_id_index_to_github_repository_links.rb
|
|
104
115
|
- db/seeds.rb
|
|
105
116
|
- lib/collavre_github.rb
|
|
106
117
|
- lib/collavre_github/engine.rb
|