lex-github 0.2.5 → 0.3.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -0
  3. data/CLAUDE.md +45 -19
  4. data/README.md +155 -83
  5. data/lex-github.gemspec +2 -0
  6. data/lib/legion/extensions/github/app/actor/token_refresh.rb +68 -0
  7. data/lib/legion/extensions/github/app/actor/webhook_poller.rb +65 -0
  8. data/lib/legion/extensions/github/app/hooks/setup.rb +19 -0
  9. data/lib/legion/extensions/github/app/hooks/webhook.rb +19 -0
  10. data/lib/legion/extensions/github/app/runners/auth.rb +48 -0
  11. data/lib/legion/extensions/github/app/runners/credential_store.rb +46 -0
  12. data/lib/legion/extensions/github/app/runners/installations.rb +56 -0
  13. data/lib/legion/extensions/github/app/runners/manifest.rb +65 -0
  14. data/lib/legion/extensions/github/app/runners/webhooks.rb +118 -0
  15. data/lib/legion/extensions/github/app/transport/exchanges/app.rb +17 -0
  16. data/lib/legion/extensions/github/app/transport/messages/event.rb +18 -0
  17. data/lib/legion/extensions/github/app/transport/queues/auth.rb +18 -0
  18. data/lib/legion/extensions/github/app/transport/queues/webhooks.rb +18 -0
  19. data/lib/legion/extensions/github/cli/app.rb +57 -0
  20. data/lib/legion/extensions/github/cli/auth.rb +99 -0
  21. data/lib/legion/extensions/github/client.rb +24 -0
  22. data/lib/legion/extensions/github/errors.rb +44 -0
  23. data/lib/legion/extensions/github/helpers/browser_auth.rb +106 -0
  24. data/lib/legion/extensions/github/helpers/cache.rb +99 -0
  25. data/lib/legion/extensions/github/helpers/callback_server.rb +89 -0
  26. data/lib/legion/extensions/github/helpers/client.rb +292 -2
  27. data/lib/legion/extensions/github/helpers/scope_registry.rb +91 -0
  28. data/lib/legion/extensions/github/helpers/token_cache.rb +86 -0
  29. data/lib/legion/extensions/github/middleware/credential_fallback.rb +76 -0
  30. data/lib/legion/extensions/github/middleware/rate_limit.rb +40 -0
  31. data/lib/legion/extensions/github/middleware/scope_probe.rb +37 -0
  32. data/lib/legion/extensions/github/oauth/actor/token_refresh.rb +76 -0
  33. data/lib/legion/extensions/github/oauth/hooks/callback.rb +19 -0
  34. data/lib/legion/extensions/github/oauth/runners/auth.rb +111 -0
  35. data/lib/legion/extensions/github/oauth/transport/exchanges/oauth.rb +17 -0
  36. data/lib/legion/extensions/github/oauth/transport/queues/auth.rb +18 -0
  37. data/lib/legion/extensions/github/runners/actions.rb +100 -0
  38. data/lib/legion/extensions/github/runners/branches.rb +5 -3
  39. data/lib/legion/extensions/github/runners/checks.rb +84 -0
  40. data/lib/legion/extensions/github/runners/comments.rb +13 -7
  41. data/lib/legion/extensions/github/runners/commits.rb +11 -6
  42. data/lib/legion/extensions/github/runners/contents.rb +3 -1
  43. data/lib/legion/extensions/github/runners/deployments.rb +76 -0
  44. data/lib/legion/extensions/github/runners/gists.rb +9 -4
  45. data/lib/legion/extensions/github/runners/issues.rb +16 -9
  46. data/lib/legion/extensions/github/runners/labels.rb +16 -9
  47. data/lib/legion/extensions/github/runners/organizations.rb +10 -8
  48. data/lib/legion/extensions/github/runners/pull_requests.rb +24 -14
  49. data/lib/legion/extensions/github/runners/releases.rb +89 -0
  50. data/lib/legion/extensions/github/runners/repositories.rb +17 -10
  51. data/lib/legion/extensions/github/runners/repository_webhooks.rb +76 -0
  52. data/lib/legion/extensions/github/runners/search.rb +11 -8
  53. data/lib/legion/extensions/github/runners/users.rb +12 -8
  54. data/lib/legion/extensions/github/version.rb +1 -1
  55. data/lib/legion/extensions/github.rb +22 -0
  56. metadata +63 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c33c30330e8ac63765232cdb2b3534bfc3142232acf3cf4bde8b9e18935e5f49
