Class: Fastlane::Helper::GithubHelper

Inherits:
Object
  • Object
show all
Defined in:
lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(github_token:) ⇒ GithubHelper

Helper for GitHub Actions

Parameters:

  • github_token (String?)

    GitHub OAuth access token



17
18
19
20
21
22
23
24
25
26
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 17

def initialize(github_token:)
  @client = Octokit::Client.new(access_token: github_token)

  # Fetch the current user
  user = @client.user
  UI.message("Logged in as: #{user.name}")

  # Auto-paginate to ensure we're not missing data
  @client.auto_paginate = true
end

Instance Attribute Details

#clientObject (readonly)

Returns the value of attribute client.



11
12
13
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 11

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

Parameters:

  • response (Sawyer::Resource)

    The API response returned by ‘#get_branch_protection` or `#set_branch_protection`

Returns:

  • (Hash)

    A hash representation of the API response—or an empty Hash if ‘response` was `nil`— with Boolean values normalized to true/false, and any extra values that would be refused if used in a subsequent API request (like legacy vs new key) removed.

See Also:



282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 282

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_itemFastlaneCore::ConfigItem

Creates a GithubToken Fastlane ConfigItem

Returns:

  • (FastlaneCore::ConfigItem)

    The Fastlane ConfigItem for GitHub OAuth access token



314
315
316
317
318
319
320
321
322
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 314

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



206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 206

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 + 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

Parameters:

  • repository (String)

    The repository name, including the organization (e.g. ‘wordpress-mobile/wordpress-ios`)

  • title (String)

    The name of the milestone we want to create (e.g.: ‘16.9`)

  • due_date (Time)

    Milestone due date—which will also correspond to the code freeze date

  • days_until_submission (Integer)

    Number of days from code freeze to submission to the App Store / Play Store

  • days_until_release (Integer)

    Number of days from code freeze to release



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 82

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

  options = {}
  # == 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`.
  options[:due_on] = due_date.strftime('%Y-%m-%dT12:00:00Z')
  options[:description] = comment
  client.create_milestone(repository, title, options)
end

#create_release(repository:, version:, description:, assets:, prerelease:, is_draft:, target: nil) ⇒ Object

Creates a Release on GitHub as a Draft

Parameters:

  • repository (String)

    The repository to create the GitHub release on. Typically a repo slug (<org>/<repo>).

  • version (String)

    The version for which to create this release. Will be used both as the name of the tag and the name of the release.

  • target (String?) (defaults to: nil)

    The commit SHA or branch name that this release will point to when it’s published and creates the tag. If nil (the default), will use the repo’s current HEAD commit at the time this method is called. Unused if the tag already exists.

  • description (String)

    The text to use as the release’s body / description (typically the release notes)

  • assets (Array<String>)

    List of file paths to attach as assets to the release

  • prerelease (TrueClass|FalseClass)

    Indicates if this should be created as a pre-release (i.e. for alpha/beta)

  • is_draft (TrueClass|FalseClass)

    Indicates if this should be created as a draft release



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 124

def create_release(repository:, version:, description:, assets:, prerelease:, is_draft:, target: nil)
  release = client.create_release(
    repository,
    version, # tag 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

Parameters:

  • repository (String)

    The repository name (including the organization)

  • tag (String)

    The name of the tag we’re downloading from

  • file_path (String)

    The path, inside the project folder, of the file to download

  • download_folder (String)

    The folder which the file should be downloaded into

Returns:

  • (String)

    The path of the downloaded file, or nil if something went wrong



185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 185

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

#generate_release_notes(repository:, tag_name:, previous_tag:, target_commitish: nil, config_file_path: nil) ⇒ String

Note:

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.

Parameters:

  • repository (String)

    The repository to create the GitHub release on. Typically a repo slug (<org>/<repo>).

  • tag_name (String)

    The name of the git tag to generate the changelog for.

  • previous_tag (String)

    The name of the git tag to compare to.

  • target_commitish (String) (defaults to: nil)

    The commit sha1 or branch name to use as the head for the comparison if the ‘tag_name` tag does not exist yet. Unused if `tag_name` exists.

  • config_file_path (String) (defaults to: nil)

    The path to the GitHub configuration file to use for generating release notes. Will use ‘.github/release.yml` by default if it exists.

Returns:

  • (String)

    The string returned by GitHub API listing PRs between ‘previous_tag` and current `tag_name`

Raises:

  • (StandardError)

    Might raise if there was an error during the API call



152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 152

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

Parameters:

  • repository (String)

    The repository name (including the organization)

  • branch (String)

    The branch name

See Also:



259
260
261
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 259

def get_branch_protection(repository:, branch:, **options)
  client.branch_protection(repository, branch)
end

#get_last_milestone(repository) ⇒ Object



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 49

def get_last_milestone(repository)
  options = {}
  options[:state] = 'open'

  milestones = client.list_milestones(repository, options)
  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) ⇒ Object



28
29
30
31
32
33
34
35
36
37
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 28

def get_milestone(repository, release)
  miles = client.list_milestones(repository)
  mile = nil

  miles&.each do |mm|
    mile = mm if mm[:title].start_with?(release)
  end

  return mile
end

#get_prs_for_milestone(repository, milestone) ⇒ <Sawyer::Resource>

Fetch all the PRs for a given milestone

Parameters:

  • repository (String)

    The repository name, including the organization (e.g. ‘wordpress-mobile/wordpress-ios`)

  • milestone (String)

    The name of the milestone we want to fetch the list of PRs for (e.g.: ‘16.9`)

Returns:

  • (<Sawyer::Resource>)

    A list of the PRs for the given milestone, sorted by number



45
46
47
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 45

def get_prs_for_milestone(repository, milestone)
  client.search_issues(%(type:pr milestone:"#{milestone}" repo:#{repository}))[:items].sort_by(&:number)
end

#get_release_url(repository:, tag_name:) ⇒ String

Returns the URL of the GitHub release pointing at a given tag

Parameters:

  • repository (String)

    The repository to create the GitHub release on. Typically a repo slug (<org>/<repo>).

  • tag_name (String)

    The name of the git tag to get the associated release of

Returns:

  • (String)

    URL of the corresponding GitHub Release, or nil if none was found.



171
172
173
174
175
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 171

def get_release_url(repository:, tag_name:)
  client.release_for_tag(repository, tag_name).html_url
rescue Octokit::NotFound
  nil
end

#remove_branch_protection(repository:, branch:) ⇒ Object

Remove the protection of a single branch from a repository

Parameters:

  • repository (String)

    The repository name (including the organization)

  • branch (String)

    The branch name

See Also:



249
250
251
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 249

def remove_branch_protection(repository:, branch:)
  client.unprotect_branch(repository, branch)
end

#set_branch_protection(repository:, branch:, **options) ⇒ Object

Protects a single branch from a repository

Parameters:

  • repository (String)

    The repository name (including the organization)

  • branch (String)

    The branch name

  • options (Hash)

    A customizable set of options.

See Also:



270
271
272
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 270

def set_branch_protection(repository:, branch:, **options)
  client.protect_branch(repository, branch, options)
end

#update_milestone(repository:, number:, **options) ⇒ Milestone

Update a milestone for a repository

Parameters:

  • repository (String)

    The repository name (including the organization)

  • number (String)

    The number of the milestone we want to fetch

  • options (Hash)

    A customizable set of options.

Options Hash (**options):

  • :title (String)

    A unique title.

  • :state (String)
  • :description (String)

    A meaningful description

  • :due_on (Time)

    Set if the milestone has a due date

Returns:

  • (Milestone)

    A single milestone object

See Also:



239
240
241
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb', line 239

def update_milestone(repository:, number:, **options)
  client.update_milestone(repository, number, options)
end