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.
Files changed (25) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/collavre_github/creatives/integrations_controller.rb +253 -36
  3. data/app/controllers/collavre_github/webhooks_controller.rb +562 -37
  4. data/app/jobs/collavre_github/webhook_delivery_prune_job.rb +14 -0
  5. data/app/models/collavre_github/github_pr_channel.rb +72 -7
  6. data/app/models/collavre_github/repository_link.rb +61 -0
  7. data/app/models/collavre_github/webhook_delivery.rb +159 -0
  8. data/app/services/collavre_github/client.rb +14 -0
  9. data/app/services/collavre_github/pr_channel_state_updater.rb +83 -0
  10. data/app/services/collavre_github/repository_identity_synchronizer.rb +145 -0
  11. data/app/services/collavre_github/repository_provisioning_lock.rb +95 -0
  12. data/app/services/collavre_github/tools/concerns/pr_channel_locator.rb +50 -0
  13. data/app/services/collavre_github/tools/pr_monitor_service.rb +80 -61
  14. data/app/services/collavre_github/tools/pr_state_set_service.rb +170 -0
  15. data/app/services/collavre_github/webhook_provisioner.rb +600 -41
  16. data/app/services/collavre_github/webhook_reprovisioner.rb +118 -0
  17. data/config/locales/en.yml +1 -0
  18. data/config/locales/ko.yml +1 -0
  19. data/config/routes.rb +13 -3
  20. data/db/migrate/20260727000001_add_webhook_hook_id_to_github_repository_links.rb +17 -0
  21. data/db/migrate/20260727000002_create_github_webhook_deliveries.rb +30 -0
  22. data/db/migrate/20260729000000_add_repository_id_index_to_github_repository_links.rb +13 -0
  23. data/db/seeds.rb +1 -0
  24. data/lib/collavre_github/version.rb +1 -1
  25. 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
@@ -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"
@@ -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
@@ -77,6 +77,7 @@ module CollavreGithub
77
77
  creative_retrieval_service
78
78
  creative_batch_service
79
79
  pr_monitor
80
+ pr_state_set
80
81
  ].freeze
81
82
 
82
83
  def self.call
@@ -1,3 +1,3 @@
1
1
  module CollavreGithub
2
- VERSION = "0.6.3"
2
+ VERSION = "0.7.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.6.3
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