4
- data.tar.gz: aeeb6eb49d4cf8c77be0d6bda5afafed25c164b5c17579f6033e1b0ea8ceb1ff
3
+ metadata.gz: 528c32f26f0baa590aa8084482cf2c006633669ed182df0e2288198fb509deb4
4
+ data.tar.gz: 453260cf722e2b8463816e4b9227ac2e6c70996063f68886f062e9710b01eda0
5
5
  SHA512:
6
- metadata.gz: 758a54fcae7dfec7ef44fbbc9311a71d324ad58b3645d6f487b699202595c44b55238adc7bd13c36e85b60fea1486b930d03dc50a9f9764393a4e27769003d3f
7
- data.tar.gz: 6ffefdc158e27d4fc689d5e36ef6a8b7805054d4f30f4ae9d440737e5bcde98d2a4bd8170e1265d0fd036eefb18ff2b40b4a96a77625d6fa057c1f929659beb0
6
+ metadata.gz: 4060b46c5f83856954a7c49e542351558202f855fe0d471a1984d59522e4d178fe78c2bfcc58d8a254b52bff4e763ff3ec233b16a8d2046fbbeb2b44cf089f4c
7
+ data.tar.gz: 7d148064e9e9b5225589f6c0d73e30938bc2d56beea9d4f89f2cb0443a2cc03d71f48fbe0b14a8a74ff150e44c4e94450185877c2539127da9ca2daa99507e2a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,55 @@
1
1
  # Changelog
2
2
 
3
+ ## [Unreleased]
4
+
5
+ ### Added
6
+ - GitHub App authentication (JWT generation, installation tokens, manifest flow)
7
+ - OAuth delegated authentication (Authorization Code + PKCE, device code fallback)
8
+ - Scope-aware credential resolution chain (8 sources, rate limit + scope fallback)
9
+ - `ScopeRegistry` for caching credential-to-owner/repo authorization status
10
+ - `CredentialFallback` Faraday middleware (transparent 403/429 retry with next credential)
11
+ - `RateLimit` Faraday middleware with automatic credential exhaustion tracking
12
+ - `ScopeProbe` Faraday middleware for passive scope learning from API responses
13
+ - `Helpers::Cache` for two-tier API response caching (global Redis + local in-memory)
14
+ - `Helpers::TokenCache` for token lifecycle management with per-installation keying
15
+ - `App::Runners::Auth` (JWT generation, installation token exchange)
16
+ - `App::Runners::Webhooks` (signature verification, event parsing, scope invalidation)
17
+ - `App::Runners::Manifest` (GitHub App manifest flow)
18
+ - `App::Runners::Installations` (list, get, suspend, unsuspend, delete)
19
+ - `App::Runners::CredentialStore` (Vault persistence after manifest flow)
20
+ - `OAuth::Runners::Auth` (authorize_url, exchange_code, refresh, device_code, revoke)
21
+ - `Runners::Actions` (GitHub Actions workflow management)
22
+ - `Runners::Checks` (check runs and check suites)
23
+ - `Runners::Releases` (release and asset management)
24
+ - `Runners::Deployments` (deployment and status management)
25
+ - `Runners::RepositoryWebhooks` (programmatic webhook management)
26
+ - `Helpers::CallbackServer` for standalone OAuth redirect handling
27
+ - `Helpers::BrowserAuth` for browser-based OAuth with PKCE
28
+ - `CLI::Auth` for `legion lex exec github auth login/status`
29
+ - `CLI::App` for `legion lex exec github app setup`
30
+ - `RateLimitError`, `AuthorizationError`, `ScopeDeniedError` error classes
31
+ - `jwt` (~> 2.7) and `base64` (>= 0.1) runtime dependencies
32
+
33
+ ### Changed
34
+ - `Helpers::Client` now uses scope-aware credential resolution (`owner:`, `repo:` context)
35
+ - All existing runners forward `owner:` and `repo:` to `connection()` for scope-aware resolution
36
+ - All existing runners now include `Helpers::Cache` for two-tier API response caching
37
+ - `Client` class includes App and OAuth runner modules
38
+ - Version bump to 0.3.0
39
+
40
+ ## [0.3.0] - 2026-03-30
41
+
42
+ ### Added
43
+ - GitHub App authentication (JWT generation, installation tokens via `App::Runners::Auth`)
44
+ - OAuth delegated user authentication (Authorization Code + PKCE, device code flow via `OAuth::Runners::Auth`)
45
+ - GitHub App manifest flow for streamlined app registration (`App::Runners::Manifest`)
46
+ - Webhook signature verification and event parsing (`App::Runners::Webhooks`)
47
+ - 8-source credential resolution chain: Vault delegated → Settings delegated → Vault App → Settings App → Vault PAT → Settings PAT → GH CLI → ENV (`Helpers::Client`)
48
+ - Rate limit fallback across credential sources with scope-aware skipping (`Helpers::ScopeRegistry`)
49
+ - Token lifecycle management with expiry tracking and rate limit recording (`Helpers::TokenCache`)
50
+ - Two-tier API response caching (global Redis + local in-memory) with configurable per-resource TTLs (`Helpers::Cache`)
51
+ - `jwt` (~> 2.7) and `base64` (>= 0.1) runtime dependencies
52
+
3
53
  ## [0.2.5] - 2026-03-30
