multi_repo 0.1.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 (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