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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6e34dfe63b704291f75021325bbd14373f550306cacc75659357fd014fe43f39
|
|
4
|
+
data.tar.gz: a5c250dbbbbac3307e1a25fe2850de5597f0ca034c16b1768f41afc46dc6550a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d79519c6b6486d4cc80e4a9d306aff4b14416f8f8610bc63ec968d6b69c2a581bc3ba350bc4b83e3bfa58584175923a94ab14b453de5a96f209df11b56fa2bf0
|
|
7
|
+
data.tar.gz: c5a798233218d63d8c5662be0d280e70010bccbc29474d194318d088645170be0a2d5c9cedc3458985003b2a3ca39e41d40b2e4455835d460f8d67f2292142d8
|
data/README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Collavre GitHub
|
|
2
|
+
|
|
3
|
+
GitHub integration engine for Collavre.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- OAuth authentication with GitHub
|
|
8
|
+
- Repository linking to Creatives
|
|
9
|
+
- Webhook → System Comment automatic creation
|
|
10
|
+
- **MCP Tools** for AI Agents to analyze PRs
|
|
11
|
+
- **Seed AI Agent**: GitHub PR Analyzer
|
|
12
|
+
|
|
13
|
+
## Architecture
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
GitHub Webhook (push, pull_request, etc.)
|
|
17
|
+
│
|
|
18
|
+
▼
|
|
19
|
+
System Comment created in linked Creative
|
|
20
|
+
│
|
|
21
|
+
▼
|
|
22
|
+
comment_created event dispatched
|
|
23
|
+
│
|
|
24
|
+
▼
|
|
25
|
+
AI Agent (GitHub PR Analyzer) triggered
|
|
26
|
+
│
|
|
27
|
+
├── github_pr_details - Get PR info
|
|
28
|
+
├── github_pr_diff - Get code changes
|
|
29
|
+
├── github_pr_commits - Get commit messages
|
|
30
|
+
└── creative_retrieval_service - Get task tree
|
|
31
|
+
│
|
|
32
|
+
▼
|
|
33
|
+
Action Comment response (JSON format)
|
|
34
|
+
│
|
|
35
|
+
▼
|
|
36
|
+
User approval → Creative updates
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## MCP Tools
|
|
40
|
+
|
|
41
|
+
Tools for AI Agents to interact with GitHub:
|
|
42
|
+
|
|
43
|
+
| Tool | Description |
|
|
44
|
+
|------|-------------|
|
|
45
|
+
| `github_pr_details` | Get PR title, body, author, files, additions/deletions |
|
|
46
|
+
| `github_pr_diff` | Get PR diff with truncation support (default: 10K chars) |
|
|
47
|
+
| `github_pr_commits` | Get list of commit messages in the PR |
|
|
48
|
+
|
|
49
|
+
### Tool Parameters
|
|
50
|
+
|
|
51
|
+
All tools require:
|
|
52
|
+
- `creative_id` - The Creative with GitHub integration
|
|
53
|
+
- `repo` - Repository full name (e.g., `owner/repo`)
|
|
54
|
+
- `pr_number` - Pull request number
|
|
55
|
+
|
|
56
|
+
The tools automatically find the GitHub account through:
|
|
57
|
+
```
|
|
58
|
+
Creative → RepositoryLink → GithubAccount → GitHub API
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Seed AI Agent
|
|
62
|
+
|
|
63
|
+
Running `rails db:seed` creates the **GitHub PR Analyzer** agent:
|
|
64
|
+
|
|
65
|
+
- **Name**: GitHub PR Analyzer
|
|
66
|
+
- **Email**: `github-pr-analyzer@collavre.local`
|
|
67
|
+
- **Trigger**: GitHub PR merged events (system comments)
|
|
68
|
+
- **Tools**: github_pr_details, github_pr_diff, github_pr_commits, creative_retrieval_service, creative_update_service
|
|
69
|
+
- **Output**: Action comments for updating Creative progress
|
|
70
|
+
|
|
71
|
+
### Routing Expression
|
|
72
|
+
|
|
73
|
+
```liquid
|
|
74
|
+
event_name == "comment_created" and chat.comment.user_id == nil and chat.comment.content contains "GitHub: Pull Request merged"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Installation
|
|
78
|
+
|
|
79
|
+
Add to your Gemfile:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
gem "collavre_github", path: "engines/collavre_github"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Configuration
|
|
86
|
+
|
|
87
|
+
### OAuth Setup
|
|
88
|
+
|
|
89
|
+
1. Create a GitHub OAuth App at https://github.com/settings/developers
|
|
90
|
+
2. Set callback URL to: `https://your-domain.com/auth/github/callback`
|
|
91
|
+
3. Add credentials:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
bin/rails credentials:edit
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```yaml
|
|
98
|
+
github:
|
|
99
|
+
client_id: your_client_id
|
|
100
|
+
client_secret: your_client_secret
|
|
101
|
+
webhook_secret: optional_fallback_secret
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### OmniAuth Configuration
|
|
105
|
+
|
|
106
|
+
In `config/initializers/omniauth.rb`:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
Rails.application.config.middleware.use OmniAuth::Builder do
|
|
110
|
+
provider :github,
|
|
111
|
+
Rails.application.credentials.dig(:github, :client_id),
|
|
112
|
+
Rails.application.credentials.dig(:github, :client_secret),
|
|
113
|
+
scope: "repo,read:org"
|
|
114
|
+
end
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Usage
|
|
118
|
+
|
|
119
|
+
1. Connect GitHub account via OAuth
|
|
120
|
+
2. Link repository to a Creative
|
|
121
|
+
3. Webhook events automatically create system comments
|
|
122
|
+
4. AI Agent analyzes PRs and suggests task updates
|
|
123
|
+
5. User approves action comments to update Creatives
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
AGPL
|
data/Rakefile
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
module CollavreGithub
|
|
2
|
+
class AccountsController < ApplicationController
|
|
3
|
+
before_action :require_account
|
|
4
|
+
|
|
5
|
+
def show
|
|
6
|
+
render json: serialize_account(Current.user.github_account)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def organizations
|
|
10
|
+
orgs = github_client.organizations.map do |org|
|
|
11
|
+
{
|
|
12
|
+
id: org[:id] || org["id"],
|
|
13
|
+
login: org[:login] || org["login"],
|
|
14
|
+
name: org[:name] || org["name"] || (org[:login] || org["login"]),
|
|
15
|
+
type: org[:type] || org["type"]
|
|
16
|
+
}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
user_org = {
|
|
20
|
+
id: Current.user.github_account.github_uid,
|
|
21
|
+
login: Current.user.github_account.login,
|
|
22
|
+
name: Current.user.github_account.name.presence || Current.user.github_account.login,
|
|
23
|
+
type: "User"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
render json: { organizations: [ user_org ] + orgs }
|
|
27
|
+
rescue Octokit::Unauthorized
|
|
28
|
+
render json: { error: "unauthorized" }, status: :unauthorized
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def repositories
|
|
32
|
+
organization = params[:organization]
|
|
33
|
+
creative = params[:creative_id].present? ? Collavre::Creative.find_by(id: params[:creative_id]) : nil
|
|
34
|
+
selected = if creative
|
|
35
|
+
creative.github_repository_links.where(github_account: Current.user.github_account)
|
|
36
|
+
.pluck(:repository_full_name)
|
|
37
|
+
else
|
|
38
|
+
[]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
repos = fetch_repositories(organization).map do |repo|
|
|
42
|
+
full_name = repo[:full_name] || repo["full_name"]
|
|
43
|
+
{
|
|
44
|
+
id: repo[:id] || repo["id"],
|
|
45
|
+
name: repo[:name] || repo["name"],
|
|
46
|
+
full_name: full_name,
|
|
47
|
+
selected: selected.include?(full_name)
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
render json: { repositories: repos }
|
|
52
|
+
rescue Octokit::NotFound
|
|
53
|
+
render json: { error: "not_found" }, status: :not_found
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def serialize_account(account)
|
|
59
|
+
{
|
|
60
|
+
login: account.login,
|
|
61
|
+
name: account.name,
|
|
62
|
+
avatar_url: account.avatar_url
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def require_account
|
|
67
|
+
return if Current.user.github_account
|
|
68
|
+
|
|
69
|
+
render json: { connected: false, error: "not_connected" }, status: :unprocessable_entity
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def github_client
|
|
73
|
+
@github_client ||= CollavreGithub::Client.new(Current.user.github_account)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def fetch_repositories(organization)
|
|
77
|
+
if organization.blank? || organization == Current.user.github_account.login
|
|
78
|
+
github_client.repositories_for_authenticated_user
|
|
79
|
+
else
|
|
80
|
+
github_client.repositories_for_organization(organization)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module CollavreGithub
|
|
2
|
+
class AuthController < ApplicationController
|
|
3
|
+
allow_unauthenticated_access only: :callback
|
|
4
|
+
before_action -> { enforce_auth_provider!(:github) }, only: :callback
|
|
5
|
+
|
|
6
|
+
def callback
|
|
7
|
+
auth = request.env["omniauth.auth"]
|
|
8
|
+
account = CollavreGithub::Account.find_or_initialize_by(github_uid: auth.uid)
|
|
9
|
+
|
|
10
|
+
if account.new_record?
|
|
11
|
+
unless Current.user
|
|
12
|
+
redirect_to collavre.new_session_path, alert: I18n.t("collavre_github.auth.login_first")
|
|
13
|
+
return
|
|
14
|
+
end
|
|
15
|
+
account.user_id = Current.user.id
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
account.token = auth.credentials.token
|
|
19
|
+
account.login = auth.info.nickname
|
|
20
|
+
account.name = auth.info.name
|
|
21
|
+
account.avatar_url = auth.info.image
|
|
22
|
+
account.save!
|
|
23
|
+
|
|
24
|
+
redirect_to collavre.creatives_path, notice: I18n.t("collavre_github.auth.connected")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
module CollavreGithub
|
|
2
|
+
module Creatives
|
|
3
|
+
class IntegrationsController < ApplicationController
|
|
4
|
+
before_action :set_creative
|
|
5
|
+
before_action :ensure_read_permission
|
|
6
|
+
before_action :ensure_admin_permission, only: [ :show, :update ]
|
|
7
|
+
|
|
8
|
+
def show
|
|
9
|
+
account = Current.user.github_account
|
|
10
|
+
links = linked_repository_links(account)
|
|
11
|
+
|
|
12
|
+
render json: {
|
|
13
|
+
connected: account.present?,
|
|
14
|
+
account: account && {
|
|
15
|
+
login: account.login,
|
|
16
|
+
name: account.name,
|
|
17
|
+
avatar_url: account.avatar_url
|
|
18
|
+
},
|
|
19
|
+
selected_repositories: links.map(&:repository_full_name),
|
|
20
|
+
webhooks: serialize_webhooks(links),
|
|
21
|
+
github_gemini_prompt: @creative.github_gemini_prompt_template
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def update
|
|
26
|
+
account = Current.user.github_account
|
|
27
|
+
unless account
|
|
28
|
+
render json: { error: I18n.t("collavre_github.errors.not_connected") }, status: :unprocessable_entity
|
|
29
|
+
return
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
integration_attributes = integration_params
|
|
33
|
+
repositories = Array(integration_attributes[:repositories]).map(&:to_s).uniq
|
|
34
|
+
prompt_param = integration_attributes[:github_gemini_prompt] if integration_attributes.key?(:github_gemini_prompt)
|
|
35
|
+
|
|
36
|
+
links = nil
|
|
37
|
+
|
|
38
|
+
CollavreGithub::RepositoryLink.transaction do
|
|
39
|
+
linked_repository_links(account)
|
|
40
|
+
.where.not(repository_full_name: repositories)
|
|
41
|
+
.delete_all
|
|
42
|
+
|
|
43
|
+
repositories.each do |full_name|
|
|
44
|
+
@creative.github_repository_links.find_or_create_by!(
|
|
45
|
+
github_account: account,
|
|
46
|
+
repository_full_name: full_name
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
links = linked_repository_links(account).to_a
|
|
51
|
+
|
|
52
|
+
if prompt_param
|
|
53
|
+
@creative.update!(github_gemini_prompt: prompt_param.presence)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
CollavreGithub::WebhookProvisioner.ensure_for_links(
|
|
58
|
+
account: account,
|
|
59
|
+
links: links,
|
|
60
|
+
webhook_url: github_webhook_url
|
|
61
|
+
) if links.present?
|
|
62
|
+
|
|
63
|
+
render json: {
|
|
64
|
+
success: true,
|
|
65
|
+
selected_repositories: links.map(&:repository_full_name),
|
|
66
|
+
webhooks: serialize_webhooks(links),
|
|
67
|
+
github_gemini_prompt: @creative.github_gemini_prompt_template
|
|
68
|
+
}
|
|
69
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
70
|
+
render json: { error: e.message }, status: :unprocessable_entity
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def destroy
|
|
74
|
+
unless @creative.has_permission?(Current.user, :write)
|
|
75
|
+
render json: { error: I18n.t("collavre_github.errors.forbidden") }, status: :forbidden
|
|
76
|
+
return
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
account = Current.user.github_account
|
|
80
|
+
unless account
|
|
81
|
+
render json: { error: I18n.t("collavre_github.errors.not_connected") }, status: :unprocessable_entity
|
|
82
|
+
return
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
repositories = Array(params[:repositories]).filter_map { |value| value.to_s.presence }
|
|
86
|
+
repository = params[:repository].presence || params[:repository_full_name].presence
|
|
87
|
+
|
|
88
|
+
scope = linked_repository_links(account)
|
|
89
|
+
|
|
90
|
+
removed_repositories = []
|
|
91
|
+
|
|
92
|
+
if repositories.present?
|
|
93
|
+
links_to_remove = scope.where(repository_full_name: repositories).to_a
|
|
94
|
+
missing_repositories = repositories - links_to_remove.map(&:repository_full_name)
|
|
95
|
+
if missing_repositories.present?
|
|
96
|
+
render json: { error: I18n.t("collavre_github.errors.not_found") }, status: :not_found
|
|
97
|
+
return
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
CollavreGithub::RepositoryLink.transaction do
|
|
101
|
+
removed_repositories = links_to_remove.map(&:repository_full_name)
|
|
102
|
+
links_to_remove.each(&:destroy!)
|
|
103
|
+
end
|
|
104
|
+
elsif repository
|
|
105
|
+
link = scope.find_by(repository_full_name: repository)
|
|
106
|
+
unless link
|
|
107
|
+
render json: { error: I18n.t("collavre_github.errors.not_found") }, status: :not_found
|
|
108
|
+
return
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
CollavreGithub::RepositoryLink.transaction do
|
|
112
|
+
removed_repositories = [ link.repository_full_name ]
|
|
113
|
+
link.destroy!
|
|
114
|
+
end
|
|
115
|
+
else
|
|
116
|
+
CollavreGithub::RepositoryLink.transaction do
|
|
117
|
+
removed_repositories = scope.pluck(:repository_full_name)
|
|
118
|
+
scope.destroy_all
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
if removed_repositories.present?
|
|
123
|
+
CollavreGithub::WebhookProvisioner.remove_for_repositories(
|
|
124
|
+
account: account,
|
|
125
|
+
repositories: removed_repositories,
|
|
126
|
+
webhook_url: github_webhook_url
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
links = linked_repository_links(account)
|
|
131
|
+
|
|
132
|
+
render json: {
|
|
133
|
+
success: true,
|
|
134
|
+
selected_repositories: links.pluck(:repository_full_name),
|
|
135
|
+
webhooks: serialize_webhooks(links),
|
|
136
|
+
github_gemini_prompt: @creative.github_gemini_prompt_template
|
|
137
|
+
}
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
private
|
|
141
|
+
|
|
142
|
+
def set_creative
|
|
143
|
+
@creative = Collavre::Creative.find(params[:creative_id])
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def ensure_read_permission
|
|
147
|
+
return if @creative.has_permission?(Current.user, :read)
|
|
148
|
+
|
|
149
|
+
render json: { error: I18n.t("collavre_github.errors.forbidden") }, status: :forbidden
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def ensure_admin_permission
|
|
153
|
+
return if @creative.has_permission?(Current.user, :admin)
|
|
154
|
+
|
|
155
|
+
render json: { error: I18n.t("collavre_github.errors.forbidden") }, status: :forbidden
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def linked_repository_links(account)
|
|
159
|
+
return CollavreGithub::RepositoryLink.none unless account
|
|
160
|
+
|
|
161
|
+
@creative.github_repository_links.where(github_account: account)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def integration_params
|
|
165
|
+
params.permit(:github_gemini_prompt, repositories: [])
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def serialize_webhooks(links)
|
|
169
|
+
return {} if links.blank?
|
|
170
|
+
|
|
171
|
+
url = github_webhook_url
|
|
172
|
+
links.each_with_object({}) do |link, hash|
|
|
173
|
+
hash[link.repository_full_name] = {
|
|
174
|
+
url: url,
|
|
175
|
+
secret: link.webhook_secret
|
|
176
|
+
}
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|