4
54
 
5
55
  ### Changed
data/CLAUDE.md CHANGED
@@ -6,11 +6,11 @@
6
6
 
7
7
  ## Purpose
8
8
 
9
- Legion Extension that connects LegionIO to GitHub. Provides runners for interacting with the GitHub REST API covering repositories, issues, pull requests, users, organizations, gists, search, labels, comments, commits, branches, and file contents.
9
+ Legion Extension that connects LegionIO to GitHub. Provides runners for interacting with the GitHub REST API covering repositories, issues, pull requests, users, organizations, gists, search, labels, comments, commits, branches, file contents, GitHub App authentication, OAuth delegated auth, and webhook handling.
10
10
 
11
11
  **GitHub**: https://github.com/LegionIO/lex-github
12
12
  **License**: MIT
13
- **Version**: 0.2.4
13
+ **Version**: 0.3.0
14
14
 
15
15
  ## Architecture
16
16
 
@@ -29,40 +29,66 @@ Legion::Extensions::Github
29
29
  │ ├── Commits # List, get, compare commits
30
30
  │ ├── Branches # Create branches via Git Data API
31
31
  │ └── Contents # Commit multiple files via Git Data API
32
+ ├── App/
33
+ │ └── Runners/
34
+ │ ├── Auth # JWT generation, installation token exchange, list/get installations
35
+ │ ├── Webhooks # HMAC signature verification, event parsing
36
+ │ ├── Manifest # GitHub App manifest flow (generate, exchange code, manifest URL)
37
+ │ └── Installations # Full installation management (list repos, suspend, delete)
38
+ ├── OAuth/
39
+ │ └── Runners/
40
+ │ └── Auth # PKCE + Authorization Code, device code, refresh, revoke
32
41
  ├── Helpers/
33
- └── Client # Faraday connection builder (GitHub API v3)
42
+ ├── Client # 8-source scope-aware credential resolution chain + Faraday builder
43
+ │ ├── Cache # Two-tier read-through/write-through API response caching
44
+ │ ├── TokenCache # Token lifecycle management (store, fetch, expiry, rate limits)
45
+ │ └── ScopeRegistry # Credential-to-scope authorization cache (org/repo level)
34
46
  └── Client # Standalone client class (includes all runners)
