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
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+ require 'legion/cache/helper'
5
+
6
+ module Legion
7
+ module Extensions
8
+ module Github
9
+ module Helpers
10
+ module TokenCache
11
+ include Legion::Cache::Helper
12
+
13
+ TOKEN_BUFFER_SECONDS = 300
14
+
15
+ def store_token(token:, auth_type:, expires_at:, installation_id: nil, metadata: {}, **)
16
+ entry = { token: token, auth_type: auth_type,
17
+ expires_at: expires_at.respond_to?(:iso8601) ? expires_at.iso8601 : expires_at,
18
+ installation_id: installation_id, metadata: metadata }
19
+ ttl = [(expires_at.respond_to?(:to_i) ? expires_at.to_i - Time.now.to_i : 3600), 60].max
20
+ key = token_cache_key(auth_type, installation_id)
21
+ cache_set(key, entry, ttl: ttl) if cache_connected?
22
+ local_cache_set(key, entry, ttl: ttl) if local_cache_connected?
23
+ end
24
+
25
+ def fetch_token(auth_type:, installation_id: nil, **)
26
+ key = token_cache_key(auth_type, installation_id)
27
+ entry = token_cache_read(key)
28
+
29
+ entry = token_cache_read(token_cache_key(auth_type, nil)) if entry.nil? && installation_id
30
+
31
+ return nil unless entry
32
+
33
+ expires = begin
34
+ Time.parse(entry[:expires_at].to_s)
35
+ rescue StandardError => _e
36
+ nil
37
+ end
38
+ return nil if expires && expires < Time.now + TOKEN_BUFFER_SECONDS
39
+
40
+ entry
41
+ end
42
+
43
+ def mark_rate_limited(auth_type:, reset_at:, **)
44
+ entry = { reset_at: reset_at.respond_to?(:iso8601) ? reset_at.iso8601 : reset_at }
45
+ ttl = [(reset_at.respond_to?(:to_i) ? reset_at.to_i - Time.now.to_i : 300), 10].max
46
+ key = "github:rate_limit:#{auth_type}"
47
+ cache_set(key, entry, ttl: ttl) if cache_connected?
48
+ local_cache_set(key, entry, ttl: ttl) if local_cache_connected?
49
+ end
50
+
51
+ def rate_limited?(auth_type:, **)
52
+ key = "github:rate_limit:#{auth_type}"
53
+ entry = if cache_connected?
54
+ cache_get(key)
55
+ elsif local_cache_connected?
56
+ local_cache_get(key)
57
+ end
58
+ return false unless entry
59
+
60
+ reset = begin
61
+ Time.parse(entry[:reset_at].to_s)
62
+ rescue StandardError => _e
63
+ nil
64
+ end
65
+ reset.nil? || reset > Time.now
66
+ end
67
+
68
+ private
69
+
70
+ def token_cache_key(auth_type, installation_id)
71
+ base = "github:token:#{auth_type}"
72
+ installation_id ? "#{base}:#{installation_id}" : base
73
+ end
74
+
75
+ def token_cache_read(key)
76
+ if cache_connected?
77
+ cache_get(key)
78
+ elsif local_cache_connected?
79
+ local_cache_get(key)
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Github
8
+ module Middleware
9
+ class CredentialFallback < ::Faraday::Middleware
10
+ RETRYABLE_STATUSES = [403, 429].freeze
11
+ IDEMPOTENT_METHODS = %w[GET HEAD OPTIONS PUT DELETE].freeze
12
+
13
+ def initialize(app, resolver: nil)
14
+ super(app)
15
+ @resolver = resolver
16
+ end
17
+
18
+ def call(env)
19
+ response = @app.call(env)
20
+ return response unless should_retry?(response)
21
+
22
+ retries = 0
23
+ max = @resolver.respond_to?(:max_fallback_retries) ? @resolver.max_fallback_retries : 3
24
+
25
+ while retries < max && should_retry?(response)
26
+ notify_resolver(response)
27
+
28
+ owner, repo = extract_owner_repo_from_env(env)
29
+ next_credential = @resolver&.resolve_next_credential(owner: owner, repo: repo)
30
+ break unless next_credential
31
+
32
+ env[:request_headers]['Authorization'] = "Bearer #{next_credential[:token]}"
33
+
34
+ response = @app.call(env)
35
+ retries += 1
36
+ end
37
+
38
+ response
39
+ end
40
+
41
+ private
42
+
43
+ def should_retry?(response)
44
+ return false unless @resolver.respond_to?(:credential_fallback?)
45
+ return false unless @resolver.credential_fallback?
46
+ return false unless IDEMPOTENT_METHODS.include?(response.env[:method].to_s.upcase)
47
+
48
+ RETRYABLE_STATUSES.include?(response.status)
49
+ end
50
+
51
+ def extract_owner_repo_from_env(env)
52
+ path = env.url&.path.to_s
53
+ match = path.match(%r{^/repos/([^/]+)/([^/]+)})
54
+ match ? [match[1], match[2]] : [nil, nil]
55
+ end
56
+
57
+ def notify_resolver(response)
58
+ if response.status == 429 && @resolver.respond_to?(:on_rate_limit)
59
+ reset = response.headers['x-ratelimit-reset']
60
+ reset_at = reset ? Time.at(reset.to_i) : Time.now + 60
61
+ @resolver.on_rate_limit(remaining: 0, reset_at: reset_at,
62
+ status: 429, url: response.env.url.to_s)
63
+ elsif response.status == 403 && @resolver.respond_to?(:on_scope_denied)
64
+ @resolver.on_scope_denied(status: 403, url: response.env.url.to_s,
65
+ path: response.env.url.path)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ Faraday::Middleware.register_middleware(
75
+ github_credential_fallback: Legion::Extensions::Github::Middleware::CredentialFallback
76
+ )
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Github
8
+ module Middleware
9
+ class RateLimit < ::Faraday::Middleware
10
+ def initialize(app, handler: nil)
11
+ super(app)
12
+ @handler = handler
13
+ end
14
+
15
+ def on_complete(env)
16
+ remaining = env.response_headers['x-ratelimit-remaining']
17
+ reset = env.response_headers['x-ratelimit-reset']
18
+ return unless remaining
19
+
20
+ remaining_int = remaining.to_i
21
+ return unless remaining_int.zero? || env.status == 429
22
+ return unless @handler.respond_to?(:on_rate_limit)
23
+
24
+ reset_at = reset ? Time.at(reset.to_i) : Time.now + 60
25
+ @handler.on_rate_limit(
26
+ remaining: remaining_int,
27
+ reset_at: reset_at,
28
+ status: env.status,
29
+ url: env.url.to_s
30
+ )
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ Faraday::Response.register_middleware(
39
+ github_rate_limit: Legion::Extensions::Github::Middleware::RateLimit
40
+ )
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Github
8
+ module Middleware
9
+ class ScopeProbe < ::Faraday::Middleware
10
+ REPO_PATH_PATTERN = %r{^/repos/([^/]+)/([^/]+)}
11
+
12
+ def initialize(app, handler: nil)
13
+ super(app)
14
+ @handler = handler
15
+ end
16
+
17
+ def on_complete(env)
18
+ return unless @handler
19
+ return unless env.url.path.match?(REPO_PATH_PATTERN)
20
+
21
+ info = { status: env.status, url: env.url.to_s, path: env.url.path }
22
+
23
+ if [403, 404].include?(env.status)
24
+ @handler.on_scope_denied(info) if @handler.respond_to?(:on_scope_denied)
25
+ elsif env.status >= 200 && env.status < 300
26
+ @handler.on_scope_authorized(info) if @handler.respond_to?(:on_scope_authorized)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ Faraday::Response.register_middleware(
36
+ github_scope_probe: Legion::Extensions::Github::Middleware::ScopeProbe
37
+ )
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Github
6
+ module OAuth
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
+ 3 * 60 * 60
15
+ end
16
+
17
+ # rubocop:disable Legion/Extension/ActorEnabledSideEffects
18
+ def enabled?
19
+ oauth_settings[:client_id] && oauth_settings[:client_secret]
20
+ rescue StandardError => _e
21
+ false
22
+ end
23
+ # rubocop:enable Legion/Extension/ActorEnabledSideEffects
24
+
25
+ def manual
26
+ settings = oauth_settings
27
+ return unless settings[:client_id] && settings[:client_secret]
28
+
29
+ token_entry = fetch_delegated_token
30
+ return unless token_entry&.dig(:refresh_token)
31
+
32
+ auth = Object.new.extend(Legion::Extensions::Github::OAuth::Runners::Auth)
33
+ result = auth.refresh_token(
34
+ client_id: settings[:client_id],
35
+ client_secret: settings[:client_secret],
36
+ refresh_token: token_entry[:refresh_token]
37
+ )
38
+ return unless result.dig(:result, 'access_token')
39
+
40
+ store_delegated_token(result[:result])
41
+ log.info('OAuth::Actor::TokenRefresh: delegated token refreshed')
42
+ rescue StandardError => e
43
+ log.error("OAuth::Actor::TokenRefresh: #{e.message}")
44
+ end
45
+
46
+ private
47
+
48
+ def oauth_settings
49
+ return {} unless defined?(Legion::Settings)
50
+
51
+ Legion::Settings[:github]&.dig(:oauth) || {}
52
+ rescue StandardError => _e
53
+ {}
54
+ end
55
+
56
+ def fetch_delegated_token
57
+ return nil unless defined?(Legion::Crypt)
58
+
59
+ vault_get('github/oauth/delegated/token')
60
+ rescue StandardError => _e
61
+ nil
62
+ end
63
+
64
+ def store_delegated_token(token_data)
65
+ return unless defined?(Legion::Crypt)
66
+
67
+ vault_write('github/oauth/delegated/token', token_data)
68
+ rescue StandardError => e
69
+ log.warn("OAuth::Actor::TokenRefresh#store_delegated_token: #{e.message}")
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Github
6
+ module OAuth
7
+ module Hooks
8
+ class Callback < Legion::Extensions::Hooks::Base # rubocop:disable Legion/Extension/HookMissingRunnerClass
9
+ mount '/callback'
10
+
11
+ def self.runner_class
12
+ 'Legion::Extensions::Github::OAuth::Runners::Auth'
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'openssl'
5
+ require 'securerandom'
6
+ require 'uri'
7
+ require 'legion/extensions/github/helpers/client'
8
+
9
+ module Legion
10
+ module Extensions
11
+ module Github
12
+ module OAuth
13
+ module Runners
14
+ module Auth
15
+ include Legion::Extensions::Github::Helpers::Client
16
+
17
+ def generate_pkce(**)
18
+ verifier = SecureRandom.urlsafe_base64(32)
19
+ challenge = ::Base64.urlsafe_encode64(
20
+ OpenSSL::Digest::SHA256.digest(verifier), padding: false
21
+ )
22
+ { result: { verifier: verifier, challenge: challenge, challenge_method: 'S256' } }
23
+ end
24
+
25
+ def authorize_url(client_id:, redirect_uri:, scope:, state:,
26
+ code_challenge:, code_challenge_method: 'S256', **)
27
+ params = URI.encode_www_form(
28
+ client_id: client_id, redirect_uri: redirect_uri,
29
+ scope: scope, state: state,
30
+ code_challenge: code_challenge,
31
+ code_challenge_method: code_challenge_method
32
+ )
33
+ { result: "https://github.com/login/oauth/authorize?#{params}" }
34
+ end
35
+
36
+ def exchange_code(client_id:, client_secret:, code:, redirect_uri:, code_verifier:, **)
37
+ response = oauth_connection.post('/login/oauth/access_token', {
38
+ client_id: client_id, client_secret: client_secret,
39
+ code: code, redirect_uri: redirect_uri,
40
+ code_verifier: code_verifier
41
+ })
42
+ { result: response.body }
43
+ end
44
+
45
+ def refresh_token(client_id:, client_secret:, refresh_token:, **)
46
+ response = oauth_connection.post('/login/oauth/access_token', {
47
+ client_id: client_id, client_secret: client_secret,
48
+ refresh_token: refresh_token,
49
+ grant_type: 'refresh_token'
50
+ })
51
+ { result: response.body }
52
+ end
53
+
54
+ def request_device_code(client_id:, scope: 'repo', **)
55
+ response = oauth_connection.post('/login/device/code', {
56
+ client_id: client_id, scope: scope
57
+ })
58
+ { result: response.body }
59
+ end
60
+
61
+ def poll_device_code(client_id:, device_code:, interval: 5, timeout: 300, **)
62
+ deadline = Time.now + timeout
63
+ current_interval = interval
64
+
65
+ loop do
66
+ response = oauth_connection.post('/login/oauth/access_token', {
67
+ client_id: client_id,
68
+ device_code: device_code,
69
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
70
+ })
71
+ body = response.body
72
+ return { result: body } if body[:access_token]
73
+
74
+ error_key = body[:error]
75
+ case error_key
76
+ when 'authorization_pending'
77
+ return { error: 'timeout', description: "Device code flow timed out after #{timeout}s" } if Time.now > deadline
78
+
79
+ sleep(current_interval) unless current_interval.zero?
80
+ when 'slow_down'
81
+ current_interval += 5
82
+ sleep(current_interval) unless current_interval.zero?
83
+ else
84
+ return { error: error_key, description: body[:error_description] }
85
+ end
86
+ end
87
+ end
88
+
89
+ def revoke_token(client_id:, client_secret:, access_token:, **)
90
+ conn = oauth_connection(client_id: client_id, client_secret: client_secret)
91
+ response = conn.delete("/applications/#{client_id}/token", { access_token: access_token })
92
+ { result: response.status == 204 }
93
+ end
94
+
95
+ def oauth_connection(client_id: nil, client_secret: nil, **)
96
+ Faraday.new(url: 'https://github.com') do |conn|
97
+ conn.request :json
98
+ conn.response :json, content_type: /\bjson$/
99
+ conn.headers['Accept'] = 'application/json'
100
+ conn.request :authorization, :basic, client_id, client_secret if client_id && client_secret
101
+ end
102
+ end
103
+
104
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
105
+ Legion::Extensions::Helpers.const_defined?(:Lex, false)
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Github
6
+ module OAuth
7
+ module Transport
8
+ module Exchanges
9
+ class Oauth < Legion::Transport::Exchange
10
+ def exchange_name = 'lex.github.oauth'
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Github
6
+ module OAuth
7
+ module Transport
8
+ module Queues
9
+ class Auth < Legion::Transport::Queue
10
+ def queue_name = 'lex.github.oauth.runners.auth'
11
+ def queue_options = { auto_delete: false }
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/github/helpers/client'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Github
8
+ module Runners
9
+ module Actions
10
+ include Legion::Extensions::Github::Helpers::Client
11
+
12
+ def list_workflows(owner:, repo:, per_page: 30, page: 1, **)
13
+ response = connection(owner: owner, repo: repo, **).get(
14
+ "/repos/#{owner}/#{repo}/actions/workflows", per_page: per_page, page: page
15
+ )
16
+ { result: response.body }
17
+ end
18
+
19
+ def get_workflow(owner:, repo:, workflow_id:, **)
20
+ response = connection(owner: owner, repo: repo, **).get(
21
+ "/repos/#{owner}/#{repo}/actions/workflows/#{workflow_id}"
22
+ )
23
+ { result: response.body }
24
+ end
25
+
26
+ def list_workflow_runs(owner:, repo:, workflow_id:, status: nil, branch: nil,
27
+ per_page: 30, page: 1, **)
28
+ params = { per_page: per_page, page: page, status: status, branch: branch }.compact
29
+ response = connection(owner: owner, repo: repo, **).get(
30
+ "/repos/#{owner}/#{repo}/actions/workflows/#{workflow_id}/runs", params
31
+ )
32
+ { result: response.body }
33
+ end
34
+
35
+ def get_workflow_run(owner:, repo:, run_id:, **)
36
+ response = connection(owner: owner, repo: repo, **).get(
37
+ "/repos/#{owner}/#{repo}/actions/runs/#{run_id}"
38
+ )
39
+ { result: response.body }
40
+ end
41
+
42
+ def trigger_workflow(owner:, repo:, workflow_id:, ref:, inputs: {}, **)
43
+ payload = { ref: ref, inputs: inputs }
44
+ response = connection(owner: owner, repo: repo, **).post(
45
+ "/repos/#{owner}/#{repo}/actions/workflows/#{workflow_id}/dispatches", payload
46
+ )
47
+ { result: response.status == 204 }
48
+ end
49
+
50
+ def cancel_workflow_run(owner:, repo:, run_id:, **)
51
+ response = connection(owner: owner, repo: repo, **).post(
52
+ "/repos/#{owner}/#{repo}/actions/runs/#{run_id}/cancel"
53
+ )
54
+ { result: [202, 204].include?(response.status) }
55
+ end
56
+
57
+ def rerun_workflow(owner:, repo:, run_id:, **)
58
+ response = connection(owner: owner, repo: repo, **).post(
59
+ "/repos/#{owner}/#{repo}/actions/runs/#{run_id}/rerun"
60
+ )
61
+ { result: [201, 204].include?(response.status) }
62
+ end
63
+
64
+ def rerun_failed_jobs(owner:, repo:, run_id:, **)
65
+ response = connection(owner: owner, repo: repo, **).post(
66
+ "/repos/#{owner}/#{repo}/actions/runs/#{run_id}/rerun-failed-jobs"
67
+ )
68
+ { result: [201, 204].include?(response.status) }
69
+ end
70
+
71
+ def list_workflow_run_jobs(owner:, repo:, run_id:, filter: 'latest', per_page: 30, page: 1, **)
72
+ params = { filter: filter, per_page: per_page, page: page }
73
+ response = connection(owner: owner, repo: repo, **).get(
74
+ "/repos/#{owner}/#{repo}/actions/runs/#{run_id}/jobs", params
75
+ )
76
+ { result: response.body }
77
+ end
78
+
79
+ def download_workflow_run_logs(owner:, repo:, run_id:, **)
80
+ response = connection(owner: owner, repo: repo, **).get(
81
+ "/repos/#{owner}/#{repo}/actions/runs/#{run_id}/logs"
82
+ )
83
+ { result: { status: response.status, headers: response.headers.to_h, body: response.body } }
84
+ end
85
+
86
+ def list_workflow_run_artifacts(owner:, repo:, run_id:, per_page: 30, page: 1, **)
87
+ params = { per_page: per_page, page: page }
88
+ response = connection(owner: owner, repo: repo, **).get(
89
+ "/repos/#{owner}/#{repo}/actions/runs/#{run_id}/artifacts", params
90
+ )
91
+ { result: response.body }
92
+ end
93
+
94
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
95
+ Legion::Extensions::Helpers.const_defined?(:Lex, false)
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'legion/extensions/github/helpers/client'
4
+ require 'legion/extensions/github/helpers/cache'
4
5
 
5
6
  module Legion
6
7
  module Extensions
@@ -8,13 +9,14 @@ module Legion
8
9
  module Runners
9
10
  module Branches
10
11
  include Legion::Extensions::Github::Helpers::Client
12
+ include Legion::Extensions::Github::Helpers::Cache
11
13
 
12
14
  def create_branch(owner:, repo:, branch:, from_ref: 'main', **)
13
- ref_response = connection(**).get("/repos/#{owner}/#{repo}/git/ref/heads/#{from_ref}")
15
+ ref_response = connection(owner: owner, repo: repo, **).get("/repos/#{owner}/#{repo}/git/ref/heads/#{from_ref}")
14
16
  sha = ref_response.body.dig('object', 'sha')
15
17
 
16
- create_response = connection(**).post("/repos/#{owner}/#{repo}/git/refs",
17
- { ref: "refs/heads/#{branch}", sha: sha })
18
+ create_response = connection(owner: owner, repo: repo, **).post("/repos/#{owner}/#{repo}/git/refs",
19
+ { ref: "refs/heads/#{branch}", sha: sha })
18
20
 
19
21
  { success: true, ref: create_response.body['ref'], sha: sha }
20
22
  rescue StandardError => e