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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +50 -0
- data/CLAUDE.md +45 -19
- data/README.md +155 -83
- data/lex-github.gemspec +2 -0
- data/lib/legion/extensions/github/app/actor/token_refresh.rb +68 -0
- data/lib/legion/extensions/github/app/actor/webhook_poller.rb +65 -0
- data/lib/legion/extensions/github/app/hooks/setup.rb +19 -0
- data/lib/legion/extensions/github/app/hooks/webhook.rb +19 -0
- data/lib/legion/extensions/github/app/runners/auth.rb +48 -0
- data/lib/legion/extensions/github/app/runners/credential_store.rb +46 -0
- data/lib/legion/extensions/github/app/runners/installations.rb +56 -0
- data/lib/legion/extensions/github/app/runners/manifest.rb +65 -0
- data/lib/legion/extensions/github/app/runners/webhooks.rb +118 -0
- data/lib/legion/extensions/github/app/transport/exchanges/app.rb +17 -0
- data/lib/legion/extensions/github/app/transport/messages/event.rb +18 -0
- data/lib/legion/extensions/github/app/transport/queues/auth.rb +18 -0
- data/lib/legion/extensions/github/app/transport/queues/webhooks.rb +18 -0
- data/lib/legion/extensions/github/cli/app.rb +57 -0
- data/lib/legion/extensions/github/cli/auth.rb +99 -0
- data/lib/legion/extensions/github/client.rb +24 -0
- data/lib/legion/extensions/github/errors.rb +44 -0
- data/lib/legion/extensions/github/helpers/browser_auth.rb +106 -0
- data/lib/legion/extensions/github/helpers/cache.rb +99 -0
- data/lib/legion/extensions/github/helpers/callback_server.rb +89 -0
- data/lib/legion/extensions/github/helpers/client.rb +292 -2
- data/lib/legion/extensions/github/helpers/scope_registry.rb +91 -0
- data/lib/legion/extensions/github/helpers/token_cache.rb +86 -0
- data/lib/legion/extensions/github/middleware/credential_fallback.rb +76 -0
- data/lib/legion/extensions/github/middleware/rate_limit.rb +40 -0
- data/lib/legion/extensions/github/middleware/scope_probe.rb +37 -0
- data/lib/legion/extensions/github/oauth/actor/token_refresh.rb +76 -0
- data/lib/legion/extensions/github/oauth/hooks/callback.rb +19 -0
- data/lib/legion/extensions/github/oauth/runners/auth.rb +111 -0
- data/lib/legion/extensions/github/oauth/transport/exchanges/oauth.rb +17 -0
- data/lib/legion/extensions/github/oauth/transport/queues/auth.rb +18 -0
- data/lib/legion/extensions/github/runners/actions.rb +100 -0
- data/lib/legion/extensions/github/runners/branches.rb +5 -3
- data/lib/legion/extensions/github/runners/checks.rb +84 -0
- data/lib/legion/extensions/github/runners/comments.rb +13 -7
- data/lib/legion/extensions/github/runners/commits.rb +11 -6
- data/lib/legion/extensions/github/runners/contents.rb +3 -1
- data/lib/legion/extensions/github/runners/deployments.rb +76 -0
- data/lib/legion/extensions/github/runners/gists.rb +9 -4
- data/lib/legion/extensions/github/runners/issues.rb +16 -9
- data/lib/legion/extensions/github/runners/labels.rb +16 -9
- data/lib/legion/extensions/github/runners/organizations.rb +10 -8
- data/lib/legion/extensions/github/runners/pull_requests.rb +24 -14
- data/lib/legion/extensions/github/runners/releases.rb +89 -0
- data/lib/legion/extensions/github/runners/repositories.rb +17 -10
- data/lib/legion/extensions/github/runners/repository_webhooks.rb +76 -0
- data/lib/legion/extensions/github/runners/search.rb +11 -8
- data/lib/legion/extensions/github/runners/users.rb +12 -8
- data/lib/legion/extensions/github/version.rb +1 -1
- data/lib/legion/extensions/github.rb +22 -0
- 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
|
-
|
|
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
|