35
47
  ```
36
48
 
49
+ ### Credential Resolution Chain (8 sources, in priority order)
50
+
51
+ 1. `resolve_vault_delegated` — OAuth user token from Vault (`github/oauth/delegated/token`)
52
+ 2. `resolve_settings_delegated` — OAuth user token from `Legion::Settings[:github][:oauth][:access_token]`
53
+ 3. `resolve_vault_app` — GitHub App installation token (requires cached token from `TokenCache`)
54
+ 4. `resolve_settings_app` — App token from settings (requires cached token)
55
+ 5. `resolve_vault_pat` — PAT from Vault (`github/token`)
56
+ 6. `resolve_settings_pat` — PAT from `Legion::Settings[:github][:token]`
57
+ 7. `resolve_gh_cli` — Token from `gh auth token` CLI command (cached 300s)
58
+ 8. `resolve_env` — `GITHUB_TOKEN` environment variable
59
+
60
+ Rate-limited credentials are skipped. Scope-denied credentials (for a given owner/repo) are skipped.
61
+
37
62
  ## Dependencies
38
63
 
39
64
  | Gem | Purpose |
40
65
  |-----|---------|
41
66
  | `faraday` | HTTP client for GitHub REST API |
67
+ | `jwt` (~> 2.7) | RS256 JWT generation for GitHub App authentication |
68
+ | `base64` (>= 0.1) | PKCE code challenge computation |
69
+ | `legion-cache` | Two-tier caching (global Redis + local in-memory) |
70
+ | `legion-crypt` | Vault secret resolution for credentials |
71
+ | `legion-settings` | Settings-based credential resolution |
42
72
 
43
73
  ## Key Files
44
74
 
45
75
  | File | Purpose |
46
76
  |------|---------|
47
- | `lib/legion/extensions/github.rb` | Extension entry point, requires all runners |
48
- | `lib/legion/extensions/github/client.rb` | Standalone client class |
49
- | `lib/legion/extensions/github/helpers/client.rb` | Faraday connection builder |
50
- | `lib/legion/extensions/github/runners/repositories.rb` | Repo CRUD, branches, tags |
51
- | `lib/legion/extensions/github/runners/issues.rb` | Issue CRUD |
52
- | `lib/legion/extensions/github/runners/pull_requests.rb` | PR CRUD, merge, files, reviews |
53
- | `lib/legion/extensions/github/runners/users.rb` | User lookup, followers/following |
54
- | `lib/legion/extensions/github/runners/organizations.rb` | Org info, repos, members |
55
- | `lib/legion/extensions/github/runners/gists.rb` | Gist CRUD |
56
- | `lib/legion/extensions/github/runners/search.rb` | Search repos/issues/users/code |
57
- | `lib/legion/extensions/github/runners/labels.rb` | Label CRUD, add/remove on issues |
58
- | `lib/legion/extensions/github/runners/comments.rb` | Issue/PR comment CRUD |
59
- | `lib/legion/extensions/github/runners/commits.rb` | List, get, compare commits |
60
- | `lib/legion/extensions/github/runners/branches.rb` | Create branches via Git Data API |
61
- | `lib/legion/extensions/github/runners/contents.rb` | Commit multiple files via Git Data API |
77
+ | `lib/legion/extensions/github.rb` | Extension entry point, requires all modules |
78
+ | `lib/legion/extensions/github/client.rb` | Standalone client class (includes all runners) |
79
+ | `lib/legion/extensions/github/helpers/client.rb` | Credential resolution chain + Faraday builder |
80
+ | `lib/legion/extensions/github/helpers/cache.rb` | Two-tier API response caching |
81
+ | `lib/legion/extensions/github/helpers/token_cache.rb` | Token lifecycle + rate limit tracking |
82
+ | `lib/legion/extensions/github/helpers/scope_registry.rb` | Credential-to-scope authorization cache |
83
+ | `lib/legion/extensions/github/app/runners/auth.rb` | JWT generation, installation tokens |
84
+ | `lib/legion/extensions/github/app/runners/webhooks.rb` | Webhook signature verification, event parsing |
85
+ | `lib/legion/extensions/github/app/runners/manifest.rb` | GitHub App manifest registration flow |
86
+ | `lib/legion/extensions/github/app/runners/installations.rb` | Installation management |
87
+ | `lib/legion/extensions/github/oauth/runners/auth.rb` | OAuth PKCE, device code, token refresh/revoke |
62
88
 
63
89
  ## Testing
64
90
 
65
- 57 specs across 14 spec files.
91
+ 131 specs across 23 spec files (growing with each new runner).
66
92
 
67
93
  ```bash
68
94
  bundle install
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # lex-github
2
2
 
