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.
- checksums.yaml +7 -0
- data/README.md +127 -0
- data/Rakefile +8 -0
- data/app/controllers/collavre_github/accounts_controller.rb +84 -0
- data/app/controllers/collavre_github/application_controller.rb +11 -0
- data/app/controllers/collavre_github/auth_controller.rb +27 -0
- data/app/controllers/collavre_github/creatives/integrations_controller.rb +181 -0
- data/app/controllers/collavre_github/webhooks_controller.rb +304 -0
- data/app/models/collavre_github/account.rb +11 -0
- data/app/models/collavre_github/application_record.rb +5 -0
- data/app/models/collavre_github/repository_link.rb +19 -0
- data/app/services/collavre_github/client.rb +110 -0
- data/app/services/collavre_github/tools/concerns/github_client_finder.rb +36 -0
- data/app/services/collavre_github/tools/github_pr_commits_service.rb +40 -0
- data/app/services/collavre_github/tools/github_pr_details_service.rb +53 -0
- data/app/services/collavre_github/tools/github_pr_diff_service.rb +57 -0
- data/app/services/collavre_github/webhook_provisioner.rb +128 -0
- data/app/views/collavre_github/integrations/_modal.html.erb +77 -0
- data/config/locales/en.yml +80 -0
- data/config/locales/ko.yml +80 -0
- data/config/routes.rb +17 -0
- data/db/migrate/20250925000000_create_github_integrations.rb +26 -0
- data/db/migrate/20250927000000_add_webhook_secret_to_github_repository_links.rb +29 -0
- data/db/migrate/20250928105957_add_github_gemini_prompt_to_creatives.rb +5 -0
- data/db/seeds.rb +117 -0
- data/lib/collavre_github/engine.rb +74 -0
- data/lib/collavre_github/version.rb +3 -0
- data/lib/collavre_github.rb +5 -0
- 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">×</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
|
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
|