collavre_github 0.2.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 (29) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +127 -0
  3. data/Rakefile +8 -0
  4. data/app/controllers/collavre_github/accounts_controller.rb +84 -0
  5. data/app/controllers/collavre_github/application_controller.rb +11 -0
  6. data/app/controllers/collavre_github/auth_controller.rb +27 -0
  7. data/app/controllers/collavre_github/creatives/integrations_controller.rb +181 -0
  8. data/app/controllers/collavre_github/webhooks_controller.rb +304 -0
  9. data/app/models/collavre_github/account.rb +11 -0
  10. data/app/models/collavre_github/application_record.rb +5 -0
  11. data/app/models/collavre_github/repository_link.rb +19 -0
  12. data/app/services/collavre_github/client.rb +110 -0
  13. data/app/services/collavre_github/tools/concerns/github_client_finder.rb +36 -0
  14. data/app/services/collavre_github/tools/github_pr_commits_service.rb +40 -0
  15. data/app/services/collavre_github/tools/github_pr_details_service.rb +53 -0
  16. data/app/services/collavre_github/tools/github_pr_diff_service.rb +57 -0
  17. data/app/services/collavre_github/webhook_provisioner.rb +128 -0
  18. data/app/views/collavre_github/integrations/_modal.html.erb +77 -0
  19. data/config/locales/en.yml +80 -0
  20. data/config/locales/ko.yml +80 -0
  21. data/config/routes.rb +17 -0
  22. data/db/migrate/20250925000000_create_github_integrations.rb +26 -0
  23. data/db/migrate/20250927000000_add_webhook_secret_to_github_repository_links.rb +29 -0
  24. data/db/migrate/20250928105957_add_github_gemini_prompt_to_creatives.rb +5 -0
  25. data/db/seeds.rb +117 -0
  26. data/lib/collavre_github/engine.rb +74 -0
  27. data/lib/collavre_github/version.rb +3 -0
  28. data/lib/collavre_github.rb +5 -0
  29. metadata +112 -0