3
- GitHub integration for [LegionIO](https://github.com/LegionIO/LegionIO). Provides runners for interacting with the GitHub REST API including repositories, issues, pull requests, labels, comments, commits, users, organizations, gists, and search.
3
+ GitHub integration for [LegionIO](https://github.com/LegionIO/LegionIO). Provides runners for interacting with the GitHub REST API including repositories, issues, pull requests, labels, comments, commits, users, organizations, gists, search, Actions workflows, checks, releases, deployments, webhooks, and full GitHub App + OAuth authentication.
4
4
 
5
5
  ## Installation
6
6
 
@@ -8,6 +8,62 @@ GitHub integration for [LegionIO](https://github.com/LegionIO/LegionIO). Provide
8
8
  gem install lex-github
9
9
  ```
10
10
 
11
+ ## Authentication
12
+
13
+ ### Personal Access Token (PAT)
14
+
15
+ ```ruby
16
+ client = Legion::Extensions::Github::Client.new(token: 'ghp_your_token')
17
+ ```
18
+
19
+ ### GitHub App (JWT + Installation Token)
20
+
21
+ ```ruby
22
+ # Set in Legion::Settings
23
+ # github.app.app_id: '12345'
24
+ # github.app.private_key_path: '/path/to/private-key.pem'
25
+ # github.app.installation_id: '67890'
26
+
27
+ client = Legion::Extensions::Github::Client.new
28
+ # Credentials resolved automatically from settings
29
+ ```
30
+
31
+ Or via Vault:
32
+ ```
33
+ vault write secret/github/app/app_id value='12345'
34
+ vault write secret/github/app/private_key value='-----BEGIN RSA PRIVATE KEY-----...'
35
+ vault write secret/github/app/installation_id value='67890'
36
+ ```
37
+
38
+ ### OAuth Delegated (browser-based login)
39
+
40
+ ```bash
41
+ # CLI login
42
+ legion lex exec github auth login
43
+ ```
44
+
45
+ ```ruby
46
+ # Programmatic (using the CLI::Auth mixin)
47
+ auth = Object.new.extend(Legion::Extensions::Github::CLI::Auth)
48
+ result = auth.login(client_id: 'Iv1.abc', client_secret: 'secret')
49
+ # Opens browser → PKCE flow; token can be stored in Vault if Vault integration is configured
50
+ ```
51
+
52
+ ### Credential Resolution Chain
53
+
54
+ lex-github resolves credentials automatically in priority order:
55
+
56
+ 1. Vault OAuth delegated token
57
+ 2. Settings OAuth access token
58
+ 3. Vault GitHub App installation token (auto-generates on miss)
59
+ 4. Settings GitHub App installation token
60
+ 5. Vault PAT
61
+ 6. Settings PAT (`github.token`)
62
+ 7. `gh` CLI token (`gh auth token`)
63
+ 8. `GITHUB_TOKEN` environment variable
64
+
65
+ Rate-limited credentials are skipped automatically. Scope-denied credentials (`403`) are skipped for the specific owner/repo and retried with the next source.
66
+
11
67
  ## Standalone Usage
12
68
 
13
69
  ```ruby
@@ -28,117 +84,133 @@ client.create_issue(owner: 'octocat', repo: 'Hello-World', title: 'Bug report')
28
84
  client.list_pull_requests(owner: 'octocat', repo: 'Hello-World')
29
85
  client.create_pull_request(owner: 'octocat', repo: 'Hello-World', title: 'Fix', head: 'fix-branch', base: 'main')
30
86
  client.merge_pull_request(owner: 'octocat', repo: 'Hello-World', pull_number: 42)
31
- client.list_pull_request_reviews(owner: 'octocat', repo: 'Hello-World', pull_number: 42)
32
-
33
- # Labels
34
- client.list_labels(owner: 'octocat', repo: 'Hello-World')
35
- client.create_label(owner: 'octocat', repo: 'Hello-World', name: 'bug', color: 'd73a4a')
36
- client.add_labels_to_issue(owner: 'octocat', repo: 'Hello-World', issue_number: 1, labels: ['bug'])
37
-
38
- # Comments
39
- client.list_comments(owner: 'octocat', repo: 'Hello-World', issue_number: 1)
40
- client.create_comment(owner: 'octocat', repo: 'Hello-World', issue_number: 1, body: 'Looks good!')
41
- client.update_comment(owner: 'octocat', repo: 'Hello-World', comment_id: 42, body: 'Updated text')
42
- client.delete_comment(owner: 'octocat', repo: 'Hello-World', comment_id: 42)
43
-
44
- # Users
45
- client.get_authenticated_user
46
- client.get_user(username: 'octocat')
47
-
48
- # Organizations
49
- client.get_org(org: 'github')
50
- client.list_org_repos(org: 'github')
51
-
52
- # Gists
53
- client.list_gists
54
- client.create_gist(files: { 'hello.rb' => { content: 'puts "hello"' } })
55
-
56
- # Search
57
- client.search_repositories(query: 'ruby language:ruby')
58
- client.search_issues(query: 'bug label:bug')
87
+
88
+ # GitHub Actions
89
+ client.list_workflows(owner: 'octocat', repo: 'Hello-World')
90
+ client.trigger_workflow(owner: 'octocat', repo: 'Hello-World', workflow_id: 'ci.yml', ref: 'main')
91
+ client.get_workflow_run(owner: 'octocat', repo: 'Hello-World', run_id: 12345)
92
+
93
+ # Check Runs (CI status)
94
+ client.create_check_run(owner: 'octocat', repo: 'Hello-World', name: 'CI', head_sha: 'abc123')
95
+ client.update_check_run(owner: 'octocat', repo: 'Hello-World', check_run_id: 1,
96
+ status: 'completed', conclusion: 'success')
97
+
98
+ # Releases
99
+ client.list_releases(owner: 'octocat', repo: 'Hello-World')
100
+ client.create_release(owner: 'octocat', repo: 'Hello-World', tag_name: 'v1.0.0')
101
+
102
+ # Deployments
103
+ client.create_deployment(owner: 'octocat', repo: 'Hello-World', ref: 'main', environment: 'production')
104
+ client.create_deployment_status(owner: 'octocat', repo: 'Hello-World', deployment_id: 1, state: 'success')
105
+
106
+ # Webhooks
107
+ client.list_webhooks(owner: 'octocat', repo: 'Hello-World')
108
+ client.create_webhook(owner: 'octocat', repo: 'Hello-World',
109
+ config: { url: 'https://example.com/webhook', content_type: 'json' })
110
+
111
+ # GitHub App
112
+ jwt_token = client.generate_jwt(app_id: '12345', private_key: File.read('private-key.pem'))
113
+ client.create_installation_token(jwt: jwt_token, installation_id: '67890')
114
+ client.list_installations(jwt: jwt_token)
115
+
116
+ # Webhook verification
117
+ client.verify_signature(payload: request.body.read, signature: request.env['HTTP_X_HUB_SIGNATURE_256'],
118
+ secret: 'webhook_secret')
59
119
  ```
60
120
 
61
121
  ## Functions
62
122
 
63
123
  ### Repositories
64
- - `list_repos` - List repositories for a user
65
- - `get_repo` - Get a single repository
66
- - `create_repo` - Create a new repository
67
- - `update_repo` - Update repository settings
68
- - `delete_repo` - Delete a repository
69
- - `list_branches` - List branches
70
- - `list_tags` - List tags
124
+ - `list_repos`, `get_repo`, `create_repo`, `update_repo`, `delete_repo`, `list_branches`, `list_tags`
71
125
 
72
126
  ### Issues
73
- - `list_issues` - List issues for a repository
74
- - `get_issue` - Get a single issue
75
- - `create_issue` - Create a new issue
76
- - `update_issue` - Update an issue
77
- - `list_issue_comments` - List comments on an issue
78
- - `create_issue_comment` - Create a comment on an issue
127
+ - `list_issues`, `get_issue`, `create_issue`, `update_issue`, `list_issue_comments`, `create_issue_comment`
79
128
 
80
129
  ### Pull Requests
81
- - `list_pull_requests` - List pull requests
82
- - `get_pull_request` - Get a single pull request
83
- - `create_pull_request` - Create a pull request
84
- - `update_pull_request` - Update a pull request
85
- - `merge_pull_request` - Merge a pull request
86
- - `list_pull_request_commits` - List commits on a PR
87
- - `list_pull_request_files` - List files changed in a PR
88
- - `list_pull_request_reviews` - List reviews on a PR
130
+ - `list_pull_requests`, `get_pull_request`, `create_pull_request`, `update_pull_request`, `merge_pull_request`
131
+ - `list_pull_request_commits`, `list_pull_request_files`, `list_pull_request_reviews`, `create_review`
89
132
 
90
133
  ### Labels
91
- - `list_labels` - List labels for a repository
92
- - `get_label` - Get a single label by name
93
- - `create_label` - Create a new label
94
- - `update_label` - Update a label
95
- - `delete_label` - Delete a label
96
- - `add_labels_to_issue` - Add labels to an issue
97
- - `remove_label_from_issue` - Remove a label from an issue
134
+ - `list_labels`, `get_label`, `create_label`, `update_label`, `delete_label`
135
+ - `add_labels_to_issue`, `remove_label_from_issue`
98
136
 
99
137
  ### Comments
100
- - `list_comments` - List comments on an issue or PR
101
- - `get_comment` - Get a single comment by ID
102
- - `create_comment` - Create a comment on an issue or PR
103
- - `update_comment` - Update a comment
104
- - `delete_comment` - Delete a comment
138
+ - `list_comments`, `get_comment`, `create_comment`, `update_comment`, `delete_comment`
105
139
 
106
140
  ### Users
107
- - `get_authenticated_user` - Get the authenticated user
108
- - `get_user` - Get a user by username
109
- - `list_followers` - List followers
110
- - `list_following` - List following
141
+ - `get_authenticated_user`, `get_user`, `list_followers`, `list_following`
111
142
 
112
143
  ### Organizations
113
- - `list_user_orgs` - List organizations for a user
114
- - `get_org` - Get an organization
115
- - `list_org_repos` - List repos in an organization
116
- - `list_org_members` - List organization members
144
+ - `list_user_orgs`, `get_org`, `list_org_repos`, `list_org_members`
117
145
 
118
146
  ### Gists
119
- - `list_gists` - List gists
120
- - `get_gist` - Get a single gist
121
- - `create_gist` - Create a gist
122
- - `update_gist` - Update a gist
123
- - `delete_gist` - Delete a gist
147
+ - `list_gists`, `get_gist`, `create_gist`, `update_gist`, `delete_gist`
124
148
 
125
149
  ### Search
126
- - `search_repositories` - Search repositories
127
- - `search_issues` - Search issues and PRs
128
- - `search_users` - Search users
129
- - `search_code` - Search code
150
+ - `search_repositories`, `search_issues`, `search_users`, `search_code`
130
151
 
131
152
  ### Commits
132
- - `list_commits` - List commits on a repository
133
- - `get_commit` - Get a single commit by SHA
134
- - `compare_commits` - Compare two commits, branches, or tags
153
+ - `list_commits`, `get_commit`, `compare_commits`
154
+
155
+ ### Branches
156
+ - `create_branch`
157
+
158
+ ### Contents
159
+ - `commit_files`
160
+
161
+ ### GitHub Actions
162
+ - `list_workflows`, `get_workflow`, `list_workflow_runs`, `get_workflow_run`, `trigger_workflow`
163
+ - `cancel_workflow_run`, `rerun_workflow`, `rerun_failed_jobs`
164
+ - `list_workflow_run_jobs`, `download_workflow_run_logs`, `list_workflow_run_artifacts`
165
+
166
+ ### Checks
167
+ - `create_check_run`, `update_check_run`, `get_check_run`
168
+ - `list_check_runs_for_ref`, `list_check_suites_for_ref`, `get_check_suite`
169
+ - `rerequest_check_suite`, `list_check_run_annotations`
170
+
171
+ ### Releases
172
+ - `list_releases`, `get_release`, `get_latest_release`, `get_release_by_tag`
173
+ - `create_release`, `update_release`, `delete_release`
174
+ - `list_release_assets`, `delete_release_asset`
175
+
176
+ ### Deployments
177
+ - `list_deployments`, `get_deployment`, `create_deployment`, `delete_deployment`
178
+ - `list_deployment_statuses`, `create_deployment_status`, `get_deployment_status`
179
+
180
+ ### Repository Webhooks
181
+ - `list_webhooks`, `get_webhook`, `create_webhook`, `update_webhook`, `delete_webhook`
182
+ - `ping_webhook`, `test_webhook`, `list_webhook_deliveries`
183
+
184
+ ### GitHub App Auth
185
+ - `generate_jwt`, `create_installation_token`, `list_installations`, `get_installation`
186
+ - `generate_manifest`, `exchange_manifest_code`, `manifest_url`
187
+ - `verify_signature`, `parse_event`, `receive_event`
188
+
189
+ ### OAuth
190
+ - `generate_pkce`, `authorize_url`, `exchange_code`, `refresh_token`
191
+ - `request_device_code`, `poll_device_code`, `revoke_token`
192
+
193
+ ## Error Handling
194
+
195
+ ```ruby
196
+ begin
197
+ client.get_repo(owner: 'org', repo: 'private-repo')
198
+ rescue Legion::Extensions::Github::RateLimitError => e
199
+ puts "Rate limited, resets at: #{e.reset_at}"
200
+ rescue Legion::Extensions::Github::ScopeDeniedError => e
201
+ puts "No credential authorized for #{e.owner}/#{e.repo}"
202
+ rescue Legion::Extensions::Github::AuthorizationError => e
203
+ puts "All credentials exhausted: #{e.attempted_sources}"
204
+ end
205
+ ```
135
206
 
136
207
  ## Requirements
137
208
 
138
209
  - Ruby >= 3.4
139
210
  - [LegionIO](https://github.com/LegionIO/LegionIO) framework (optional for standalone client usage)
140
- - GitHub personal access token or app token
141
211
  - `faraday` >= 2.0
212
+ - `jwt` ~> 2.7 (for GitHub App authentication)
213
+ - `base64` >= 0.1 (for OAuth PKCE)
142
214
 
143
215
  ## License
144
216
 
data/lex-github.gemspec CHANGED
@@ -26,7 +26,9 @@ Gem::Specification.new do |spec|
26
26
  end
27
27
  spec.require_paths = ['lib']
28
28
 
29
+ spec.add_dependency 'base64', '>= 0.1'
29
30
  spec.add_dependency 'faraday', '>= 2.0'
31
+ spec.add_dependency 'jwt', '~> 2.7'
30
32
  spec.add_dependency 'legion-cache', '>= 1.3.11'
31
33
  spec.add_dependency 'legion-crypt', '>= 1.4.9'
32
34
  spec.add_dependency 'legion-data', '>= 1.4.17'
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Github
6
+ module App
7
+ module Actor
8
+ class TokenRefresh < Legion::Extensions::Actors::Every # rubocop:disable Legion/Extension/SelfContainedActorRunnerClass,Legion/Extension/EveryActorRequiresTime
9
+ def use_runner? = false
10
+ def check_subtask? = false
11
+ def generate_task? = false
12
+
13
+ def time
14
+ 45 * 60
15
+ end
16
+
17
+ # rubocop:disable Legion/Extension/ActorEnabledSideEffects
18
+ def enabled?
19
+ defined?(Legion::Extensions::Github::Helpers::TokenCache)
20
+ rescue StandardError => _e
21
+ false
22
+ end
23
+ # rubocop:enable Legion/Extension/ActorEnabledSideEffects
24
+
25
+ def manual
26
+ log.info('App::Actor::TokenRefresh: refreshing installation token')
27
+ settings = github_app_settings
28
+ return unless settings[:app_id] && settings[:private_key] && settings[:installation_id]
29
+
30
+ auth = Object.new.extend(Legion::Extensions::Github::App::Runners::Auth)
31
+ jwt_result = auth.generate_jwt(app_id: settings[:app_id], private_key: settings[:private_key])
32
+ return unless jwt_result[:result]
33
+
34
+ token_result = auth.create_installation_token(
35
+ jwt: jwt_result[:result],
36
+ installation_id: settings[:installation_id]
37
+ )
38
+ return unless token_result.dig(:result, 'token')
39
+
40
+ token_cache.store_token(
41
+ token: token_result[:result]['token'],
42
+ auth_type: :app_installation,
43
+ expires_at: Time.parse(token_result[:result]['expires_at'])
44
+ )
45
+ log.info('App::Actor::TokenRefresh: installation token refreshed')
46
+ rescue StandardError => e
47
+ log.error("App::Actor::TokenRefresh: #{e.message}")
48
+ end
49
+
50
+ private
51
+
52
+ def github_app_settings
53
+ return {} unless defined?(Legion::Settings)
54
+
55
+ Legion::Settings[:github]&.dig(:app) || {}
56
+ rescue StandardError => _e
57
+ {}
58
+ end
59
+
60
+ def token_cache
61
+ Object.new.extend(Legion::Extensions::Github::Helpers::TokenCache)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Github
6
+ module App
7
+ module Actor
8
+ class WebhookPoller < Legion::Extensions::Actors::Poll # rubocop:disable Legion/Extension/SelfContainedActorRunnerClass,Legion/Extension/EveryActorRequiresTime
9
+ def use_runner? = false
10
+ def check_subtask? = false
11
+ def generate_task? = false
12
+
13
+ def time
14
+ 60
15
+ end
16
+
17
+ # rubocop:disable Legion/Extension/ActorEnabledSideEffects
18
+ def enabled?
19
+ github_poll_settings[:owner] && github_poll_settings[:repo]
20
+ rescue StandardError => _e
21
+ false
22
+ end
23
+ # rubocop:enable Legion/Extension/ActorEnabledSideEffects
24
+
25
+ def manual
26
+ settings = github_poll_settings
27
+ owner = settings[:owner]
28
+ repo = settings[:repo]
29
+ return unless owner && repo
30
+
31
+ client = Legion::Extensions::Github::Client.new
32
+ return unless client.respond_to?(:list_events)
33
+
34
+ result = client.list_events(owner: owner, repo: repo)
35
+ events = result[:result]
36
+ return unless events.is_a?(Array)
37
+
38
+ events.each do |event|
39
+ publish_event(event)
40
+ end
41
+ rescue StandardError => e
42
+ log.error("App::Actor::WebhookPoller: #{e.message}")
43
+ end
44
+
45
+ private
46
+
47
+ def github_poll_settings
48
+ return {} unless defined?(Legion::Settings)
49
+
50
+ Legion::Settings[:github]&.dig(:webhook_poller) || {}
51
+ rescue StandardError => _e
52
+ {}
53
+ end
54
+
55
+ def publish_event(event)
56
+ Legion::Extensions::Github::App::Transport::Messages::Event.new(event).publish
57
+ rescue StandardError => e
58
+ log.warn("WebhookPoller#publish_event: #{e.message}")
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Github
6
+ module App
7
+ module Hooks
8
+ class Setup < Legion::Extensions::Hooks::Base # rubocop:disable Legion/Extension/HookMissingRunnerClass
9
+ mount '/setup/callback'
10
+
11
+ def self.runner_class
12
+ 'Legion::Extensions::Github::App::Runners::Manifest'
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Github
6
+ module App
7
+ module Hooks
8
+ class Webhook < Legion::Extensions::Hooks::Base # rubocop:disable Legion/Extension/HookMissingRunnerClass
9
+ mount '/webhook'
10
+
11
+ def self.runner_class
12
+ 'Legion::Extensions::Github::App::Runners::Webhooks'
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end