multi_repo 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +16 -0
  3. data/.github/workflows/ci.yaml +32 -0
  4. data/.gitignore +6 -0
  5. data/.rspec +2 -0
  6. data/.rubocop.yml +4 -0
  7. data/.rubocop_cc.yml +4 -0
  8. data/.rubocop_local.yml +0 -0
  9. data/.whitesource +3 -0
  10. data/Gemfile +6 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +90 -0
  13. data/Rakefile +6 -0
  14. data/bin/console +8 -0
  15. data/exe/multi_repo +29 -0
  16. data/lib/multi_repo/cli.rb +92 -0
  17. data/lib/multi_repo/helpers/git_mirror.rb +198 -0
  18. data/lib/multi_repo/helpers/license.rb +106 -0
  19. data/lib/multi_repo/helpers/pull_request_blaster_outer.rb +129 -0
  20. data/lib/multi_repo/helpers/readme_badges.rb +84 -0
  21. data/lib/multi_repo/helpers/rename_labels.rb +26 -0
  22. data/lib/multi_repo/helpers/update_branch_protection.rb +24 -0
  23. data/lib/multi_repo/helpers/update_labels.rb +34 -0
  24. data/lib/multi_repo/helpers/update_milestone.rb +33 -0
  25. data/lib/multi_repo/helpers/update_repo_settings.rb +23 -0
  26. data/lib/multi_repo/labels.rb +31 -0
  27. data/lib/multi_repo/repo.rb +56 -0
  28. data/lib/multi_repo/repo_set.rb +27 -0
  29. data/lib/multi_repo/service/artifactory.rb +122 -0
  30. data/lib/multi_repo/service/code_climate.rb +119 -0
  31. data/lib/multi_repo/service/docker.rb +178 -0
  32. data/lib/multi_repo/service/git/minigit_capturing_patch.rb +12 -0
  33. data/lib/multi_repo/service/git.rb +90 -0
  34. data/lib/multi_repo/service/github.rb +238 -0
  35. data/lib/multi_repo/service/rubygems_stub.rb +103 -0
  36. data/lib/multi_repo/service/travis.rb +68 -0
  37. data/lib/multi_repo/version.rb +3 -0
  38. data/lib/multi_repo.rb +44 -0
  39. data/multi_repo.gemspec +44 -0
  40. data/repos/.gitkeep +0 -0
  41. data/scripts/delete_labels +23 -0
  42. data/scripts/destroy_branch +23 -0
  43. data/scripts/destroy_remote +26 -0
  44. data/scripts/destroy_tag +31 -0
  45. data/scripts/each_repo +23 -0
  46. data/scripts/fetch_repos +18 -0
  47. data/scripts/git_mirror +9 -0
  48. data/scripts/github_rate_limit +10 -0
  49. data/scripts/hacktoberfest +138 -0
  50. data/scripts/make_alumni +50 -0
  51. data/scripts/new_rubygems_stub +17 -0
  52. data/scripts/pull_request_blaster_outer +24 -0
  53. data/scripts/pull_request_labeler +59 -0
  54. data/scripts/pull_request_merger +63 -0
  55. data/scripts/reenable_repo_workflows +33 -0
  56. data/scripts/rename_labels +22 -0
  57. data/scripts/restart_travis_builds +31 -0
  58. data/scripts/show_commit_history +86 -0
  59. data/scripts/show_org_members +19 -0
  60. data/scripts/show_org_repos +13 -0
  61. data/scripts/show_org_stats +82 -0
  62. data/scripts/show_project_cards +35 -0
  63. data/scripts/show_repo_set +13 -0
  64. data/scripts/show_tag +33 -0
  65. data/scripts/show_travis_status +63 -0
  66. data/scripts/update_branch_protection +22 -0
  67. data/scripts/update_labels +16 -0
  68. data/scripts/update_milestone +21 -0
  69. data/scripts/update_repo_settings +15 -0
  70. metadata +366 -0