@@ -0,0 +1,128 @@
1
+ module CollavreGithub
2
+ class WebhookProvisioner
3
+ EVENTS = %w[pull_request].freeze
4
+ CONTENT_TYPE = "json".freeze
5
+
6
+ def self.ensure_for_links(account:, links:, webhook_url:)
7
+ new(account: account, webhook_url: webhook_url).ensure_for_links(Array(links))
8
+ end
9
+
10
+ def self.remove_for_repositories(account:, repositories:, webhook_url:)
11
+ new(account: account, webhook_url: webhook_url).remove_for_repositories(Array(repositories))
12
+ end
13
+
14
+ def initialize(account:, webhook_url:, client: CollavreGithub::Client.new(account))
15
+ @client = client
16
+ @webhook_url = webhook_url
17
+ end
18
+
19
+ def ensure_for_links(links)
20
+ links.each do |link|
21
+ ensure_webhook(link)
22
+ end
23
+ end
24
+
25
+ def remove_for_repositories(repositories)
26
+ repositories.each do |repository_full_name|
27
+ next if CollavreGithub::RepositoryLink.where(repository_full_name: repository_full_name).exists?
28
+
29
+ remove_webhook(repository_full_name)
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :client, :webhook_url
36
+
37
+ def ensure_webhook(link)
38
+ repository_full_name = link.repository_full_name
39
+ primary_link = primary_link_for(repository_full_name)
40
+ hook = find_existing_hook(repository_full_name)
41
+
42
+ if hook
43
+ if primary_link && primary_link != link
44
+ align_link_secret(link, primary_link.webhook_secret)
45
+ else
46
+ update_webhook(repository_full_name, hook.id, link.webhook_secret)
47
+ end
48
+ else
49
+ secret = link.webhook_secret
50
+
51
+ if primary_link && primary_link != link
52
+ secret = primary_link.webhook_secret
53
+ align_link_secret(link, secret)
54
+ end
55
+
56
+ create_webhook(repository_full_name, secret)
57
+ end
58
+ rescue Octokit::Error => e
59
+ Rails.logger.warn(
60
+ "GitHub webhook provisioning failed for #{repository_full_name}: #{e.message}"
61
+ )
62
+ end
63
+
64
+ def remove_webhook(repository_full_name)
65
+ hook = find_existing_hook(repository_full_name)
66
+ return unless hook
67
+
68
+ client.delete_repository_webhook(repository_full_name, hook.id)
69
+ rescue Octokit::Error => e
70
+ Rails.logger.warn(
71
+ "GitHub webhook removal failed for #{repository_full_name}: #{e.message}"
72
+ )
73
+ end
74
+
75
+ def find_existing_hook(repository_full_name)
76
+ client.repository_hooks(repository_full_name).find do |hook|
77
+ config = normalize_config(hook.config)
78
+ config["url"] == webhook_url
79
+ end
80
+ end
81
+
82
+ def create_webhook(repository_full_name, secret)
83
+ client.create_repository_webhook(
84
+ repository_full_name,
85
+ url: webhook_url,
86
+ secret: secret,
87
+ events: EVENTS,
88
+ content_type: CONTENT_TYPE
89
+ )
90
+ end
91
+
92
+ def update_webhook(repository_full_name, hook_id, secret)
93
+ client.update_repository_webhook(
94
+ repository_full_name,
95
+ hook_id,
96
+ url: webhook_url,
97
+ secret: secret,
98
+ events: EVENTS,
99
+ content_type: CONTENT_TYPE
100
+ )
101
+ end
102
+
103
+ def primary_link_for(repository_full_name)
104
+ CollavreGithub::RepositoryLink
105
+ .where(repository_full_name: repository_full_name)
106
+ .order(:id)
107
+ .first
108
+ end
109
+
110
+ def align_link_secret(link, secret)
111
+ return if secret.blank? || link.webhook_secret == secret
112
+
113
+ link.update!(webhook_secret: secret)
114
+ end
115
+
116
+ def normalize_config(config)
117
+ hash =
118
+ case config
119
+ when Hash
120
+ config
121
+ else
122
+ config.respond_to?(:to_h) ? config.to_h : {}
123
+ end
124
+
125
+ hash.with_indifferent_access
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,77 @@
1
+ <div id="github-integration-modal"
2
+ data-success-message="<%= t('collavre_github.integration.saved', default: 'Github integration saved successfully.') %>"
3
+ data-login-required="<%= t('collavre_github.integration.login_required', default: 'Sign in with your Github account to start the integration.') %>"
4
+ data-no-creative="<%= t('collavre_github.integration.missing_creative', default: 'No Creative selected for integration.') %>"
5
+ data-webhook-url-label="<%= t('collavre_github.integration.webhook_url_label', default: 'Webhook URL') %>"
6
+ data-webhook-secret-label="<%= t('collavre_github.integration.webhook_secret_label', default: 'Webhook secret') %>"
7
+ data-existing-message="<%= t('collavre_github.integration.existing_message', default: 'You\'re already connected to the repositories below.') %>"
8
+ data-delete-confirm="<%= t('collavre_github.integration.delete_confirm', default: 'Do you want to remove the Github integration?') %>"
9
+ data-delete-success="<%= t('collavre_github.integration.delete_success', default: 'Github integration removed successfully.') %>"
10
+ data-delete-error="<%= t('collavre_github.integration.delete_error', default: 'Failed to remove the Github integration.') %>"
11
+ data-delete-button-label="<%= t('collavre_github.integration.delete_button', default: 'Remove integration') %>"
12
+ data-delete-select-warning="<%= t('collavre_github.integration.delete_select_warning', default: 'Select repositories to delete.') %>"
13
+ style="display:none;position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:10000;align-items:center;justify-content:center;">
14
+ <div class="popup-box" style="min-width:360px;max-width:90vw;">
15
+ <button type="button" id="close-github-modal" class="popup-close-btn">&times;</button>
16
+ <h2><%= t('collavre_github.integration.title', default: 'Configure Github integration') %></h2>
17
+ <p id="github-integration-status" class="github-modal-status"></p>
18
+
19
+ <div class="github-wizard-step" id="github-step-connect">
20
+ <p id="github-connect-message" class="github-modal-subtext"><%= t('collavre_github.integration.connect', default: 'Sign in with your Github account to start linking.') %></p>
21
+ <form id="github-login-form" action="/auth/github" method="post" target="github-auth-window" style="display:none;">
22
+ <input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>">
23
+ </form>
24
+ <button type="button" id="github-login-btn" class="btn btn-primary" data-window-width="620" data-window-height="720">
25
+ <%= t('collavre_github.integration.login_button', default: 'Sign in with Github') %>
26
+ </button>
27
+ <div id="github-existing-connections" style="display:none;margin-top:1.25em;">
28
+ <p style="margin-bottom:0.5em;"><%= t('collavre_github.integration.existing_intro', default: 'Connected repositories:') %></p>
29
+ <ul id="github-existing-repo-list" style="padding-left:1.2em;margin-bottom:0.75em;color:var(--color-text);"></ul>
30
+ <button type="button" id="github-delete-btn" class="btn btn-danger" style="display:none;">
31
+ <%= t('collavre_github.integration.delete_button', default: 'Remove integration') %>
32
+ </button>
33
+ </div>
34
+ </div>
35
+
36
+ <div class="github-wizard-step" id="github-step-organization" style="display:none;">
37
+ <p class="github-modal-subtext"><%= t('collavre_github.integration.choose_org', default: 'Select the organization that owns the repositories.') %></p>
38
+ <div id="github-organization-list" class="github-list github-modal-list-box" style="max-height:200px;overflow:auto;"></div>
39
+ </div>
40
+
41
+ <div class="github-wizard-step" id="github-step-repositories" style="display:none;">
42
+ <p class="github-modal-subtext"><%= t('collavre_github.integration.choose_repo', default: 'Select repositories to link. You can choose multiple.') %></p>
43
+ <div id="github-repository-list" class="github-list github-modal-list-box" style="max-height:240px;overflow:auto;"></div>
44
+ </div>
45
+
46
+ <div class="github-wizard-step" id="github-step-summary" style="display:none;">
47
+ <p class="github-modal-subtext"><%= t('collavre_github.integration.summary', default: 'The following repositories will be linked to this Creative:') %></p>
48
+ <p id="github-webhook-instructions" class="github-modal-subtext" style="display:none;margin-bottom:0.5em;"><%= t('collavre_github.integration.webhook_instructions', default: 'Configure each repository with the webhook details below.') %></p>
49
+ <ul id="github-selected-repos" style="padding-left:1.2em;color:var(--color-text);"></ul>
50
+ <p id="github-summary-empty" class="github-modal-empty" style="display:none;"><%= t('collavre_github.integration.summary_empty', default: 'No repositories selected.') %></p>
51
+ </div>
52
+
53
+ <div class="github-wizard-step" id="github-step-prompt" style="display:none;">
54
+ <p class="github-modal-subtext"><%= t('collavre_github.integration.prompt_title', default: 'Review the Gemini analysis prompt and edit it if needed.') %></p>
55
+ <textarea id="github-gemini-prompt" style="width:100%;min-height:220px;padding:0.75em;border:1px solid var(--color-border);border-radius:4px;font-family:monospace;font-size:0.95em;"></textarea>
56
+ <p class="github-modal-subtext" style="margin-top:0.75em;font-size:0.9em;">
57
+ <%= t('collavre_github.integration.prompt_help', default: 'Use the placeholders below to inject runtime details as needed:') %>
58
+ <code>#{pr_title}</code>,
59
+ <code>#{pr_body}</code>,
60
+ <code>#{commit_messages}</code>,
61
+ <code>#{diff}</code>,
62
+ <code>#{creative_tree}</code>,
63
+ <code>#{language_instructions}</code>
64
+ </p>
65
+ </div>
66
+
67
+ <div id="github-wizard-error" style="display:none;margin:0.5em 0;color:#c0392b;font-weight:bold;"></div>
68
+
69
+ <div class="github-wizard-footer" style="display:flex;justify-content:space-between;gap:0.5em;margin-top:1.5em;">
70
+ <button type="button" id="github-prev-btn" class="btn btn-secondary" style="display:none;"><%= t('app.previous', default: 'Previous') %></button>
71
+ <div style="margin-left:auto;display:flex;gap:0.5em;">
72
+ <button type="button" id="github-next-btn" class="btn btn-primary" style="display:none;"><%= t('app.next', default: 'Next') %></button>
73
+ <button type="button" id="github-finish-btn" class="btn btn-primary" style="display:none;"><%= t('app.finish', default: 'Finish') %></button>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ </div>
@@ -0,0 +1,80 @@
1
+ en:
2
+ collavre_github:
3
+ integration:
4
+ label: "GitHub"
5
+ description: "Connect repositories and receive webhook events"
6
+ title: "Configure Github integration"
7
+ saved: "Github integration saved successfully."
8
+ login_required: "Sign in with your Github account to start the integration."
9
+ missing_creative: "No Creative selected for integration."
10
+ webhook_url_label: "Webhook URL"
11
+ webhook_secret_label: "Webhook secret"
12
+ existing_message: "You're already connected to the repositories below."
13
+ existing_intro: "Connected repositories:"
14
+ delete_confirm: "Do you want to remove the Github integration?"
15
+ delete_success: "Github integration removed successfully."
16
+ delete_error: "Failed to remove the Github integration."
17
+ delete_button: "Remove integration"
18
+ delete_select_warning: "Select repositories to delete."
19
+ connect: "Sign in with your Github account to start linking."
20
+ login_button: "Sign in with Github"
21
+ choose_org: "Select the organization that owns the repositories."
22
+ choose_repo: "Select repositories to link. You can choose multiple."
23
+ summary: "The following repositories will be linked to this Creative:"
24
+ webhook_instructions: "Configure each repository with the webhook details below."
25
+ summary_empty: "No repositories selected."
26
+ prompt_title: "Review the Gemini analysis prompt and edit it if needed."
27
+ prompt_help: "Use the placeholders below to inject runtime details as needed:"
28
+ auth:
29
+ login_first: "Please log in first to connect your GitHub account"
30
+ connected: "GitHub account connected successfully"
31
+ errors:
32
+ not_connected: "GitHub account not connected"
33
+ forbidden: "You don't have permission to perform this action"
34
+ not_found: "Repository not found"
35
+ webhooks:
36
+ pull_request:
37
+ title: "GitHub: Pull Request %{action}"
38
+ repository: "Repository"
39
+ pr: "PR"
40
+ author: "Author"
41
+ action: "Action"
42
+ merged: "(merged)"
43
+ description: "Description"
44
+ push:
45
+ title: "GitHub: Push to %{branch}"
46
+ repository: "Repository"
47
+ branch: "Branch"
48
+ pusher: "Pusher"
49
+ commits: "Commits"
50
+ recent_commits: "Recent commits"
51
+ no_message: "(no message)"
52
+ more: "..."
53
+ issue:
54
+ title: "GitHub: Issue %{action}"
55
+ repository: "Repository"
56
+ issue: "Issue"
57
+ author: "Author"
58
+ action: "Action"
59
+ description: "Description"
60
+ issue_comment:
61
+ title: "GitHub: Comment %{action} on Issue #%{number}"
62
+ repository: "Repository"
63
+ issue: "Issue"
64
+ comment_by: "Comment by"
65
+ link: "Link"
66
+ view_comment: "View comment"
67
+ comment: "Comment"
68
+ generic:
69
+ title: "GitHub: %{event}"
70
+ repository: "Repository"
71
+ action: "Action"
72
+ sender: "Sender"
73
+ actions:
74
+ opened: "Opened"
75
+ closed: "Closed"
76
+ merged: "Merged"
77
+ reopened: "Reopened"
78
+ updated: "Updated"
79
+ ready_for_review: "Ready for Review"
80
+ event: "Event"
@@ -0,0 +1,80 @@
1
+ ko:
2
+ collavre_github:
3
+ integration:
4
+ label: "GitHub"
5
+ description: "저장소를 연결하고 웹훅 이벤트를 받습니다"
6
+ title: "Github 연동 설정"
7
+ saved: "Github 연동이 저장되었습니다."
8
+ login_required: "Github 계정으로 로그인하여 연동을 시작하세요."
9
+ missing_creative: "연동할 Creative가 선택되지 않았습니다."
10
+ webhook_url_label: "Webhook URL"
11
+ webhook_secret_label: "Webhook 시크릿"
12
+ existing_message: "이미 아래 저장소와 연결되어 있습니다."
13
+ existing_intro: "연결된 저장소:"
14
+ delete_confirm: "Github 연동을 삭제하시겠습니까?"
15
+ delete_success: "Github 연동이 삭제되었습니다."
16
+ delete_error: "Github 연동 삭제에 실패했습니다."
17
+ delete_button: "연동 삭제"
18
+ delete_select_warning: "삭제할 Repository를 선택하세요."
19
+ connect: "Github 계정으로 로그인하여 연동을 시작하세요."
20
+ login_button: "Github으로 로그인"
21
+ choose_org: "저장소가 속한 조직을 선택하세요."
22
+ choose_repo: "연동할 저장소를 선택하세요. 여러 개 선택 가능합니다."
23
+ summary: "다음 저장소가 이 Creative에 연동됩니다:"
24
+ webhook_instructions: "각 저장소에 아래 웹훅 정보를 설정하세요."
25
+ summary_empty: "선택된 저장소가 없습니다."
26
+ prompt_title: "Gemini 분석 프롬프트를 검토하고 필요시 수정하세요."
27
+ prompt_help: "다음 플레이스홀더를 사용하여 런타임 정보를 주입할 수 있습니다:"
28
+ auth:
29
+ login_first: "GitHub 계정을 연결하려면 먼저 로그인하세요"
30
+ connected: "GitHub 계정이 연결되었습니다"
31
+ errors:
32
+ not_connected: "GitHub 계정이 연결되지 않았습니다"
33
+ forbidden: "이 작업을 수행할 권한이 없습니다"
34
+ not_found: "저장소를 찾을 수 없습니다"
35
+ webhooks:
36
+ pull_request:
37
+ title: "GitHub: Pull Request %{action}"
38
+ repository: "저장소"
39
+ pr: "PR"
40
+ author: "작성자"
41
+ action: "작업"
42
+ merged: "(머지됨)"
43
+ description: "설명"
44
+ push:
45
+ title: "GitHub: %{branch} 브랜치에 Push"
46
+ repository: "저장소"
47
+ branch: "브랜치"
48
+ pusher: "푸셔"
49
+ commits: "커밋"
50
+ recent_commits: "최근 커밋"
51
+ no_message: "(메시지 없음)"
52
+ more: "..."
53
+ issue:
54
+ title: "GitHub: Issue %{action}"
55
+ repository: "저장소"
56
+ issue: "이슈"
57
+ author: "작성자"
58
+ action: "작업"
59
+ description: "설명"
60
+ issue_comment:
61
+ title: "GitHub: Issue #%{number}에 댓글 %{action}"
62
+ repository: "저장소"
63
+ issue: "이슈"
64
+ comment_by: "댓글 작성자"
65
+ link: "링크"
66
+ view_comment: "댓글 보기"
67
+ comment: "댓글"
68
+ generic:
69
+ title: "GitHub: %{event}"
70
+ repository: "저장소"
71
+ action: "작업"
72
+ sender: "보낸 사람"
73
+ actions:
74
+ opened: "열림"
75
+ closed: "닫힘"
76
+ merged: "머지됨"
77
+ reopened: "다시 열림"
78
+ updated: "업데이트됨"
79
+ ready_for_review: "리뷰 준비됨"
80
+ event: "이벤트"
data/config/routes.rb ADDED
@@ -0,0 +1,17 @@
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
5
+ post "webhooks", to: "webhooks#create"
6
+
7
+ # Account endpoints
8
+ resource :account, only: [ :show ] do
9
+ get :organizations
10
+ get :repositories
11
+ end
12
+
13
+ # Creative integration endpoints
14
+ resources :creatives, only: [] do
15
+ resource :integration, module: :creatives, only: [ :show, :update, :destroy ]
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ class CreateGithubIntegrations < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :github_accounts do |t|
4
+ t.references :user, null: false, foreign_key: true, index: { unique: true }
5
+ t.string :github_uid, null: false
6
+ t.string :login, null: false
7
+ t.string :name
8
+ t.string :avatar_url
9
+ t.string :token, null: false
10
+ t.datetime :token_expires_at
11
+ t.timestamps
12
+ end
13
+
14
+ create_table :github_repository_links do |t|
15
+ t.references :creative, null: false, foreign_key: true
16
+ t.references :github_account, null: false, foreign_key: true
17
+ t.bigint :repository_id
18
+ t.string :repository_full_name, null: false
19
+ t.timestamps
20
+ end
21
+
22
+ add_index :github_accounts, :github_uid, unique: true
23
+ add_index :github_repository_links, :repository_full_name
24
+ add_index :github_repository_links, [ :creative_id, :repository_full_name ], unique: true, name: "index_github_links_on_creative_and_repo"
25
+ end
26
+ end
@@ -0,0 +1,29 @@
1
+ class AddWebhookSecretToGithubRepositoryLinks < ActiveRecord::Migration[8.0]
2
+ class GithubRepositoryLink < ApplicationRecord
3
+ self.table_name = "github_repository_links"
4
+ end
5
+
6
+ def up
7
+ add_column :github_repository_links, :webhook_secret, :string
8
+
9
+ GithubRepositoryLink.reset_column_information
10
+
11
+ say_with_time "Backfilling webhook secrets" do
12
+ GithubRepositoryLink.find_each do |link|
13
+ link.update_columns(webhook_secret: generate_secret)
14
+ end
15
+ end
16
+
17
+ change_column_null :github_repository_links, :webhook_secret, false
18
+ end
19
+
20
+ def down
21
+ remove_column :github_repository_links, :webhook_secret
22
+ end
23
+
24
+ private
25
+
26
+ def generate_secret
27
+ SecureRandom.hex(20)
28
+ end
29
+ end
@@ -0,0 +1,5 @@
1
+ class AddGithubGeminiPromptToCreatives < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :creatives, :github_gemini_prompt, :text
4
+ end
5
+ end
data/db/seeds.rb ADDED
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Seed data for CollavreGithub engine
4
+ # Creates the GitHub PR Analyzer AI Agent
5
+
6
+ module CollavreGithub
7
+ class Seeds
8
+ AGENT_EMAIL = "github-pr-analyzer@collavre.local"
9
+
10
+ SYSTEM_PROMPT = <<~PROMPT
11
+ You are a GitHub Pull Request Analyzer. Your role is to analyze merged PRs and help maintain the project task tree (Creatives).
12
+
13
+ ## When you receive a GitHub PR merged event:
14
+ 1. Use `github_pr_details` to get PR information
15
+ 2. Use `github_pr_diff` to analyze code changes
16
+ 3. Use `github_pr_commits` to understand the work done
17
+ 4. Use `creative_retrieval_service` to see the current task tree
18
+
19
+ ## Analysis Guidelines:
20
+ - Match PR changes to existing tasks in the Creative tree
21
+ - Identify which tasks were completed by the PR
22
+ - Suggest new tasks if the PR reveals additional work needed
23
+ - Consider the PR title, description, and commit messages for context
24
+
25
+ ## Response Format:
26
+ After your analysis, if you identify tasks to update or create, respond with an action comment.
27
+
28
+ For completed tasks, suggest updating progress to 1.0:
29
+ ```json
30
+ {
31
+ "actions": [
32
+ { "action": "update_creative", "creative_id": <id>, "attributes": { "progress": 1.0 } }
33
+ ]
34
+ }
35
+ ```
36
+
37
+ For new tasks discovered:
38
+ ```json
39
+ {
40
+ "actions": [
41
+ { "action": "create_creative", "parent_id": <parent_id>, "attributes": { "description": "Task description" } }
42
+ ]
43
+ }
44
+ ```
45
+
46
+ You can combine multiple actions in a single response.
47
+
48
+ ## Important:
49
+ - Only suggest updates for tasks that are clearly addressed by the PR
50
+ - Be conservative - don't mark tasks complete unless the PR clearly resolves them
51
+ - Provide a brief summary of your analysis before the action JSON
52
+ - If no task updates are needed, just provide your analysis summary without action JSON
53
+ PROMPT
54
+
55
+ # Matches GitHub PR merged events
56
+ # The comment content will contain "### GitHub: Pull Request merged"
57
+ ROUTING_EXPRESSION = <<~EXPR.strip
58
+ event_name == "comment_created" and chat.comment.user_id == nil and chat.comment.content contains "GitHub: Pull Request merged"
59
+ EXPR
60
+
61
+ TOOLS = %w[
62
+ github_pr_details
63
+ github_pr_diff
64
+ github_pr_commits
65
+ creative_retrieval_service
66
+ creative_update_service
67
+ ].freeze
68
+
69
+ def self.call
70
+ new.call
71
+ end
72
+
73
+ def call
74
+ agent = find_or_initialize_agent
75
+ configure_agent(agent)
76
+ agent.save!
77
+
78
+ puts "[CollavreGithub] GitHub PR Analyzer agent created/updated: #{agent.email}"
79
+ agent
80
+ end
81
+
82
+ private
83
+
84
+ def find_or_initialize_agent
85
+ user_class.find_or_initialize_by(email: AGENT_EMAIL)
86
+ end
87
+
88
+ def configure_agent(agent)
89
+ agent.assign_attributes(
90
+ name: "GitHub PR Analyzer",
91
+ password: SecureRandom.hex(32),
92
+ email_verified_at: Time.current,
93
+ llm_vendor: default_llm_vendor,
94
+ llm_model: default_llm_model,
95
+ system_prompt: SYSTEM_PROMPT,
96
+ routing_expression: ROUTING_EXPRESSION,
97
+ tools: TOOLS,
98
+ searchable: true # Can analyze PRs for any Creative with GitHub integration
99
+ )
100
+ end
101
+
102
+ def user_class
103
+ Collavre.user_class
104
+ end
105
+
106
+ def default_llm_vendor
107
+ ENV.fetch("COLLAVRE_DEFAULT_LLM_VENDOR", "gemini")
108
+ end
109
+
110
+ def default_llm_model
111
+ ENV.fetch("COLLAVRE_DEFAULT_LLM_MODEL", "gemini-2.5-flash")
112
+ end
113
+ end
114
+ end
115
+
116
+ # Run seeds when this file is loaded
117
+ CollavreGithub::Seeds.call
@@ -0,0 +1,74 @@
1
+ module CollavreGithub
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace CollavreGithub
4
+
5
+ config.generators do |g|
6
+ g.test_framework :minitest
7
+ end
8
+
9
+ def self.javascript_path
10
+ root.join("app/javascript")
11
+ end
12
+
13
+ config.i18n.load_path += Dir[root.join("config", "locales", "*.yml")]
14
+
15
+ initializer "collavre_github.routes", before: :add_routing_paths do |app|
16
+ app.routes.append do
17
+ mount CollavreGithub::Engine => "/github", as: :github_engine
18
+ match "/auth/github/callback", to: "collavre_github/auth#callback", via: [ :get, :post ]
19
+ end
20
+ end
21
+
22
+ initializer "collavre_github.migrations" do |app|
23
+ unless app.root.to_s.match?(root.to_s)
24
+ config.paths["db/migrate"].expanded.each do |expanded_path|
25
+ app.config.paths["db/migrate"] << expanded_path
26
+ end
27
+ end
28
+ end
29
+
30
+ initializer "collavre_github.register_integration", after: :load_config_initializers do
31
+ Rails.application.config.to_prepare do
32
+ if defined?(Collavre::IntegrationRegistry)
33
+ Collavre::IntegrationRegistry.register(:github, {
34
+ label: I18n.t("collavre_github.integration.label", default: "GitHub"),
35
+ icon: "github",
36
+ description: I18n.t("collavre_github.integration.description", default: "Connect repositories and receive webhook events"),
37
+ routes: CollavreGithub::Engine.routes.url_helpers,
38
+ creative_menu_partial: "collavre_github/integrations/modal"
39
+ })
40
+ end
41
+ end
42
+ end
43
+
44
+ initializer "collavre_github.user_associations", after: :load_config_initializers do
45
+ Rails.application.config.to_prepare do
46
+ user_class = Collavre.user_class rescue nil
47
+ next unless user_class
48
+
49
+ unless user_class.reflect_on_association(:github_account)
50
+ user_class.has_one :github_account,
51
+ class_name: "CollavreGithub::Account",
52
+ foreign_key: :user_id,
53
+ dependent: :destroy
54
+ Rails.logger.info("[CollavreGithub] Added github_account association to #{user_class.name}")
55
+ end
56
+ end
57
+ end
58
+
59
+ initializer "collavre_github.creative_associations", after: :load_config_initializers do
60
+ Rails.application.config.to_prepare do
61
+ creative_class = Collavre::Creative rescue nil
62
+ next unless creative_class
63
+
64
+ unless creative_class.reflect_on_association(:github_repository_links)
65
+ creative_class.has_many :github_repository_links,
66
+ class_name: "CollavreGithub::RepositoryLink",
67
+ foreign_key: :creative_id,
68
+ dependent: :destroy
69
+ Rails.logger.info("[CollavreGithub] Added github_repository_links association to #{creative_class.name}")
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,3 @@
1
+ module CollavreGithub
2
+ VERSION = "0.2.0"
3
+ end
@@ -0,0 +1,5 @@
1
+ require "collavre_github/version"
2
+ require "collavre_github/engine"
3
+
4
+ module CollavreGithub
5
+ end