Class: Fastlane::Helper::GithubHelper
- Inherits:
-
Object
- Object
- Fastlane::Helper::GithubHelper
- Defined in:
- lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb
Instance Attribute Summary collapse
-
#client ⇒ Object
readonly
Returns the value of attribute client.
Class Method Summary collapse
-
.branch_protection_api_response_to_normalized_hash(response) ⇒ Hash
Convert a response from the
/branch-protectionAPI endpoint into a Hash suitable to be returned and/or reused to pass to a subsequent/branch-protectionAPI request. -
.github_token_config_item ⇒ FastlaneCore::ConfigItem
Creates a GithubToken Fastlane ConfigItem.
Instance Method Summary collapse
-
#comment_on_pr(project_slug:, pr_number:, body:, reuse_identifier: SecureRandom.uuid) ⇒ Object
Creates (or updates an existing) GitHub PR Comment.
-
#create_milestone(repository:, title:, due_date:, days_until_submission:, days_until_release:) ⇒ Object
Creates a new milestone.
-
#create_release(repository:, version:, description:, assets:, prerelease:, is_draft:, target: nil, name: nil) ⇒ Object
Creates a Release on GitHub as a Draft.
-
#download_file_from_tag(repository:, tag:, file_path:, download_folder:) ⇒ String
Downloads a file from the given GitHub tag.
-
#find_pull_request(repository:, head:, base: nil, state: 'open') ⇒ Sawyer::Resource?
Find an existing Pull Request matching the given head (and optionally base) branch.
-
#generate_release_notes(repository:, tag_name:, previous_tag:, target_commitish: nil, config_file_path: nil) ⇒ String
Use the GitHub API to generate release notes based on the list of PRs between current tag and previous tag.
-
#get_branch_protection(repository:, branch:, **options) ⇒ Object
Get the list of branch protection settings for a given branch of a repository.
- #get_last_milestone(repository) ⇒ Object
-
#get_milestone(repository, release) ⇒ Sawyer::Resource
A milestone object in a repository, or nil if none matches.
-
#get_prs_and_issues_for_milestone(repository:, milestone:, include_closed: false) ⇒ Array<Sawyer::Resource>
Fetch all the PRs and issues for a given milestone.
-
#get_release(repository:, version:) ⇒ Sawyer::Resource
Returns the GitHub release matching a given tag/version, including draft releases.
-
#get_release_url(repository:, tag_name:) ⇒ String
Returns the URL of the published GitHub release pointing at a given tag.
-
#initialize(github_token:) ⇒ GithubHelper
constructor
Helper for GitHub Actions.
-
#publish_release(repository:, name:, prerelease: nil) ⇒ String
Publishes an existing GitHub Release still in draft mode.
-
#remove_branch_protection(repository:, branch:) ⇒ Object
Remove the protection of a single branch from a repository.
-
#search_milestone_items(repository:, milestone:, type:, include_closed: false) ⇒ Array<Sawyer::Resource>
Search for issues or PRs for a given milestone.
-
#set_branch_protection(repository:, branch:, **options) ⇒ Object
Protects a single branch from a repository.
-
#set_milestone(repository:, number:, milestone:) ⇒ Object
Set/Update the milestone assigned to a given PR or issue.
-
#update_milestone(repository:, number:, **options) ⇒ Milestone
Update a milestone for a repository.
-
#upload_release_assets(repository:, version:, assets:, replace_existing: true) ⇒ String
Uploads assets to an existing GitHub release, optionally replacing matching filenames.
Constructor Details
#initialize(github_token:) ⇒ GithubHelper
Helper for GitHub Actions
19 20 21 22 23 24 25 26 27 28 |
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 19 def initialize(github_token:) @client = Octokit::Client.new(access_token: github_token) # Fetch the current user user = @client.user UI.("Logged in as: #{user.name}") # Auto-paginate to ensure we're not missing data @client.auto_paginate = true end |
Instance Attribute Details
#client ⇒ Object (readonly)
Returns the value of attribute client.
13 14 15 |
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 13 def client @client end |
Class Method Details
.branch_protection_api_response_to_normalized_hash(response) ⇒ Hash
Convert a response from the /branch-protection API endpoint into a Hash
suitable to be returned and/or reused to pass to a subsequent /branch-protection API request
505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 |
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 505 def self.branch_protection_api_response_to_normalized_hash(response) return {} if response.nil? normalize_values = lambda do |hash| hash.each do |k, v| # Boolean values appear as { "enabled" => true/false } in the Response, while they must appear as true/false in Request hash[k] = v[:enabled] if v.is_a?(Hash) && v.key?(:enabled) # References to :users, :teams and :apps are expanded as Objects in the Response, while they must just be the login or slug in Request hash[k] = v.map { |item| item[:login] } if k == :users && v.is_a?(Array) hash[k] = v.map { |item| item[:slug] } if %i[teams apps].include?(k) && v.is_a?(Array) # Response contains lots of `*url` keys that are useless in practice and makes the returned hash harder to parse visually hash.delete(k) if k.to_s == 'url' || k.to_s.end_with?('_url') # Recurse into Hashes and Array of Hashes normalize_values.call(v) if v.is_a?(Hash) v.each { |item| normalize_values.call(item) if item.is_a?(Hash) } if v.is_a?(Array) end end hash = response.to_hash normalize_values.call(hash) # Response contains both (legacy) `:contexts` key and new `:checks` key, but only one of the two should be passed in Request hash[:required_status_checks].delete(:contexts) unless hash.dig(:required_status_checks, :checks).nil? hash end |
.github_token_config_item ⇒ FastlaneCore::ConfigItem
Creates a GithubToken Fastlane ConfigItem
537 538 539 540 541 542 543 544 545 |
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 537 def self.github_token_config_item FastlaneCore::ConfigItem.new( key: :github_token, env_name: 'GITHUB_TOKEN', description: 'The GitHub OAuth access token', optional: false, type: String ) end |
Instance Method Details
#comment_on_pr(project_slug:, pr_number:, body:, reuse_identifier: SecureRandom.uuid) ⇒ Object
Creates (or updates an existing) GitHub PR Comment
392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 |
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 392 def comment_on_pr(project_slug:, pr_number:, body:, reuse_identifier: SecureRandom.uuid) comments = client.issue_comments(project_slug, pr_number) reuse_marker = "<!-- REUSE_ID: #{reuse_identifier} -->" existing_comment = comments.find do |comment| # Only match comments posted by the owner of the GitHub Token, and with the given reuse ID comment.user.id == client.user.id and comment.body.include?(reuse_marker) end comment_body = "#{reuse_marker}\n\n#{body}" if existing_comment.nil? client.add_comment(project_slug, pr_number, comment_body) else client.update_comment(project_slug, existing_comment.id, comment_body) end reuse_identifier end |
#create_milestone(repository:, title:, due_date:, days_until_submission:, days_until_release:) ⇒ Object
Creates a new milestone
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 136 def create_milestone(repository:, title:, due_date:, days_until_submission:, days_until_release:) UI.user_error!('days_until_release must be greater than zero.') unless days_until_release.positive? UI.user_error!('days_until_submission must be greater than zero.') unless days_until_submission.positive? UI.user_error!('days_until_release must be greater or equal to days_until_submission.') unless days_until_release >= days_until_submission submission_date = due_date.to_datetime.next_day(days_until_submission) release_date = due_date.to_datetime.next_day(days_until_release) comment = <<~MILESTONE_DESCRIPTION Code freeze: #{due_date.to_datetime.strftime('%B %d, %Y')} App Store submission: #{submission_date.strftime('%B %d, %Y')} Release: #{release_date.strftime('%B %d, %Y')} MILESTONE_DESCRIPTION = {} # == Workaround for GitHub API bug == # # It seems that whatever date we send to the API, GitHub will 'floor' it to the date that seems to be at # 00:00 PST/PDT and then discard the time component of the date we sent. # This means that, when we cross the November DST change date, where the due date of the previous milestone # was e.g. `2022-10-31T07:00:00Z` and `.next_day(14)` returns `2022-11-14T07:00:00Z` and we send that value # for the `due_on` field via the API, GitHub ends up creating a milestone with a due of `2022-11-13T08:00:00Z` # instead, introducing an off-by-one error on that due date. # # This is a bug in the GitHub API, not in our date computation logic. # To solve this, we trick it by forcing the time component of the ISO date we send to be `12:00:00Z`. [:due_on] = due_date.strftime('%Y-%m-%dT12:00:00Z') [:description] = comment client.create_milestone(repository, title, ) end |
#create_release(repository:, version:, description:, assets:, prerelease:, is_draft:, target: nil, name: nil) ⇒ Object
Creates a Release on GitHub as a Draft
179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 |
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 179 def create_release(repository:, version:, description:, assets:, prerelease:, is_draft:, target: nil, name: nil) release = client.create_release( repository, version, # tag name name: name || version, # release name target_commitish: target || Git.open(Dir.pwd).log.first.sha, prerelease: prerelease, draft: is_draft, body: description ) assets.each do |file_path| client.upload_asset(release[:url], file_path, content_type: 'application/octet-stream') end release[:html_url] end |
#download_file_from_tag(repository:, tag:, file_path:, download_folder:) ⇒ String
Downloads a file from the given GitHub tag
371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 |
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 371 def download_file_from_tag(repository:, tag:, file_path:, download_folder:) repository = repository.delete_prefix('/').chomp('/') file_path = file_path.delete_prefix('/').chomp('/') file_name = File.basename(file_path) download_path = File.join(download_folder, file_name) download_url = client.contents(repository, path: file_path, ref: tag).download_url begin uri = URI.parse(download_url) uri.open do |remote_file| File.write(download_path, remote_file.read) end rescue OpenURI::HTTPError return nil end download_path end |
#find_pull_request(repository:, head:, base: nil, state: 'open') ⇒ Sawyer::Resource?
Find an existing Pull Request matching the given head (and optionally base) branch.
422 423 424 425 426 427 428 |
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 422 def find_pull_request(repository:, head:, base: nil, state: 'open') qualified_head = head.include?(':') ? head : "#{repository.split('/').first}:#{head}" = { state: state, head: qualified_head } [:base] = base unless base.nil? client.pull_requests(repository, ).first end |
#generate_release_notes(repository:, tag_name:, previous_tag:, target_commitish: nil, config_file_path: nil) ⇒ String
This API uses the .github/release.yml config file to classify the PRs by category in the generated list according to PR labels.
Use the GitHub API to generate release notes based on the list of PRs between current tag and previous tag.
306 307 308 309 310 311 312 313 314 315 316 317 |
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 306 def generate_release_notes(repository:, tag_name:, previous_tag:, target_commitish: nil, config_file_path: nil) repo_path = Octokit::Repository.path(repository) api_url = "#{repo_path}/releases/generate-notes" res = client.post( api_url, tag_name: tag_name, target_commitish: target_commitish, # Only used if no git tag named `tag_name` exists yet previous_tag_name: previous_tag, config_file_path: config_file_path ) res.body end |
#get_branch_protection(repository:, branch:, **options) ⇒ Object
Get the list of branch protection settings for a given branch of a repository
462 463 464 |
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 462 def get_branch_protection(repository:, branch:, **) client.branch_protection(repository, branch) end |
#get_last_milestone(repository) ⇒ Object
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 103 def get_last_milestone(repository) = {} [:state] = 'open' milestones = client.list_milestones(repository, ) return nil if milestones.nil? last_stone = nil milestones.each do |mile| mile_vcomps = mile[:title].split[0].split('.') if last_stone.nil? last_stone = mile unless mile_vcomps.length < 2 else begin last_vcomps = last_stone[:title].split[0].split('.') last_stone = mile if Integer(mile_vcomps[0]) > Integer(last_vcomps[0]) || Integer(mile_vcomps[1]) > Integer(last_vcomps[1]) rescue StandardError puts 'Found invalid milestone' end end end last_stone end |
#get_milestone(repository, release) ⇒ Sawyer::Resource
This relies on the release version string being at the start of the milestone's title
Returns A milestone object in a repository, or nil if none matches.
35 36 37 38 39 40 |
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 35 def get_milestone(repository, release) milestones = client.list_milestones(repository) milestones&.reverse&.find do |m| m[:title].start_with?(release) end end |
#get_prs_and_issues_for_milestone(repository:, milestone:, include_closed: false) ⇒ Array<Sawyer::Resource>
Fetch all the PRs and issues for a given milestone
49 50 51 52 53 54 55 56 |
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 49 def get_prs_and_issues_for_milestone(repository:, milestone:, include_closed: false) # While the `/search` API used with a classic tokens returns both issues and PRs, using a fine-grained tokens always require either 'is:issue' or 'is:pull-request', # therefore we need to make two separate calls to cover both cases issues = search_milestone_items(repository: repository, milestone: milestone, type: :issue, include_closed: include_closed) prs = search_milestone_items(repository: repository, milestone: milestone, type: :pr, include_closed: include_closed) (issues + prs).sort_by(&:number) end |
#get_release(repository:, version:) ⇒ Sawyer::Resource
Returns the GitHub release matching a given tag/version, including draft releases.
202 203 204 205 206 207 |
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 202 def get_release(repository:, version:) release = find_release(repository: repository, version: version) return release unless release.nil? UI.user_error!("Could not find GitHub Release for tag #{version} in #{repository}") end |
#get_release_url(repository:, tag_name:) ⇒ String
Returns the URL of the published GitHub release pointing at a given tag
325 326 327 328 329 |
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 325 def get_release_url(repository:, tag_name:) client.release_for_tag(repository, tag_name).html_url rescue Octokit::NotFound nil end |
#publish_release(repository:, name:, prerelease: nil) ⇒ String
If several GitHub Releases share that same name—which happens when finalize_release is run more than once
for the same version, each run creating its own draft—the most recently created one is the one being published,
as it is the one targeting the latest commit of the release branch. The other, staler ones are left untouched.
Publishes an existing GitHub Release still in draft mode.
344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 |
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 344 def publish_release(repository:, name:, prerelease: nil) releases = matching_releases(repository: repository) { |release| release.name == name } UI.user_error!("No release found with name #{name}") if releases.empty? release = releases.last UI.important("Found #{releases.count} GitHub Releases named `#{name}`. Publishing the most recently created one, targeting #{release.target_commitish}, and leaving the #{releases.count - 1} older one(s) untouched.") if releases.count > 1 UI.important("The most recent GitHub Release named `#{name}` (#{release.html_url}) has already been published. Updating it nonetheless.") unless release.draft client.update_release( release.url, { draft: false, prerelease: prerelease }.compact ) release.html_url end |
#remove_branch_protection(repository:, branch:) ⇒ Object
Remove the protection of a single branch from a repository
452 453 454 |
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 452 def remove_branch_protection(repository:, branch:) client.unprotect_branch(repository, branch) end |
#search_milestone_items(repository:, milestone:, type:, include_closed: false) ⇒ Array<Sawyer::Resource>
Search for issues or PRs for a given milestone
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 66 def search_milestone_items(repository:, milestone:, type:, include_closed: false) milestone_title = milestone.is_a?(Sawyer::Resource) ? milestone.title : milestone # Map type symbol to GitHub search qualifier type_qualifier = case type when :issue 'is:issue' when :pr, :pull_request 'is:pull-request' else raise ArgumentError, "Invalid type: #{type}. Must be :issue or :pr" end query = %(repo:#{repository} milestone:"#{milestone_title}" #{type_qualifier}) query += ' is:open' unless include_closed client.search_issues(query)[:items] end |
#set_branch_protection(repository:, branch:, **options) ⇒ Object
Protects a single branch from a repository
473 474 475 |
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 473 def set_branch_protection(repository:, branch:, **) client.protect_branch(repository, branch, ) end |
#set_milestone(repository:, number:, milestone:) ⇒ Object
Use get_milestone to get a milestone object from a version number
Set/Update the milestone assigned to a given PR or issue
93 94 95 96 97 98 99 100 101 |
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 93 def set_milestone(repository:, number:, milestone:) milestone_num = milestone.is_a?(Sawyer::Resource) ? milestone.number : milestone client.update_issue(repository, number, { milestone: milestone_num }) rescue Octokit::NotFound UI.user_error!("Could not find PR or issue ##{number} in #{repository}") rescue Octokit::UnprocessableEntity UI.user_error!("Invalid milestone #{milestone_num}") end |
#update_milestone(repository:, number:, **options) ⇒ Milestone
Update a milestone for a repository
442 443 444 |
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 442 def update_milestone(repository:, number:, **) client.update_milestone(repository, number, ) end |
#upload_release_assets(repository:, version:, assets:, replace_existing: true) ⇒ String
Uploads assets to an existing GitHub release, optionally replacing matching filenames.
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 |
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 218 def upload_release_assets(repository:, version:, assets:, replace_existing: true) asset_paths = validate_release_assets!(assets) release = get_release(repository: repository, version: version) existing_assets = client.release_assets(release.url) asset_paths.each do |file_path| file_name = File.basename(file_path) matching_assets = existing_assets.select { |asset| asset.name == file_name } unless matching_assets.empty? if replace_existing matching_assets.each do |asset| UI.("Deleting existing GitHub Release asset #{asset.name}") client.delete_release_asset(asset.url) end existing_assets -= matching_assets else UI.user_error!("GitHub Release #{version} already has an asset named #{file_name}. Set replace_existing: true to replace it.") end end UI.("Uploading #{file_path} to GitHub Release #{version}") uploaded_asset = client.upload_asset(release.url, file_path, content_type: 'application/octet-stream') existing_assets << uploaded_asset unless uploaded_asset.nil? end release.html_url end |