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
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,8 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
@@ -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,11 @@
1
+ module CollavreGithub
2
+ class ApplicationController < ::ApplicationController
3
+ private
4
+
5
+ def github_webhook_url
6
+ CollavreGithub::Engine.routes.url_helpers.webhooks_url(
7
+ Rails.application.config.action_mailer.default_url_options
8
+ )
9
+ end
10
+ end
11
+ 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