@@ -0,0 +1,119 @@
1
+ module MultiRepo::Service
2
+ class CodeClimate
3
+ def self.api_token
4
+ @api_token ||= ENV["CODECLIMATE_API_TOKEN"]
5
+ end
6
+
7
+ def self.api_token=(token)
8
+ @api_token = token
9
+ end
10
+
11
+ def self.badge_name
12
+ "Code Climate"
13
+ end
14
+
15
+ def self.badge_details(repo)
16
+ {
17
+ "description" => badge_name,
18
+ "image" => "https://codeclimate.com/github/#{repo.name}.svg",
19
+ "url" => "https://codeclimate.com/github/#{repo.name}"
20
+ }
21
+ end
22
+
23
+ def self.coverage_badge_name
24
+ "Test Coverage"
25
+ end
26
+
27
+ def self.coverage_badge_details(repo)
28
+ {
29
+ "description" => coverage_badge_name,
30
+ "image" => "https://codeclimate.com/github/#{repo.name}/badges/coverage.svg",
31
+ "url" => "https://codeclimate.com/github/#{repo.name}/coverage"
32
+ }
33
+ end
34
+
35
+ attr_reader :repo, :dry_run
36
+
37
+ def initialize(repo, dry_run: false, **_)
38
+ @repo = repo
39
+ @dry_run = dry_run
40
+ end
41
+
42
+ def save!
43
+ write_codeclimate_yaml
44
+ write_rubocop_yamls
45
+ end
46
+
47
+ def enable
48
+ ensure_enabled
49
+ end
50
+
51
+ def badge_details
52
+ self.class.badge_details(repo)
53
+ end
54
+
55
+ def coverage_badge_details
56
+ self.class.coverage_badge_details(repo)
57
+ end
58
+
59
+ def test_reporter_id
60
+ ensure_enabled
61
+ @response.dig("data", 0, "attributes", "test_reporter_id")
62
+ end
63
+
64
+ def create_repo_secret
65
+ Github.new(dry_run: dry_run).create_or_update_repository_secret(repo.name, "CC_TEST_REPORTER_ID", test_reporter_id)
66
+ end
67
+
68
+ private
69
+
70
+ def ensure_enabled
71
+ return if @enabled
72
+
73
+ require 'rest-client'
74
+ require 'json'
75
+
76
+ @response =
77
+ if dry_run
78
+ puts "** dry-run: RestClient.get(\"https://api.codeclimate.com/v1/repos?github_slug=#{repo.name}\", #{headers})".light_black
79
+ {"data" => [{"attributes" => {"badge_token" => "0123456789abdef01234", "test_reporter_id" => "0123456789abcedef0123456789abcedef0123456789abcedef0123456789abc"}}]}
80
+ else
81
+ JSON.parse(RestClient.get("https://api.codeclimate.com/v1/repos?github_slug=#{repo.name}", headers))
82
+ end
83
+
84
+ if @response["data"].empty?
85
+ payload = {"data" => {"type" => "repos", "attributes" => {"url" => "https://github.com/#{repo.name}"}}}.to_json
86
+ @response = JSON.parse(RestClient.post("https://api.codeclimate.com/v1/github/repos", payload, headers))
87
+ @response["data"] = [@response["data"]]
88
+ end
89
+
90
+ @enabled = true
91
+ end
92
+
93
+ def headers
94
+ token = self.class.api_token
95
+ raise "Missing CodeClimate API Token" if token.nil?
96
+
97
+ {
98
+ :accept => "application/vnd.api+json",
99
+ :content_type => "application/vnd.api+json",
100
+ :authorization => "Token token=#{token}"
101
+ }
102
+ end
103
+
104
+ def write_codeclimate_yaml
105
+ write_generator_file(".codeclimate.yml")
106
+ end
107
+
108
+ def write_rubocop_yamls
109
+ %w[.rubocop.yml .rubocop_cc.yml .rubocop_local.yml].each do |file|
110
+ write_generator_file(file)
111
+ end
112
+ end
113
+
114
+ def write_generator_file(file)
115
+ content = RestClient.get("https://raw.githubusercontent.com/ManageIQ/manageiq/master/lib/generators/manageiq/plugin/templates/#{file}").body
116
+ repo.write_file(file, content, dry_run: dry_run)
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,178 @@
1
+ module MultiRepo::Service
2
+ class Docker
3
+ def self.registry
4
+ @registry ||= ENV.fetch("DOCKER_REGISTRY")
5
+ end
6
+
7
+ def self.registry=(endpoint)
8
+ @registry = endpoint
9
+ end
10
+
11
+ def self.clear_cache
12
+ FileUtils.rm_f(Dir.glob("/tmp/docker-*"))
13
+ end
14
+
15
+ SMALL_IMAGE = "hello-world:latest".freeze
16
+
17
+ def self.ensure_small_image
18
+ return @has_small_image if defined?(@has_small_image)
19
+
20
+ return false unless system?("docker pull #{SMALL_IMAGE} &>/dev/null")
21
+
22
+ @has_small_image = true
23
+ end
24
+
25
+ def self.tag_small_image(fq_path)
26
+ return false unless ensure_small_image
27
+
28
+ system?("docker tag #{SMALL_IMAGE} #{fq_path}") &&
29
+ system?("docker push #{fq_path}") &&
30
+ system?("docker rmi #{fq_path}")
31
+ end
32
+
33
+ def self.system?(command, dry_run: false, verbose: true)
34
+ if dry_run
35
+ puts "+ dry_run: #{command}".light_black
36
+ true
37
+ else
38
+ puts "+ #{command}".light_black
39
+ system(command)
40
+ end
41
+ end
42
+
43
+ def self.system!(command, **kwargs)
44
+ exit($?.exitstatus) unless system?(command, **kwargs)
45
+ end
46
+
47
+ attr_accessor :registry, :cache, :dry_run
48
+
49
+ def initialize(registry: self.class.registry, cache: true, dry_run: false)
50
+ require "rest-client"
51
+ require "fileutils"
52
+ require "json"
53
+
54
+ @registry = registry
55
+
56
+ @cache = cache
57
+ @dry_run = dry_run
58
+
59
+ self.class.clear_cache unless cache
60
+ end
61
+
62
+ def tags(image, **kwargs)
63
+ path = File.join("v2", image, "tags/list")
64
+ cache_file = "/tmp/docker-tags-#{image.tr("/", "-")}-raw-#{Date.today}.json"
65
+ request(:get, path, **kwargs).tap do |data|
66
+ File.write(cache_file, JSON.pretty_generate(data))
67
+ end["tags"]
68
+ end
69
+
70
+ def retag(image, new_image)
71
+ system?("skopeo copy --multi-arch all docker://#{image} docker://#{new_image}", dry_run: dry_run)
72
+ end
73
+
74
+ def delete_registry_tag(image, tag, **kwargs)
75
+ path = File.join("v2", image, "manifests", tag)
76
+ request(:delete, path, **kwargs)
77
+ true
78
+ rescue RestClient::NotFound => err
79
+ # Ignore deletes on 404s because they are either already deleted or the tag is orphaned.
80
+ raise unless err.http_code == 404
81
+ false
82
+ end
83
+
84
+ def force_delete_registry_tag(image, tag, **kwargs)
85
+ return true if delete_registry_tag(image, tag, **kwargs)
86
+
87
+ # The tag is likely orphaned, so recreate the tag with a new image, then immediately delete it
88
+ fq_path = File.join(registry, "#{image}:#{tag}")
89
+ self.class.tag_small_image(fq_path) &&
90
+ delete_registry_tag(image, tag, **kwargs)
91
+ end
92
+
93
+ def run(image, command, platform: nil)
94
+ system_capture!("docker run --rm -it #{"--platform=#{platform} " if platform} #{image} #{command}")
95
+ end
96
+
97
+ def fetch_image_by_sha(source_image, tag: nil, platform: nil)
98
+ source_image_name, _source_image_sha = source_image.split("@")
99
+
100
+ system!("docker pull #{"--platform=#{platform} " if platform}#{source_image}")
101
+ system!("docker tag #{source_image} #{source_image_name}:#{tag}") if tag
102
+
103
+ true
104
+ end
105
+
106
+ def remove_images(*images)
107
+ command = "docker rmi #{images.join(" ")}"
108
+
109
+ # Don't use system_capture! as this is expected to fail if the image does not exist.
110
+ if dry_run
111
+ puts "+ dry_run: #{command}".light_black
112
+ else
113
+ puts "+ #{command}".light_black
114
+ `#{command} 2>/dev/null`
115
+ end
116
+ end
117
+
118
+ def manifest_inspect(image)
119
+ command = "docker manifest inspect #{image}"
120
+
121
+ cache_file = "/tmp/docker-manifest-#{image.split("@").last}.txt"
122
+ if cache && File.exist?(cache_file)
123
+ puts "+ cached: #{command}".light_black
124
+ data = File.read(cache_file)
125
+ else
126
+ data = system_capture(command)
127
+ File.write(cache_file, data)
128
+ end
129
+
130
+ data.blank? ? {} : JSON.parse(data)
131
+ end
132
+
133
+ private
134
+
135
+ def request(verb, path, body: nil, headers: {}, verbose: true)
136
+ path = File.join(registry, path)
137
+
138
+ if dry_run && %i[delete put post patch].include?(verb)
139
+ puts "+ dry_run: #{verb.to_s.upcase} #{path}".light_black if verbose
140
+ {}
141
+ else
142
+ puts "+ #{verb.to_s.upcase} #{path}".light_black if verbose
143
+ response =
144
+ if %i[put post patch].include?(verb)
145
+ RestClient.send(verb, path, body, headers)
146
+ else
147
+ RestClient::Request.execute(:method => verb, :url => path, :headers => headers, :read_timeout => 300) do |response, request, result|
148
+ if verb == :delete && response.code == 301 # Moved Permanently
149
+ response.follow_redirection
150
+ else
151
+ response.return!
152
+ end
153
+ end
154
+ end
155
+ response.empty? ? {} : JSON.parse(response)
156
+ end
157
+ end
158
+
159
+ def system?(command, **kwargs)
160
+ self.class.system?(command, **kwargs)
161
+ end
162
+
163
+ def system!(command, **kwargs)
164
+ self.class.system!(command, **kwargs)
165
+ end
166
+
167
+ def system_capture(command)
168
+ puts "+ #{command}".light_black
169
+ `#{command}`.chomp
170
+ end
171
+
172
+ def system_capture!(command)
173
+ system_capture(command).tap do
174
+ exit($?.exitstatus) if $?.exitstatus != 0
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,12 @@
1
+ module MultiRepo
2
+ module Git
3
+ module MiniGitCapturingPatch
4
+ def system(*args)
5
+ `#{Shellwords.join(args)} 2>&1`
6
+ end
7
+ end
8
+ end
9
+ end
10
+
11
+ require "minigit"
12
+ MiniGit::Capturing.prepend(MultiRepo::Git::MiniGitCapturingPatch)
@@ -0,0 +1,90 @@
1
+ require "active_support/core_ext/object/blank"
2
+
3
+ module MultiRepo::Service
4
+ class Git
5
+ def self.client(path:, clone_source:)
6
+ require "minigit"
7
+ require_relative "git/minigit_capturing_patch"
8
+
9
+ retried = false
10
+ MiniGit.debug = true if ENV["GIT_DEBUG"]
11
+ MiniGit.new(path)
12
+ rescue ArgumentError => err
13
+ raise if retried
14
+ raise unless err.message.include?("does not seem to exist")
15
+
16
+ clone(clone_source: clone_source, path: path)
17
+ retried = true
18
+ retry
19
+ end
20
+
21
+ def self.clone(clone_source:, path:)
22
+ require "minigit"
23
+ require "shellwords"
24
+
25
+ args = ["clone", clone_source, path]
26
+ command = Shellwords.join(["git", *args])
27
+ command << " &>/dev/null" unless ENV["GIT_DEBUG"]
28
+ puts "+ #{command}" if ENV["GIT_DEBUG"] # Matches the output of MiniGit
29
+
30
+ raise MiniGit::GitError.new(args, $?) unless system(command)
31
+ end
32
+
33
+ attr_reader :dry_run, :client
34
+
35
+ def initialize(path:, clone_source:, dry_run: false)
36
+ require "minigit"
37
+
38
+ @dry_run = dry_run
39
+ @client = self.class.client(path: path, clone_source: clone_source)
40
+ end
41
+
42
+ def fetch(output: false)
43
+ client = output ? self.client : self.client.capturing
44
+
45
+ client.fetch(:all => true, :tags => true)
46
+ end
47
+
48
+ def hard_checkout(branch, source = "origin/#{branch}", output: false)
49
+ client = output ? self.client : self.client.capturing
50
+
51
+ client.reset(:hard => true)
52
+ client.clean("-xdf")
53
+ client.checkout("-B", branch, source)
54
+ end
55
+
56
+ def destroy_tag(tag, output: false)
57
+ client = output ? self.client : self.client.capturing
58
+
59
+ if dry_run
60
+ puts "** dry-run: git tag --delete #{tag}".light_black
61
+ else
62
+ client.tag({:delete => true}, tag)
63
+ end
64
+ rescue MiniGit::GitError
65
+ # Ignore missing tags because we want them destroyed anyway
66
+ nil
67
+ end
68
+
69
+ def branch?(branch)
70
+ client.capturing.rev_parse("--verify", branch)
71
+ rescue MiniGit::GitError
72
+ false
73
+ else
74
+ true
75
+ end
76
+ alias_method :tag?, :branch?
77
+
78
+ def remote?(remote)
79
+ client.capturing.remote("show", remote)
80
+ rescue MiniGit::GitError => e
81
+ false
82
+ else
83
+ true
84
+ end
85
+
86
+ def remote_branch?(remote, branch)
87
+ client.capturing.ls_remote(remote, branch).present?
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,238 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+
3
+ module MultiRepo::Service
4
+ class Github
5
+ def self.api_token
6
+ @api_token ||= ENV["GITHUB_API_TOKEN"]
7
+ end
8
+
9
+ def self.api_token=(token)
10
+ @api_token = token
11
+ end
12
+
13
+ def self.api_endpoint
14
+ @api_endpoint ||= ENV["GITHUB_API_ENDPOINT"]
15
+ end
16
+
17
+ def self.api_endpoint=(endpoint)
18
+ @api_endpoint = endpoint
19
+ end
20
+
21
+ def self.client
22
+ @client ||= begin
23
+ raise "Missing GitHub API Token" if api_token.nil?
24
+
25
+ params = {
26
+ :api_endpoint => api_endpoint,
27
+ :access_token => api_token,
28
+ :auto_paginate => true
29
+ }.compact
30
+
31
+ require 'octokit'
32
+ Octokit::Client.new(params)
33
+ end
34
+ end
35
+
36
+ def self.org_repo_names(org, include_forks: false, include_archived: false)
37
+ repos = client.list_repositories(org, :type => "sources")
38
+ repos.reject!(&:fork?) unless include_forks
39
+ repos.reject!(&:archived?) unless include_archived
40
+ repos.map(&:full_name).sort
41
+ end
42
+
43
+ def self.valid_milestone_date?(date)
44
+ !!parse_milestone_date(date)
45
+ end
46
+
47
+ def self.parse_milestone_date(date)
48
+ require "active_support/core_ext/time"
49
+ ActiveSupport::TimeZone.new('Pacific Time (US & Canada)').parse(date) # LOL GitHub, TimeZones are hard
50
+ end
51
+
52
+ def self.find_milestone_by_title(repo_name, title)
53
+ client.list_milestones(repo_name, :state => :all).detect { |m| m.title.casecmp?(title) }
54
+ end
55
+
56
+ def self.org_member_names(org)
57
+ client.org_members(org).map(&:login).sort_by(&:downcase)
58
+ end
59
+
60
+ def self.find_team_by_name(org, team)
61
+ client.org_teams(org).detect { |t| t.slug == team }
62
+ end
63
+
64
+ def self.team_members(org, team)
65
+ team_id = find_team_by_name(org, team)&.id
66
+ team_id ? client.team_members(team_id) : []
67
+ end
68
+
69
+ def self.team_member_names(org, team)
70
+ team_members(org, team).map(&:login).sort_by(&:downcase)
71
+ end
72
+
73
+ def self.team_ids_by_name(org)
74
+ @team_ids ||= {}
75
+ @team_ids[org] ||= client.org_teams(org).map { |t| [t.slug, t.id] }.sort.to_h
76
+ end
77
+
78
+ def self.team_names(org)
79
+ team_ids(org).keys
80
+ end
81
+
82
+ def self.disabled_workflows(repo_name)
83
+ client.workflows(repo_name)[:workflows].select { |w| w.state == "disabled_inactivity" }
84
+ end
85
+
86
+ attr_reader :dry_run
87
+
88
+ def initialize(dry_run: false)
89
+ require "octokit"
90
+
91
+ @dry_run = dry_run
92
+ end
93
+
94
+ delegate :client,
95
+ :org_repo_names,
96
+ :find_milestone_by_title,
97
+ :org_member_names,
98
+ :find_team_by_name,
99
+ :team_members,
100
+ :team_member_names,
101
+ :team_ids_by_name,
102
+ :team_names,
103
+ :disabled_workflows,
104
+ :to => :class
105
+
106
+ def edit_repository(repo_name, settings)
107
+ if dry_run
108
+ puts "** dry-run: github.edit_repository(#{repo_name.inspect}, #{settings.inspect[1..-2]})".light_black
109
+ else
110
+ client.edit_repository(repo_name, settings)
111
+ end
112
+ end
113
+
114
+ def create_label(repo_name, label, color)
115
+ if dry_run
116
+ puts "** dry-run: github.add_label(#{repo_name.inspect}, #{label.inspect}, #{color.inspect})".light_black
117
+ else
118
+ client.add_label(repo_name, label, color)
119
+ end
120
+ end
121
+
122
+ def update_label(repo_name, label, color: nil, name: nil)
123
+ settings = {:color => color, :name => name}.compact
124
+ raise ArgumentError, "one of color or name must be passed" if settings.empty?
125
+
126
+ if dry_run
127
+ puts "** dry-run: github.update_label(#{repo_name.inspect}, #{label.inspect}, #{settings.inspect[1..-2]})".light_black
128
+ else
129
+ client.update_label(repo_name, label, settings)
130
+ end
131
+ end
132
+
133
+ def delete_label!(repo_name, label)
134
+ if dry_run
135
+ puts "** dry-run: github.delete_label!(#{repo_name.inspect}, #{label.inspect})".light_black
136
+ else
137
+ client.delete_label!(repo_name, label)
138
+ end
139
+ end
140
+
141
+ def create_milestone(repo_name, title, due_on)
142
+ if dry_run
143
+ puts "** dry-run: github.create_milestone(#{repo_name.inspect}, #{title.inspect}, :due_on => #{due_on.strftime("%Y-%m-%d").inspect})".light_black
144
+ else
145
+ client.create_milestone(repo_name, title, :due_on => due_on)
146
+ end
147
+ end
148
+
149
+ def update_milestone(repo_name, milestone_number, due_on)
150
+ if dry_run
151
+ puts "** dry-run: github.update_milestone(#{repo_name.inspect}, #{milestone_number}, :due_on => #{due_on.strftime("%Y-%m-%d").inspect})".light_black
152
+ else
153
+ client.update_milestone(repo_name, milestone_number, :due_on => due_on)
154
+ end
155
+ end
156
+
157
+ def close_milestone(repo_name, milestone_number)
158
+ if dry_run
159
+ puts "** dry-run: github.update_milestone(#{repo_name.inspect}, #{milestone_number}, :state => 'closed')".light_black
160
+ else
161
+ client.update_milestone(repo_name, milestone_number, :state => "closed")
162
+ end
163
+ end
164
+
165
+ def protect_branch(repo_name, branch, settings)
166
+ if dry_run
167
+ puts "** dry-run: github.protect_branch(#{repo_name.inspect}, #{branch.inspect}, #{settings.inspect[1..-2]})".light_black
168
+ else
169
+ client.protect_branch(repo_name, branch, settings)
170
+ end
171
+ end
172
+
173
+ def add_team_membership(org, team, user)
174
+ team_id = team_ids_by_name(org)[team]
175
+
176
+ if dry_run
177
+ puts "** dry-run: github.add_team_membership(#{team_id.inspect}, #{user.inspect})".light_black
178
+ else
179
+ client.add_team_membership(team_id, user)
180
+ end
181
+ end
182
+
183
+ def remove_team_membership(org, team, user)
184
+ team_id = team_ids_by_name(org)[team]
185
+
186
+ if dry_run
187
+ puts "** dry-run: github.remove_team_membership(#{team_id.inspect}, #{user.inspect})".light_black
188
+ else
189
+ client.remove_team_membership(team_id, user)
190
+ end
191
+ end
192
+
193
+ def remove_collaborator(repo_name, user)
194
+ if dry_run
195
+ puts "** dry-run: github.remove_collaborator(#{repo_name.inspect}, #{user.inspect})".light_black
196
+ else
197
+ client.remove_collaborator(repo_name, user)
198
+ end
199
+ end
200
+
201
+ def enable_workflow(repo_name, workflow_number)
202
+ command = "repos/#{repo_name}/actions/workflows/#{workflow_number}/enable"
203
+
204
+ if dry_run
205
+ puts "** dry-run: github.put(#{command.inspect})".light_black
206
+ else
207
+ client.put(command)
208
+ end
209
+ end
210
+
211
+ def create_or_update_repository_secret(repo_name, key, value)
212
+ payload = encode_secret(repo_name, value)
213
+
214
+ if dry_run
215
+ puts "** dry-run: github.create_or_update_secret(#{repo_name.inspect}, #{key.inspect}, #{payload.inspect})".light_black
216
+ else
217
+ client.create_or_update_secret(repo_name, key, payload)
218
+ end
219
+ end
220
+
221
+ private def encode_secret(repo_name, value)
222
+ require "rbnacl"
223
+ require "base64"
224
+
225
+ repo_public_key = client.get_public_key(repo_name)
226
+ decoded_repo_public_key = Base64.decode64(repo_public_key.key)
227
+ public_key = RbNaCl::PublicKey.new(decoded_repo_public_key)
228
+ box = RbNaCl::Boxes::Sealed.from_public_key(public_key)
229
+ encrypted_value = box.encrypt(value)
230
+ encoded_encrypted_value = Base64.strict_encode64(encrypted_value)
231
+
232
+ {
233
+ "encrypted_value" => encoded_encrypted_value,
234
+ "key_id" => repo_public_key.key_id
235
+ }
236
+ end
237
+ end
238
+ end