main
rb 101 lines 3.6 KB
Raw
1 # frozen_string_literal: true
2
3 # Receives GitHub App webhooks for siGit Code PR reviews. Public endpoint,
4 # authenticated by the HMAC signature — never a user token.
5 #
6 # GitHub gives a delivery 10 seconds and never retries on its own, so this
7 # controller only verifies, syncs installation state, and enqueues; the review
8 # itself runs in GithubPrReviewJob.
9 class GithubWebhooksController < ActionController::API
10 REVIEWABLE_PR_ACTIONS = %w[opened reopened ready_for_review synchronize].freeze
11
12 def create
13 payload = request.body.read
14
15 unless GithubAppService.configured?
16 Rails.logger.error("GitHub webhook received but the GitHub App is not configured")
17 return head :service_unavailable
18 end
19
20 unless GithubAppService.verify_webhook_signature?(payload, request.headers["X-Hub-Signature-256"])
21 Rails.logger.warn("GitHub webhook rejected: bad signature")
22 return head :unauthorized
23 end
24
25 handle(request.headers["X-GitHub-Event"], JSON.parse(payload))
26 head :ok
27 rescue JSON::ParserError
28 head :bad_request
29 rescue StandardError => e
30 Rails.logger.error("GitHub webhook handling failed: #{e.class}: #{e.message}")
31 head :ok # GitHub never retries; nothing gained by 500ing a poison event. We logged it.
32 end
33
34 private
35
36 def handle(event, payload)
37 case event
38 when "installation" then sync_installation(payload)
39 when "installation_repositories" then update_repository_selection(payload)
40 when "pull_request" then handle_pull_request(payload)
41 else Rails.logger.debug { "GitHub webhook ignored: #{event}" }
42 end
43 end
44
45 def sync_installation(payload)
46 installation = upsert_installation(payload["installation"])
47 return if installation.nil?
48
49 case payload["action"]
50 when "deleted"
51 installation.update!(deleted_at: Time.current)
52 installation.clear_access_token!
53 when "suspend"
54 installation.update!(suspended_at: Time.current)
55 when "unsuspend"
56 installation.update!(suspended_at: nil)
57 end
58 end
59
60 def update_repository_selection(payload)
61 upsert_installation(payload["installation"])
62 end
63
64 def handle_pull_request(payload)
65 return unless REVIEWABLE_PR_ACTIONS.include?(payload["action"])
66 return if payload.dig("pull_request", "draft")
67
68 unless GithubReviewConfig.enabled?
69 Rails.logger.info("GitHub PR review skipped: reviews are disabled")
70 return
71 end
72
73 installation = upsert_installation(payload["installation"])
74 return if installation.nil? || !installation.active?
75
76 GithubPrReviewJob.perform_later(
77 installation_id: installation.id,
78 repo_full_name: payload.dig("repository", "full_name"),
79 pr_number: payload.dig("pull_request", "number"),
80 head_sha: payload.dig("pull_request", "head", "sha")
81 )
82 end
83
84 # Upserts the local mirror from a webhook's `installation` object. Creating
85 # from PR events too is the safety net for missed/late installation events;
86 # a resurfacing installation (reinstall) clears the soft delete.
87 def upsert_installation(data)
88 return nil if data.nil? || data["id"].blank?
89
90 installation = GithubAppInstallation.find_or_initialize_by(installation_id: data["id"])
91 installation.deleted_at = nil
92 installation.assign_attributes(
93 account_login: data.dig("account", "login") || installation.account_login || "unknown",
94 account_type: data.dig("account", "type") || installation.account_type,
95 account_id: data.dig("account", "id") || installation.account_id,
96 repository_selection: data["repository_selection"] || installation.repository_selection
97 )
98 installation.save!
99 installation
100 end
101 end